Files
PlantAI-dev/docs/folder_based_workflow.md
2026-08-03 18:42:47 +08:00

9.5 KiB
Executable File

基于文件夹的多物种工作流设计

核心设计

通过物理文件夹结构明确区分目标物种和参考物种:

input/{job_id}/
├── target/                 # 目标物种(必需,唯一)
│   ├── Ft.pep.fasta       # 蛋白序列
│   ├── Ft.gff3            # 注释文件
│   └── Ft.genome.fasta    # 基因组(可选)
│
├── references/             # 参考物种(可选,可多个)
│   ├── Ath/               # 拟南芥
│   │   ├── Ath.pep.fasta
│   │   └── Ath.gff3
│   ├── Os/                # 水稻
│   │   ├── Os.pep.fasta
│   │   └── Os.gff3
│   └── Sl/                # 番茄
│       ├── Sl.pep.fasta
│       └── Sl.gff3
│
└── optional/               # 其他可选文件
    ├── SBP.hmm            # HMM文件
    └── custom_config.yaml # 自定义配置

文件夹结构说明

文件夹 用途 数量限制 必需文件
target/ 目标物种数据 1个物种 *.pep.fasta 或 *.pep.fa
references/ 参考物种数据 0-N个物种 每个子文件夹需要 *.pep.fasta
optional/ 可选文件 任意 无

工作流程

阶段1: 数据准备 (genome_prepare)

输入: input/{job_id}/target/ 和 input/{job_id}/references/

处理:

def process_target_folder(target_dir: Path, output_dir: Path, prefix: str):
    """处理目标物种文件夹"""
    # 查找蛋白文件
    pep_files = list(target_dir.glob("*.pep.fasta")) + list(target_dir.glob("*.pep.fa"))
    if not pep_files:
        raise FileNotFoundError("目标物种文件夹缺少蛋白文件 (*.pep.fasta)")
    
    # 处理目标物种
    process_species(target_dir, output_dir, prefix, is_target=True)

def process_reference_folders(ref_dir: Path, output_dir: Path):
    """处理参考物种文件夹"""
    references = []
    
    for species_dir in ref_dir.iterdir():
        if not species_dir.is_dir():
            continue
        
        species_code = species_dir.name  # Ath, Os, Sl, etc.
        
        # 检查必需文件
        pep_files = list(species_dir.glob("*.pep.fasta")) + list(species_dir.glob("*.pep.fa"))
        if not pep_files:
            logger.warning(f"参考物种 {species_code} 缺少蛋白文件,跳过")
            continue
        
        # 处理参考物种
        process_species(species_dir, output_dir, species_code, is_target=False)
        references.append(species_code)
    
    return references

输出:

input/{job_id}/
├── target/                 # 原始目标物种数据
├── references/             # 原始参考物种数据
├── processed/              # 处理后的标准化数据
│   ├── target.pep.fasta   # 目标物种蛋白(重命名)
│   ├── target.gff3
│   ├── Ath_ref.pep.fasta  # 参考物种1(添加_ref后缀)
│   ├── Ath_ref.gff3
│   ├── Os_ref.pep.fasta   # 参考物种2
│   ├── Os_ref.gff3
│   └── ...
└── genome_prepare.manifest.json

阶段2: 基因家族鉴定 (identify)

输入:

  • 目标蛋白: processed/target.pep.fasta
  • 参考蛋白: processed/*_ref.pep.fasta(多个)

处理:

def run_identify_with_references(
    target_pep: Path,
    reference_peps: List[Path],
    hmm_file: Optional[Path],
    output_dir: Path
):
    """使用多参考物种进行基因家族鉴定"""
    
    # 1. HMM搜索目标物种
    hmm_results = run_hmmsearch(target_pep, hmm_file)
    
    # 2. 合并参考物种构建BLAST数据库
    if reference_peps:
        combined_db = combine_references(reference_peps)
        blast_db = build_blast_db(combined_db)
        
        # 3. BLAST搜索
        blast_results = run_blastp(target_pep, blast_db)
        
        # 4. 分析参考物种来源
        ref_analysis = analyze_reference_sources(blast_results)
        logger.info(f"参考物种匹配情况: {ref_analysis}")
    
    # 5. 整合结果
    final_members = integrate_results(hmm_results, blast_results)
    
    return final_members

def combine_references(reference_peps: List[Path]) -> Path:
    """合并多个参考物种的蛋白序列"""
    combined = Path("combined_ref.pep.fa")
    
    with open(combined, 'w') as out:
        for ref_pep in reference_peps:
            species_code = ref_pep.stem.replace('_ref', '')  # Ath_ref -> Ath
            
            with open(ref_pep) as f:
                for line in f:
                    if line.startswith('>'):
                        # 添加物种标识
                        out.write(f">{species_code}|{line[1:]}")
                    else:
                        out.write(line)
    
    return combined

输出:

final/
├── {prefix}.final_members.pep.fa      # 目标物种基因家族成员
├── {prefix}.final_members.id
├── {prefix}.identify.manifest.json
├── {prefix}.identify_report.md
└── reference_analysis.json             # 参考物种分析

阶段3: 进化分析 (phylogeny)

输入:

  • 目标物种基因家族成员
  • 参考物种同源基因

处理: 构建包含多物种的系统发育树

前端UI设计

上传界面

┌─────────────────────────────────────────┐
│  📁 目标物种 (Target Species)            │
│  [选择文件夹] 或 [拖拽文件夹到此处]        │
│  必需: 蛋白序列文件 (*.pep.fasta)        │
│  可选: 注释文件 (*.gff3), 基因组文件     │
├─────────────────────────────────────────┤
│  📁 参考物种 (Reference Species)         │
│  [添加参考物种]                          │
│  ├─ 📁 Ath (拟南芥) [删除]              │
│  │   [选择文件夹]                        │
│  ├─ 📁 Os (水稻) [删除]                 │
│  │   [选择文件夹]                        │
│  └─ [+ 添加更多参考物种]                 │
│  可选: 用于辅助鉴定的已知物种            │
├─────────────────────────────────────────┤
│  📁 可选文件 (Optional)                  │
│  [选择文件] HMM文件, 配置文件等          │
├─────────────────────────────────────────┤
│  [开始上传并处理]                        │
└─────────────────────────────────────────┘

参数配置

# 自动从文件夹结构生成
analysis:
  target_species: "Ft"           # 从target/文件夹名获取
  target_prefix: "Ft.SPL"        # 用户输入
  reference_species:             # 从references/子文件夹获取
    - "Ath"
    - "Os"
    - "Sl"
  gene_family: "SPL"             # 从prefix解析

实现代码

文件夹扫描

class FolderScanner:
    """扫描输入文件夹结构"""
    
    def scan(self, input_dir: Path) -> Dict[str, Any]:
        """扫描文件夹,返回结构信息"""
        result = {
            "target": None,
            "references": [],
            "optional": []
        }
        
        # 扫描目标物种
        target_dir = input_dir / "target"
        if target_dir.exists():
            result["target"] = self.scan_species_folder(target_dir, is_target=True)
        
        # 扫描参考物种
        ref_dir = input_dir / "references"
        if ref_dir.exists():
            for species_dir in ref_dir.iterdir():
                if species_dir.is_dir():
                    species_info = self.scan_species_folder(species_dir, is_target=False)
                    result["references"].append(species_info)
        
        # 扫描可选文件
        opt_dir = input_dir / "optional"
        if opt_dir.exists():
            result["optional"] = list(opt_dir.iterdir())
        
        return result
    
    def scan_species_folder(self, species_dir: Path, is_target: bool) -> Dict[str, Any]:
        """扫描物种文件夹"""
        species_code = species_dir.name
        
        files = {
            "code": species_code,
            "is_target": is_target,
            "pep": None,
            "gff": None,
            "genome": None,
            "cds": None
        }
        
        for file in species_dir.iterdir():
            if not file.is_file():
                continue
            
            name_lower = file.name.lower()
            
            if 'pep' in name_lower or 'protein' in name_lower:
                files["pep"] = file
            elif 'cds' in name_lower:
                files["cds"] = file
            elif name_lower.endswith('.gff3') or name_lower.endswith('.gff'):
                files["gff"] = file
            elif 'genome' in name_lower or 'chromosome' in name_lower:
                files["genome"] = file
        
        return files

处理流程

def process_folder_structure(input_dir: Path, output_dir: Path, prefix: str):
    """处理文件夹结构"""
    scanner = FolderScanner()
    structure = scanner.scan(input_dir)
    
    # 检查目标物种
    if not structure["target"]:
        raise ValueError("缺少目标物种文件夹: input/{job_id}/target/")
    
    if not structure["target"]["pep"]:
        raise ValueError("目标物种文件夹缺少蛋白文件 (*.pep.fasta)")