By jiangchenhao, 30 June, 2026
Forums

金山文档link

背景

应该是CHS的项目,需要为二十个品种设计SSRs分子标记,数据是转录组数据,也就是说,如果要设计的话,中间应该是转录组作为输入。

参考文献

基于EST-SSR设计分子标记的参考

这个文章里写的很清楚,使用的是无参组装的转录本,然后他使用了一个工具:

https://github.com/xiaenhua/CandiSSR

老熟人啊我靠

没事,这何尝不是一种NTR

他这个环境依赖有点老了,不过他在他的github仓库中上传了全部依赖,这是很好的习惯,我下载下来后直接上传到服务器上。

这个环境太难配了,放弃,再找一篇文献,然后用更简单的方法配吧。

实操

Trinity组装

使用trinity_assemble_parallel.py这个脚本进行trinity组装

#! /usr/bin/env python3
"""
trinity_assemble_parallel.py
多样本并行Trinity转录组组装脚本
每个样本独立日志,支持单个Trinity多线程和多样本并行
"""
import os
import subprocess
import argparse
from multiprocessing import Pool
def Trinity_assemble_single(R1_file, R2_file, output_dir, threads_per_trinity=20, max_memory="20G"):
    """单样本Trinity组装,日志写入独立文件"""
    prefix = os.path.basename(R1_file).split("_")[0]
    trinity_out_dir = os.path.join(output_dir, prefix + "_Trinity_assemble")
    os.makedirs(trinity_out_dir, exist_ok=True)
    log_file = os.path.join(trinity_out_dir, f"{prefix}_trinity.log")

    command = [
        "Trinity",
        "--seqType", "fq",
        "--left", R1_file,
        "--right", R2_file,
        "--CPU", str(threads_per_trinity),
        "--max_memory", max_memory,
        "--output", trinity_out_dir
    ]

    print(f"[{prefix}] 开始Trinity组装,日志: {log_file}")
    with open(log_file, "w") as lf:
        subprocess.run(command, stdout=lf, stderr=subprocess.STDOUT, check=True)

    assemble_file = os.path.join(trinity_out_dir, "Trinity.fasta")
    assemble_rename_file = os.path.join(trinity_out_dir, prefix + "_trinity_assemble.fasta")
    os.rename(assemble_file, assemble_rename_file)
    print(f"[{prefix}] Trinity完成,输出: {assemble_rename_file}")
    return assemble_rename_file

# 顶层函数,multiprocessing 可调用
def run_sample(pair_and_params):
    R1_file, R2_file, output_dir, threads_per_trinity, max_memory = pair_and_params
    return Trinity_assemble_single(R1_file, R2_file, output_dir, threads_per_trinity, max_memory)

def Trinity_assemble_parallel(sample_file_list, output_dir, threads_per_trinity=20, max_memory="20G", max_parallel=2):
    """
    多样本并行Trinity组装
    sample_file_list: [(R1_file, R2_file), ...]
    threads_per_trinity: 每个Trinity进程使用的线程数
    max_memory: 每个Trinity进程使用的内存
    max_parallel: 同时运行的Trinity进程数
    """
    os.makedirs(output_dir, exist_ok=True)

    # 构建 starmap 参数列表,每个元素是一个元组
    params_list = [(R1, R2, output_dir, threads_per_trinity, max_memory) for (R1, R2) in sample_file_list]

    with Pool(processes=max_parallel) as pool:
        results = pool.map(run_sample, params_list)

    return results

def read_index_file(index_file, delimiter='\t', col1=0, col2=1):
    """读取索引文件,返回样本(R1, R2)列表"""
    sample_pairs = []
    with open(index_file, 'r') as f:
        for line in f:
            if line.startswith("#") or not line.strip():
                continue
            cols = line.strip().split(delimiter)
            if len(cols) <= max(col1, col2):
                print(f"跳过行(列数不足): {line.strip()}")
                continue
            sample_pairs.append((cols[col1], cols[col2]))
    return sample_pairs

def main():
    parser = argparse.ArgumentParser(description="多样本Trinity转录组并行组装工具")
    parser.add_argument('-i', '--index_txt', required=True, help="索引文件,两列,R1和R2")
    parser.add_argument('-o', '--output_dir', required=True, help="Trinity输出文件夹")
    parser.add_argument('--threads_trinity', type=int, default=20, help="每个Trinity进程使用的线程数")
    parser.add_argument('--max_parallel', type=int, default=2, help="同时运行的Trinity进程数")
    parser.add_argument('--max_memory', type=str, default="50G", help="每个Trinity进程使用的内存")
    args = parser.parse_args()

    sample_pairs = read_index_file(args.index_txt)
    print(f"找到 {len(sample_pairs)} 个样本对进行组装")

    Trinity_assemble_parallel(
        sample_pairs,
        args.output_dir,
        threads_per_trinity=args.threads_trinity,
        max_memory=args.max_memory,
        max_parallel=args.max_parallel
    )

if __name__ == "__main__":
    main()

CD-hit去冗余

for f in *Trinity.fasta; do prefix=${f%.fasta}; cd-hit-est -i "$f" -o "${prefix}.cdhit95.fasta" -c 0.95 -n 10 -T 8 -M 0; done

Misa跑SSR

使用以下配置文件进行misa跑SSR

definition(unit_size,min_repeats):                   1-20 3-6 3-5 4-5 5-5 6-4
interruptions(max_difference_between_2_SSRs):        100
GFF:                                                        false

处理SSR输出文件

  1. 解析 misa 文件。

①-我希望从每个misa文件,得到一个矩阵,一列为SSR的序列,一列为这个SSR所对应的序列id,在母本序列上的起始和终止位置。
由于一个SSR可以反复多次出现,因此一个SSR可以有多个id,多个起始和终止位置。
②-由于每个样本都可以有这样的信息,因此我们可以得到一个新的表格,相当于再添加一列,即样本号,所以我们有样本,SSR序列,SSR序列所对应的id:起始和终止位置,可以吗?
类似于:
Sample SSR sequence_id1:SSR_start,SSR_end; sequence_id2:SSR_start,SSR_end;
也就是说,写一个解析当前路径下所有misa文件的代码,汇总成这些信息,文件名信息如下:
G30201_Trinity_assemble.Trinity.cdhit95.fasta.misa:G30201是样本id

# process_misa_file.py
#!/usr/bin/env python3
import os
import glob
import pandas as pd
input_dir = "."
files = glob.glob(os.path.join(input_dir, "*.misa"))
records = []
for f in files:
    sample_id = os.path.basename(f).split("_")[0]
    with open(f) as fh:
        for line in fh:
            line = line.strip()

            # 跳过表头
            if not line or line.startswith("ID") or line.startswith("SSR nr"):
                continue

            cols = line.split("\t")
            if len(cols) < 7:
                continue

            seq_id = cols[0]
            ssr_seq = cols[3]
            start = cols[5]
            end = cols[6]

            records.append([
                sample_id,
                ssr_seq,
                f"{seq_id}:{start}-{end}"
            ])
df = pd.DataFrame(records, columns=["Sample", "SSR_sequence", "Location"])
# =========================
# 核心:合并成一行
# =========================
df_merged = (
    df.groupby(["Sample", "SSR_sequence"])["Location"]
      .apply(lambda x: "; ".join(x))
      .reset_index()
)
df_merged.to_csv("SSR_all_samples_merged.tsv", sep="\t", index=False)
print("Done -> SSR_all_samples_merged.tsv")
B30108  (AAC)7  TRINITY_DN23_c1_g1_i1_len=2390_path=[0:0-2389]:1635-1655; TRINITY_DN1518_c0_g1_i1_len=2886_path=[0:0-890_1:891-1060_2:1061-1677_3:1678-1686_4:1687-2885]:286-306; TRINITY_DN1518_c0_g1_i3_len=2716_path=[0:0-890_2:891-1507_3:1508-1516_4:1517-2715]:286-306
B30108  (AACA)5 TRINITY_DN744_c0_g3_i1_len=2959_path=[0:0-1863_2:1864-2958]:96-115; TRINITY_DN3180_c0_g1_i6_len=8501_path=[0:0-573_2:574-584_4:585-1366_5:1367-1367_6:1368-1528_7:1529-1634_8:1635-2526_9:2527-2598_10:2599-7308_12:7309-7310_15:7311-8500]:6941-6960; TRINITY_DN3180_c0_g1_i9_len=8526_path=[0:0-573_2:574-584_3:585-585_4:586-1367_6:1368-1528_7:1529-1634_8:1635-2526_9:2527-2598_10:2599-7308_11:7309-7309_13:7310-7334_14:7335-7335_15:7336-8525]:6941-6960
B30108  (AACA)6 TRINITY_DN9750_c0_g2_i1_len=1143_path=[0:0-1142]:1055-1078
B30108  (AACAAA)4       TRINITY_DN4266_c0_g1_i2_len=2116_path=[1:0-624_2:625-938_3:939-1647_5:1648-1685_6:1686-1705_7:1706-2115]:1287-1310
B30108  (AACAGT)5       TRINITY_DN437_c1_g1_i4_len=1030_path=[2:0-353_3:354-453_5:454-482_6:483-1029]:1-30
  1. 计算出样本唯一 SSR 。

好了,现在SSR_all_samples_merged.tsv这个文件中包含了所有的样本的SSR信息,我希望你只根据第一列和第二列,把样本特异存在的SSR,筛选出来,可以做到吗?也就是说,在这个表格基础上做一次过滤,剩下的全部都是样本唯一的SSR

(variety3) root@14dc642d5aaf:/project1/bin# cat process_misa_file2.py 
#!/usr/bin/env python3

import pandas as pd

# 读入合并后的文件
df = pd.read_csv("SSR_all_samples_merged.tsv", sep="\t")

# =========================
# 关键步骤:判断 SSR 是否跨样本
# =========================
# 统计每个 SSR_sequence 出现在多少个 Sample 中
ssr_sample_count = df.groupby("SSR_sequence")["Sample"].nunique()

# 找出“只在一个样本中出现”的 SSR
unique_ssr = ssr_sample_count[ssr_sample_count == 1].index

# 过滤原表
df_unique = df[df["SSR_sequence"].isin(unique_ssr)].copy()

# 输出
df_unique.to_csv("SSR_sample_specific.tsv", sep="\t", index=False)

print("Done -> SSR_sample_specific.tsv")
  1. 为样本唯一 SSR 设计两侧引物。

现在我们有了SSR_sample_specific.tsv这个文件后,我们需要设计SSR两侧引物
输入文件的格式是这样的:
(gmata_env) root@14dc642d5aaf:/project1/output/fasta_dir# head SSR_sample_specific.tsv
Sample SSR_sequence Location
B30108 (AAAGG)6 TRINITY_DN3841_c0_g1_i14_len=2724_path=[3:0-154_5:155-183_6:184-216_8:217-937_9:938-966_11:967-1043_14:1044-1111_16:1112-1206_17:1207-1616_18:1617-1825_19:1826-2723]:185-214; TRINITY_DN3841_c0_g1_i18_len=2515_path=[3:0-154_5:155-183_6:184-216_8:217-937_9:938-966_11:967-1043_14:1044-1111_16:1112-1206_17:1207-1616_19:1617-2514]:185-214; TRINITY_DN3841_c0_g1_i5_len=3231_path=[3:0-154_5:155-183_6:184-216_8:217-937_9:938-966_11:967-1043_13:1044-1220_14:1221-1288_15:1289-1618_16:1619-1713_17:1714-2123_18:2124-2332_19:2333-3230]:185-214
其中 TRINITY_DN3841_c0_g1_i14_len=2724_path=[3:0-154_5:155-183_6:184-216_8:217-937_9:938-966_11:967-1043_14:1044-1111_16:1112-1206_17:1207-1616_18:1617-1825_19:1826-2723]:185-214;这样一个部分中:以前代表序列id,:以后代表SSR在序列上的起始和终止位置,请注意,Location可以有多个sequence_id
这里我以这一行作为例子,sample为B30108,你需要找到对应样本B30108的fasta文件,
B30108_Trinity_assemble.Trinity.cdhit95.fasta(所有样本命名格式均相同),
然后找到序列TRINITY_DN3841_c0_g1_i14,根据坐标185-214设计引物。
一个样本的一个SSR序列可能要设计多个引物,比如这一行,你还要找到序列TRINITY_DN3841_c0_g1_i18和序列TRINITY_DN3841_c0_g1_i5。这里你要格外注意序列id的格式

设计引物的代码可以参考这部分:
=========================
FASTA dict cleanup
=========================
for k in list(fasta_dict.keys()):
seq_dict = SeqIO.to_dict(SeqIO.parse(fasta_dict[k], "fasta"))
fasta_dict[k] = {
kk.split(" ")[0]: vv for kk, vv in seq_dict.items()
}
=========================
helper functions
=========================
def gc(seq):
seq = seq.upper()
return (seq.count("G") + seq.count("C")) / len(seq) * 100
def tm(seq):
seq = seq.upper()
return 2*(seq.count("A")+seq.count("T")) + 4*(seq.count("G")+seq.count("C"))
def qc(f, r):
if "NA" in (f, r):
return False
if not (40 <= gc(f) <= 60 and 40 <= gc(r) <= 60):
return False
if abs(tm(f) - tm(r)) > 2:
return False
return True
=========================
primer design
=========================
results = []
for _, row in loc_df.iterrows():
sample = row["Sample"]
ssr = row["SSR"]
seq_id = row["SeqID"]
start = int(row["Start"])
end = int(row["End"])
if sample not in fasta_dict:
continue
if seq_id not in fasta_dict[sample]:
continue
full_seq = str(fasta_dict[sample][seq_id].seq)
left = max(0, start - FLANK - 1)
right = min(len(full_seq), end + FLANK)
flank = full_seq[left:right]
tstart = start - left
tlen = end - start + 1
try:
p = primer3.bindings.designPrimers(
{
'SEQUENCE_TEMPLATE': flank,
'SEQUENCE_TARGET': [tstart, tlen]
},
{
'PRIMER_OPT_SIZE': 20,
'PRIMER_MIN_SIZE': 18,
'PRIMER_MAX_SIZE': 25,
'PRIMER_PRODUCT_SIZE_RANGE': [[100, 300]],
'PRIMER_NUM_RETURN': 1
}
)
f = p.get("PRIMER_LEFT_0_SEQUENCE", "NA")
r = p.get("PRIMER_RIGHT_0_SEQUENCE", "NA")
prod = p.get("PRIMER_PAIR_0_PRODUCT_SIZE", "NA")
pass_qc = qc(f, r)
except:
f, r, prod = "NA", "NA", "NA"
pass_qc = False
results.append([
sample, ssr, seq_id, start, end,
f, r, prod, pass_qc
])
=========================
output
=========================
df = pd.DataFrame(results, columns=[
"Sample", "SSR", "SeqID", "Start", "End",
"Primer_F", "Primer_R", "Product_Size", "PASS"
])
请你给我完整的代码

# process_misa_file3.py
#!/usr/bin/env python3

import os
import re
import pandas as pd
from Bio import SeqIO
import primer3

# ==========================
# 参数设置(可以自己调)
# ==========================
FLANK = 200   # SSR两侧扩展长度(建议100~300)

# ==========================
# 读取SSR表
# ==========================
df = pd.read_csv("SSR_sample_specific.tsv", sep="\t")

# ==========================
# Step1:解析Location字段
# ==========================
records = []

for _, row in df.iterrows():
    sample = row["Sample"]
    ssr = row["SSR_sequence"]
    locs = row["Location"].split(";")

    for loc in locs:
        loc = loc.strip()
        if not loc:
            continue

        # 分割:seq_id 和 坐标
        try:
            seq_part, pos_part = loc.rsplit(":", 1)
        except:
            continue

        # 🔥 关键:去掉 path,只保留 TRINITY ID
        seq_id = seq_part.split("_len=")[0]

        # 提取坐标
        try:
            start, end = map(int, pos_part.split("-"))
        except:
            continue

        records.append([sample, ssr, seq_id, start, end])

loc_df = pd.DataFrame(records, columns=[
    "Sample", "SSR", "SeqID", "Start", "End"
])

# ==========================
# Step2:加载所有fasta
# ==========================
fasta_dict = {}

samples = loc_df["Sample"].unique()

for s in samples:
    fasta_file = f"{s}_Trinity_assemble.Trinity.cdhit95.fasta"

    if not os.path.exists(fasta_file):
        print(f"[WARN] missing fasta: {fasta_file}")
        continue

    seq_dict = SeqIO.to_dict(SeqIO.parse(fasta_file, "fasta"))

    # 🔥 去掉 header 后面的乱七八糟信息
    fasta_dict[s] = {
        k.split(" ")[0]: v for k, v in seq_dict.items()
    }

# ==========================
# Step3:辅助函数
# ==========================
def gc(seq):
    seq = seq.upper()
    return (seq.count("G") + seq.count("C")) / len(seq) * 100 if len(seq) > 0 else 0

def tm(seq):
    seq = seq.upper()
    return 2*(seq.count("A")+seq.count("T")) + 4*(seq.count("G")+seq.count("C"))

def qc(f, r):
    if "NA" in (f, r):
        return False
    if not (40 <= gc(f) <= 60 and 40 <= gc(r) <= 60):
        return False
    if abs(tm(f) - tm(r)) > 2:
        return False
    return True

# ==========================
# Step4:设计引物
# ==========================
results = []

for _, row in loc_df.iterrows():
    sample = row["Sample"]
    ssr = row["SSR"]
    seq_id = row["SeqID"]
    start = int(row["Start"])
    end = int(row["End"])

    if sample not in fasta_dict:
        continue

    if seq_id not in fasta_dict[sample]:
        # 🔥 debug关键
        print(f"[MISS] {sample} {seq_id}")
        continue

    full_seq = str(fasta_dict[sample][seq_id].seq)

    # 取flanking区域
    left = max(0, start - FLANK - 1)
    right = min(len(full_seq), end + FLANK)

    flank = full_seq[left:right]

    # SSR在flank中的位置
    tstart = start - left - 1
    tlen = end - start + 1

    try:
        p = primer3.bindings.designPrimers(
            {
                'SEQUENCE_TEMPLATE': flank,
                'SEQUENCE_TARGET': [tstart, tlen]
            },
            {
                'PRIMER_OPT_SIZE': 20,
                'PRIMER_MIN_SIZE': 18,
                'PRIMER_MAX_SIZE': 25,
                'PRIMER_PRODUCT_SIZE_RANGE': [[100, 300]],
                'PRIMER_NUM_RETURN': 1
            }
        )

        f = p.get("PRIMER_LEFT_0_SEQUENCE", "NA")
        r = p.get("PRIMER_RIGHT_0_SEQUENCE", "NA")
        prod = p.get("PRIMER_PAIR_0_PRODUCT_SIZE", "NA")
        pass_qc = qc(f, r)

    except Exception as e:
        f, r, prod = "NA", "NA", "NA"
        pass_qc = False

    results.append([
        sample, ssr, seq_id, start, end,
        f, r, prod, pass_qc
    ])

# ==========================
# Step5:输出
# ==========================
out_df = pd.DataFrame(results, columns=[
    "Sample", "SSR", "SeqID", "Start", "End",
    "Primer_F", "Primer_R", "Product_Size", "PASS"
])

out_df.to_csv("SSR_primers.tsv", sep="\t", index=False)

print("Done -> SSR_primers.tsv")

只保留计算通过的标记和简单的SSR重复

awk 'NR==1 || ($9=="True" && split($2,a,"(")==2)' SSR_primers.tsv > SSR_primers.filtered.tsv

去重复,去掉两侧引物完全相同,SSR也相同的行

awk 'NR==1 {print; next} !seen[$2 FS $6 FS $7]++' SSR_primers.filtered.tsv > SSR_primers.unique.tsv

26年5月29日记录

为了节约空间,删除了所有trinity组装文件