Files
PlantAI-dev/apps/cli/run_pep_legacy_20260603.py
2026-08-03 18:42:47 +08:00

1041 lines
40 KiB
Python
Executable File
Raw Permalink 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
"""
PEP阶段:蛋白特征分析(完整版)
输入:
- Identify阶段的蛋白序列文件 (final/{prefix}.final_members.pep.fa)
输出:
- final/{prefix}.pep_properties.pdf/png (理化性质图)
- final/{prefix}.pep_tmhmm.pdf/png (跨膜结构图)
- final/{prefix}.pep_subcellular.pdf/png (亚细胞定位图)
- final/{prefix}.pep_report.md (分析报告)
- final/{prefix}.pep.manifest.json (机器清单)
"""
import os
import argparse
import json
import hashlib
import subprocess
import sys
import time
import shutil
import re
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional
PROJECT_ROOT = Path(__file__).resolve().parents[2]
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
CONDA_BIN = "/home/xzq/miniconda3/envs/test4py311/bin"
if CONDA_BIN not in os.environ.get("PATH", ""):
os.environ["PATH"] = CONDA_BIN + ":" + os.environ.get("PATH", "")
# Plant-mPLoc code is retained, but the default PEP subcellular localization
# method is WoLF PSORT because it preserves per-protein predictions reliably.
PLANT_MPLOC_ENABLED = False
def load_manifest(manifest_file: Path) -> Optional[Dict[str, Any]]:
try:
return json.loads(manifest_file.read_text(encoding="utf-8"))
except Exception:
return None
def count_fasta_records(fasta_file: Path) -> int:
count = 0
with open(fasta_file, encoding="utf-8", errors="ignore") as handle:
for line in handle:
if line.startswith(">"):
count += 1
return count
def ensure_protein_fasta_without_stop_markers(protein_fasta: Path) -> Path:
has_stop = False
with open(protein_fasta, encoding="utf-8", errors="ignore") as handle:
for line in handle:
if not line.startswith(">") and "*" in line:
has_stop = True
break
if not has_stop:
return protein_fasta
clean_name = re.sub(r"(\.fa(?:sta)?)$", r".clean\1", protein_fasta.name, flags=re.IGNORECASE)
if clean_name == protein_fasta.name:
clean_name = protein_fasta.name + ".clean.fa"
clean_fasta = protein_fasta.with_name(clean_name)
if clean_fasta.exists() and clean_fasta.stat().st_mtime >= protein_fasta.stat().st_mtime:
return clean_fasta
with open(protein_fasta, "r", encoding="utf-8", errors="ignore") as src, open(clean_fasta, "w", encoding="utf-8") as dst:
for line in src:
dst.write(line if line.startswith(">") else line.replace("*", ""))
print(f"[INFO] Created stop-marker-clean FASTA for PEP: {clean_fasta}")
return clean_fasta
def resolve_identify_protein_fasta_for_scope(workspace: Path, prefix: str, scope_data: Dict[str, Any]) -> Optional[Path]:
scope_mode = str((scope_data or {}).get("scope_mode") or "").strip()
if scope_mode not in {"", "target_only"}:
return None
final_dir = workspace / "final"
family = str((scope_data or {}).get("family") or (prefix.split(".")[-1] if "." in prefix else prefix)).strip()
targets = [str(x).strip() for x in (scope_data or {}).get("target_species") or [] if str(x).strip()]
candidates = [final_dir / f"{prefix}.final_members.pep.fa"]
if len(targets) == 1:
species = targets[0]
candidates = [
workspace / "identify" / species / "final" / f"{species}.final_gfmembers.pep.fa",
final_dir / f"Target.{species}.{family}.final_members.pep.fa",
final_dir / f"{species}.{family}.final_members.pep.fa",
*candidates,
]
for candidate in candidates:
if candidate.exists() and candidate.stat().st_size > 0:
return ensure_protein_fasta_without_stop_markers(candidate)
return None
def resolve_scope_protein_fasta(workspace: Path, prefix: str, scope_data: Dict[str, Any], cli_fasta: Optional[str] = None) -> Path:
if cli_fasta:
fasta_path = Path(cli_fasta)
if fasta_path.exists() and fasta_path.stat().st_size > 0:
return ensure_protein_fasta_without_stop_markers(fasta_path)
raise FileNotFoundError(f"Specified protein FASTA not found: {cli_fasta}")
final_dir = workspace / "final"
scope_id = str(scope_data.get("scope_id") or "")
manifest_candidates = []
if scope_id:
manifest_candidates.append(final_dir / f"{prefix}.{sanitize_token(scope_id)}.phylogeny.manifest.json")
manifest_candidates.append(final_dir / f"{prefix}.phylogeny.manifest.json")
for manifest_file in manifest_candidates:
if not manifest_file.exists():
continue
manifest_data = load_manifest(manifest_file)
if not manifest_data:
continue
fasta_text = manifest_data.get("parameters", {}).get("scope_input_fasta")
if fasta_text:
fasta_path = Path(fasta_text)
if fasta_path.exists() and fasta_path.stat().st_size > 0:
print(f"[INFO] Found scoped protein FASTA from phylogeny manifest: {fasta_path}")
return ensure_protein_fasta_without_stop_markers(fasta_path)
fallback = resolve_identify_protein_fasta_for_scope(workspace, prefix, scope_data)
if fallback:
print(f"[INFO] Using target-only Identify protein FASTA: {fallback}")
return fallback
raise FileNotFoundError(
"Cannot find scoped protein FASTA. Run phylogeny with the same scope first, "
"or provide --fasta explicitly."
)
def scope_parameters(scope_file: Optional[Path], scope_data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
if not scope_file or not scope_data:
return {}
return {
"scope_file": str(scope_file),
"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", []),
"family": scope_data.get("family", ""),
}
def compute_file_hash(filepath: Path) -> str:
"""计算文件的SHA256哈希值"""
sha256_hash = hashlib.sha256()
try:
with open(filepath, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
except Exception:
return ""
def get_tool_versions() -> Dict[str, str]:
"""获取工具版本"""
versions = {}
try:
result = subprocess.run(["R", "--version"], capture_output=True, text=True, check=False, timeout=5)
versions["R"] = result.stdout.split('\n')[0] if result.stdout else "available"
except Exception:
versions["R"] = "not_found"
try:
biolib_bin = "/home/xzq/miniconda3/envs/test4py311/bin/biolib"
result = subprocess.run([biolib_bin, "--version"], capture_output=True, text=True, check=False, timeout=5)
versions["biolib"] = "available" if result.returncode == 0 else "not_found"
except Exception:
versions["biolib"] = "not_found"
return versions
def prepare_directories(workspace: Path) -> Dict[str, Path]:
"""创建目录"""
paths = {
"analysis_dir": workspace / "pep" / "analysis",
"final_dir": workspace / "final",
"logs_dir": workspace / "logs",
}
for name, path in paths.items():
path.mkdir(parents=True, exist_ok=True)
return paths
def parse_fasta(fasta_file: Path) -> Dict[str, str]:
"""解析FASTA文件"""
sequences = {}
current_id = None
current_seq = []
with open(fasta_file, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('>'):
if current_id:
sequences[current_id] = ''.join(current_seq)
current_id = line[1:].split()[0]
current_seq = []
else:
current_seq.append(line)
if current_id:
sequences[current_id] = ''.join(current_seq)
return sequences
def external_tool_timeout(fasta_file: Path, seconds_per_sequence: int = 90, minimum: int = 300, maximum: int = 7200) -> int:
"""Estimate a conservative timeout for slow web/cloud predictors."""
try:
count = sum(1 for line in open(fasta_file, 'r') if line.startswith('>'))
except Exception:
count = 1
return max(minimum, min(maximum, count * seconds_per_sequence))
def write_clean_fasta(fasta_file: Path, output_file: Path) -> Path:
"""Write DeepTMHMM-friendly FASTA with short IDs and canonical amino acids."""
allowed = set("ACDEFGHIKLMNPQRSTVWY")
sequences = parse_fasta(fasta_file)
with open(output_file, "w") as f:
for seq_id, seq in sequences.items():
clean = "".join(aa for aa in seq.upper() if aa in allowed)
f.write(f">{seq_id}\n{clean}\n")
return output_file
def run_deeptmhmm_dtu_web(fasta_file: Path, output_dir: Path, timeout_sec: int) -> Dict[str, Any]:
"""Run DeepTMHMM through the DTU web form when BioLib file staging fails."""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
geckodriver = shutil.which("geckodriver")
if not geckodriver:
return {"success": False, "stderr": "geckodriver not found"}
clean_fasta = write_clean_fasta(fasta_file, output_dir / f"{fasta_file.stem}.deeptmhmm.clean.fa")
html_file = output_dir / "deeptmhmm_dtu.html"
topologies = output_dir / "predicted_topologies.3line"
gff_file = output_dir / "deeptmhmm_results.gff3"
options = Options()
options.add_argument("-headless")
driver = webdriver.Firefox(service=Service(geckodriver), options=options)
try:
driver.get("https://services.healthtech.dtu.dk/services/DeepTMHMM-1.0/")
WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.NAME, "uploadfile")))
driver.find_element(By.NAME, "uploadfile").send_keys(str(clean_fasta.resolve()))
driver.find_element(By.CSS_SELECTOR, 'input[type="submit"]').click()
start = time.time()
while time.time() - start < timeout_sec:
body = driver.find_element(By.TAG_NAME, "body").text
if "Predicted Topologies" in body or "Predicted topologies" in body:
html_file.write_text(driver.page_source, encoding="utf-8")
lines = body.splitlines()
start_idx = None
for i, line in enumerate(lines):
if line.strip() == "Predicted Topologies":
start_idx = i + 1
break
if start_idx is None:
return {"success": False, "stderr": "Predicted Topologies section not found"}
topo_lines = []
gff_lines = []
in_gff = False
for line in lines[start_idx:]:
if line.startswith("##gff-version"):
in_gff = True
if line.startswith("DTU Health Tech") or line.startswith("ADDRESS & INFO"):
break
if in_gff:
if line.strip():
gff_lines.append(line)
else:
if line.strip():
topo_lines.append(line)
topologies.write_text("\n".join(topo_lines).strip() + "\n", encoding="utf-8")
gff_file.write_text("\n".join(gff_lines).strip() + "\n", encoding="utf-8")
return {
"success": topologies.exists() and topologies.stat().st_size > 0,
"results_md": html_file,
"topologies": topologies,
"gff": gff_file,
"method": "DTU DeepTMHMM web",
}
if "failed" in body.lower() or "error" in body.lower():
html_file.write_text(driver.page_source, encoding="utf-8")
return {"success": False, "stderr": body[:500]}
time.sleep(20)
html_file.write_text(driver.page_source, encoding="utf-8")
return {"success": False, "stderr": "DTU DeepTMHMM web job timed out"}
finally:
driver.quit()
def calculate_physicochemical_properties(sequences: Dict[str, str]) -> List[Dict]:
"""计算理化性质"""
print(f" 计算理化性质...")
# 氨基酸分子量
aa_weights = {
'A': 89.09, 'R': 174.20, 'N': 132.12, 'D': 133.10, 'C': 121.16,
'E': 147.13, 'Q': 146.15, 'G': 75.07, 'H': 155.16, 'I': 131.17,
'L': 131.17, 'K': 146.19, 'M': 149.21, 'F': 165.19, 'P': 115.13,
'S': 105.09, 'T': 119.12, 'W': 204.23, 'Y': 181.19, 'V': 117.15
}
# 氨基酸电荷
aa_charge = {
'D': -1, 'E': -1, 'K': 1, 'R': 1, 'H': 0.5
}
properties = []
for seq_id, sequence in sequences.items():
seq = sequence.upper()
length = len(seq)
# 分子量
mw = sum(aa_weights.get(aa, 110) for aa in seq) - (length - 1) * 18.02
# 等电点估算
acidic = seq.count('D') + seq.count('E')
basic = seq.count('K') + seq.count('R') + seq.count('H')
if basic > acidic:
pI = 7.0 + (basic - acidic) * 0.5
elif acidic > basic:
pI = 7.0 - (acidic - basic) * 0.3
else:
pI = 7.0
# 疏水性 (简化的GRAVY)
hydrophobic_aa = 'AILMFWV'
hydrophilic_aa = 'RNDQEK'
hydrophobic_count = sum(seq.count(aa) for aa in hydrophobic_aa)
hydrophilic_count = sum(seq.count(aa) for aa in hydrophilic_aa)
gravy = (hydrophobic_count - hydrophilic_count) / length if length > 0 else 0
# 脂肪族指数
aliphatic = (seq.count('A') + seq.count('V') + seq.count('I') + seq.count('L')) / length * 100
properties.append({
"id": seq_id,
"length": length,
"molecular_weight": mw,
"isoelectric_point": pI,
"gravy": gravy,
"aliphatic_index": aliphatic
})
return properties
def run_deeptmhmm(fasta_file: Path, output_dir: Path) -> Dict[str, Any]:
"""运行DeepTMHMM跨膜预测"""
print(f" 运行DeepTMHMM...")
results_md = output_dir / "deeptmhmm_results.md"
topologies = output_dir / "predicted_topologies.3line"
for old_file in (results_md, topologies):
if old_file.exists():
old_file.unlink()
timeout_sec = external_tool_timeout(fasta_file, seconds_per_sequence=120, minimum=900, maximum=10800)
web_result = run_deeptmhmm_dtu_web(fasta_file, output_dir, timeout_sec)
if web_result.get("success"):
return web_result
print(f" DTU网页DeepTMHMM失败,尝试BioLib: {web_result.get('stderr', '')[:200]}")
try:
biolib_bin = "/home/xzq/miniconda3/envs/test4py311/bin/biolib"
cmd = [biolib_bin, "run", "DTU/DeepTMHMM", "--fasta", str(fasta_file)]
result = subprocess.run(cmd, capture_output=True, text=True,
cwd=str(output_dir), timeout=timeout_sec)
if result.returncode != 0 or not topologies.exists():
stderr = result.stderr[:300] if result.stderr else "DeepTMHMM did not generate topology output"
print(f" DeepTMHMM未生成有效结果: {stderr}")
print(" 尝试通过DTU官方网页运行DeepTMHMM...")
return run_deeptmhmm_dtu_web(fasta_file, output_dir, timeout_sec)
return {
"success": True,
"results_md": results_md,
"topologies": topologies
}
except Exception as e:
print(f" DeepTMHMM错误: {e}")
return {"success": False, "stderr": str(e)}
def run_plant_mploc(fasta_file: Path, output_file: Path) -> Dict[str, Any]:
"""运行Plant-mPLoc"""
print(f" 运行Plant-mPLoc...")
try:
script = Path("scripts/get_Plant_mPLoc_out.py")
if script.exists():
cmd = [sys.executable, str(script), "--input", str(fasta_file), "--output", str(output_file)]
if output_file.exists():
output_file.unlink()
started_at = time.time()
timeout_sec = external_tool_timeout(fasta_file, seconds_per_sequence=90)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
if result.returncode != 0:
return {"success": False, "stderr": result.stderr or result.stdout}
else:
return {"success": False, "stderr": f"Plant-mPLoc script not found: {script}"}
if not output_file.exists() or output_file.stat().st_size == 0 or output_file.stat().st_mtime < started_at:
return {"success": False, "stderr": "Plant-mPLoc did not generate output"}
return {"success": True, "output_file": output_file, "method": "Plant-mPLoc website automation"}
except Exception as e:
print(f" Plant-mPLoc错误: {e}")
return {"success": False, "stderr": str(e)}
def run_wolfpsort(fasta_file: Path, output_file: Path) -> Dict[str, Any]:
"""运行WoLF PSORT"""
print(f" 运行WoLF PSORT...")
try:
script = Path("scripts/wolfpsort_batch.py")
if script.exists():
cmd = [sys.executable, str(script), "--input", str(fasta_file), "--output", str(output_file)]
if output_file.exists():
output_file.unlink()
started_at = time.time()
timeout_sec = external_tool_timeout(fasta_file, seconds_per_sequence=90)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
if result.returncode != 0:
return {"success": False, "stderr": result.stderr or result.stdout}
else:
return {"success": False, "stderr": f"WoLF PSORT script not found: {script}"}
if not output_file.exists() or output_file.stat().st_size == 0 or output_file.stat().st_mtime < started_at:
return {"success": False, "stderr": "WoLF PSORT did not generate output"}
return {"success": True, "output_file": output_file, "method": "WoLF PSORT website automation"}
except Exception as e:
print(f" WoLF PSORT错误: {e}")
return {"success": False, "stderr": str(e)}
def plot_properties_r(properties: List[Dict], output_pdf: Path, output_png: Path) -> bool:
"""使用R绘制理化性质图"""
print(f" 绘制理化性质图...")
# 准备数据文件
data_file = output_pdf.parent / "properties_data.tsv"
with open(data_file, 'w') as f:
f.write("id\tlength\tmw\tpI\tgravy\taliphatic\n")
for p in properties:
f.write(f"{p['id']}\t{p['length']}\t{p['molecular_weight']:.2f}\t"
f"{p['isoelectric_point']:.2f}\t{p['gravy']:.3f}\t{p['aliphatic_index']:.2f}\n")
r_script = f'''
library(ggplot2)
library(patchwork)
data <- read.delim("{data_file}", sep="\t", header=TRUE)
# 分子量分布
p1 <- ggplot(data, aes(x=mw/1000)) +
geom_histogram(bins=15, fill="steelblue", color="white") +
labs(title="Molecular Weight Distribution",
x="Molecular Weight (kDa)", y="Count") +
theme_minimal()
# 等电点分布
p2 <- ggplot(data, aes(x=pI)) +
geom_histogram(bins=15, fill="coral", color="white") +
labs(title="Isoelectric Point Distribution", x="pI", y="Count") +
theme_minimal()
# 序列长度分布
p3 <- ggplot(data, aes(x=length)) +
geom_histogram(bins=15, fill="seagreen", color="white") +
labs(title="Sequence Length Distribution", x="Length (aa)", y="Count") +
theme_minimal()
# 疏水性分布
p4 <- ggplot(data, aes(x=gravy)) +
geom_histogram(bins=15, fill="purple", color="white") +
labs(title="GRAVY (Hydrophobicity)", x="GRAVY Score", y="Count") +
theme_minimal()
combined <- (p1 + p2) / (p3 + p4)
ggsave("{output_pdf}", combined, width=12, height=10)
png("{output_png}", width=1200, height=1000, res=150)
print(combined)
dev.off()
'''
r_file = output_pdf.parent / "plot_properties.R"
with open(r_file, 'w') as f:
f.write(r_script)
try:
rscript = "/home/xzq/miniconda3/envs/test4py311/bin/Rscript"
result = subprocess.run([rscript, str(r_file)],
capture_output=True, text=True, timeout=120)
if result.returncode != 0:
print(f" R绘图错误: {result.stderr[:300]}")
return output_pdf.exists() and output_png.exists()
except Exception as e:
print(f" R绘图错误: {e}")
return False
def plot_tmhmm_results(topologies_file: Path, output_pdf: Path, output_png: Path) -> bool:
"""绘制跨膜结构图"""
print(f" 绘制跨膜结构图...")
# 解析拓扑结构
transmembrane_count = {}
if topologies_file.exists():
with open(topologies_file, 'r') as f:
current_id = None
for line in f:
line = line.strip()
if line.startswith('>'):
current_id = line[1:]
elif current_id and line and not line.startswith('>'):
# 统计M(跨膜)的数量
tm_count = line.count('M')
transmembrane_count[current_id] = tm_count
current_id = None
# 准备数据
data_file = output_pdf.parent / "tmhmm_data.tsv"
with open(data_file, 'w') as f:
f.write("id\ttm_count\n")
for seq_id, count in transmembrane_count.items():
f.write(f"{seq_id}\t{count}\n")
r_script = f'''
library(ggplot2)
data <- read.delim("{data_file}", sep="\t", header=TRUE)
# 跨膜螺旋数量分布
p <- ggplot(data, aes(x=tm_count)) +
geom_bar(fill="darkblue", color="white") +
labs(title="Transmembrane Helices Distribution",
x="Number of TM Helices", y="Count") +
theme_minimal()
ggsave("{output_pdf}", p, width=8, height=6)
png("{output_png}", width=800, height=600, res=150)
print(p)
dev.off()
'''
r_file = output_pdf.parent / "plot_tmhmm.R"
with open(r_file, 'w') as f:
f.write(r_script)
try:
rscript = "/home/xzq/miniconda3/envs/test4py311/bin/Rscript"
result = subprocess.run([rscript, str(r_file)],
capture_output=True, text=True, timeout=120)
if result.returncode != 0:
print(f" R绘图错误: {result.stderr[:300]}")
return output_pdf.exists() and output_png.exists()
except Exception as e:
print(f" R绘图错误: {e}")
return False
LOCATION_LABELS = {
"nucl": "Nucleus",
"nucleus": "Nucleus",
"cyto": "Cytoplasm",
"cytoplasm": "Cytoplasm",
"chlo": "Chloroplast",
"chloroplast": "Chloroplast",
"mito": "Mitochondrion",
"mitochondrion": "Mitochondrion",
"plas": "Plasma membrane",
"plasma membrane": "Plasma membrane",
"vacu": "Vacuole",
"vacuole": "Vacuole",
"golg": "Golgi apparatus",
"golgi": "Golgi apparatus",
"extr": "Extracellular",
"extracellular": "Extracellular",
"pero": "Peroxisome",
"peroxisome": "Peroxisome",
"e.r.": "Endoplasmic reticulum",
"er": "Endoplasmic reticulum",
"cysk": "Cytoskeleton",
"cytoskeleton": "Cytoskeleton",
}
def normalize_location_label(label: str) -> str:
clean = re.sub(r"\s+", " ", str(label or "").strip())
if not clean:
return ""
key = clean.lower().replace("_", " ")
if key in LOCATION_LABELS:
return LOCATION_LABELS[key]
if " " not in clean and "_" in clean:
parts = [normalize_location_label(part) for part in clean.split("_")]
parts = [part for part in parts if part]
if parts:
return " / ".join(parts)
return clean[:1].upper() + clean[1:]
def parse_plant_mploc_counts(plant_mploc_file: Path) -> tuple[Dict[str, int], int]:
counts: Dict[str, int] = {}
rows = 0
if not plant_mploc_file.exists() or plant_mploc_file.stat().st_size == 0:
return counts, rows
with open(plant_mploc_file, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split("\t")
if len(parts) < 2:
continue
first = parts[0].strip().lower()
second = parts[1].strip()
if first in {"protein_id", "protein", "id", "gene_id", "sequence_id"}:
continue
loc = normalize_location_label(second)
if not loc:
continue
counts[loc] = counts.get(loc, 0) + 1
rows += 1
return counts, rows
def parse_wolfpsort_counts(wolfpsort_file: Path) -> tuple[Dict[str, int], int]:
counts: Dict[str, int] = {}
rows = 0
if not wolfpsort_file.exists() or wolfpsort_file.stat().st_size == 0:
return counts, rows
with open(wolfpsort_file, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split("\t")
if len(parts) < 2 or parts[0].strip().lower() in {"protein_id", "protein", "id"}:
continue
prediction = parts[1].strip()
if not prediction:
continue
primary = prediction.split(",", 1)[0]
primary = primary.split(":", 1)[0].strip()
loc = normalize_location_label(primary)
if not loc:
continue
counts[loc] = counts.get(loc, 0) + 1
rows += 1
return counts, rows
def resolve_subcellular_counts(plant_mploc_file: Path, wolfpsort_file: Path,
allow_plant_fallback: bool = False) -> tuple[Dict[str, int], str, int]:
wolf_counts, wolf_rows = parse_wolfpsort_counts(wolfpsort_file)
if wolf_rows:
return wolf_counts, "WoLF PSORT", wolf_rows
if allow_plant_fallback:
plant_counts, plant_rows = parse_plant_mploc_counts(plant_mploc_file)
if plant_rows:
return plant_counts, "Plant-mPLoc", plant_rows
return {}, "WoLF PSORT", 0
def plot_subcellular_results(plant_mploc_file: Path, wolfpsort_file: Path,
output_pdf: Path, output_png: Path) -> bool:
"""绘制亚细胞定位图"""
print(f" 绘制亚细胞定位图...")
location_counts, source, parsed_rows = resolve_subcellular_counts(plant_mploc_file, wolfpsort_file)
if not location_counts:
print(" 亚细胞定位结果为空,跳过绘图")
return False
# 准备数据
data_file = output_pdf.parent / "subcellular_data.tsv"
with open(data_file, 'w') as f:
f.write("location\tcount\tsource\n")
for loc, count in sorted(location_counts.items(), key=lambda item: item[1], reverse=True):
f.write(f"{loc}\t{count}\t{source}\n")
r_script = f'''
library(ggplot2)
data <- read.delim("{data_file}", sep="\t", header=TRUE)
# 按count排序
data$location <- factor(data$location, levels=data$location[order(data$count, decreasing=TRUE)])
plot_source <- unique(data$source)[1]
p <- ggplot(data, aes(x=location, y=count, fill=location)) +
geom_bar(stat="identity", width=0.55, color="white") +
geom_text(aes(label=count), vjust=-0.3, size=4) +
labs(title=paste0("Subcellular Localization (", plot_source, ")"),
x="Location", y="Count") +
theme_minimal() +
theme(axis.text.x=element_text(angle=45, hjust=1), legend.position="none")
ggsave("{output_pdf}", p, width=8, height=6)
png("{output_png}", width=800, height=600, res=150)
print(p)
dev.off()
'''
r_file = output_pdf.parent / "plot_subcellular.R"
with open(r_file, 'w') as f:
f.write(r_script)
try:
rscript = "/home/xzq/miniconda3/envs/test4py311/bin/Rscript"
result = subprocess.run([rscript, str(r_file)],
capture_output=True, text=True, timeout=120)
if result.returncode != 0:
print(f" R绘图错误: {result.stderr[:300]}")
return output_pdf.exists() and output_png.exists()
except Exception as e:
print(f" R绘图错误: {e}")
return False
def write_report(workspace: Path, prefix: str, metrics: Dict, parameters: Dict,
tool_versions: Dict) -> Path:
"""写入报告"""
report_path = workspace / "final" / f"{prefix}.pep_report.md"
with open(report_path, "w") as f:
f.write(f"# 蛋白特征分析报告 - {prefix}\n\n")
f.write(f"**生成时间**: {datetime.now().isoformat()}\n\n")
f.write("## 1. 分析概述\n")
f.write("- **分析阶段**: PEP (Protein Features)\n")
f.write("- **分析内容**: \n")
f.write(" - 理化性质 (分子量、等电点、疏水性)\n")
f.write(" - 跨膜结构预测 (DeepTMHMM)\n")
f.write(" - 亚细胞定位 (WoLF PSORT;Plant-mPLoc代码保留但默认关闭)\n\n")
if "protein_count" in metrics:
f.write(f"- **蛋白数量**: {metrics['protein_count']}\n\n")
subcellular_data = workspace / "final" / "subcellular_data.tsv"
subcellular_rows = []
if subcellular_data.exists() and subcellular_data.stat().st_size > 0:
with open(subcellular_data, "r", encoding="utf-8", errors="ignore") as sf:
header = sf.readline().strip().split("\t")
for line in sf:
parts = line.rstrip("\n").split("\t")
if len(parts) < 2:
continue
row = dict(zip(header, parts))
subcellular_rows.append(row)
if subcellular_rows:
f.write("## 2. 亚细胞定位数量\n")
f.write("| Location | Count | Source |\n")
f.write("|---|---:|---|\n")
for row in subcellular_rows:
f.write(
f"| {row.get('location', '')} | {row.get('count', '')} | "
f"{row.get('source', '')} |\n"
)
f.write("\n")
f.write("## 3. 工具版本\n")
for tool, version in tool_versions.items():
f.write(f"- **{tool}**: {version}\n")
f.write("\n")
f.write("## 4. 输出文件\n")
f.write(f"- `{prefix}.pep_properties.pdf/png` - 理化性质分布图\n")
f.write(f"- `{prefix}.pep_tmhmm.pdf/png` - 跨膜结构预测图\n")
f.write(f"- `{prefix}.pep_subcellular.pdf/png` - 亚细胞定位图\n")
f.write("\n")
return report_path
def write_manifest(workspace: Path, job_id: str, run_id: str, prefix: str,
start_time: datetime, end_time: datetime, artifacts: Dict,
metrics: Dict, parameters: Dict, tool_versions: Dict) -> Path:
"""写入manifest"""
elapsed_sec = (end_time - start_time).total_seconds()
manifest_data = {
"manifest_version": "1.0",
"job_id": job_id,
"stage": "pep",
"run_id": run_id,
"prefix": prefix,
"timestamp": end_time.isoformat(),
"status": "success",
"elapsed_sec": round(elapsed_sec, 2),
"tool_versions": tool_versions,
"artifacts": artifacts,
"parameters": parameters,
"metrics": metrics
}
final_dir = workspace / "final"
final_dir.mkdir(parents=True, exist_ok=True)
for key in (
"scope_id",
"scope_mode",
"scope_file",
"target_species",
"reference_species",
"comparative_species",
"selected_species",
"family",
):
if key in parameters:
manifest_data[key] = parameters[key]
manifest_path = final_dir / f"{prefix}.pep.manifest.json"
with open(manifest_path, "w", encoding='utf-8') as f:
json.dump(manifest_data, f, indent=2, ensure_ascii=False)
if parameters.get("scope_id"):
scoped_manifest = final_dir / f"{prefix}.{sanitize_token(str(parameters['scope_id']))}.pep.manifest.json"
manifest_data["scope_manifest"] = str(scoped_manifest.resolve())
with open(scoped_manifest, "w", encoding="utf-8") as f:
json.dump(manifest_data, f, indent=2, ensure_ascii=False)
return manifest_path
def main():
parser = argparse.ArgumentParser(description="蛋白特征分析")
parser.add_argument("--prefix", required=True)
parser.add_argument("--workspace", required=True)
parser.add_argument("--input-dir")
parser.add_argument("--fasta", help="蛋白FASTA文件;用于显式指定PEP输入")
parser.add_argument("--scope-file", help="target/reference/comparative scope JSON")
parser.add_argument("--cpu", type=int, default=8)
parser.add_argument("--log-file", help="日志文件路径(用于前端兼容)")
args = parser.parse_args()
start_time = datetime.now()
run_id = f"pep_{start_time.strftime('%Y%m%d_%H%M%S')}"
workspace = Path(args.workspace)
job_id = workspace.name
print("=" * 60)
print(f"🚀 蛋白特征分析")
print("=" * 60)
try:
scope_file = Path(args.scope_file) if args.scope_file else None
scope_data = None
if scope_file:
if not scope_file.exists():
raise FileNotFoundError(f"Scope file not found: {scope_file}")
scope_data = read_scope(scope_file)
# 1. 准备目录
print("\n📁 准备目录...")
paths = prepare_directories(workspace)
# 2. 获取工具版本
print("\n🔧 检查工具...")
tool_versions = get_tool_versions()
# 3. 查找输入文件
if scope_data:
protein_file = resolve_scope_protein_fasta(workspace, args.prefix, scope_data, args.fasta)
elif args.fasta:
protein_file = Path(args.fasta)
if not protein_file.exists() or protein_file.stat().st_size == 0:
print(f"❌ 找不到蛋白文件: {protein_file}")
return 1
protein_file = ensure_protein_fasta_without_stop_markers(protein_file)
else:
protein_file = workspace / "final" / f"{args.prefix}.final_members.pep.fa"
if not protein_file.exists() or protein_file.stat().st_size == 0:
print(f"❌ 找不到蛋白文件: {protein_file}")
return 1
protein_file = ensure_protein_fasta_without_stop_markers(protein_file)
print(f" 蛋白文件: {protein_file}")
# 解析序列
sequences = parse_fasta(protein_file)
print(f" 包含 {len(sequences)} 个序列")
artifacts = {}
metrics = {"protein_count": len(sequences)}
# 4. 理化性质分析
print("\n🧮 理化性质分析...")
properties = calculate_physicochemical_properties(sequences)
# 绘制理化性质图
props_pdf = paths["final_dir"] / f"{args.prefix}.pep_properties.pdf"
props_png = paths["final_dir"] / f"{args.prefix}.pep_properties.png"
if plot_properties_r(properties, props_pdf, props_png):
print(f" ✓ 理化性质图: {props_pdf}, {props_png}")
artifacts["properties_pdf"] = {"path": f"final/{args.prefix}.pep_properties.pdf"}
artifacts["properties_png"] = {"path": f"final/{args.prefix}.pep_properties.png"}
# 5. 跨膜结构预测
print("\n🌊 跨膜结构预测 (DeepTMHMM)...")
tmhmm_result = run_deeptmhmm(protein_file, paths["analysis_dir"])
metrics["tmhmm_success"] = bool(tmhmm_result.get("success"))
if not tmhmm_result.get("success"):
metrics["tmhmm_error"] = str(tmhmm_result.get("stderr") or tmhmm_result.get("error") or "DeepTMHMM unavailable")[:500]
if tmhmm_result["success"]:
# 绘制跨膜结构图
tmhmm_pdf = paths["final_dir"] / f"{args.prefix}.pep_tmhmm.pdf"
tmhmm_png = paths["final_dir"] / f"{args.prefix}.pep_tmhmm.png"
if plot_tmhmm_results(tmhmm_result["topologies"], tmhmm_pdf, tmhmm_png):
print(f" ✓ 跨膜结构图: {tmhmm_pdf}, {tmhmm_png}")
artifacts["tmhmm_pdf"] = {"path": f"final/{args.prefix}.pep_tmhmm.pdf"}
artifacts["tmhmm_png"] = {"path": f"final/{args.prefix}.pep_tmhmm.png"}
# 6. 亚细胞定位预测
print("\n🎯 亚细胞定位预测...")
# Plant-mPLoc code is retained but disabled by default.
plant_mploc_file = paths["analysis_dir"] / f"{args.prefix}_plant_mploc.txt"
if PLANT_MPLOC_ENABLED:
plant_result = run_plant_mploc(protein_file, plant_mploc_file)
else:
print(" Plant-mPLoc已关闭;亚细胞定位默认使用WoLF PSORT")
plant_result = {
"success": False,
"disabled": True,
"method": "Plant-mPLoc website automation",
"stderr": "Plant-mPLoc disabled; WoLF PSORT is the configured subcellular localization method",
}
# WoLF PSORT
wolfpsort_file = paths["analysis_dir"] / f"{args.prefix}_wolfpsort.txt"
wolf_result = run_wolfpsort(protein_file, wolfpsort_file)
metrics["subcellular_method"] = "WoLF PSORT"
metrics["plant_mploc_enabled"] = PLANT_MPLOC_ENABLED
metrics["plant_mploc_success"] = bool(plant_result.get("success"))
metrics["wolfpsort_success"] = bool(wolf_result.get("success"))
if plant_result.get("disabled"):
metrics["plant_mploc_status"] = "disabled"
elif not plant_result.get("success"):
metrics["plant_mploc_error"] = str(plant_result.get("stderr") or plant_result.get("error") or "Plant-mPLoc unavailable")[:500]
if not wolf_result.get("success"):
metrics["wolfpsort_error"] = str(wolf_result.get("stderr") or wolf_result.get("error") or "WoLF PSORT unavailable")[:500]
# 绘制亚细胞定位图;默认只接受WoLF PSORT逐蛋白预测作为数量来源。
if wolf_result["success"]:
subcell_pdf = paths["final_dir"] / f"{args.prefix}.pep_subcellular.pdf"
subcell_png = paths["final_dir"] / f"{args.prefix}.pep_subcellular.png"
if plot_subcellular_results(plant_mploc_file, wolfpsort_file, subcell_pdf, subcell_png):
sub_counts, sub_source, sub_rows = resolve_subcellular_counts(plant_mploc_file, wolfpsort_file)
metrics["subcellular_source"] = sub_source
metrics["subcellular_prediction_count"] = sub_rows
metrics["subcellular_counts"] = sub_counts
print(f" ✓ 亚细胞定位图: {subcell_pdf}, {subcell_png}")
artifacts["subcellular_pdf"] = {"path": f"final/{args.prefix}.pep_subcellular.pdf"}
artifacts["subcellular_png"] = {"path": f"final/{args.prefix}.pep_subcellular.png"}
artifacts["subcellular_data"] = {"path": "final/subcellular_data.tsv"}
# 7. 生成报告
end_time = datetime.now()
print("\n📝 生成报告...")
parameters = {
"prefix": args.prefix,
"cpu": args.cpu,
"protein_fasta": str(protein_file),
"protein_count": count_fasta_records(protein_file),
}
parameters.update(scope_parameters(scope_file, scope_data))
report_path = write_report(workspace, args.prefix, metrics, parameters, tool_versions)
artifacts["report"] = {"path": f"final/{args.prefix}.pep_report.md"}
# 8. 生成manifest
print("\n📋 生成Manifest...")
manifest_path = write_manifest(
workspace, job_id, run_id, args.prefix,
start_time, end_time, artifacts, metrics, parameters, tool_versions
)
print("\n" + "=" * 60)
print(f"✅ 蛋白特征分析完成!")
print("=" * 60)
return 0
except Exception as e:
print(f"\n❌ 失败: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())