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

199 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import gzip
import re
import shutil
from pathlib import Path
from typing import Dict, Tuple, Optional, List
FASTA_SUFFIX = {".fa", ".fasta", ".faa", ".fna", ".pep"}
GFF_SUFFIX = {".gff", ".gff3"}
def is_gz(p: Path) -> bool:
return p.name.lower().endswith(".gz")
def strip_gz(p: Path) -> Path:
return Path(p.name[:-3]) if is_gz(p) else Path(p.name)
def gunzip_to(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
with gzip.open(src, "rb") as f_in, dst.open("wb") as f_out:
shutil.copyfileobj(f_in, f_out)
def read_fasta(p: Path) -> Dict[str, str]:
seqs = {}
name = None
chunks = []
with p.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
if line.startswith(">"):
if name is not None:
seqs[name] = "".join(chunks)
name = line[1:].split()[0]
chunks = []
else:
chunks.append(line)
if name is not None:
seqs[name] = "".join(chunks)
return seqs
def write_fasta(p: Path, seqs: Dict[str, str]):
p.parent.mkdir(parents=True, exist_ok=True)
with p.open("w", encoding="utf-8") as w:
for k, v in seqs.items():
w.write(f">{k}\n")
for i in range(0, len(v), 60):
w.write(v[i:i+60] + "\n")
def gene_key(seq_id: str) -> str:
# 常见转录本:Gene.T01 / Gene.t1 / Gene.1 / Gene.P01 等
for pat in [r"^(.*)\.[Tt]\d+$", r"^(.*)\.[Pp]\d+$", r"^(.*)\.\d+$"]:
m = re.match(pat, seq_id)
if m:
return m.group(1)
return seq_id
def pick_longest_isoform(seqs: Dict[str, str]) -> Dict[str, str]:
best = {}
best_len = {}
for sid, s in seqs.items():
g = gene_key(sid)
L = len(s.replace("-", "").replace(".", ""))
if g not in best_len or L > best_len[g]:
best_len[g] = L
best[g] = s
# 输出的 header 用 gene_key(更干净)
return {g: best[g] for g in best}
def detect_roles(files: List[Path]) -> Dict[str, List[Path]]:
roles = {"genome": [], "gff": [], "pep": [], "cds": [], "other": []}
for p in files:
name = strip_gz(p).name.lower()
suf = Path(name).suffix.lower()
if suf in GFF_SUFFIX:
roles["gff"].append(p)
elif suf in FASTA_SUFFIX:
# 用文件名关键词粗分 pep/cds/genome
if "cds" in name:
roles["cds"].append(p)
elif any(k in name for k in ["pep", "prot", "pros", "protein"]):
roles["pep"].append(p)
else:
roles["genome"].append(p)
else:
roles["other"].append(p)
return roles
def choose_best(paths: List[Path]) -> Optional[Path]:
if not paths:
return None
return sorted(paths, key=lambda x: x.stat().st_size, reverse=True)[0]
def main():
ap = argparse.ArgumentParser(description="Prepare inputs from input/<job>/ into workspace/<job>/prepared/")
ap.add_argument("--input-dir", required=True)
ap.add_argument("--workspace", required=True)
ap.add_argument("--prefix", required=True)
ap.add_argument("--force", action="store_true")
args = ap.parse_args()
in_dir = Path(args.input_dir).resolve()
ws = Path(args.workspace).resolve()
prepared = (ws / "prepared").resolve()
prepared.mkdir(parents=True, exist_ok=True)
# 收集文件
files = [p for p in in_dir.iterdir() if p.is_file()]
roles = detect_roles(files)
# 选择“最像 genome”的 fasta:体积最大的那个
genome_src = choose_best(roles["genome"])
gff_src = choose_best(roles["gff"])
pep_src = choose_best(roles["pep"])
cds_src = choose_best(roles["cds"])
# 解压/拷贝到 prepared
def materialize(src: Optional[Path], dst_name: str) -> Optional[Path]:
if not src:
return None
dst = prepared / dst_name
if dst.exists() and (not args.force):
return dst
if is_gz(src):
gunzip_to(src, dst)
else:
shutil.copy(src, dst)
return dst
genome = materialize(genome_src, f"{args.prefix}.genome.fasta") if genome_src else None
gff = materialize(gff_src, f"{args.prefix}.gff3") if gff_src else None
pep = materialize(pep_src, f"{args.prefix}.pep.raw.fasta") if pep_src else None
cds = materialize(cds_src, f"{args.prefix}.cds.raw.fasta") if cds_src else None
# 如果没 pep/cds,但有 genome+gff:尝试用 gffread 生成
if (pep is None or cds is None) and genome and gff:
gffread = shutil.which("gffread")
if gffread:
pep_out = prepared / f"{args.prefix}.pep.raw.fasta"
cds_out = prepared / f"{args.prefix}.cds.raw.fasta"
cmd = [gffread, str(gff), "-g", str(genome), "-y", str(pep_out), "-x", str(cds_out)]
print("CMD:", " ".join(cmd))
import subprocess
subprocess.run(cmd, check=True)
pep = pep_out if pep_out.exists() else pep
cds = cds_out if cds_out.exists() else cds
# 最长转录本
final_pep = prepared / f"{args.prefix}.pep.fasta"
final_cds = prepared / f"{args.prefix}.cds.fasta"
if pep and (args.force or not final_pep.exists()):
seqs = read_fasta(pep)
write_fasta(final_pep, pick_longest_isoform(seqs))
if cds and (args.force or not final_cds.exists()):
seqs = read_fasta(cds)
write_fasta(final_cds, pick_longest_isoform(seqs))
# 输出 manifest
out_manifest = prepared / "input.manifest.json"
manifest = {
"prefix": args.prefix,
"input_dir": str(in_dir),
"prepared_dir": str(prepared),
"paths": {
"genome": str(genome) if genome else None,
"gff3": str(gff) if gff else None,
"pep": str(final_pep) if final_pep.exists() else None,
"cds": str(final_cds) if final_cds.exists() else None,
},
"note": "pep/cds are longest-isoform fasta if available; requires gffread to derive pep/cds from genome+gff.",
}
out_manifest.write_text(__import__("json").dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print("Prepared manifest:", out_manifest)
print("pep:", manifest["paths"]["pep"])
print("cds:", manifest["paths"]["cds"])
print("gff3:", manifest["paths"]["gff3"])
print("genome:", manifest["paths"]["genome"])
if __name__ == "__main__":
main()