By jiangchenhao, 30 June, 2026
Forums

金山文档link

基因组数据集#

首先,下载了梁老师给我的数据集,我补充了侧柏的基因组

/HDD3/jch/Gymnosperm_genome_26_5_11/genome_data

再补充了白豆杉的基因组,但是白豆杉的格式是gbff,搞明白了,现在大家都知道gbff不好用,会选择上传一份新的数据到figshare上

对于gff和fasta不匹配的巨柏基因组,校准了fasta文件

perl -pe 's/^>\S+(.*OriSeqID=)(\S+)/>$2$1$2/' Cgigantea.fa > Cgigantea_renamed.fa && mv Cgigantea_renamed.fa Cgigantea.fa

对于地中海柏木也是一样

perl -pe 'if(/^>/){$i++;$_=">Scaffold_".sprintf("%05d",$i)."\t".substr($_,1)}' Csempervirens.fa > Csempervirens_renamed.fa && mv Csempervirens_renamed.fa Csempervirens.fa

这俩物种都是2n=11

检测这些数据集有没有去除最长转录本

#!/bin/bash
# 用法: bash check_transcript_filter.sh species_index.tsv 数据目录

TSV="${1:-species_index.tsv}"
DATADIR="${2:-.}"

echo -e "物种\tGFF基因\tGFF转录本\t转录本/基因\tPEP条数\t判定"

tail -n +2 "$TSV" | while IFS=$'\t' read -r name cat latin fa gff pep cds status; do
    # 跳过注释和不完整物种
    [[ "$name" =~ ^# ]] && continue
    [ "$pep" = "-" ] && continue
    [ "$gff" = "-" ] && continue

    gff_path="$DATADIR/$gff"
    pep_path="$DATADIR/$pep"
    [ -f "$gff_path" ] || { echo -e "$name\tGFF不存在\t-\t-\t-\t-"; continue; }
    [ -f "$pep_path" ] || { echo -e "$name\tPEP不存在\t-\t-\t-\t-"; continue; }

    genes=$(awk '$3=="gene"' "$gff_path" | wc -l)
    mrnas=$(awk '$3=="mRNA" || $3=="transcript"' "$gff_path" | wc -l)
    pep_count=$(grep -c "^>" "$pep_path")

    # 基因数为0时特殊处理
    if [ "$genes" -eq 0 ]; then
        genes=$(awk -F'[=;]' '{for(i=1;i<=NF;i++) if($i=="gene_id") print $(i+1)}' "$gff_path" | sort -u | wc -l)
    fi

    # 判定
    if [ "$mrnas" -eq 0 ] && [ "$genes" -gt 0 ]; then
        if [ "$pep_count" -le $((genes + genes/10)) ]; then
            verdict="✓ PEP≈基因"
        else
            verdict="⚠ 需检查"
        fi
    elif [ "$mrnas" -gt 0 ]; then
        diff_gene=$((pep_count - genes))
        diff_mrna=$((pep_count - mrnas))
        [ $diff_gene -lt 0 ] && diff_gene=$((-diff_gene))
        [ $diff_mrna -lt 0 ] && diff_mrna=$((-diff_mrna))

        if [ $diff_gene -le $((genes / 50)) ] || [ $diff_gene -le 50 ]; then
            verdict="✓ 已筛选"
        elif [ $diff_mrna -le $((mrnas / 50)) ] || [ $diff_mrna -le 50 ]; then
            verdict="✗ 未筛选"
        else
            verdict="⚠ 需手动检查"
        fi
    else
        verdict="⚠ 无gene/mRNA"
    fi

    [[ "$pep" =~ longest ]] && verdict="$verdict [pre-filtered]"

    if [ "$genes" -gt 0 ]; then
        ratio=$(awk "BEGIN {printf \"%.2f\", $mrnas/$genes}")
    else
        ratio="N/A"
    fi

    echo -e "$name\t$genes\t$mrnas\t$ratio\t$pep_count\t$verdict"
done

species_index.tsv文件

物种名    分类    完整拉丁名    基因组(fa)    GFF    PEP    CDS    完整性
Aalba    [G]    Abies alba    Aalba.fa    Aalba.gff    Aalba.pep    Aalba.cds    完整
Agramineus    [AG?]    待确认    Agramineus.fa    Agramineus.gff    Agramineus.pep    Agramineus.cds    完整
Aspinulosa    [OT?]    待确认 (疑似 Alsophila spinulosa 桫椤)    Aspinulosa.fa    Aspinulosa.gff    Aspinulosa.pep    Aspinulosa.cds    完整
Athaliana    [AG]    Arabidopsis thaliana    Athaliana.fa    Athaliana.gff    Athaliana.pep    Athaliana.cds    完整
Atrichopoda    [AG]    Amborella trichopoda    Atrichopoda.fa    Atrichopoda.gff    Atrichopoda.pep    Atrichopoda.cds    完整
Cgigantea    [G]    Cupressus gigantea    Cgigantea.fa    Cgigantea.gff    Cgigantea.pep    Cgigantea.cds    完整
Cjaponica    [G]    Cryptomeria japonica    Cjaponica.fa    Cjaponica.gff    Cjaponica.pep    Cjaponica.cds    完整
Cpanzhihuaensis    [G]    Cycas panzhihuaensis    Cpanzhihuaensis.fa    Cpanzhihuaensis.gff    Cpanzhihuaensis.pep    Cpanzhihuaensis.cds    完整
Csempervirens    [G]    Cupressus sempervirens    Csempervirens.fa    Csempervirens.gff    Csempervirens.pep    Csempervirens.cds    完整
Fhodginsii    [G]    Fokienia hodginsii    Fhodginsii.fa    Fhodginsii.gff    Fhodginsii.pep    Fhodginsii.cds    完整
Gbiloba    [G]    Ginkgo biloba    Gbiloba.fa    Gbiloba.gff    Gbiloba.pep    Gbiloba.cds    完整
Gmontanum    [G]    Gnetum montanum    Gmontanum.fa    Gmontanum.gff    Gmontanum.pep    Gmontanum.cds    完整
Lkaempferi    [G]    Larix kaempferi    Lkaempferi.fa    Lkaempferi.gff    Lkaempferi.longest.pep    Lkaempferi.longest.cds    完整
Mglyptostroboides    [G]    Metasequoia glyptostroboides    Mglyptostroboides.fa    Mglyptostroboides.gff    Mglyptostroboides.pep    Mglyptostroboides.cds    完整
Mpolymorpha    [OT]    Marchantia polymorpha    Mpolymorpha.fa    Mpolymorpha.gff    Mpolymorpha.pep    Mpolymorpha.cds    完整
Ncolorata    [AG]    Nymphaea colorata    Ncolorata.fa    Ncolorata.gff    Ncolorata.pep    Ncolorata.cds    完整
Osativa    [AG]    Oryza sativa    Osativa.fa    Osativa.gff    Osativa.pep    Osativa.cds    完整
Pabies    [G]    Picea abies    Pabies.fa    Pabies.gff    Pabies.pep    Pabies.cds    完整
Pdensiflora    [G]    Pinus densiflora    Pdensiflora.fa    Pdensiflora.gff    Pdensiflora.pep    Pdensiflora.cds    完整
Pglauca    [G]    Picea glauca    Pglauca.fa    Pglauca.gff    Pglauca.pep    Pglauca.cds    完整
Plambertiana    [G]    Pinus lambertiana    Plambertiana.fa    Plambertiana.gff    Plambertiana.pep    Plambertiana.cds    完整
Platycladus_orientalis    [G]    Platycladus orientalis    Platycladus_orientalis.fa    Platycladus_orientalis_mRNA_longest.gff3    Platycladus_orientalis.pep    Platycladus_orientalis.cds完整
Pmenziesii    [G]    Pseudotsuga menziesii    Pmenziesii.fa    Pmenziesii.gff    Pmenziesii.pep    Pmenziesii.cds    完整
Pseudotaxus_chienii    [G]    Pseudotaxus chienii    Pseudotaxus_chienii_pep_chr.fa    -    -    -    仅基因组
Ptabuliformis    [G]    Pinus tabuliformis    Ptabuliformis.fa    Ptabuliformis.gff    Ptabuliformis.pep    Ptabuliformis.cds    完整
Ptaeda    [G]    Pinus taeda    Ptaeda.fa    Ptaeda.gff    Ptaeda.pep    Ptaeda.cds    完整
Sgiganteum    [G]    Sequoiadendron giganteum    Sgiganteum.fa    Sgiganteum.gff    Sgiganteum.pep    Sgiganteum.cds    完整
Ssempervirens    [G]    Sequoia sempervirens    Ssempervirens.fa    Ssempervirens.gff    Ssempervirens.pep    Ssempervirens.cds    完整
Tchinensis    [G]    Taxus chinensis    Tchinensis.fa    Tchinensis.gff    Tchinensis.pep    Tchinensis.cds    完整
Tdistichum    [G]    Taxodium distichum    Tdistichum.fa    Tdistichum.gff    Tdistichum.pep    Tdistichum.cds    完整
Tgrandis    [G]    Torreya grandis    Tgrandis.fa    Tgrandis.gff    Tgrandis.pep    Tgrandis.cds    完整
Twallichiana    [G]    Taxus wallichiana    Twallichiana.1.fa    Twallichiana.1.gff    Twallichiana_longest.pep    Twallichiana_longest.cds    完整
Vvinifera    [AG]    Vitis vinifera    Vvinifera.fa    Vvinifera.gff    Vvinifera.pep    Vvinifera.cds    完整
Wmirabilis    [G]    Welwitschia mirabilis    Wmirabilis.fa    Wmirabilis.gff    Wmirabilis.pep    Wmirabilis.cds    完整

结果

(SNP_calling) zhanglab@zhanglab-Precision-3680:/HDD3/jch/Gymnosperm_genome_26_5_11/genome_data$ bash check_transcript_filter.sh
物种    GFF基因    GFF转录本    转录本/基因    PEP条数    判定
Aalba    94205    98227    1.04    97750    ✗ 未筛选
Agramineus    13986    13986    1.00    13986    ✓ 已筛选
Aspinulosa    67831    71488    1.05    67831    ✓ 已筛选
Athaliana    27655    48456    1.75    27654    ✓ 已筛选
Atrichopoda    26846    26846    1.00    26846    ✓ 已筛选
Cgigantea    35384    40910    1.16    40910    ✗ 未筛选
Cjaponica    55246    55246    1.00    55246    ✓ 已筛选
Cpanzhihuaensis    0    32353    N/A    32353    ✓ 未筛选
#攀枝花苏铁是通过了的,他gff里就没有基因这一行
#Csempervirens    670001    720048    1.07    365410    ⚠ 需手动检查
#??????
#明白了,Csempervirens这个物种的注释有问题,不可用
Fhodginsii    50521    50521    1.00    50521    ✓ 已筛选
Gbiloba    27836    27836    1.00    22152    ⚠ 需手动检查
#银杏怎么还能少了一些转录本的?!莫非手动去除了?有可能

Gmontanum    0    27354    N/A    27354    ✓ 未筛选
#这个没问题,可以使用,是因为统计的gene行没有出现。
Lkaempferi    45826    55814    1.22    42608    ⚠ 需手动检查 [pre-filtered]
Mglyptostroboides    32174    32174    1.00    32174    ✓ 已筛选
Mpolymorpha    19287    24674    1.28    19287    ✓ 已筛选
Ncolorata    24105    44024    1.83    24059    ✓ 已筛选
Osativa    42189    52424    1.24    42189    ✓ 已筛选
Pabies    70736    0    0.00    66632    ✓ PEP≈基因
#为什么pep文件还能比基因组上的注释要少啊
Pdensiflora    44233    4637    0.10    44233    ✓ 已筛选
Pglauca    0    0    N/A    567    ⚠ 无gene/mRNA
#这个物种不使用,注释很烂,组装应该也一般。
Plambertiana    38518    0    0.00    38518    ✓ PEP≈基因
#这个物种不使用,注释很烂,肯定没有达到可用的水平。
Platycladus_orientalis    52897    52897    1.00    52897    ✓ 已筛选
Pmenziesii    51419    0    0.00    51419    ✓ PEP≈基因
Ptabuliformis    80495    144584    1.80    144552    ✗ 未筛选
#100%是把俩单倍型都合并了
Ptaeda    51751    0    0.00    51751    ✓ PEP≈基因
#2017年发表的,不可用
Sgiganteum    41631    0    0.00    41631    ✓ PEP≈基因
#这个可以用,张仁纲用了
Ssempervirens    118906    0    0.00    118906    ✓ PEP≈基因
#这个张仁纲用了,不对。
Tchinensis    44770    44770    1.00    44770    ✓ 已筛选
Tdistichum    42307    42307    1.00    44010    ⚠ 需手动检查
#!!!!!!!!!!!!!!
Tgrandis    47089    47089    1.00    47089    ✓ 已筛选
#要换成李俞鹏ba版本的
Twallichiana    37766    47722    1.26    37766    ✓ 已筛选 [pre-filtered]
Vvinifera    31845    55564    1.74    31845    ✓ 已筛选
Wmirabilis    0    26990    N/A    26990    ✗ 未筛选
#这个是没问题的。

拯救油松的十几万个mRNA的办法:

#略AGAT去冗余
# 从 AGAT 处理过的 GFF 提取 mRNA ID
awk '$3=="mRNA"' Ptabuliformis.gff | grep -oP 'ID=\K[^;]+' > agat_ids.txt

# seqkit 按 ID 提取
seqkit grep -f agat_ids.txt Ptabuliformis.pep > Ptabuliformis_longest.pep

# 验证
echo "GFF mRNA 数: $(wc -l < agat_ids.txt)"
echo "提取 PEP 数: $(grep -c '^>' Ptabuliformis_longest.pep)"
(SNP_calling) zhanglab@zhanglab-Precision-3680:/HDD3/jch/Gymnosperm_genome_26_5_11/genome_data$ echo "GFF mRNA 数: $(wc -l < agat_ids.txt)"
echo "提取 PEP 数: $(grep -c '^>' Ptabuliformis_longest.pep)"
GFF mRNA 数: 80495
提取 PEP 数: 80464
提取 CDS 数 80464

目前能做到蛋白数接近需求数目(去除最长转录本后的数量),但是这三十多个基因我确实没招了。

白豆杉的基因组文件重命名

awk '
/^>/ {
    # 提取第一个空格后的全部内容作为描述(保留原始格式)
    space_pos = index($0, " ")
    if (space_pos > 0) {
        rest = substr($0, space_pos)   # 包含前导空格
    } else {
        rest = ""
    }

    # 定位 “ZQBX ” 之后的标记部分(到逗号为止)
    marker = ""
    if (match($0, /ZQBX /)) {
        start_pos = RSTART + RLENGTH
        # 在 start_pos 之后查找逗号
        after = substr($0, start_pos)
        comma_in_after = index(after, ",")
        if (comma_in_after == 0) {
            marker = after
        } else {
            marker = substr(after, 1, comma_in_after - 1)
        }
    }

    # 去掉 marker 的前导空格
    sub(/^ +/, "", marker)

    # 根据模式生成新 ID
    if (marker ~ /^chromosome /) {
        # 提取数字部分(去掉 “chromosome ”)
        num = marker
        sub(/^chromosome /, "", num)
        new_id = "chr" num
    } else if (marker ~ /^Scaffold/) {
        new_id = marker
    } else {
        # 兜底:保留原始 accession(如 CM132175.1)
        tmp = substr($0, 2)      # 去掉 “>”
        sub(/ .*/, "", tmp)
        new_id = tmp
    }

    print ">" new_id rest
    next
}
{ print }
' Pseudotaxus_chienii.fa > Pseudotaxus_chienii_renamed.fa
seqkit seq -w 60 Pseudotaxus_chienii_renamed.fa -o Pseudotaxus_chienii_renamed_formatted.fa

最后保留下来的物种

(base) zhanglab@zhanglab-Precision-3680:/HDD3/jch/Gymnosperm_genome_26_5_11/genome_data/genome_saved_26_6_4$ cat 21species_working_set.tsv 
物种名    分类    完整拉丁名    中文名    基因组(fa)    GFF    PEP    CDS    完整性    用途
# === 核心裸子植物 (16) ===
Cgigantea    [G]    Cupressus gigantea    巨柏    Cgigantea.fa    Cgigantea.gff    Cgigantea.pep    Cgigantea.cds    完整    内群
Cjaponica    [G]    Cryptomeria japonica    日本柳杉    Cjaponica.fa    Cjaponica.gff    Cjaponica.pep    Cjaponica.cds    完整    内群
Cpanzhihuaensis    [G]    Cycas panzhihuaensis    攀枝花苏铁    Cpanzhihuaensis.fa    Cpanzhihuaensis.gff    Cpanzhihuaensis.pep    Cpanzhihuaensis.cds    完整    内群
Fhodginsii    [G]    Fokienia hodginsii    福建柏    Fhodginsii.fa    Fhodginsii.gff    Fhodginsii.pep    Fhodginsii.cds    完整    内群
Gbiloba    [G]    Ginkgo biloba    银杏    Gbiloba.fa    Gbiloba.gff    Gbiloba.pep    Gbiloba.cds    完整    内群
Mglyptostroboides    [G]    Metasequoia glyptostroboides    水杉    Mglyptostroboides.fa    Mglyptostroboides.gff    Mglyptostroboides.pep    Mglyptostroboides.cds    完整    内群
Pdensiflora    [G]    Pinus densiflora    赤松    Pdensiflora.fa    Pdensiflora.gff    Pdensiflora.pep    Pdensiflora.cds    完整    内群
Platycladus_orientalis    [G]    Platycladus orientalis    侧柏    Platycladus_orientalis.fa    Platycladus_orientalis_mRNA_longest.gff3    Platycladus_orientalis.pep    Platycladus_orientalis.cds    完整    内群
Pseudotaxus_chienii    [G]    Pseudotaxus chienii    白豆杉    Pseudotaxus_chienii.fa    Pseudotaxus_chienii.gff    Pseudotaxus_chienii.pep    Pseudotaxus_chienii.cds    完整    内群
Ptabuliformis    [G]    Pinus tabuliformis    油松    Ptabuliformis.fa    Ptabuliformis.gff    Ptabuliformis.pep    Ptabuliformis.cds    完整    内群
Sgiganteum    [G]    Sequoiadendron giganteum    巨杉    Sgiganteum.fa    Sgiganteum.gff    Sgiganteum.pep    Sgiganteum.cds    完整    内群
Tchinensis    [G]    Taxus chinensis    红豆杉    Tchinensis.fa    Tchinensis.gff    Tchinensis.pep    Tchinensis.cds    完整    内群
Tdistichum    [G]    Taxodium distichum    落羽杉    Tdistichum.fa    Tdistichum.gff    Tdistichum.pep    Tdistichum.cds    完整    内群
Tgrandis    [G]    Torreya grandis    香榧    female_chr_Nc.fna    female_ba_last.gff3    Tgrandis.pep    Tgrandis.cds    完整    内群
Twallichiana    [G]    Taxus wallichiana    喜马拉雅红豆杉    Twallichiana.1.fa    Twallichiana.1.gff    Twallichiana_longest.pep    Twallichiana_longest.cds    完整    内群
Clanceolata    [G]    Cunninghamia lanceolata    杉木    Clanceolata.fa    Clanceolata.gff    Clanceolata.pep    Clanceolata.cds    完整    内群
# === 买麻藤类 (2, 裸子植物外类群) ===
Gmontanum    [G]    Gnetum montanum    买麻藤    Gmontanum.fa    Gmontanum.gff    Gmontanum.pep    Gmontanum.cds    完整    外类群
Wmirabilis    [G]    Welwitschia mirabilis    百岁兰    Wmirabilis.fa    Wmirabilis.gff    Wmirabilis.pep    Wmirabilis.cds    完整    外类群
# === 被子植物/苔藓外类群 (3) ===
Atrichopoda    [AG]    Amborella trichopoda    无油樟    Atrichopoda.fa    Atrichopoda.gff    Atrichopoda.pep    Atrichopoda.cds    完整    外类群
Mpolymorpha    [OT]    Marchantia polymorpha    地钱    Mpolymorpha.fa    Mpolymorpha.gff    Mpolymorpha.pep    Mpolymorpha.cds    完整    外类群
Ncolorata    [AG]    Nymphaea colorata    蓝睡莲    Ncolorata.fa    Ncolorata.gff    Ncolorata.pep    Ncolorata.cds    完整    外类群
# === 补充信息 ==
# 这个数据集中的相当部分是梁老师下载的,我补充了其中的杉木,侧柏,白豆杉,对于所有物种进行了最长转录本检查。
# 检查了pep数目是否与注释数目符合。对于不符合的物种进行了校正。

Python复写代码

我让AI复写了这个代码之后,他把这个代码分成了五部分,包括以下内容:
主要步骤:
读取控制文件(ctl file),获取物种信息、BLAST程序路径等配置。
执行BLASTP搜索(如需要),或跳过使用已有结果。
解析BLASTP输出,识别候选嵌合基因的长拷贝(long copy)和短拷贝(short copy)。
根据系统发育树和物种分组(内群/中群/外群)对候选基因进行过滤和展示。
生成HTML格式的最终结果表格。

#!/usr/bin/env python3
import os
import sys
import re
import argparse
import subprocess
from ete3 import Tree
from Bio import SeqIO

def parse_args_and_build_config():
    parser = argparse.ArgumentParser(description='GFFRIN: fusion gene analysis')
    # 参数定义保持不变
    parser.add_argument('--pep_folder', required=True, help='蛋白序列文件夹路径')
    parser.add_argument('--tree', required=True, help='系统发育树文件 (Newick格式)')
    parser.add_argument('--ingroups', required=True, help='内群物种,逗号分隔')
    parser.add_argument('--outgroups', required=True, help='外群物种,逗号分隔')
    parser.add_argument('--output_dir', required=True, help='输出目录')
    parser.add_argument('--num_threads', type=int, default=10, help='BLASTP 线程数,默认 10')
    args = parser.parse_args()

    # 1. 仅保存树文件路径,不做任何解析
    tree_path = args.tree
    if not os.path.isfile(tree_path):
        print(f"树文件不存在: {tree_path}")
        sys.exit(1)

    # 2. 扫描 PEP 文件夹,构建 protein_path
    protein_path = {}
    found_species = []  # 替代原来的 intree_check
    if not os.path.isdir(args.pep_folder):
        print(f"PEP 文件夹不存在: {args.pep_folder}")
        sys.exit(1)

    for fname in os.listdir(args.pep_folder):
        if fname.endswith(('.fa', '.fasta', '.faa')):
            species = fname.split('.')[0]
            if not species:
                continue
            full_path = os.path.join(args.pep_folder, fname)
            if not os.path.isfile(full_path):
                continue
            if species in protein_path:
                print(f"警告:物种名 {species} 重复,使用第一个文件。")
            else:
                protein_path[species] = full_path
                found_species.append(species)

    if not protein_path:
        print(f"错误:在 {args.pep_folder} 中未找到任何蛋白文件。")
        sys.exit(1)

    # 3. 解析 ingroups / outgroups
    focus_species_name = {}
    for sp in args.ingroups.split(','):
        sp = sp.strip()
        if sp:
            focus_species_name[sp] = True

    outgroup_species_name = {}
    for sp in args.outgroups.split(','):
        sp = sp.strip()
        if sp:
            outgroup_species_name[sp] = True

    # 4. 基本校验:内群/外群不能重叠,且必须存在于 PEP 文件夹的物种列表中
    all_species = set(found_species)
    for sp in focus_species_name:
        if sp in outgroup_species_name:
            print(f"错误:{sp} 同时在内群和外群中!")
            sys.exit(1)
        if sp not in all_species:
            print(f"错误:内群物种 {sp} 不在 PEP 文件夹中。")
            sys.exit(1)
    for sp in outgroup_species_name:
        if sp not in all_species:
            print(f"错误:外群物种 {sp} 不在 PEP 文件夹中。")
            sys.exit(1)

    output_dir = args.output_dir

    # 5. 构建精简配置字典
    config = {
        'tree_path': tree_path,          # 只存路径,留给后续包处理
        'protein_path': protein_path,
        'focus_species_name': focus_species_name,
        'outgroup_species_name': outgroup_species_name,
        'output_dir': output_dir,
        'found_species': found_species,  # 可选,替代原来的 intree_check
        'num_threads': args.num_threads,
    }
    return config
## 章老师原始的版本是设置了关注的物种,事实上,因为现在的电脑的算力资源是充分的。
## 因此完全可以考虑进行全部的blatp比对,并解析所有的分支上的结果。

def run_blastp_all_pairs(config):
    """
    对 config['found_species'] 中的所有物种两两进行 BLASTP 比对。
    """
    protein_path = config['protein_path']
    species_list = config['found_species']
    output_dir = config['output_dir']
    num_threads = config['num_threads']

    # 创建 BLAST 缓存目录
    blast_cache_dir = os.path.join(output_dir, 'blast_cache')
    os.makedirs(blast_cache_dir, exist_ok=True)

    # 1. 为每个物种建库(如果 .phr 文件不存在)
    for sp, fa_path in protein_path.items():
        db_name = os.path.join(blast_cache_dir, sp)
        # 简单的存在判断:查找 .phr 文件
        if not os.path.exists(db_name + '.phr'):
            print(f"[建库] {sp} <- {fa_path}")
            cmd = ['makeblastdb',
                   '-in', fa_path,
                   '-dbtype', 'prot',
                   '-out', db_name]
            try:
                subprocess.run(cmd, check=True, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                print(f"建库失败 {sp}:\n{e.stderr}")
                raise
        else:
            print(f"[跳过建库] {sp} 已存在")

    # 2. 两两比对
    for sp_db in species_list:
        db_path = os.path.join(blast_cache_dir, sp_db)
        for sp_qry in species_list:
            query_path = protein_path[sp_qry]
            out_file = os.path.join(
                blast_cache_dir,
                f"{sp_qry}.2.{sp_db}_aa2aa_blast+.query"
            )
            print(f"[BLASTP] query={sp_qry} db={sp_db}")
            cmd = [
                'blastp',
                '-db', db_path,
                '-query', query_path,
                '-out', out_file,
                '-evalue', '0.00001',
                '-outfmt', '7',
                '-max_target_seqs', '10',
                '-num_threads', str(num_threads)
            ]
            try:
                subprocess.run(cmd, check=True, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                print(f"BLASTP 失败 {sp_qry} vs {sp_db}:\n{e.stderr}")
                raise


def read_fasta_lengths(fasta_path):
    """读取蛋白质FASTA文件,返回 {序列ID: 长度} 字典"""
    lengths = {}
    for record in SeqIO.parse(fasta_path, "fasta"):
        # 仅使用ID(空格前部分),对应原Perl的“space”分隔方式
        seq_id = record.id.split()[0]
        lengths[seq_id] = len(record.seq)
    return lengths


def is_overlap(start1, stop1, start2, stop2):
    """判断两个区间 [start1,stop1] 和 [start2,stop2] 是否有重叠(均视为闭区间)"""
    return max(start1, start2) <= min(stop1, stop2)


def process_blastp_results(blastp_file, query_fasta, subject_fasta,
                           long_copy_out, short_copy_out):
    """
    解析 BLASTP 的 -outfmt 7 输出,并将命中分为长拷贝和短拷贝。

    参数
    ----
    blastp_file : str    BLASTP 结果文件路径
    query_fasta : str    查询物种的蛋白质FASTA文件
    subject_fasta : str  目标物种的蛋白质FASTA文件
    long_copy_out : str  长拷贝输出文件路径
    short_copy_out : str 短拷贝输出文件路径
    """

    # 获取序列长度
    query_len = read_fasta_lengths(query_fasta)
    subject_len = read_fasta_lengths(subject_fasta)

    # 存储当前 query 的所有命中:{subject_id: (q_start, q_end, t_start, t_end)}
    hits = {}
    current_query = None

    # 打开输出文件(句柄传给 scan_chimeric 写入)
    long_fh = open(long_copy_out, 'w')
    short_fh = open(short_copy_out, 'w')

    try:
        # 读取 BLASTP 结果
        with open(blastp_file, 'r') as f:
            for line in f:
                # 跳过注释行
                if line.startswith('#'):
                    # 遇到新 query 时,处理之前累积的 hits
                    if line.startswith('# Query: ') and current_query is not None:
                        scan_chimeric(current_query, hits, query_len, subject_len,
                                      long_fh, short_fh)
                        hits = {}
                    if line.startswith('# Query: '):
                        current_query = line.split(': ')[1].strip()
                    continue

                # 解析数据行
                parts = line.strip().split('\t')
                if len(parts) < 10:
                    continue
                qid = parts[0]
                sid = parts[1]
                q_start = int(parts[6])
                q_end   = int(parts[7])
                t_start = int(parts[8])
                t_end   = int(parts[9])

                if qid != current_query:
                    # 理论上不会发生,但安全处理
                    if current_query is not None:
                        scan_chimeric(current_query, hits, query_len, subject_len,
                                      long_fh, short_fh)
                        hits = {}
                    current_query = qid

                # 合并同一 query-subject 对(取坐标的并集)
                if sid in hits:
                    old_qs, old_qe, old_ts, old_te = hits[sid]
                    hits[sid] = (min(q_start, old_qs),
                                 max(q_end, old_qe),
                                 min(t_start, old_ts),
                                 max(t_end, old_te))
                else:
                    hits[sid] = (q_start, q_end, t_start, t_end)

        # 处理最后一个 query
        if current_query is not None and hits:
            scan_chimeric(current_query, hits, query_len, subject_len,
                          long_fh, short_fh)
    finally:
        long_fh.close()
        short_fh.close()


def scan_chimeric(query_id, hits, query_len_dict, subject_len_dict,long_fh, short_fh):
    """
    对一个 query 的所有 subject 命中进行分析,
    分出长拷贝(覆盖度>=80%)和短拷贝(去重叠后保留覆盖度>=20%的)。
    """
    if query_id not in query_len_dict:
        return
    qlen = query_len_dict[query_id]

    short_candidates = []  # 列表元素: (subject_id, q_start, q_end, t_start, t_end, coverage)

    # 第一步:分类
    for subject_id, (qs, qe, ts, te) in hits.items():
        if subject_id not in subject_len_dict:
            continue
        slen = subject_len_dict[subject_id]

        # 计算相对位置(归一化到0~1,保留三位小数,与原Perl一致)
        qs_rel = round(qs / qlen, 3)
        qe_rel = round(qe / qlen, 3)
        ts_rel = round(ts / slen, 3)
        te_rel = round(te / slen, 3)
        coverage = round(qe_rel - qs_rel, 3)

        if coverage >= 0.8:
            # 长拷贝:直接输出
            long_fh.write(f"{query_id}\t{subject_id}\t{qs_rel}\t{qe_rel}\t"
                          f"{coverage}\t{ts_rel}\t{te_rel}\n")
        else:
            short_candidates.append((subject_id, qs_rel, qe_rel, ts_rel, te_rel, coverage))

    # 第二步:去重叠,保留覆盖度最大的短拷贝
    # 按 coverage 降序排序
    short_candidates.sort(key=lambda x: x[5], reverse=True)

    retained = []
    for item in short_candidates:
        sid, qs, qe, ts, te, cov = item
        # 检查是否与已保留的短拷贝重叠
        overlap = False
        for r in retained:
            if r[0] == sid:  # 同一 subject 不去重
                continue
            if is_overlap(qs, qe, r[1], r[2]):
                overlap = True
                break
        if not overlap:
            retained.append(item)

    # 第三步:输出覆盖度 >= 0.2 的短拷贝
    for sid, qs, qe, ts, te, cov in retained:
        if cov >= 0.2:
            short_fh.write(f"{query_id}\t{sid}\t{qs}\t{qe}\t{cov}\t{ts}\t{te}\n")




def filter_short_copies(short_copy_file, ingroup_sp, target_sp, output_dir):
    """
    对短拷贝结果进行二次筛选(对应 Perl 的 "more_than_2" 逻辑):
    1. 统计每个基因的短拷贝数,过滤掉自比(query 核心名 == subject 核心名)
    2. 保留有 >1 个短拷贝的基因,且确保来自不同的目标基因
    3. 累计每个基因的总覆盖度,保留 >= 0.8 的
    4. 去重后输出 .ids 文件

    返回: (more_than_2_file, ids_file)
    """
    chimeric = {}   # gene_id -> count
    info = {}       # gene_id -> {number -> line}

    with open(short_copy_file, 'r') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            parts = line.split('\t')
            if len(parts) < 7:
                continue

            # 与 Perl 完全一致:整个行按 [\s+|.] 分割,比较索引0和索引2
            svg_parts = re.split(r'[\s\+\|\.]', line)
            if len(svg_parts) >= 3 and svg_parts[0] == svg_parts[2]:
                continue  # 跳过自比

            query_id = parts[0]
            if query_id not in chimeric:
                chimeric[query_id] = 0
                info[query_id] = {}
            chimeric[query_id] += 1
            info[query_id][chimeric[query_id]] = line

    # 写入 more_than_2 文件(按短拷贝数降序)
    out_dir_ingroup = os.path.join(output_dir, ingroup_sp)
    more_than_2_file = os.path.join(
        out_dir_ingroup,
        f"{ingroup_sp}.2.{target_sp}.more_than_2_short_copy.ids"
    )

    with open(more_than_2_file, 'w') as out_f:
        for gene_id, count in sorted(chimeric.items(), key=lambda x: x[1], reverse=True):
            if count <= 1:
                continue
            # 按不同目标基因去重(与 Perl 的 tmphash 逻辑一致)
            distinct = {}
            for i in range(1, count + 1):
                line_i = info[gene_id][i]
                line_parts = line_i.split('\t')
                subj_id = line_parts[1]
                # 确定去重 key:如果 query_id 含 .+| 则取 subject 的第一段,否则取完整 subject_id
                if '.' in line_parts[0] or '+' in line_parts[0] or '|' in line_parts[0]:
                    subj_key = re.split(r'[\.\+\|]', subj_id)[0]
                else:
                    subj_key = subj_id
                if subj_key not in distinct:
                    distinct[subj_key] = line_i

            if len(distinct) > 1:
                for key in sorted(distinct.keys()):
                    out_f.write(distinct[key] + '\n')

    # 累计覆盖度,保留 >= 0.8 的基因
    coverage_sum = {}
    with open(more_than_2_file, 'r') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            parts = line.split('\t')
            gene_id = parts[0]
            cov = float(parts[4])
            coverage_sum[gene_id] = coverage_sum.get(gene_id, 0.0) + cov

    tmpids_file = os.path.join(out_dir_ingroup, f"{target_sp}.tmpids")
    with open(tmpids_file, 'w') as f:
        for gene_id, total_cov in coverage_sum.items():
            if total_cov >= 0.8:
                f.write(f"{gene_id}\n")

    # awk '!a[$1]++' 去重
    ids_file = os.path.join(out_dir_ingroup, f"{target_sp}.ids")
    seen = set()
    with open(tmpids_file, 'r') as fin, open(ids_file, 'w') as fout:
        for line in fin:
            gene_id = line.strip()
            if gene_id and gene_id not in seen:
                seen.add(gene_id)
                fout.write(f"{gene_id}\n")

    return more_than_2_file, ids_file


def process_ingroup_results(config, skip_step2=False):
    """
    处理所有内群物种的 BLAST 结果,并构建 group1 / group2。
    对应 Perl 中遍历 %focus_species_name 的主循环 + 第六步筛选。

    参数
    ----
    config : dict  配置字典
    skip_step2 : bool  如果为 True,跳过处理,直接从已有文件构建 group1/group2

    返回
    -------
    (group1, group2) : (list of str, list of str)
        group1 格式: "target_sp:ingroup_sp/target_sp.ids"
        group2 格式: "target_sp:ingroup_sp/ingroup_sp.2.target_sp.candidate_chimerics_long_copy.ids"
    """
    protein_path = config['protein_path']
    focus_species_name = config['focus_species_name']
    output_dir = config['output_dir']
    blast_cache_dir = os.path.join(output_dir, 'blast_cache')

    group1 = []
    group2 = []

    if skip_step2:
        for ingroup_sp in sorted(focus_species_name.keys()):
            for target_sp in sorted(protein_path.keys()):
                group1.append(f"{target_sp}:{ingroup_sp}/{target_sp}.ids")
                file4 = f"{ingroup_sp}.2.{target_sp}.candidate_chimerics_long_copy.ids"
                group2.append(f"{target_sp}:{ingroup_sp}/{file4}")
        return group1, group2

    for ingroup_sp in sorted(focus_species_name.keys()):
        ingroup_dir = os.path.join(output_dir, ingroup_sp)
        os.makedirs(ingroup_dir, exist_ok=True)

        query_fasta = protein_path[ingroup_sp]

        for target_sp in sorted(protein_path.keys()):
            target_fasta = protein_path[target_sp]

            blast_file = os.path.join(
                blast_cache_dir,
                f"{ingroup_sp}.2.{target_sp}_aa2aa_blast+.query"
            )

            if not os.path.isfile(blast_file):
                print(f"[跳过] BLAST 结果不存在: {blast_file}")
                continue

            long_copy_file = os.path.join(
                ingroup_dir,
                f"{ingroup_sp}.2.{target_sp}.candidate_chimerics_long_copy.ids"
            )
            short_copy_file = os.path.join(
                ingroup_dir,
                f"{ingroup_sp}.2.{target_sp}.candidate_chimeric_short_copy.ids"
            )

            print(f"[解析 BLAST] {ingroup_sp} vs {target_sp}")
            process_blastp_results(
                blast_file, query_fasta, target_fasta,
                long_copy_file, short_copy_file
            )

            # group2: 记录长拷贝文件
            group2.append(
                f"{target_sp}:{ingroup_sp}/"
                f"{ingroup_sp}.2.{target_sp}.candidate_chimerics_long_copy.ids"
            )

        # 第六步:对短拷贝进行二次筛选
        for target_sp in sorted(protein_path.keys()):
            short_copy_file = os.path.join(
                ingroup_dir,
                f"{ingroup_sp}.2.{target_sp}.candidate_chimeric_short_copy.ids"
            )

            if not os.path.isfile(short_copy_file):
                continue

            print(f"[筛选短拷贝] {ingroup_sp} vs {target_sp}")
            filter_short_copies(short_copy_file, ingroup_sp, target_sp, output_dir)
            group1.append(f"{target_sp}:{ingroup_sp}/{target_sp}.ids")

    return group1, group2


def get_tree_levels(tree, focus_species, outgroup_species):
    """
    从系统发育树生成不同分类层级(对应 Perl 的 @species_in_edit_tree 迭代)。
    从焦点物种向根节点逐层上移,每层产生 (ingroup, midgroup, outgroup) 三元组。
    仅当 midgroup 非空时才产出(与 Perl 的 check_midgroup 逻辑一致)。
    """
    all_leaves = set(tree.get_leaf_names())
    outgroup_set = set(outgroup_species)

    focus_leaf = tree.search_nodes(name=focus_species)[0]

    node = focus_leaf
    while node.up is not None:
        parent = node.up
        if parent.is_root():
            ingroup_set = set(parent.get_leaf_names())
            midgroup_set = all_leaves - ingroup_set - outgroup_set
            if midgroup_set:
                yield (sorted(ingroup_set), sorted(midgroup_set), sorted(outgroup_set))
            # 也尝试仅焦点物种自身作为内群
            ingroup_set = {focus_species}
            midgroup_set = all_leaves - ingroup_set - outgroup_set
            if midgroup_set:
                yield (sorted(ingroup_set), sorted(midgroup_set), sorted(outgroup_set))
            break

        ingroup_set = set(parent.get_leaf_names())
        midgroup_set = all_leaves - ingroup_set - outgroup_set
        if midgroup_set:
            yield (sorted(ingroup_set), sorted(midgroup_set), sorted(outgroup_set))

        node = parent


def build_chimeric_matrix(config, group1, group2, focus_species):
    """
    从 group1 / group2 构建嵌合基因矩阵。
    返回 (all_chimeric_hash, species_chimeric_hash, candidate_orthologous)

    - all_chimeric_hash: set of all candidate gene IDs
    - species_chimeric_hash: {gene_id: {species: 0/1}}  短拷贝存在性
    - candidate_orthologous:  {gene_id: {species: 0/1}}  长拷贝存在性
    """
    all_species = config['found_species']
    output_dir = config['output_dir']

    all_chimeric = set()
    species_chimeric = {}    # {gene_id: {species: 0/1}}
    candidate_orthologous = {}  # {gene_id: {species: 0/1}}

    # 从 group1 加载短拷贝信息
    for entry in group1:
        species, file_name = entry.split(':', 1)
        parts = file_name.split('/')
        if parts[0] != focus_species:
            continue
        file_path = os.path.join(output_dir, file_name)
        if not os.path.isfile(file_path):
            continue
        with open(file_path, 'r') as f:
            for line in f:
                gene_id = line.strip()
                if not gene_id:
                    continue
                all_chimeric.add(gene_id)
                if gene_id not in species_chimeric:
                    species_chimeric[gene_id] = {}
                species_chimeric[gene_id][species] = 1

    # 初始化:所有物种的短拷贝/长拷贝都设为 0
    for gene_id in all_chimeric:
        for sp in all_species:
            if sp not in species_chimeric[gene_id]:
                species_chimeric[gene_id][sp] = 0
        if gene_id not in candidate_orthologous:
            candidate_orthologous[gene_id] = {}
        for sp in all_species:
            candidate_orthologous[gene_id][sp] = 0

    # 从 group2 加载长拷贝信息
    for entry in group2:
        species, file_name = entry.split(':', 1)
        parts = file_name.split('/')
        if parts[0] != focus_species:
            continue
        file_path = os.path.join(output_dir, file_name)
        if not os.path.isfile(file_path):
            continue
        with open(file_path, 'r') as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                cols = line.split('\t')
                gene_id = cols[0]
                if gene_id in all_chimeric:
                    candidate_orthologous[gene_id][species] = 1

    return all_chimeric, species_chimeric, candidate_orthologous


def compute_pattern(count_in_group, group_size):
    """
    与 Perl 一致的 0/1 判定逻辑:
    - 如果 group_size == 1: 只要有就算 1
    - 如果 group_size >= 2: 需要 >= (group_size - 1) 个物种中都有才算 1
    """
    if group_size <= 1:
        return '1' if count_in_group >= 1 else '0'
    else:
        return '1' if count_in_group >= (group_size - 1) else '0'


def get_long_copy_details(gene_id, ingroup_sp, species, output_dir):
    """
    从长拷贝文件中提取该基因在指定物种中的详细信息。
    返回 HTML 格式的字符串(不含基因ID前缀,<br>分隔),与 Perl 的 grep+awk 一致。
    """
    file_path = os.path.join(
        output_dir, ingroup_sp,
        f"{ingroup_sp}.2.{species}.candidate_chimerics_long_copy.ids"
    )
    if not os.path.isfile(file_path):
        return ""
    lines = []
    with open(file_path, 'r') as f:
        for line in f:
            line = line.strip()
            if line.startswith(gene_id + '\t'):
                # 去掉基因ID前缀,追加 <br>(对应 Perl: $info[$i]=~s/$key//; $info[$i].="<br>")
                detail = line[len(gene_id):]
                lines.append(detail + "<br>")
    return ''.join(lines)


def get_short_copy_details(gene_id, ingroup_sp, species, output_dir):
    """
    从 more_than_2 或原始 short_copy 文件中提取短拷贝详细信息。
    返回 (details_html, lines_for_draw)。
    lines_for_draw 是用于绘图的原始行列表。
    """
    # 先尝试 more_than_2 文件
    more_file = os.path.join(
        output_dir, ingroup_sp,
        f"{ingroup_sp}.2.{species}.more_than_2_short_copy.ids"
    )
    sc_file = os.path.join(
        output_dir, ingroup_sp,
        f"{ingroup_sp}.2.{species}.candidate_chimeric_short_copy.ids"
    )

    info_lines = []
    if os.path.isfile(more_file):
        with open(more_file, 'r') as f:
            for line in f:
                line = line.strip()
                if line.startswith(gene_id + '\t'):
                    info_lines.append(line)
    elif os.path.isfile(sc_file):
        with open(sc_file, 'r') as f:
            for line in f:
                line = line.strip()
                if line.startswith(gene_id + '\t'):
                    info_lines.append(line)

    details = []
    for line in info_lines:
        detail = line[len(gene_id):]
        details.append(detail + "<br>")
    return ''.join(details), info_lines


def draw_chimeric_region(info_lines):
    """
    对应 Perl 的 draw_chimeric_region 子程序。
    根据短拷贝坐标绘制红绿相间的条形图(HTML 表格)。
    """
    if not info_lines:
        return ""

    positions = []
    for line in info_lines:
        parts = line.strip().split('\t')
        # 格式: query_id\tsubject_id\tqs\tqe\tcov\tts\tte
        if len(parts) >= 4:
            positions.append(float(parts[2]))  # q_start
            positions.append(float(parts[3]))  # q_end

    positions.sort()

    html_parts = [
        '<table border="0" cellspacing="0" cellpadding="0" width="100%"><tr bgcolor="red">'
    ]

    for i, pos in enumerate(positions):
        if i == 0:
            percent = max(0, int(pos * 100 - 1))
        else:
            percent = max(0, int((pos - positions[i - 1]) * 100))

        if i % 2 == 0:
            # 偶数索引:空白(红色背景下的空白区域 = 非嵌合区)
            html_parts.append(f'<td width="{percent}%">&nbsp;</td>')
        else:
            # 奇数索引:绿色(嵌合区域)
            html_parts.append(f'<td width="{percent}%" bgcolor="green">&nbsp;</td>')

    # 最后一段空白
    final_percent = max(0, 100 - int(positions[-1] * 100))
    html_parts.append(f'<td width="{final_percent}%">&nbsp;</td>')
    html_parts.append('</tr></table>')

    return ''.join(html_parts)


def generate_final_output(config, group1, group2):
    """
    第三步:构建嵌合基因矩阵,按树层级分类,检测最终嵌合基因,生成 HTML 报告。
    对应 Perl 第 463-819 行。
    """
    tree_path = config['tree_path']
    focus_species_name = config['focus_species_name']
    outgroup_species_name = config['outgroup_species_name']
    output_dir = config['output_dir']

    # 解析系统发育树
    with open(tree_path, 'r') as f:
        newick_str = f.read().strip()
    tree = Tree(newick_str, format=1)

    # 打开 all_details.txt
    all_details_path = os.path.join(output_dir, 'all_details.txt')
    all_details_fh = open(all_details_path, 'w')

    for focus_sp in sorted(focus_species_name.keys()):
        # 输出最终嵌合基因 ID 列表
        final_ids_path = os.path.join(output_dir, f"{focus_sp}.final_chimeric_gene_ids")
        final_ids_fh = open(final_ids_path, 'w')

        # 构建矩阵
        all_chimeric, species_chimeric, candidate_orthologous = \
            build_chimeric_matrix(config, group1, group2, focus_sp)

        # 生成 HTML
        html_path = os.path.join(output_dir, f"{focus_sp}_final_chimeric_genes.html")
        html_fh = open(html_path, 'w')
        html_fh.write(f"""<head>
        <title>Chimeric genes at {focus_sp}</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <meta name="keywords" content="chimeric gene, whole genome level scan" />
        <meta name="description" content="chimeric gene scan results" />
        <meta name="author" content="Chengjun Zhang" />
        <meta name="copyright" content="Copyright @ Long lab" />
        </head><body>
        <STYLE type="text/css">
            td {{white-space:nowrap}}
        </STYLE>
""")

        # 遍历树的不同层级
        for ingroup_names, midgroup_names, outgroup_names in \
                get_tree_levels(tree, focus_sp, list(outgroup_species_name.keys())):

            ingroup = [x for x in ingroup_names if x]
            midgroup = [x for x in midgroup_names if x]
            outgroup = [x for x in outgroup_names if x]

            n_ingroup = len(ingroup)
            n_midgroup = len(midgroup)
            n_outgroup = len(outgroup)

            all_details_fh.write(f"INGROUP {' '.join(ingroup)}\n")
            all_details_fh.write(f"MIDGROUP {' '.join(midgroup)}\n")
            all_details_fh.write(f"OUTGROUP {' '.join(outgroup)}\n")

            # HTML 表格头部
            html_fh.write(f"<div><h3>{focus_sp}</h3></div>\n")
            html_fh.write(
                '<div><table border="1" cellspacing="1" cellpadding="1" width="100%">\n'
            )
            html_fh.write(
                f'<TR><td>chimeric gene id</td>'
                f'<td colspan={n_ingroup}>INGROUP</td>'
                f'<td colspan={n_midgroup}>MIDGROUP</td>'
                f'<td colspan={n_outgroup}>OUTGROUP</td></tr>\n'
                f'<TR bgcolor="#F0FFFF"><td>&nbsp;</td>'
            )
            for sp in ingroup:
                html_fh.write(f"<td>{sp}</td>\n")
            for sp in midgroup:
                html_fh.write(f"<td>{sp}</td>\n")
            for sp in outgroup:
                html_fh.write(f"<td>{sp}</td>\n")
            html_fh.write("</tr>\n")

            # 遍历每个候选嵌合基因
            for gene_id in sorted(all_chimeric):
                # 计算各群中的短拷贝和长拷贝计数
                ingroup_sc = sum(species_chimeric[gene_id].get(sp, 0) for sp in ingroup)
                midgroup_sc = sum(species_chimeric[gene_id].get(sp, 0) for sp in midgroup)
                outgroup_sc = sum(species_chimeric[gene_id].get(sp, 0) for sp in outgroup)

                ingroup_lc = sum(candidate_orthologous[gene_id].get(sp, 0) for sp in ingroup)
                midgroup_lc = sum(candidate_orthologous[gene_id].get(sp, 0) for sp in midgroup)
                outgroup_lc = sum(candidate_orthologous[gene_id].get(sp, 0) for sp in outgroup)

                # 构建 0/1 模式字符串
                short_pattern = (
                    compute_pattern(ingroup_sc, n_ingroup) +
                    compute_pattern(midgroup_sc, n_midgroup) +
                    compute_pattern(outgroup_sc, n_outgroup)
                )
                long_pattern = (
                    compute_pattern(ingroup_lc, n_ingroup) +
                    compute_pattern(midgroup_lc, n_midgroup) +
                    compute_pattern(outgroup_lc, n_outgroup)
                )

                # 判断是否为最终嵌合基因
                is_chimeric = (
                    (long_pattern == "110" and short_pattern == "111") or
                    (long_pattern == "100" and short_pattern in ("111", "110"))
                )

                if not is_chimeric:
                    continue

                final_ids_fh.write(f"{gene_id}\n")

                # 输出 HTML 表格行:第一行 = 长拷贝信息
                html_fh.write(f"<tr><td rowspan=2>{gene_id}</td>")
                for sp in ingroup:
                    details = get_long_copy_details(gene_id, focus_sp, sp, output_dir)
                    ortho_val = candidate_orthologous[gene_id].get(sp, 0)
                    html_fh.write(
                        f'<td><a href="#">{ortho_val}</a><br>{details}</td>\n'
                    )
                for sp in midgroup:
                    details = get_long_copy_details(gene_id, focus_sp, sp, output_dir)
                    ortho_val = candidate_orthologous[gene_id].get(sp, 0)
                    html_fh.write(
                        f'<td><a href="#">{ortho_val}</a><br>{details}</td>\n'
                    )
                for sp in outgroup:
                    details = get_long_copy_details(gene_id, focus_sp, sp, output_dir)
                    ortho_val = candidate_orthologous[gene_id].get(sp, 0)
                    html_fh.write(
                        f'<td><a href="#">{ortho_val}</a><br>{details}</td>\n'
                    )

                html_fh.write("</tr><tr>")

                # 第二行:短拷贝信息 + 可视化
                for sp in ingroup:
                    details, draw_lines = get_short_copy_details(
                        gene_id, focus_sp, sp, output_dir
                    )
                    sc_val = species_chimeric[gene_id].get(sp, 0)

                    if sc_val == 1:
                        # 短拷贝恰好 1:直接用 more_than_2 数据绘图
                        region_html = draw_chimeric_region(draw_lines)
                        html_fh.write(
                            f'<td><a href="#">{sc_val}</a><br>{details}\n'
                            f'{region_html}</td>\n'
                        )
                    else:
                        # 短拷贝 >1 或不存在:从原始 short_copy 文件获取绘图数据
                        sc_file = os.path.join(
                            output_dir, focus_sp,
                            f"{focus_sp}.2.{sp}.candidate_chimeric_short_copy.ids"
                        )
                        raw_draw_lines = []
                        if os.path.isfile(sc_file):
                            with open(sc_file, 'r') as f:
                                for line in f:
                                    line = line.strip()
                                    if line.startswith(gene_id + '\t'):
                                        raw_draw_lines.append(line)
                        if raw_draw_lines:
                            if draw_lines:
                                region_html = draw_chimeric_region(draw_lines)
                            else:
                                region_html = draw_chimeric_region(raw_draw_lines[:1])
                            # 显示原始短拷贝信息
                            raw_details = []
                            for rl in raw_draw_lines:
                                raw_details.append(rl[len(gene_id):] + "<br>")
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>'
                                f'{"".join(raw_details)}\n'
                                f'{region_html}</td>\n'
                            )
                        else:
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>{details}</td>\n'
                            )

                for sp in midgroup:
                    details, draw_lines = get_short_copy_details(
                        gene_id, focus_sp, sp, output_dir
                    )
                    sc_val = species_chimeric[gene_id].get(sp, 0)

                    if sc_val == 1:
                        region_html = draw_chimeric_region(draw_lines)
                        html_fh.write(
                            f'<td><a href="#">{sc_val}</a><br>{details}\n'
                            f'{region_html}</td>\n'
                        )
                    else:
                        sc_file = os.path.join(
                            output_dir, focus_sp,
                            f"{focus_sp}.2.{sp}.candidate_chimeric_short_copy.ids"
                        )
                        raw_draw_lines = []
                        if os.path.isfile(sc_file):
                            with open(sc_file, 'r') as f:
                                for line in f:
                                    line = line.strip()
                                    if line.startswith(gene_id + '\t'):
                                        raw_draw_lines.append(line)
                        if raw_draw_lines:
                            if draw_lines:
                                region_html = draw_chimeric_region(draw_lines)
                            else:
                                region_html = draw_chimeric_region(raw_draw_lines[:1])
                            raw_details = []
                            for rl in raw_draw_lines:
                                raw_details.append(rl[len(gene_id):] + "<br>")
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>'
                                f'{"".join(raw_details)}\n'
                                f'{region_html}</td>\n'
                            )
                        else:
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>{details}</td>\n'
                            )

                for sp in outgroup:
                    details, draw_lines = get_short_copy_details(
                        gene_id, focus_sp, sp, output_dir
                    )
                    sc_val = species_chimeric[gene_id].get(sp, 0)

                    if sc_val == 1:
                        region_html = draw_chimeric_region(draw_lines)
                        html_fh.write(
                            f'<td><a href="#">{sc_val}</a><br>{details}\n'
                            f'{region_html}</td>\n'
                        )
                    else:
                        sc_file = os.path.join(
                            output_dir, focus_sp,
                            f"{focus_sp}.2.{sp}.candidate_chimeric_short_copy.ids"
                        )
                        raw_draw_lines = []
                        if os.path.isfile(sc_file):
                            with open(sc_file, 'r') as f:
                                for line in f:
                                    line = line.strip()
                                    if line.startswith(gene_id + '\t'):
                                        raw_draw_lines.append(line)
                        if raw_draw_lines:
                            if draw_lines:
                                region_html = draw_chimeric_region(draw_lines)
                            else:
                                region_html = draw_chimeric_region(raw_draw_lines[:1])
                            raw_details = []
                            for rl in raw_draw_lines:
                                raw_details.append(rl[len(gene_id):] + "<br>")
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>'
                                f'{"".join(raw_details)}\n'
                                f'{region_html}</td>\n'
                            )
                        else:
                            html_fh.write(
                                f'<td><a href="#">{sc_val}</a><br>{details}</td>\n'
                            )

                html_fh.write("</tr>\n")

            html_fh.write("</table></div>\n")

        html_fh.write("</body></html>\n")
        html_fh.close()
        final_ids_fh.close()

        print(f"[完成] {focus_sp}: 最终嵌合基因 -> {final_ids_path}")
        print(f"[完成] {focus_sp}: HTML 报告 -> {html_path}")

    all_details_fh.close()
    print(f"[完成] 全部分类明细 -> {all_details_path}")


if __name__ == "__main__":
    config = parse_args_and_build_config()
    print("控制台输入解析成功。")
    print("树文件路径:", config['tree_path'])
    print("发现物种:", config['found_species'])
    print("内群:", list(config['focus_species_name'].keys()))
    print("外群:", list(config['outgroup_species_name'].keys()))
    print("输出目录:", config['output_dir'])

    # 第一步:运行所有两两 BLASTP 比对
    run_blastp_all_pairs(config)

    # 第二步:解析 BLAST 结果并筛选短拷贝
    group1, group2 = process_ingroup_results(config)
    print(f"\n处理完成。group1: {len(group1)} 条, group2: {len(group2)} 条")

    # 第三步:树层级分类 + 嵌合基因检测 + HTML 报告
    generate_final_output(config, group1, group2)

我跑的out忘记指定了,明天重新跑吧,现在来看blastp消耗了大量的时间,还是很有必要处理一下的。

对blatsp版本不通的解决办法

在数据集相同的情况下,让python版本的程序和perl版本的程序调用相同的blastp文件。

blastp命令报错

我也遇到了这个错误,上github查看,果然有:

https://github.com/zhangcj2022/GriffinDetector/issues/4

这个是因为2.2.31版本以及我们目前最新版本的blastp建库命令已经不是这个命令

$cmd="$makeblastdb -in $dbfile -hash_index -dbtype prot -parse_seqids -gi_mask";

手动修改成这个命令

makeblastdb -in $dbfile -dbtype prot

还有一个bug,他这里的配置文件的文件名必须是锁死的GriffinDetector.ctl

但是命令行调用的时候又要手动输入,所以很容易让人以为这个文件是可以随意修改的。

comm -23 <(sort -u /project1/data/identified_chimerics/grandisfinal_chimeric_gene_ids) <(sort -u /project1/data/used_for_fusion/output/grandis.final_chimeric_gene_ids)

Perl版本与python版本结果的匹配问题

perl版本比最后一个python版本多了4个基因(去冗余后),先检查python侧

for G in evm_model_PTG001531L_56 evm_model_PTG001984L_93 evm_model_PTG004650L_4 evm_model_PTG011191L_11; do
    echo "=== $G ==="
    for f in grandis.2.*.candidate_chimeric_short_copy.ids; do
        count=$(grep -c "$G" "$f" 2>/dev/null)
        [ "$count" -gt 0 ] && echo "  $f: $count 行"
    done
    echo "--- long_copy ---"
    grep -l "$G" grandis.2.*.candidate_chimerics_long_copy.ids 2>/dev/null || echo "  无"
    echo "--- more_than_2 ---"
    grep -l "$G" grandis.2.*.more_than_2_short_copy.ids 2>/dev/null || echo "  无"
    echo "--- .ids ---"
    grep -l "$G" *.ids 2>/dev/null | grep -v candidate | grep -v more | grep -v tmp || echo "  无"
    echo
done

检测perl一侧的输出

(gffrin) root@53d03939188f:/project1/data/identified_chimerics/grandis# for G in evm_model_PTG001531L_56 evm_model_PTG004650L_4 evm_model_PTG011191L_11; do
    echo "=== $G ==="
    for f in grandis2*candidate_chimeric_short_copy.ids; do
        count=$(grep -c "$G" "$f" 2>/dev/null)
        [ "$count" -gt 0 ] && echo "  $f: $count 行"
    done
    echo
done
=== evm_model_PTG001531L_56 ===
  grandis2californicacandidate_chimeric_short_copy.ids: 2 行
  grandis2chinese_pinecandidate_chimeric_short_copy.ids: 2 行
  grandis2fargesiicandidate_chimeric_short_copy.ids: 2 行
  grandis2grandiscandidate_chimeric_short_copy.ids: 2 行
  grandis2jackiicandidate_chimeric_short_copy.ids: 2 行
  grandis2nuciferacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxifoliacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxus_chinensiscandidate_chimeric_short_copy.ids: 2 行

=== evm_model_PTG004650L_4 ===
  grandis2californicacandidate_chimeric_short_copy.ids: 2 行
  grandis2chinese_pinecandidate_chimeric_short_copy.ids: 2 行
  grandis2fargesiicandidate_chimeric_short_copy.ids: 2 行
  grandis2grandiscandidate_chimeric_short_copy.ids: 2 行
  grandis2jackiicandidate_chimeric_short_copy.ids: 2 行
  grandis2nuciferacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxifoliacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxus_chinensiscandidate_chimeric_short_copy.ids: 2 行

=== evm_model_PTG011191L_11 ===
  grandis2californicacandidate_chimeric_short_copy.ids: 2 行
  grandis2chinese_pinecandidate_chimeric_short_copy.ids: 2 行
  grandis2fargesiicandidate_chimeric_short_copy.ids: 2 行
  grandis2grandiscandidate_chimeric_short_copy.ids: 2 行
  grandis2jackiicandidate_chimeric_short_copy.ids: 2 行
  grandis2nuciferacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxifoliacandidate_chimeric_short_copy.ids: 2 行
  grandis2taxus_chinensiscandidate_chimeric_short_copy.ids: 2 行
(gffrin) root@53d03939188f:/project1/data/identified_chimerics/grandis# for G in evm_model_PTG001531L_56 evm_model_PTG001984L_93 evm_model_PTG004650L_4 evm_model_PTG011191L_11; do
    echo "=== $G (californica) ==="
    grep "$G" grandis2californicacandidate_chimeric_short_copy.ids
    echo
done
=== evm_model_PTG001531L_56 (californica) ===
evm_model_PTG001531L_56 californica_JS2-8gene32672      0.009   0.486   0.477   0.961   0.997
evm_model_PTG001531L_56 californica_JS2-8gene886        0.009   0.486   0.477   0.788   0.981

=== evm_model_PTG001984L_93 (californica) ===
evm_model_PTG001984L_93 californica_JS2-6gene27489      0.007   0.472   0.465   0.505   0.996
evm_model_PTG001984L_93 californica_JS2-8gene17272      0.57    0.972   0.402   0.119   0.661
evm_model_PTG001984L_93 californica_JS2-6gene27489      0.007   0.472   0.465   0.505   0.996
evm_model_PTG001984L_93 californica_JS2-8gene17272      0.57    0.972   0.402   0.119   0.661

=== evm_model_PTG004650L_4 (californica) ===
evm_model_PTG004650L_4  californica_JS2-6gene22450      0.138   0.713   0.575   0.554   0.76
evm_model_PTG004650L_4  californica_JS2-6gene22450      0.138   0.713   0.575   0.554   0.76

=== evm_model_PTG011191L_11 (californica) ===
evm_model_PTG011191L_11 californica_JS2-8gene9117       0.02    0.56    0.54    0.955   0.971
evm_model_PTG011191L_11 californica_JS2-8gene9115       0.02    0.56    0.54    0.768   0.851

本质上就是一个query比对到两个id不同的,但是比对区域完全相同的基因,对不对?这种情况理论上应该是要删除的,但是在perl版本里被保留了。你能找到perl版本对应的代码,并告诉我为什么吗?

一直到最后都没能复刻这四个基因,但是可以很明确的是,这4个基因是假阳性,没有复刻的必要。

 

 

Iqtree 所构建进化树

这个树的和下面这个树的区别是买麻藤和百岁兰分在中间,银杏在外面,两个被子植物中有一个和裸子植物聚类到一起了。

群体数据集的可用性:

为了将来计算群体的选择压力,我想要搞清楚,这些数据集里面,有群体vcf的数据集有哪些。

很多物种在文章中做了群体工作,但是未必有vcf数据集。

香榧肯定是有的,Tgrandis;

 

 

Ortherfinder所构建进化树

这两个树都是可用的,

更新计划

  1. 考虑短拷贝之间的同源性,用于判断fusion事件是否是独立起源的。
  2. 分branchers遍历所有的fusion事件,构建矩阵,这样一来,如果不同物种的fusion基因内部考虑同源性,那么就可以把基因fusion事件在不同branch上的发生事件和特定物种上的丢失事件找出来。
  3. 根据gff找到fusion在不同染色体上的发生位置,并判定其发生方式(染色体重排?逆转录转座?)。

原始的fusion代码考虑了基因在不同染色体上的位置,但是仅仅用作提取注释之用;
22年gb的文章仅仅做了理论分类,而没有具体分析起源的发生机制。
由于裸子植物本身保持着良好的基因组共线性,在对染色体臂进行合理编号的情况下,或许可以对发生的机制进行分析。

短拷贝保守性检验

一个真实的融合基因 = 多个祖先基因片段拼接而成。每个片段在外群物种中应有保守的直系同源。短拷贝在这些外群物种中的位置应当是聚集的,而非散乱分布。

同一个 branch 上的融合基因
        │
        ▼
按短拷贝位置聚类 → 得到每个基因的"位置签名"
        │
        ▼
相同签名的基因 ──→ 同一祖先融合事件(各物种继承或独立dup)
不同签名的基因 ──→ 独立起源的融合事件

 

进化树的动态划分

原始的划分方法是这样的:

对,是动态划分的。没有阈值——完全靠树的拓扑结构逐层压缩。过程如下:
原始树(简化版,假设 focus=Tchinensis,外群=Mpolymorpha):
第 1 轮:regex 找到 (Tchinensis,Sgiganteum) → 标为 _a1
树变成:(Mpolymorpha,(Atrichopoda,(Gmontanum,(Gbiloba,(Ptabuliformis,_a1)))))
划分结果:
ingroup:Tchinensis(focus 本身)
midgroup:Sgiganteum、Ptabuliformis、Gbiloba、Gmontanum、Atrichopoda(树上夹在 focus 和外群之间的所有物种)
outgroup:Mpolymorpha
第 2 轮:regex 找到 (Ptabuliformis,_a1) → 标为 _b1
划分:
ingroup:Tchinensis、Sgiganteum、Ptabuliformis(_b1 展开后包含 focus)
midgroup:Gbiloba、Gmontanum、Atrichopoda
outgroup:Mpolymorpha
第 3 轮(Gbiloba,_b1)_c1,ingroup 继续扩大...
直到 midgroup 为空(所有物种都进了 ingroup)。
核心规律
ingroup 每次都向根方向"吞掉"一个物种
midgroup 逐渐缩小
同一基因在任意一层匹配 pattern 就算嵌合
这就是为什么 midgroup 越大 → 融合越古老——那时候 ingroup 还很小,外群之外的物种都还在中群里。

但是啊,我觉得这样的划分方法有问题

但是啊,这里有个问题,有时候,对于一个内fucusion物种,midgroup中某两个物种可能是等同的,即处于系统发育树的相同拓扑位置,所以我开始觉得原始perl代码这样划分是有失平衡的。
我觉得应当按照相对于fucus物种的节点来划分,你觉得对吗?

对了,顺便看一下原始perl代码有什么问题:

现在版本的代码的处理方式,以下面这个树为例:

[v8 tree] focus=Tgrandis 祖先层级数=7
  Level 1: +4 species ingroup=(4) | midgroup=(15)['Cgigantea', 'Cjaponica', 'Clanceolata']...
DEBUG: [Tgrandis] level ingroup=['Pseudotaxus_chienii', 'Tchinensis', 'Tgrandis', 'Twallichiana'] midgroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Cpanzhihuaensis', 'Fhodginsii', 'Gbiloba', 'Gmontanum', 'Mglyptostroboides', 'Pdensiflora', 'Platycladus_orientalis', 'Pmassoniana', 'Ptabuliformis', 'Sgiganteum', 'Tdistichum', 'Wmirabilis'] -> total_chimeric=5403
DEBUG: [Tgrandis] gene=TgF01G00100-mRNA1 sc=(0/4,1/15,0/3) lc=(1/4,1/15,0/3) -> short=000 long=000 FAIL
DEBUG: [Tgrandis] gene=TgF01G00240-mRNA1 sc=(0/4,1/15,0/3) lc=(3/4,14/15,3/3) -> short=000 long=011 FAIL
DEBUG: [Tgrandis] gene=TgF01G00260-mRNA1 sc=(0/4,1/15,0/3) lc=(3/4,15/15,3/3) -> short=000 long=011 FAIL
DEBUG: [Tgrandis] level ingroup=['Pseudotaxus_chienii', 'Tchinensis', 'Tgrandis', 'Twallichiana'] -> pass/total = 0/5403
  Level 2: +8 species ingroup=(12) | midgroup=(7)['Cpanzhihuaensis', 'Gbiloba', 'Gmontanum']...
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Fhodginsii', 'Mglyptostroboides', 'Platycladus_orientalis', 'Pseudotaxus_chienii', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] midgroup=['Cpanzhihuaensis', 'Gbiloba', 'Gmontanum', 'Pdensiflora', 'Pmassoniana', 'Ptabuliformis', 'Wmirabilis'] -> total_chimeric=5403
DEBUG: [Tgrandis] gene=TgF01G00100-mRNA1 sc=(1/12,0/7,0/3) lc=(1/12,1/7,0/3) -> short=000 long=000 FAIL
DEBUG: [Tgrandis] gene=TgF01G00240-mRNA1 sc=(0/12,1/7,0/3) lc=(10/12,7/7,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] gene=TgF01G00260-mRNA1 sc=(1/12,0/7,0/3) lc=(11/12,7/7,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Fhodginsii', 'Mglyptostroboides', 'Platycladus_orientalis', 'Pseudotaxus_chienii', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] -> pass/total = 0/5403
  Level 3: +3 species ingroup=(15) | midgroup=(4)['Cpanzhihuaensis', 'Gbiloba', 'Gmontanum']...
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Fhodginsii', 'Mglyptostroboides', 'Pdensiflora', 'Platycladus_orientalis', 'Pmassoniana', 'Pseudotaxus_chienii', 'Ptabuliformis', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] midgroup=['Cpanzhihuaensis', 'Gbiloba', 'Gmontanum', 'Wmirabilis'] -> total_chimeric=5403
DEBUG: [Tgrandis] gene=TgF01G00100-mRNA1 sc=(1/15,0/4,0/3) lc=(1/15,1/4,0/3) -> short=000 long=000 FAIL
DEBUG: [Tgrandis] gene=TgF01G00240-mRNA1 sc=(1/15,0/4,0/3) lc=(13/15,4/4,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] gene=TgF01G00260-mRNA1 sc=(1/15,0/4,0/3) lc=(14/15,4/4,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Fhodginsii', 'Mglyptostroboides', 'Pdensiflora', 'Platycladus_orientalis', 'Pmassoniana', 'Pseudotaxus_chienii', 'Ptabuliformis', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] -> pass/total = 0/5403
  Level 4: +2 species ingroup=(17) | midgroup=(2)['Gmontanum', 'Wmirabilis']
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Cpanzhihuaensis', 'Fhodginsii', 'Gbiloba', 'Mglyptostroboides', 'Pdensiflora', 'Platycladus_orientalis', 'Pmassoniana', 'Pseudotaxus_chienii', 'Ptabuliformis', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] midgroup=['Gmontanum', 'Wmirabilis'] -> total_chimeric=5403
DEBUG: [Tgrandis] gene=TgF01G00100-mRNA1 sc=(1/17,0/2,0/3) lc=(2/17,0/2,0/3) -> short=000 long=000 FAIL
DEBUG: [Tgrandis] gene=TgF01G00240-mRNA1 sc=(1/17,0/2,0/3) lc=(15/17,2/2,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] gene=TgF01G00260-mRNA1 sc=(1/17,0/2,0/3) lc=(16/17,2/2,3/3) -> short=000 long=111 FAIL
DEBUG: [Tgrandis] level ingroup=['Cgigantea', 'Cjaponica', 'Clanceolata', 'Cpanzhihuaensis', 'Fhodginsii', 'Gbiloba', 'Mglyptostroboides', 'Pdensiflora', 'Platycladus_orientalis', 'Pmassoniana', 'Pseudotaxus_chienii', 'Ptabuliformis', 'Sgiganteum', 'Tchinensis', 'Tdistichum', 'Tgrandis', 'Twallichiana'] -> pass/total = 0/5403

在v8版本的基础上我又想出了一个新的版本:

即双扩散迭代和S层。特别是S层,被专门用于寻找那些物种特异的fusion事件。

这个迭代虽然能检测出物种特异性分质上的fusion事件。

但是,对于fusion发生branch和发生类型(短拷贝保留or不保留)的处理上,带来了困扰。

但是,现在我又有了更好的办法:

是的,我们能不能用两层划分法?
只划分内群和外群?
然后在分析的时候,可以放弃一些物种。
具体来说,是这样?
以Tgrandis为例,第一步我们只考虑最近外类群,即第一个祖先节点上除去Tgrandis后剩下的三个物种。
然后把这三个物种作为外群。
如果long=10,short=01,则代表fusion发生在最近谱系上,也就是说物种特异的,同时短拷贝丢失或者消耗。
如果long=10,short=11,则代表fusion发生在在最近谱系上,同时短拷贝保留。
如果long=11 short=11,则代表fusion可能发生在更古老的分支上。
这个时候需要向外延申,再把Tgrandis+最近外类群,也就是这四个物种作为内群。
把外侧第一个近源group作为外群。
你理解我的思路了吗?
这样一来,我们能更轻松的对与fusion事件进行brchan划分和短拷贝存续统计,对不对。

这个框架的首要优点是,他是一个很美观的框架,任何一个清晰的东西,都应当是简洁而美观的。

  • 不考虑物种特异性丢失的情况下,任何一个fusion事件都应当发生在特定的branch上,而这个branch内部,应当保留long_copy,branch外部,没有longe_copy
  • 而这个brach以外的外类群,应当保留short_copy。
  • 如果这个branch内部没有short copy呢?那就是说明short_copy丢失了,或者在融合阶段就被消耗掉了。

所以:两群融合判定的核心原则

1. 分支定位原则

任何融合事件都发生在系统发育树的特定分支上。该分支内部的物种保留长拷贝(long copy),分支外部的物种不保留长拷贝。因此 long = "10"(内群有、外群无)是融合发生在当前分支的充要条件。

2. 外群证据原则

融合事件的亲本基因早于融合分支存在。分支外部的近缘物种(外群)应当保留该亲本基因的短拷贝碎片(short copy),作为追溯融合事件的进化证据。

3. 短拷贝丢失原则

如果融合分支内部(内群)没有检出短拷贝,说明亲本基因的碎片在融合发生时已被消耗(如通过重组、缺失等机制),或在融合后的进化过程中已完全丢失。这对应 short = "01"——短拷贝仅在外群保留,内群已无。


这三条直接翻译成代码逻辑就是:

long="10" short="11" → 融合在此分支, 亲本碎片仍存 (retained)
long="10" short="01" → 融合在此分支, 亲本碎片已丢 (lost)
long="11" short="11" → 融合早于此分支, 扩大内群继续追溯

所以只有到这一步为止,这个代码的逻辑判定才会是准确的。

  • 这个代码完美融合了所有外群,内群,和中群。
  • 以上框架,还有利于我们对一个分支内部的,物种间的,谱系特异性的long_copy进行合并。
  • 还有利于我们对谱系内部的long_copy再次复制的事件,进行更深入的分析。

此外,我还发现:

  1. 我发现对于long_copy=1的标准要放的很宽松,只要有一个物种,就视为1,只有全部物种都没有,才能计算为0.
  2. 原始perl代码是怎么处理的。

所以long_copy应当使用宽松阈值,short_copy应当使用严格阈值。

Fusion发生branch标记与整合

这个部分具体是指:在获得fusion基因的融合时间后,对不同物种的,同一fusion事件产生的直系同源基因进行整合,从而标注出不同branch上发生的fusion事件的总数。

基因id处理逻辑

这个很简单,就是biopython取代原始章老师的错误划分方式,他那个划分方式对于部分物种来说,会有bug。

对fusion后fusion产生基因复制事件进行标记(还没做)

要不要加入ks标定呢?加入ks标定有一个好处,可以对每个融合基因的复制时间进行标定。

阶段性总结

以上内容包含了Fusion软件的全部,榧属融合基因的鉴定部分,接下来的内容主要是为了达到一个文章发表框架而填充内容所做的,具体而言。整个工作可以划分为以下几个部分。

  1. Fusion工具的升级
  2. 裸子植物的全部fusion事件鉴定
  3. fusion基因的蛋白质结构(含IDR)

蛋白质结构不急于跑。我们先看4

  1. fusion基因的群体动态选择压力
1. 融合基因在多个物种间(L1+ 共享事件)
可行性:低。只有少数 L1+ 共享融合事件有跨物种直系同源——每个 clade 几个到几十个。
能算的:每对物种的 pairwise Ka/Ks
dN/dS    含义
< 1    融合蛋白在物种分化后一直被保守,功能稳定
≈ 1    纯中性漂变,功能不受约束(在退化?)
> 1    某条 lineage 上融合蛋白加速演化
局限:dN/dS 是两个物种的平均值,无法区分"是在 A 变快了还是 B 变慢了"。

2. 融合基因 vs 短拷贝(同物种内,旁系同源)
可行性:高。每个融合基因都有 short copy(亲本),全在同物种内,不需跨物种。
能算的:每个融合基因 vs 亲本 A、vs 亲本 B
dN/dS    含义
< 1    融合后蛋白未退化,受净化选择约束 — 基因仍然有功能
≈ 1    融合后蛋白在漂变 — 可能是假基因化过程中
> 1    融合后蛋白被正选择推向新功能 — 极其重要的信号
这个能做批量:28 个融合基因 × 2 个亲本 = 56 对 pairwise dN/dS,一网打尽。

3. 群体水平正选择
可行性:需要多个体 SNP 数据(VCF 或重测序 reads)
能算的:
指标    含义
Tajima's D < 0    群体中稀有等位基因过剩 → 近期净化选择或选择性清除
融合基因区域的 π 显著低于基因组背景    融合基因在群体中被保守
McDonald-Kreitman 检验    融合基因在种内多态 vs 种间固定的比例偏离中性预期
没有群体数据的话,这个做不了。

对于短拷贝重叠逻辑的修改

原始版本的代码中,对于短拷贝重叠情况,章老师使用了有重叠就删除较短者的思路。

在这版本的代码中,我发现了这个问题,我对于短拷贝做出了调整,我要求重叠达到70%才能删除一个短拷贝,在修改之后,我们找到了三个