Files
2026-08-03 18:42:47 +08:00

394 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Motif Analysis Skill - 保守基序分析技能
使用MEME Suite进行基因家族保守基序分析
Usage:
python run.py --input protein.fa --output ./motif
python run.py --config config.yaml --output ./motif
python run.py --demo --output ./motif
"""
import argparse
import hashlib
import json
import logging
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import yaml
# 设置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class MotifSkill:
"""保守基序分析技能主类"""
def __init__(self, config_path: Optional[str] = None):
self.config = self._load_config(config_path)
self.commands = [] # 记录所有执行的命令
self.checksums = {} # 记录文件校验
def _load_config(self, config_path: Optional[str]) -> Dict:
"""加载配置文件"""
if config_path and os.path.exists(config_path):
with open(config_path) as f:
return yaml.safe_load(f)
default_config = Path(__file__).parent / "config.yaml"
with open(default_config) as f:
return yaml.safe_load(f)
def _which(self, cmd: str) -> Optional[str]:
"""检查命令是否存在"""
return shutil.which(cmd)
def _run_command(self, cmd: List[str], description: str):
"""运行命令并记录"""
logger.info(f"Running: {description}")
logger.info(f"Command: {' '.join(cmd)}")
self.commands.append({
"description": description,
"command": " ".join(cmd),
"timestamp": datetime.now().isoformat()
})
subprocess.run(cmd, check=True)
def _compute_checksum(self, filepath: Path) -> str:
"""计算文件SHA256校验"""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest()
def _record_file(self, filepath: Path, file_type: str = "output"):
"""记录文件信息"""
if filepath.exists():
self.checksums[str(filepath)] = {
"sha256": self._compute_checksum(filepath),
"size": filepath.stat().st_size,
"type": file_type
}
def run_meme(self, protein_fasta: Path, output_dir: Path) -> Dict:
"""运行MEME motif分析"""
params = self.config["parameters"]["meme"]
cpu = self.config["parameters"]["cpu"]
meme = self._which("meme")
if not meme:
raise RuntimeError("meme not found. Please install MEME Suite.")
output_dir.mkdir(parents=True, exist_ok=True)
# 构建MEME命令
cmd = [
meme, str(protein_fasta),
"-protein",
"-oc", str(output_dir),
"-mod", params["model"],
"-nmotifs", str(params["nmotifs"]),
"-minw", str(params["minw"]),
"-maxw", str(params["maxw"]),
"-p", str(cpu),
]
self._run_command(cmd, "MEME保守基序分析")
meme_txt = output_dir / "meme.txt"
meme_html = output_dir / "meme.html"
self._record_file(meme_txt)
self._record_file(meme_html)
return {
"meme_txt": str(meme_txt),
"meme_html": str(meme_html)
}
def generate_report(self, output_dir: Path, nmotifs: int,
minw: int, maxw: int, model: str) -> Path:
"""生成分析报告"""
report_file = output_dir / "report.md"
report_content = f"""# 保守基序分析报告
## 基本信息
- **Motif数量**: {nmotifs}
- **最小宽度**: {minw}
- **最大宽度**: {maxw}
- **搜索模式**: {model}
- **分析时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
- **技能版本**: {self.config["skill"]["version"]}
## 分析结果
| 文件 | 描述 |
|------|------|
| meme.txt | Motif文本报告 |
| meme.html | Motif HTML报告 |
## 可复现性
所有分析命令已记录在 `commands.sh` 中,
环境配置见 `environment.yml`。
## 校验信息
文件SHA256校验值见 `checksums.sha256`。
"""
with open(report_file, "w") as f:
f.write(report_content)
self._record_file(report_file)
return report_file
def write_reproducibility_bundle(self, output_dir: Path):
"""写入可复现性文件包"""
# commands.sh
commands_file = output_dir / "commands.sh"
with open(commands_file, "w") as f:
f.write("#!/bin/bash\n# 保守基序分析复现命令\n\n")
for cmd in self.commands:
f.write(f"# {cmd['description']}\n")
f.write(f"{cmd['command']}\n\n")
os.chmod(commands_file, 0o755)
# environment.yml
env_file = output_dir / "environment.yml"
env_config = {
"name": "motif-analysis",
"channels": self.config["dependencies"]["conda"]["channels"],
"dependencies": self.config["dependencies"]["conda"]["packages"]
}
with open(env_file, "w") as f:
yaml.dump(env_config, f, default_flow_style=False)
# checksums.sha256
checksums_file = output_dir / "checksums.sha256"
with open(checksums_file, "w") as f:
for filepath, info in self.checksums.items():
f.write(f"{info['sha256']} {filepath}\n")
def run(self, protein_fasta: Path, output_dir: Path) -> Dict:
"""运行完整的保守基序分析流程"""
logger.info(f"Starting motif analysis")
# 记录输入文件
self._record_file(protein_fasta, "input")
# 运行MEME
params = self.config["parameters"]["meme"]
meme_results = self.run_meme(protein_fasta, output_dir)
# 生成报告
report = self.generate_report(
output_dir,
params["nmotifs"],
params["minw"],
params["maxw"],
params["model"]
)
# 写入可复现性文件
self.write_reproducibility_bundle(output_dir)
logger.info(f"Motif analysis completed. Report: {report}")
return {
"status": "success",
"output_dir": str(output_dir),
"report": str(report),
"meme_txt": meme_results["meme_txt"],
"meme_html": meme_results["meme_html"]
}
def run_demo(self, output_dir: Path) -> Dict:
"""运行Demo模式"""
logger.info("Running in demo mode")
# 创建模拟数据
demo_dir = output_dir / "demo_data"
demo_dir.mkdir(parents=True, exist_ok=True)
# 模拟蛋白质FASTA
protein_fasta = demo_dir / "demo_proteins.fa"
with open(protein_fasta, "w") as f:
f.write(">FtGene001\nMKTLLLPLLLLVLLLLLVQGSISAQ\n"
">FtGene002\nMKTLLLPLLLLVLLLLLVQGSISAQ\n"
">AtGene001\nMKTLLLPLLLLVLLLLLVQGSISAQ\n")
# 创建模拟输出文件
output_dir.mkdir(parents=True, exist_ok=True)
# meme.txt (模拟)
meme_txt = output_dir / "meme.txt"
meme_txt.write_text("""MEME VERSION 5.5.0
ALPHABET= ACGT
Strands: + -
Cache size: 10000
Sequence name Weight Sequence length
------------- ------ -----
FtGene001 1.000 25
FtGene002 1.000 25
AtGene001 1.000 25
MOTIF 1
Letter-probability matrix: alength= 4 w= 8 nsites= 3 E= 0.21
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
0.125 0.125 0.625 0.125
""")
# meme.html (模拟)
meme_html = output_dir / "meme.html"
meme_html.write_text("""<!DOCTYPE html>
<html>
<head><title>MEME - Motif Analysis Results</title></head>
<body>
<h1>MEME Motif Analysis Results (Demo)</h1>
<p>Found 1 motif</p>
</body>
</html>
""")
# report.md
report_file = output_dir / "report.md"
report_file.write_text(f"""# 保守基序分析报告
## 基本信息
- **Motif数量**: 10 (demo)
- **最小宽度**: 6 (demo)
- **最大宽度**: 200 (demo)
- **搜索模式**: anr (demo)
- **分析时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
- **技能版本**: {self.config["skill"]["version"]}
## 分析结果
| 文件 | 描述 |
|------|------|
| meme.txt | Motif文本报告 |
| meme.html | Motif HTML报告 |
## 可复现性
所有分析命令已记录在 `commands.sh` 中,环境配置见 `environment.yml`。
## 校验信息
文件SHA256校验值见 `checksums.sha256`。
""")
# commands.sh
commands_file = output_dir / "commands.sh"
commands_file.write_text("""#!/bin/bash
# 保守基序分析复现命令 (Demo模式)
meme input_proteins.fa -protein -oc ./motif -mod anr -nmotifs 10 -minw 6 -maxw 200 -p 8
""")
# environment.yml
env_file = output_dir / "environment.yml"
env_file.write_text("""name: motif-analysis
channels:
- bioconda
- conda-forge
dependencies:
- meme=5.5.0
- biopython=1.81
""")
# checksums.sha256
checksums_file = output_dir / "checksums.sha256"
for f in [meme_txt, meme_html, report_file, commands_file, env_file]:
if f.exists():
sha256 = hashlib.sha256()
with open(f, "rb") as fp:
for chunk in iter(lambda: fp.read(8192), b""):
sha256.update(chunk)
checksums_file.write_text(f"{sha256.hexdigest()} {f.name}\n")
logger.info(f"Motif demo completed. Report: {report_file}")
return {
"status": "success",
"output_dir": str(output_dir),
"report": str(report_file),
"meme_txt": str(meme_txt),
"meme_html": str(meme_html)
}
def main():
parser = argparse.ArgumentParser(
description="Motif Analysis Skill - 保守基序分析"
)
parser.add_argument("--input", "-i", type=Path, help="蛋白序列FASTA文件")
parser.add_argument("--nmotifs", type=int, help="发现的motif数量")
parser.add_argument("--minw", type=int, help="最小motif宽度")
parser.add_argument("--maxw", type=int, help="最大motif宽度")
parser.add_argument("--model", choices=["anr", "zoops", "oops"], help="搜索模式")
parser.add_argument("--cpu", type=int, help="CPU线程数")
parser.add_argument("--output", "-o", type=Path, required=True, help="输出目录")
parser.add_argument("--config", "-c", type=Path, help="配置文件路径")
parser.add_argument("--demo", action="store_true", help="运行Demo模式")
args = parser.parse_args()
# 初始化技能
skill = MotifSkill(args.config)
# 更新配置参数
if args.nmotifs:
skill.config["parameters"]["meme"]["nmotifs"] = args.nmotifs
if args.minw:
skill.config["parameters"]["meme"]["minw"] = args.minw
if args.maxw:
skill.config["parameters"]["meme"]["maxw"] = args.maxw
if args.model:
skill.config["parameters"]["meme"]["model"] = args.model
if args.cpu:
skill.config["parameters"]["cpu"] = args.cpu
# 运行
if args.demo:
result = skill.run_demo(args.output)
else:
if not args.input:
parser.error("--input is required (unless using --demo)")
result = skill.run(args.input, args.output)
# 输出结果
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result["status"] == "success" else 1
if __name__ == "__main__":
sys.exit(main())