233 lines
7.0 KiB
Python
233 lines
7.0 KiB
Python
"""
|
|||
|
|
可复现性支持模块
|
||
|
|
自动生成 commands.sh, environment.yml, checksums.sha256
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import json
|
||
|
|
import hashlib
|
||
|
|
from pathlib import Path
|
||
|
|
from datetime import datetime
|
||
|
|
from typing import List, Dict, Any
|
||
|
|
|
||
|
|
from gfa_core.project_paths import PROJECT_ROOT, WORKSPACE_ROOT
|
||
|
|
|
||
|
|
|
||
|
|
class ReproducibilityBundle:
|
||
|
|
"""可复现性包生成器"""
|
||
|
|
|
||
|
|
def __init__(self, job_id: str, prefix: str, workspace_root: str):
|
||
|
|
self.job_id = job_id
|
||
|
|
self.prefix = prefix
|
||
|
|
self.workspace = Path(workspace_root) / job_id
|
||
|
|
self.final_dir = self.workspace / "final"
|
||
|
|
self.reproducibility_dir = self.workspace / "reproducibility"
|
||
|
|
self.commands = []
|
||
|
|
|
||
|
|
def add_command(self, stage: str, cmd: List[str], description: str = ""):
|
||
|
|
"""记录命令"""
|
||
|
|
self.commands.append({
|
||
|
|
"stage": stage,
|
||
|
|
"cmd": " ".join(cmd),
|
||
|
|
"description": description,
|
||
|
|
"timestamp": datetime.now().isoformat()
|
||
|
|
})
|
||
|
|
|
||
|
|
def generate_commands_sh(self) -> Path:
|
||
|
|
"""生成 commands.sh"""
|
||
|
|
self.reproducibility_dir.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
script_path = self.reproducibility_dir / "commands.sh"
|
||
|
|
|
||
|
|
lines = [
|
||
|
|
"#!/bin/bash",
|
||
|
|
"# =============================================================================",
|
||
|
|
f"# bbav20311 Gene Family Analysis - Reproducibility Script",
|
||
|
|
"# =============================================================================",
|
||
|
|
f"# Job ID: {self.job_id}",
|
||
|
|
f"# Prefix: {self.prefix}",
|
||
|
|
f"# Generated: {datetime.now().isoformat()}",
|
||
|
|
"# =============================================================================",
|
||
|
|
"",
|
||
|
|
"set -e # Exit on error",
|
||
|
|
"",
|
||
|
|
f"JOB_ID=\"{self.job_id}\"",
|
||
|
|
f"PREFIX=\"{self.prefix}\"",
|
||
|
|
f"WORKSPACE=\"{WORKSPACE_ROOT}/${{JOB_ID}}\"",
|
||
|
|
"",
|
||
|
|
"echo \"==========================================\"",
|
||
|
|
"echo \"bbav20311 Gene Family Analysis Pipeline\"",
|
||
|
|
"echo \"Job ID: ${JOB_ID}\"",
|
||
|
|
"echo \"==========================================\"",
|
||
|
|
"",
|
||
|
|
]
|
||
|
|
|
||
|
|
for i, cmd_info in enumerate(self.commands, 1):
|
||
|
|
lines.extend([
|
||
|
|
"",
|
||
|
|
f"# {cmd_info['description'] or f'Stage {i}'}"
|
||
|
|
f"echo \"[STAGE {i}/{len(self.commands)}] {cmd_info['stage']}...\"",
|
||
|
|
cmd_info['cmd'],
|
||
|
|
])
|
||
|
|
|
||
|
|
lines.extend([
|
||
|
|
"",
|
||
|
|
"echo \"\"",
|
||
|
|
"echo \"==========================================\"",
|
||
|
|
"echo \"Analysis Complete!\"",
|
||
|
|
"echo \"Results: ${WORKSPACE}/final/\"",
|
||
|
|
"echo \"==========================================\"",
|
||
|
|
"",
|
||
|
|
])
|
||
|
|
|
||
|
|
with open(script_path, 'w') as f:
|
||
|
|
f.write("\n".join(lines))
|
||
|
|
|
||
|
|
# 添加执行权限
|
||
|
|
os.chmod(script_path, 0o755)
|
||
|
|
|
||
|
|
return script_path
|
||
|
|
|
||
|
|
def generate_environment_yml(self, conda_env: str = "test4py311") -> Path:
|
||
|
|
"""生成 environment.yml"""
|
||
|
|
self.reproducibility_dir.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
env_path = self.reproducibility_dir / "environment.yml"
|
||
|
|
|
||
|
|
# 从当前 conda 环境导出(简化版)
|
||
|
|
content = f"""name: bbav20311
|
||
|
|
channels:
|
||
|
|
- bioconda
|
||
|
|
- conda-forge
|
||
|
|
- defaults
|
||
|
|
dependencies:
|
||
|
|
# Python
|
||
|
|
- python=3.11
|
||
|
|
- pip
|
||
|
|
|
||
|
|
# Bioinformatics Tools
|
||
|
|
- hmmer>=3.4
|
||
|
|
- blast>=2.17.0
|
||
|
|
- mafft
|
||
|
|
- iqtree
|
||
|
|
- meme>=4.11.2
|
||
|
|
- seqkit>=2.12.0
|
||
|
|
- bedtools
|
||
|
|
- samtools
|
||
|
|
|
||
|
|
# R
|
||
|
|
- r-base>=4.4.3
|
||
|
|
- r-ggplot2
|
||
|
|
- r-ape
|
||
|
|
|
||
|
|
# Python packages
|
||
|
|
- biopython
|
||
|
|
- numpy
|
||
|
|
- pandas
|
||
|
|
- matplotlib
|
||
|
|
- fastapi
|
||
|
|
- uvicorn
|
||
|
|
- pydantic
|
||
|
|
|
||
|
|
# Current environment: {conda_env}
|
||
|
|
# Generated: {datetime.now().isoformat()}
|
||
|
|
"""
|
||
|
|
|
||
|
|
with open(env_path, 'w') as f:
|
||
|
|
f.write(content)
|
||
|
|
|
||
|
|
return env_path
|
||
|
|
|
||
|
|
def generate_checksums(self) -> Path:
|
||
|
|
"""生成 SHA-256 校验文件"""
|
||
|
|
self.reproducibility_dir.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
checksum_path = self.reproducibility_dir / "checksums.sha256"
|
||
|
|
|
||
|
|
checksums = []
|
||
|
|
|
||
|
|
if self.final_dir.exists():
|
||
|
|
for file_path in self.final_dir.rglob("*"):
|
||
|
|
if file_path.is_file():
|
||
|
|
sha256 = self._calculate_sha256(file_path)
|
||
|
|
rel_path = file_path.relative_to(self.final_dir)
|
||
|
|
checksums.append(f"{sha256} {rel_path}")
|
||
|
|
|
||
|
|
with open(checksum_path, 'w') as f:
|
||
|
|
f.write(f"# SHA-256 Checksums for {self.job_id}\n")
|
||
|
|
f.write(f"# Generated: {datetime.now().isoformat()}\n")
|
||
|
|
f.write(f"# Prefix: {self.prefix}\n\n")
|
||
|
|
f.write("\n".join(sorted(checksums)))
|
||
|
|
|
||
|
|
return checksum_path
|
||
|
|
|
||
|
|
def _calculate_sha256(self, file_path: Path) -> str:
|
||
|
|
"""计算文件 SHA-256"""
|
||
|
|
sha256_hash = hashlib.sha256()
|
||
|
|
with open(file_path, "rb") as f:
|
||
|
|
for byte_block in iter(lambda: f.read(4096), b""):
|
||
|
|
sha256_hash.update(byte_block)
|
||
|
|
return sha256_hash.hexdigest()
|
||
|
|
|
||
|
|
def copy_to_project_reproducibility(self):
|
||
|
|
"""复制到项目级 reproducibility 目录"""
|
||
|
|
project_repro_dir = PROJECT_ROOT / "reproducibility" / self.job_id
|
||
|
|
project_repro_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
# 复制文件
|
||
|
|
for file in self.reproducibility_dir.iterdir():
|
||
|
|
if file.is_file():
|
||
|
|
shutil.copy2(file, project_repro_dir / file.name)
|
||
|
|
|
||
|
|
return project_repro_dir
|
||
|
|
|
||
|
|
|
||
|
|
def create_reproducibility_bundle(
|
||
|
|
job_id: str,
|
||
|
|
prefix: str,
|
||
|
|
workspace_root: str = str(WORKSPACE_ROOT),
|
||
|
|
commands: List[Dict[str, Any]] = None
|
||
|
|
) -> Dict[str, Path]:
|
||
|
|
"""
|
||
|
|
创建完整的可复现性包
|
||
|
|
|
||
|
|
Args:
|
||
|
|
job_id: 任务ID
|
||
|
|
prefix: 前缀
|
||
|
|
workspace_root: workspace 根目录
|
||
|
|
commands: 命令列表 [{"stage": "", "cmd": "", "description": ""}]
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
生成的文件路径字典
|
||
|
|
"""
|
||
|
|
bundle = ReproducibilityBundle(job_id, prefix, workspace_root)
|
||
|
|
|
||
|
|
# 添加命令
|
||
|
|
if commands:
|
||
|
|
for cmd in commands:
|
||
|
|
bundle.add_command(
|
||
|
|
stage=cmd.get("stage", ""),
|
||
|
|
cmd=cmd.get("cmd", []),
|
||
|
|
description=cmd.get("description", "")
|
||
|
|
)
|
||
|
|
|
||
|
|
# 生成文件
|
||
|
|
result = {
|
||
|
|
"commands_sh": bundle.generate_commands_sh(),
|
||
|
|
"environment_yml": bundle.generate_environment_yml(),
|
||
|
|
"checksums_sha256": bundle.generate_checksums(),
|
||
|
|
}
|
||
|
|
|
||
|
|
# 复制到项目目录
|
||
|
|
result["project_copy"] = bundle.copy_to_project_reproducibility()
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
# 兼容旧版导入
|
||
|
|
if __name__ == "__main__":
|
||
|
|
# 测试
|
||
|
|
bundle = ReproducibilityBundle("test_job", "Ft.TEST", "/tmp/test_workspace")
|
||
|
|
bundle.add_command("identify", ["python", "run_identify.py", "--input", "test.fa"])
|
||
|
|
print(bundle.generate_commands_sh())
|