607 lines
24 KiB
Python
Executable File
607 lines
24 KiB
Python
Executable File
#!/home/xzq/miniconda3/envs/test4py311/bin/python
|
||
"""
|
||
Combo Plot Stage - Tree + Motif + Structure + Domain 组合图
|
||
|
||
将多个分析阶段的结果整合到一张图中:
|
||
- Panel A: Phylogenetic tree
|
||
- Panel B: Subfamily
|
||
- Panel C: Motif
|
||
- Panel D: Gene structure
|
||
- Panel E: Domain
|
||
"""
|
||
|
||
import os
|
||
import argparse
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple
|
||
|
||
# 项目根目录
|
||
PROJECT_ROOT = Path(__file__).parent.parent.parent.resolve()
|
||
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||
if str(PROJECT_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
from gfa_core.gene_family_scope import read_scope, sanitize_token, scope_hash
|
||
from gfa_core.gene_family_stage_context import build_stage_context, user_output_prefix_for, write_legacy_manifest_copy
|
||
|
||
# R 脚本路径由统一路径配置解析;gfpy312 环境没有 Rscript。
|
||
from gfa_core.project_paths import get_conda_env_bin, get_conda_env_rscript
|
||
RSCRIPT_PATH = get_conda_env_rscript("test4py311")
|
||
|
||
|
||
# 添加 conda 环境 bin 目录到 PATH。
|
||
CONDA_BIN = str(get_conda_env_bin("test4py311"))
|
||
if CONDA_BIN not in os.environ.get("PATH", ""):
|
||
os.environ["PATH"] = CONDA_BIN + os.pathsep + os.environ.get("PATH", "")
|
||
def find_file_by_glob(pattern: Path, name: str) -> Optional[Path]:
|
||
"""递归查找文件"""
|
||
matches = list(pattern.rglob(name))
|
||
return matches[0] if matches else None
|
||
|
||
|
||
def find_meme_xml(workspace: Path, prefix: str) -> Optional[Path]:
|
||
"""查找 MEME XML 文件"""
|
||
# 优先在 motif/analysis/meme_out/ 中查找
|
||
meme_dir = workspace / "motif" / "analysis" / "meme_out"
|
||
if meme_dir.exists():
|
||
xml_file = meme_dir / "meme.xml"
|
||
if xml_file.exists():
|
||
return xml_file
|
||
|
||
# 备选:递归搜索
|
||
return find_file_by_glob(workspace, "meme.xml")
|
||
|
||
|
||
def resolve_meme_xml_from_manifest(workspace: Path, motif_data: Dict, prefix: str) -> Optional[Path]:
|
||
artifacts = motif_data.get("artifacts", {}) or {}
|
||
meme_xml = artifacts.get("meme_xml") if isinstance(artifacts, dict) else None
|
||
candidates = []
|
||
if isinstance(meme_xml, dict):
|
||
for key in ("full_path", "path"):
|
||
text = str(meme_xml.get(key) or "").strip()
|
||
if text:
|
||
path = Path(text)
|
||
candidates.append(path if path.is_absolute() else workspace / path)
|
||
|
||
meme_results = artifacts.get("meme_results") if isinstance(artifacts, dict) else None
|
||
if isinstance(meme_results, dict):
|
||
for key in ("full_path", "path"):
|
||
text = str(meme_results.get(key) or "").strip()
|
||
if text:
|
||
path = Path(text)
|
||
path = path if path.is_absolute() else workspace / path
|
||
candidates.append(path / "meme.xml")
|
||
|
||
motif_txt = artifacts.get("motif_prot_txt") if isinstance(artifacts, dict) else None
|
||
if isinstance(motif_txt, dict):
|
||
text = str(motif_txt.get("full_path") or motif_txt.get("path") or "").strip()
|
||
if text:
|
||
path = Path(text)
|
||
path = path if path.is_absolute() else workspace / path
|
||
candidates.append(path.parent / "meme_out" / "meme.xml")
|
||
|
||
for candidate in candidates:
|
||
if candidate.exists() and candidate.stat().st_size > 0:
|
||
return candidate.resolve()
|
||
return find_meme_xml(workspace, prefix)
|
||
|
||
|
||
def load_manifest(manifest_path: Path) -> Dict:
|
||
"""加载 manifest 文件"""
|
||
if not manifest_path.exists():
|
||
raise FileNotFoundError(f"Manifest not found: {manifest_path}")
|
||
return json.loads(manifest_path.read_text(encoding="utf-8"))
|
||
|
||
|
||
def manifest_artifact_path(workspace: Path, manifest_path: Path, artifact: Dict) -> Optional[Path]:
|
||
if not isinstance(artifact, dict):
|
||
return None
|
||
raw = str(artifact.get("full_path") or artifact.get("path") or "").strip()
|
||
if not raw:
|
||
return None
|
||
path = Path(raw)
|
||
if path.is_absolute():
|
||
return path
|
||
candidate = workspace / path
|
||
if candidate.exists():
|
||
return candidate
|
||
return manifest_path.parent / path.name
|
||
|
||
|
||
def materialize_aggregate_subfamily_file(workspace: Path, prefix: str, scope_data: Optional[Dict]) -> Optional[Path]:
|
||
"""Build a combined subfamily table from per-target reference-classification manifests."""
|
||
scope_id = str((scope_data or {}).get("scope_id") or "").strip()
|
||
if not scope_id:
|
||
return None
|
||
final_dir = workspace / "final"
|
||
aggregate_manifest = final_dir / f"{prefix}.{sanitize_token(scope_id)}.reference_classification.manifest.json"
|
||
if not aggregate_manifest.exists():
|
||
return None
|
||
try:
|
||
aggregate = load_manifest(aggregate_manifest)
|
||
except Exception:
|
||
return None
|
||
rows = []
|
||
for artifact in (aggregate.get("artifacts") or {}).values():
|
||
child_manifest = manifest_artifact_path(workspace, aggregate_manifest, artifact)
|
||
if not child_manifest or not child_manifest.exists() or not child_manifest.name.endswith(".manifest.json"):
|
||
continue
|
||
try:
|
||
child = load_manifest(child_manifest)
|
||
except Exception:
|
||
continue
|
||
child_artifacts = child.get("artifacts") or {}
|
||
sub_artifact = child_artifacts.get("subfamily_file") or child_artifacts.get("geneID_subfamily_compat")
|
||
sub_path = manifest_artifact_path(workspace, child_manifest, sub_artifact)
|
||
if not sub_path or not sub_path.exists():
|
||
continue
|
||
for raw in sub_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||
line = raw.strip()
|
||
if not line or line.lower().startswith("gene"):
|
||
continue
|
||
rows.append(line)
|
||
if not rows:
|
||
return None
|
||
out = final_dir / f"{prefix}.{sanitize_token(scope_id)}.reference_classification.subfamily.tsv"
|
||
seen = set()
|
||
with out.open("w", encoding="utf-8") as handle:
|
||
for line in rows:
|
||
key = line.split("\t", 1)[0] if "\t" in line else line.split(None, 1)[0]
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
handle.write(line + "\n")
|
||
print(f"[INFO] Materialized aggregate subfamily file: {out}")
|
||
return out
|
||
|
||
|
||
def find_latest_manifest(workspace: Path, stage: str, prefix: str, scope_data: Optional[Dict] = None) -> Optional[Path]:
|
||
"""查找最新的 manifest 文件"""
|
||
final_dir = workspace / "final"
|
||
if not final_dir.exists():
|
||
return None
|
||
|
||
scope_id = str((scope_data or {}).get("scope_id") or "").strip()
|
||
if scope_data and scope_data.get("scope_id"):
|
||
scoped = final_dir / f"{prefix}.{sanitize_token(scope_id)}.{stage}.manifest.json"
|
||
if scoped.exists():
|
||
return scoped
|
||
compact = final_dir / f"{compact_output_prefix(prefix, scope_data)}.{stage}.manifest.json"
|
||
if compact.exists():
|
||
return compact
|
||
|
||
pattern = f"*{stage}*.manifest.json"
|
||
candidates = list(final_dir.glob(pattern))
|
||
if scope_id:
|
||
for path in sorted(candidates, key=lambda p: p.stat().st_mtime, reverse=True):
|
||
if scope_id in path.name:
|
||
return path
|
||
for path in sorted(candidates, key=lambda p: p.stat().st_mtime, reverse=True):
|
||
try:
|
||
data = json.loads(path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
continue
|
||
if data.get("stage") == stage and data.get("scope_id") == scope_id:
|
||
return path
|
||
|
||
# 按修改时间排序,返回最新的
|
||
if candidates:
|
||
return max(candidates, key=lambda p: p.stat().st_mtime)
|
||
return None
|
||
|
||
|
||
def compact_output_prefix(prefix: str, scope_data: Optional[Dict]) -> str:
|
||
"""Return a bounded user-facing output prefix.
|
||
|
||
Full scope details stay in scope JSON and manifest. Result filenames should
|
||
not enumerate every target/reference/comparative species.
|
||
"""
|
||
if not scope_data:
|
||
return prefix
|
||
family = str(scope_data.get("family") or (prefix.split(".")[-1] if "." in prefix else "")).strip()
|
||
mode = str(scope_data.get("scope_mode") or "").strip()
|
||
targets = [str(x) for x in scope_data.get("target_species") or [] if str(x).strip()]
|
||
scope_id = str(scope_data.get("scope_id") or "").strip()
|
||
if len(targets) == 1 and family:
|
||
return ".".join(x for x in [targets[0], family, mode] if x)
|
||
if mode:
|
||
base = prefix if prefix else f"Target.{family}".strip(".")
|
||
if scope_id:
|
||
return f"{base}.{mode}.h{scope_hash(scope_id, 6)}"
|
||
return f"{base}.{mode}"
|
||
return prefix
|
||
|
||
|
||
def normalize_panels(raw_panels: str) -> List[str]:
|
||
"""Normalize the panel selector used by the isolated combo replot test."""
|
||
allowed = ["motif", "structure", "domain"]
|
||
aliases = {
|
||
"gene_structure": "structure",
|
||
"gene-structure": "structure",
|
||
"genestructure": "structure",
|
||
"domains": "domain",
|
||
"motifs": "motif",
|
||
"all": "all",
|
||
}
|
||
selected: List[str] = []
|
||
for token in str(raw_panels or "").replace(";", ",").split(","):
|
||
key = token.strip().lower().replace(" ", "_")
|
||
if not key:
|
||
continue
|
||
key = aliases.get(key, key)
|
||
if key == "all":
|
||
return allowed
|
||
if key not in allowed:
|
||
raise ValueError(f"Unsupported combo panel: {token}. Allowed: {', '.join(allowed)}")
|
||
if key not in selected:
|
||
selected.append(key)
|
||
selected = selected or allowed
|
||
if len(selected) < 2:
|
||
raise ValueError("Combo panel test requires at least two data panels; single-panel views already exist in motif/structure/domain stages.")
|
||
return selected
|
||
|
||
|
||
def run_combo_plot(
|
||
workspace: Path,
|
||
prefix: str,
|
||
output_prefix: str,
|
||
tree_file: Path,
|
||
subfamily_file: Optional[Path],
|
||
meme_xml: Optional[Path],
|
||
gff3_file: Optional[Path],
|
||
domtblout_file: Optional[Path],
|
||
panels: List[str],
|
||
evalue_cutoff: float = 1e-5,
|
||
coverage_percent: Optional[float] = None,
|
||
rscript: Optional[Path] = None,
|
||
) -> Dict:
|
||
"""
|
||
运行 R 脚本绘制组合图
|
||
"""
|
||
if rscript is None:
|
||
rscript = SCRIPTS_DIR / "combo_tree_motif_structure_domain_panel_test.R"
|
||
|
||
if not rscript.exists():
|
||
return {
|
||
"success": False,
|
||
"error": f"R script not found: {rscript}"
|
||
}
|
||
|
||
# 输出文件
|
||
output_pdf = workspace / "final" / f"{output_prefix}.combo_panel_test.pdf"
|
||
|
||
# 构建命令
|
||
cmd = [
|
||
str(RSCRIPT_PATH), str(rscript),
|
||
str(tree_file),
|
||
str(subfamily_file) if subfamily_file else "NONE", # 传递 NONE 表示无亚家族
|
||
str(meme_xml) if meme_xml else "NONE",
|
||
str(gff3_file) if gff3_file else "NONE",
|
||
str(domtblout_file) if domtblout_file else "NONE",
|
||
str(evalue_cutoff), # E-value cutoff
|
||
]
|
||
|
||
# 添加 coverage(可选)
|
||
if coverage_percent is not None:
|
||
cmd.append(str(coverage_percent))
|
||
else:
|
||
cmd.append("0") # 不启用 coverage 过滤
|
||
|
||
# 添加输出文件和 panel 选择
|
||
cmd.append(str(output_pdf))
|
||
cmd.append(",".join(panels))
|
||
|
||
print(f"[CMD] {' '.join(cmd)}")
|
||
|
||
try:
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
timeout=300
|
||
)
|
||
|
||
success = output_pdf.exists()
|
||
|
||
return {
|
||
"success": success,
|
||
"output_pdf": str(output_pdf) if success else None,
|
||
"output_png": str(output_pdf.with_suffix(".png")) if success else None,
|
||
"returncode": result.returncode,
|
||
"stdout": result.stdout[-2000:] if result.stdout else "",
|
||
"stderr": result.stderr[-2000:] if result.stderr else "",
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
|
||
def promote_scoped_combo_outputs(workspace: Path, prefix: str, scope_id: str, result: Dict) -> Dict:
|
||
"""Materialize scoped combo outputs and make them the manifest artifacts.
|
||
|
||
The R plotting command writes the short user-facing filename first for
|
||
backwards compatibility. When a scope is active, the manifest should point
|
||
at the scoped copies so strict artifact validation follows the same identity
|
||
as the stage manifest.
|
||
"""
|
||
if not scope_id or not result.get("success"):
|
||
return result
|
||
scoped_prefix = f"{prefix}.{sanitize_token(scope_id)}"
|
||
final_dir = workspace / "final"
|
||
promoted = dict(result)
|
||
for key, suffix in (("output_pdf", ".combo_panel_test.pdf"), ("output_png", ".combo_panel_test.png")):
|
||
source_text = promoted.get(key)
|
||
if not source_text:
|
||
continue
|
||
source = Path(source_text)
|
||
if not source.exists():
|
||
continue
|
||
target = final_dir / f"{scoped_prefix}{suffix}"
|
||
if source.resolve() != target.resolve():
|
||
shutil.copy2(source, target)
|
||
promoted[f"short_{key}"] = str(source)
|
||
promoted[key] = str(target)
|
||
return promoted
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Combo Plot: Tree + Motif + Structure + Domain"
|
||
)
|
||
parser.add_argument("--prefix", required=True, help="Analysis prefix (e.g., Ft.SPL)")
|
||
parser.add_argument("--workspace", required=True, help="Workspace directory")
|
||
parser.add_argument("--evalue", type=float, default=1e-5, help="E-value cutoff")
|
||
parser.add_argument("--coverage", type=float, default=None, help="Coverage percent (0-100)")
|
||
parser.add_argument("--subfamily", type=str, default=None, help="Subfamily file path")
|
||
parser.add_argument("--scope-file", type=str, default=None, help="Gene-family scope JSON from resolve_scope.py")
|
||
parser.add_argument("--analysis-route", type=str, default="", help="Analysis route identity, e.g. single_species/subfamily_prediction")
|
||
parser.add_argument("--member-set-id", type=str, default="", help="Member set identity for scoped outputs")
|
||
parser.add_argument("--cpu", type=int, default=8, help="CPU cores (unused, for compatibility)")
|
||
parser.add_argument("--run-id", type=str, default=None, help="Run ID")
|
||
parser.add_argument("--replot", action="store_true", help="Replot even if exists")
|
||
parser.add_argument("--panels", type=str, default="motif,structure,domain", help="Comma-separated panels to show; choose at least two from motif,structure,domain")
|
||
|
||
args = parser.parse_args()
|
||
|
||
workspace = Path(args.workspace)
|
||
prefix = args.prefix
|
||
try:
|
||
panels = normalize_panels(args.panels)
|
||
except ValueError as exc:
|
||
print(f"[ERROR] {exc}")
|
||
sys.exit(1)
|
||
scope_data: Optional[Dict] = None
|
||
scope_file: Optional[Path] = None
|
||
if args.scope_file:
|
||
scope_file = Path(args.scope_file)
|
||
if not scope_file.exists():
|
||
print(f"[ERROR] Scope file not found: {args.scope_file}")
|
||
sys.exit(1)
|
||
scope_data = read_scope(scope_file)
|
||
|
||
print(f"[INFO] Combo panel test stage for {prefix}")
|
||
print(f"[INFO] Workspace: {workspace}")
|
||
print(f"[INFO] Panels: {', '.join(panels)}")
|
||
scope_id = str((scope_data or {}).get("scope_id") or "").strip()
|
||
output_prefix = user_output_prefix_for(
|
||
"combo",
|
||
prefix,
|
||
scope_data or {},
|
||
analysis_route=args.analysis_route,
|
||
member_set_id=args.member_set_id,
|
||
) if scope_id else prefix
|
||
if output_prefix != prefix:
|
||
print(f"[INFO] Output prefix: {output_prefix}")
|
||
|
||
# 1. 查找 tree 文件
|
||
phylo_manifest = find_latest_manifest(workspace, "phylogeny", prefix, scope_data)
|
||
if not phylo_manifest:
|
||
print("[ERROR] Phylogeny manifest not found")
|
||
sys.exit(1)
|
||
|
||
phylo_data = load_manifest(phylo_manifest)
|
||
tree_file = Path(phylo_data["artifacts"]["tree"]["full_path"])
|
||
print(f"[INFO] Tree file: {tree_file}")
|
||
|
||
# 2. 查找 subfamily 文件
|
||
if args.subfamily:
|
||
subfamily_file = Path(args.subfamily)
|
||
else:
|
||
# 自动查找只接受 confirmed_subfamily;prediction/geneID 必须由 --subfamily 显式传入。
|
||
confirmed_name = f"{prefix}.confirmed_subfamily.tsv"
|
||
scoped_confirmed_name = f"{output_prefix}.confirmed_subfamily.tsv"
|
||
search_paths = [
|
||
workspace / "final" / scoped_confirmed_name,
|
||
workspace / "final" / confirmed_name,
|
||
workspace / "input" / confirmed_name,
|
||
workspace.parent.parent / "input" / workspace.name / confirmed_name,
|
||
]
|
||
if scope_data and scope_data.get("scope_mode") in {"target_only", "target_plus_reference"}:
|
||
family = str(scope_data.get("family") or (prefix.split(".")[-1] if "." in prefix else "")).strip()
|
||
targets = [str(x) for x in scope_data.get("target_species") or [] if str(x).strip()]
|
||
refs = [str(x) for x in scope_data.get("reference_species") or [] if str(x).strip()]
|
||
if len(targets) == 1 and family:
|
||
cross_prefix = f"{targets[0]}.{family}.target_plus_reference"
|
||
scoped_prediction = None
|
||
if refs:
|
||
scoped_prediction = f"Target.{family}.target-{targets[0]}.ref-{'-'.join(refs)}.subfamily_prediction"
|
||
extra_paths = [
|
||
workspace / "final" / f"{cross_prefix}.confirmed_subfamily.tsv",
|
||
]
|
||
if scoped_prediction:
|
||
extra_paths.extend([
|
||
workspace / "final" / f"{scoped_prediction}.confirmed_subfamily.tsv",
|
||
])
|
||
search_paths[0:0] = extra_paths
|
||
subfamily_file = None
|
||
for p in search_paths:
|
||
if p.exists():
|
||
subfamily_file = p
|
||
break
|
||
# Do not materialize aggregate prediction labels during automatic lookup.
|
||
# Unconfirmed prediction tables require an explicit --subfamily path.
|
||
|
||
if not subfamily_file or not subfamily_file.exists():
|
||
print("[INFO] Subfamily file not found, will run without subfamily coloring")
|
||
subfamily_file = None
|
||
else:
|
||
print(f"[INFO] Subfamily file: {subfamily_file}")
|
||
|
||
# 3. 查找 panel 需要的输入文件
|
||
meme_xml: Optional[Path] = None
|
||
if "motif" in panels:
|
||
motif_manifest = find_latest_manifest(workspace, "motif", prefix, scope_data)
|
||
if not motif_manifest:
|
||
print("[ERROR] Motif manifest not found")
|
||
sys.exit(1)
|
||
motif_data = load_manifest(motif_manifest)
|
||
meme_xml = resolve_meme_xml_from_manifest(workspace, motif_data, prefix)
|
||
if not meme_xml or not meme_xml.exists():
|
||
print("[ERROR] MEME XML not found")
|
||
sys.exit(1)
|
||
print(f"[INFO] MEME XML: {meme_xml}")
|
||
|
||
gff3_file: Optional[Path] = None
|
||
if "structure" in panels:
|
||
struct_manifest = find_latest_manifest(workspace, "structure", prefix, scope_data)
|
||
if not struct_manifest:
|
||
print("[ERROR] Structure manifest not found")
|
||
sys.exit(1)
|
||
struct_data = load_manifest(struct_manifest)
|
||
gff3_file = Path(struct_data["artifacts"]["structure_gff3_filtered"]["full_path"])
|
||
print(f"[INFO] GFF3 file: {gff3_file}")
|
||
|
||
domtblout_file: Optional[Path] = None
|
||
if "domain" in panels:
|
||
domain_manifest = find_latest_manifest(workspace, "domain", prefix, scope_data)
|
||
if not domain_manifest:
|
||
print("[ERROR] Domain manifest not found")
|
||
sys.exit(1)
|
||
domain_data = load_manifest(domain_manifest)
|
||
domtblout_file = Path(domain_data["artifacts"]["domtblout"]["full_path"])
|
||
print(f"[INFO] domtblout file: {domtblout_file}")
|
||
|
||
# 6. 运行组合图
|
||
result = run_combo_plot(
|
||
workspace=workspace,
|
||
prefix=prefix,
|
||
output_prefix=output_prefix,
|
||
tree_file=tree_file,
|
||
subfamily_file=subfamily_file,
|
||
meme_xml=meme_xml,
|
||
gff3_file=gff3_file,
|
||
domtblout_file=domtblout_file,
|
||
panels=panels,
|
||
evalue_cutoff=args.evalue,
|
||
coverage_percent=args.coverage,
|
||
)
|
||
result = promote_scoped_combo_outputs(workspace, prefix, scope_id, result)
|
||
|
||
if result.get("success"):
|
||
print(f"[OK] Combo plot generated: {result['output_pdf']}")
|
||
|
||
# 检查 PNG 是否存在
|
||
png_path = result.get("output_png")
|
||
png_exists = png_path and Path(png_path).exists() if png_path else False
|
||
|
||
# 生成 manifest
|
||
artifacts = {
|
||
"combo_plot_pdf": {
|
||
"artifact": "combo_plot",
|
||
"path": str(Path(result["output_pdf"]).relative_to(workspace)),
|
||
"full_path": result["output_pdf"],
|
||
"type": "image",
|
||
"format": "pdf",
|
||
"description": f"Tree + {' + '.join(p.title() for p in panels)} combo panel test visualization"
|
||
}
|
||
}
|
||
|
||
if png_exists:
|
||
artifacts["combo_plot_png"] = {
|
||
"artifact": "combo_plot",
|
||
"path": str(Path(png_path).relative_to(workspace)),
|
||
"full_path": png_path,
|
||
"type": "image",
|
||
"format": "png",
|
||
"description": f"Tree + {' + '.join(p.title() for p in panels)} combo panel test visualization (PNG)"
|
||
}
|
||
|
||
stage_context = build_stage_context(
|
||
stage="combo",
|
||
prefix=prefix,
|
||
scope_data=scope_data or {},
|
||
scope_file=str(scope_file or ""),
|
||
output_prefix=output_prefix,
|
||
subfamily_file=str(subfamily_file or ""),
|
||
analysis_route=args.analysis_route or "",
|
||
member_set_id=args.member_set_id or "",
|
||
).to_dict()
|
||
manifest = {
|
||
"manifest_version": "1.0",
|
||
"job_id": workspace.name,
|
||
"stage": "combo",
|
||
"run_id": args.run_id or "unknown",
|
||
"prefix": prefix,
|
||
"output_prefix": output_prefix,
|
||
"status": "success",
|
||
"analysis_route": stage_context.get("analysis_route", ""),
|
||
"result_group": stage_context.get("result_group", ""),
|
||
"member_set_id": stage_context.get("member_set_id", ""),
|
||
"stage_context": stage_context,
|
||
"artifacts": artifacts,
|
||
"parameters": {
|
||
"evalue": args.evalue,
|
||
"coverage": args.coverage,
|
||
"output_prefix": output_prefix,
|
||
"subfamily_file": str(subfamily_file),
|
||
"tree_file": str(tree_file),
|
||
"meme_xml": str(meme_xml or ""),
|
||
"gff3_file": str(gff3_file or ""),
|
||
"domtblout_file": str(domtblout_file or ""),
|
||
"panels": panels,
|
||
"analysis_route": stage_context.get("analysis_route", ""),
|
||
"member_set_id": stage_context.get("member_set_id", ""),
|
||
}
|
||
}
|
||
if scope_data:
|
||
manifest["scope_id"] = scope_data.get("scope_id", "")
|
||
manifest["scope_mode"] = scope_data.get("scope_mode", "")
|
||
manifest["scope_file"] = str(scope_file)
|
||
manifest["target_species"] = scope_data.get("target_species", [])
|
||
manifest["reference_species"] = scope_data.get("reference_species", [])
|
||
manifest["comparative_species"] = scope_data.get("comparative_species", [])
|
||
manifest["selected_species"] = scope_data.get("selected_species", [])
|
||
manifest["scope"] = {
|
||
"family": scope_data.get("family", ""),
|
||
"prefix": scope_data.get("prefix", ""),
|
||
"scope_id": scope_data.get("scope_id", ""),
|
||
"scope_mode": scope_data.get("scope_mode", ""),
|
||
"target_species": scope_data.get("target_species", []),
|
||
"reference_species": scope_data.get("reference_species", []),
|
||
"comparative_species": scope_data.get("comparative_species", []),
|
||
"selected_species": scope_data.get("selected_species", []),
|
||
}
|
||
|
||
manifest_path = workspace / "final" / f"{output_prefix}.combo_panel_test.manifest.json"
|
||
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
|
||
print(f"[INFO] Manifest: {manifest_path}")
|
||
if scope_data and scope_data.get("scope_id"):
|
||
scoped_manifest = write_legacy_manifest_copy(manifest_path, prefix, str(scope_data.get("scope_id") or ""), "combo_panel_test")
|
||
if scoped_manifest:
|
||
print(f"[INFO] Legacy scoped manifest: {scoped_manifest}")
|
||
|
||
else:
|
||
print(f"[ERROR] Combo plot failed: {result.get('error', result.get('stderr', 'unknown'))}")
|
||
if result.get("stdout"):
|
||
print(result["stdout"])
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|