158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
#!/home/xzq/miniconda3/envs/test4py311/bin/python
|
|||
|
|
"""
|
||
|
|
同源基因识别 (Ortholog Identification)
|
||
|
|
使用BLAST/Diamond在两个物种间识别同源基因对
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import argparse
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
|
||
|
|
CONDA_BIN = "/home/xzq/miniconda3/envs/test4py311/bin"
|
||
|
|
if CONDA_BIN not in os.environ.get("PATH", ""):
|
||
|
|
os.environ["PATH"] = CONDA_BIN + ":" + os.environ.get("PATH", "")
|
||
|
|
CONDA_LIB = "/home/xzq/miniconda3/envs/test4py311/lib"
|
||
|
|
if CONDA_LIB not in os.environ.get("LD_LIBRARY_PATH", ""):
|
||
|
|
os.environ["LD_LIBRARY_PATH"] = CONDA_LIB + ":" + os.environ.get("LD_LIBRARY_PATH", "")
|
||
|
|
def run_blast(query_fasta: Path, subject_fasta: Path, output_file: Path, evalue: float = 1e-5):
|
||
|
|
"""运行BLASTP比对,优先使用conda环境中的blastp"""
|
||
|
|
# 尝试使用conda环境中的blastp
|
||
|
|
blastp_paths = [
|
||
|
|
"/home/xzq/miniconda3/envs/test4py311/bin/blastp",
|
||
|
|
"/home/xzq/miniconda3/bin/blastp",
|
||
|
|
"blastp", # 系统PATH中的blastp
|
||
|
|
]
|
||
|
|
|
||
|
|
blastp_cmd = None
|
||
|
|
for path in blastp_paths:
|
||
|
|
if Path(path).exists():
|
||
|
|
blastp_cmd = path
|
||
|
|
break
|
||
|
|
|
||
|
|
if not blastp_cmd:
|
||
|
|
print("[ERROR] blastp not found. Please install BLAST+.")
|
||
|
|
return False
|
||
|
|
|
||
|
|
cmd = [
|
||
|
|
blastp_cmd,
|
||
|
|
"-query", str(query_fasta),
|
||
|
|
"-subject", str(subject_fasta),
|
||
|
|
"-outfmt", "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send evalue bitscore",
|
||
|
|
"-evalue", str(evalue),
|
||
|
|
"-out", str(output_file),
|
||
|
|
"-num_threads", "8"
|
||
|
|
]
|
||
|
|
|
||
|
|
print(f"[INFO] Running BLASTP: {query_fasta.name} vs {subject_fasta.name}")
|
||
|
|
result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ.copy())
|
||
|
|
|
||
|
|
if result.returncode != 0:
|
||
|
|
print(f"[ERROR] BLASTP failed: {result.stderr}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def parse_blast_results(blast_file: Path, output_pairs_file: Path,
|
||
|
|
identity_threshold: float = 30.0,
|
||
|
|
coverage_threshold: float = 50.0):
|
||
|
|
"""解析BLAST结果,提取最佳同源基因对"""
|
||
|
|
|
||
|
|
from collections import defaultdict
|
||
|
|
|
||
|
|
# 读取BLAST结果
|
||
|
|
hits = defaultdict(list)
|
||
|
|
|
||
|
|
with open(blast_file, 'r') as f:
|
||
|
|
for line in f:
|
||
|
|
if line.strip():
|
||
|
|
parts = line.strip().split('\t')
|
||
|
|
if len(parts) >= 12:
|
||
|
|
query_id = parts[0]
|
||
|
|
subject_id = parts[1]
|
||
|
|
identity = float(parts[2])
|
||
|
|
length = int(parts[3])
|
||
|
|
bitscore = float(parts[11])
|
||
|
|
|
||
|
|
hits[query_id].append({
|
||
|
|
'subject': subject_id,
|
||
|
|
'identity': identity,
|
||
|
|
'length': length,
|
||
|
|
'bitscore': bitscore
|
||
|
|
})
|
||
|
|
|
||
|
|
# 提取最佳匹配(双向最佳匹配 = 直系同源)
|
||
|
|
ortholog_pairs = []
|
||
|
|
|
||
|
|
for query_id, hit_list in hits.items():
|
||
|
|
# 按bitscore排序,取最佳
|
||
|
|
hit_list.sort(key=lambda x: x['bitscore'], reverse=True)
|
||
|
|
best_hit = hit_list[0]
|
||
|
|
|
||
|
|
# 过滤条件
|
||
|
|
if best_hit['identity'] >= identity_threshold:
|
||
|
|
ortholog_pairs.append((query_id, best_hit['subject'], best_hit['identity']))
|
||
|
|
|
||
|
|
# 写入输出文件
|
||
|
|
with open(output_pairs_file, 'w') as f:
|
||
|
|
f.write("# Ortholog pairs identified by BLASTP\n")
|
||
|
|
f.write("# Format: Subject_ID\tQuery_ID\tIdentity(%)\n")
|
||
|
|
for query, subject, identity in ortholog_pairs:
|
||
|
|
f.write(f"{subject}\t{query}\t{identity:.2f}\n")
|
||
|
|
|
||
|
|
print(f"[INFO] Found {len(ortholog_pairs)} ortholog pairs")
|
||
|
|
print(f"[INFO] Output: {output_pairs_file}")
|
||
|
|
|
||
|
|
return len(ortholog_pairs)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description='Identify orthologs between two species')
|
||
|
|
parser.add_argument('-q', '--query', required=True, help='Query species protein FASTA')
|
||
|
|
parser.add_argument('-s', '--subject', required=True, help='Subject species protein FASTA (e.g., Ath)')
|
||
|
|
parser.add_argument('-o', '--output', required=True, help='Output directory')
|
||
|
|
parser.add_argument('-p', '--prefix', default='ortholog', help='Output file prefix')
|
||
|
|
parser.add_argument('--evalue', type=float, default=1e-5, help='BLAST e-value threshold')
|
||
|
|
parser.add_argument('--identity', type=float, default=30.0, help='Identity threshold (%)')
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
query_fasta = Path(args.query)
|
||
|
|
subject_fasta = Path(args.subject)
|
||
|
|
output_dir = Path(args.output)
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
# 检查输入文件
|
||
|
|
if not query_fasta.exists():
|
||
|
|
print(f"[ERROR] Query file not found: {query_fasta}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
if not subject_fasta.exists():
|
||
|
|
print(f"[ERROR] Subject file not found: {subject_fasta}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
# 运行BLAST
|
||
|
|
blast_output = output_dir / f"{args.prefix}.blastp.txt"
|
||
|
|
if not run_blast(query_fasta, subject_fasta, blast_output, args.evalue):
|
||
|
|
print("[ERROR] BLASTP failed")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
# 解析结果
|
||
|
|
pairs_file = output_dir / f"{args.prefix}_pairs.txt"
|
||
|
|
count = parse_blast_results(blast_output, pairs_file, args.identity)
|
||
|
|
|
||
|
|
if count > 0:
|
||
|
|
print(f"[SUCCESS] Ortholog identification complete. Found {count} pairs.")
|
||
|
|
sys.exit(0)
|
||
|
|
else:
|
||
|
|
print("[WARNING] No ortholog pairs found.")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|