Files
PlantAI-dev/benchmarks/plantgenebench/scripts/convert_pgb100_md.py
T
2026-08-03 18:42:47 +08:00

239 lines
7.6 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Convert the PlantGeneBench 100-question Markdown table to JSONL tasks/gold."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
LAYER_CATEGORY = {
"TU": "task_understanding",
"EP": "execution_planning",
"EC": "evidence_constraint",
"BI": "biological_interpretation",
"BC": "boundary_control",
}
ANALYSIS_TYPE_BY_STAGE = {
"rnaseq_qc": "rnaseq",
"rnaseq_align": "rnaseq",
"rnaseq_quant": "rnaseq",
"rnaseq_sample_qc": "rnaseq",
"rnaseq_de": "rnaseq",
"rnaseq_enrich": "rnaseq",
"rnaseq_wgcna": "rnaseq",
"rnaseq_fgsea": "rnaseq",
"rnaseq_candidate_family": "rnaseq",
}
def split_md_row(line: str) -> list[str]:
return [cell.strip() for cell in line.strip().strip("|").split("|")]
def parse_list_expr(text: str, key: str) -> list[str]:
match = re.search(rf"{re.escape(key)}=\[([^\]]*)\]", text)
if not match:
return []
value = match.group(1)
return [
item.strip().strip("'\"` ")
for item in re.split(r",|,", value)
if item.strip().strip("'\"` ")
]
def parse_target_stages(text: str) -> list[str]:
stages = parse_list_expr(text, "target_stages")
if stages:
return stages
stages = parse_list_expr(text, "target")
if stages:
return stages
# A few rows describe executable stages without literal target_stages.
if "map to " in text:
mapped: list[str] = []
for stage in [
"identify",
"motif",
"domain",
"phylogeny",
"synteny",
"interspecies_synteny",
"promoter",
"pep",
"kaks",
]:
if re.search(rf"\b{stage}\b", text):
mapped.append(stage)
return mapped
return []
def parse_execution_plan(text: str, target_stages: list[str]) -> list[str]:
stages = parse_list_expr(text, "execution_plan")
if stages:
return stages
arrow = re.search(r"([a-zA-Z0-9_]+(?:\s*->\s*[a-zA-Z0-9_]+)+)", text)
if arrow:
return [part.strip() for part in arrow.group(1).split("->")]
return target_stages
def parse_params(text: str) -> dict[str, Any]:
params: dict[str, Any] = {}
patterns = {
"promoter_length": r"promoter_length=([0-9]+)",
"max_motifs": r"max_motifs=([0-9]+)",
"bootstrap": r"bootstrap=([0-9]+)",
"geneFamily": r"family=([A-Za-z0-9\-]+)",
"species": r"species=([A-Za-z_]+)",
}
for key, pattern in patterns.items():
match = re.search(pattern, text)
if match:
value: Any = match.group(1)
if key in {"promoter_length", "max_motifs", "bootstrap"}:
value = int(value)
params[key] = value
if "tree_method=ML" in text:
params["tree_method"] = "ML"
if "analysis_input=DEG_result" in text:
params["analysis_input"] = "DEG_result"
return params
def infer_analysis_type(stages: list[str], category: str) -> str:
if category in {"evidence_constraint", "biological_interpretation", "boundary_control"}:
return category
if any(stage in ANALYSIS_TYPE_BY_STAGE for stage in stages):
return "rnaseq"
return "gene_family"
def forbidden_stages(must_not: str) -> list[str]:
found: list[str] = []
stage_terms = {
"identify": ["identify"],
"phylogeny": ["phylogeny"],
"motif": ["motif"],
"domain": ["domain"],
"structure": ["structure"],
"promoter": ["promoter"],
"synteny": ["synteny"],
"interspecies_synteny": ["interspecies_synteny"],
"karyotype": ["karyotype"],
"combo": ["combo"],
"ppi": ["PPI", "ppi"],
"expression": ["expression"],
"pep": ["pep"],
"rnaseq_qc": ["rnaseq_qc"],
"rnaseq_align": ["rnaseq_align"],
"rnaseq_quant": ["rnaseq_quant"],
"rnaseq_de": ["rnaseq_de"],
"rnaseq_enrich": ["rnaseq_enrich"],
}
action_patterns = [
"不要安排",
"不要运行",
"不要重新跑",
"不要加入",
"不要调用",
"不要转到",
"不要启动",
"不要强制",
"不要直接",
"不要只运行",
"不要创建",
]
for stage, terms in stage_terms.items():
for term in terms:
if any(f"{action}{term}" in must_not or f"{action} {term}" in must_not for action in action_patterns):
found.append(stage)
if "不要安排 expression、motif 或 RNA-seq" in must_not or "不要运行 raw RNA-seq" in must_not:
found.extend(["rnaseq_qc", "rnaseq_align", "rnaseq_quant", "rnaseq_de", "rnaseq_enrich"])
return sorted(set(found))
def convert(md_path: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
tasks: list[dict[str, Any]] = []
gold: list[dict[str, Any]] = []
for line in md_path.read_text(encoding="utf-8").splitlines():
if not line.startswith("| PGB-"):
continue
cells = split_md_row(line)
if len(cells) < 5:
continue
task_id, difficulty, query, expected_text, must_not = cells[:5]
layer_code = task_id.split("-")[1]
category = LAYER_CATEGORY.get(layer_code, "unknown")
target_stages = parse_target_stages(expected_text)
execution_plan = parse_execution_plan(expected_text, target_stages)
params = parse_params(expected_text)
analysis_type = infer_analysis_type(target_stages or execution_plan, category)
task = {
"task_id": task_id,
"category": category,
"difficulty": difficulty,
"source_type": "synthetic_expert_curated",
"tags": [category],
"user_query": query,
"input_context": {
"job_id": "",
"prefix": "",
"intent": {
"analysis_type": analysis_type,
"stages": target_stages,
"params": params,
},
},
"gold_summary": expected_text,
"must_not_do": must_not,
}
tasks.append(task)
gold.append(
{
"task_id": task_id,
"gold": {
"analysis_type": analysis_type,
"expected_stages": target_stages,
"expected_execution_plan": execution_plan,
"required_params": params,
"required_dependencies": {},
"forbidden_stages": forbidden_stages(must_not),
"can_execute": "depends_on_inputs",
"rationale": expected_text,
"must_not_do": must_not,
},
}
)
return tasks, gold
def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--md", type=Path, required=True)
parser.add_argument("--tasks-out", type=Path, required=True)
parser.add_argument("--gold-out", type=Path, required=True)
args = parser.parse_args()
tasks, gold = convert(args.md)
write_jsonl(args.tasks_out, tasks)
write_jsonl(args.gold_out, gold)
print(json.dumps({"tasks": len(tasks), "gold": len(gold)}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())