Files

233 lines
7.0 KiB
Python
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
"""
可复现性支持模块
自动生成 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())