itol-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- itol_cli/__init__.py +8 -0
- itol_cli/account.py +161 -0
- itol_cli/aesthetics.py +248 -0
- itol_cli/cli.py +887 -0
- itol_cli/client.py +200 -0
- itol_cli/errors.py +56 -0
- itol_cli/learn.py +174 -0
- itol_cli/lint.py +250 -0
- itol_cli/render.py +137 -0
- itol_cli/templates.py +389 -0
- itol_cli/tree.py +507 -0
- itol_cli-0.2.0.dist-info/METADATA +97 -0
- itol_cli-0.2.0.dist-info/RECORD +16 -0
- itol_cli-0.2.0.dist-info/WHEEL +5 -0
- itol_cli-0.2.0.dist-info/entry_points.txt +2 -0
- itol_cli-0.2.0.dist-info/top_level.txt +1 -0
itol_cli/templates.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""场景模板注册表(价值核心:每模板=输入规范→生成脚本→预设参数)。
|
|
2
|
+
|
|
3
|
+
P0 三模板(agent6 场景库实证):
|
|
4
|
+
- subfamily 亚家族着色主树(6/6 项目,痛点 5)→ TREE_COLORS + COLORSTRIP
|
|
5
|
+
- bootstrap 支持率展示(6/6)→ iTOL 内置 metadata 四态,本模块产出导出参数预设
|
|
6
|
+
- heatmap 表达热图联树(4/6,痛点 4)→ DATASET_HEATMAP + 行序校验
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# NPG(Nature Publishing Group)出版级色板——2026-11-04 ARR-B 看图打分自评胜出(8/10)
|
|
14
|
+
# 和谐专业、亚家族区分度好,定为亚家族主图默认
|
|
15
|
+
NPG = ["#E64B35", "#4DBBD5", "#00A087", "#3C5488", "#F39B7F",
|
|
16
|
+
"#8491B4", "#91D1C2", "#DC0000", "#7E6148", "#B09C85"]
|
|
17
|
+
|
|
18
|
+
# Okabe-Ito 色盲安全色板(agent6 建议固化)——可及性优先时的备选(--palette okabeito)
|
|
19
|
+
OKABE_ITO = ["#E69F00", "#56B4E9", "#009E73", "#F0E442",
|
|
20
|
+
"#0072B2", "#D55E00", "#CC79A7", "#999999"]
|
|
21
|
+
|
|
22
|
+
NAMED_PALETTES = {"npg": NPG, "okabeito": OKABE_ITO}
|
|
23
|
+
|
|
24
|
+
# 扩展色板:NPG 10 色 + Tol bright 8 色(>10 分组时用,17 亚家族实证需求)
|
|
25
|
+
PALETTE_18 = NPG + [
|
|
26
|
+
"#4477AA", "#EE6677", "#228833", "#CCBB44", "#66CCEE",
|
|
27
|
+
"#AA3377", "#BBBBBB", "#333333", "#7FCD9B", "#C9A0DC",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def palette_for(n: int, name: str = "npg") -> list:
|
|
32
|
+
"""按分组数给色板:≤base 用 base(默认 NPG,可选 okabeito),>base 用 18 色扩展。"""
|
|
33
|
+
base = NAMED_PALETTES.get(name, NPG)
|
|
34
|
+
return PALETTE_18 if n > len(base) else base
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TemplateSpec:
|
|
39
|
+
name: str
|
|
40
|
+
summary: str
|
|
41
|
+
inputs: str # 输入规范一句话
|
|
42
|
+
outputs: list # 产出的注解文件类型
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
TEMPLATES = {
|
|
46
|
+
"subfamily": TemplateSpec(
|
|
47
|
+
"subfamily", "亚家族着色主树(家族论文 Fig1 标配)",
|
|
48
|
+
"分组 tsv:node_id<TAB>group_name(叶名=树文件叶标签;内部节点用 ID1|ID2 LCA 写法)",
|
|
49
|
+
["TREE_COLORS(枝+标签按组着色)", "DATASET_COLORSTRIP(外圈色带)"],
|
|
50
|
+
),
|
|
51
|
+
"bootstrap": TemplateSpec(
|
|
52
|
+
"bootstrap", "bootstrap 支持率展示(iTOL 内置 metadata 四态,无需注解文件)",
|
|
53
|
+
"含 UFBoot 节点 label 的 .contree/.treefile(IQ-TREE 输出直接可用)",
|
|
54
|
+
["导出参数预设(bootstrap_display + 阈值分级)"],
|
|
55
|
+
),
|
|
56
|
+
"heatmap": TemplateSpec(
|
|
57
|
+
"heatmap", "表达热图联树(行序必须=树叶序,头号事故源已内建校验)",
|
|
58
|
+
"矩阵 tsv:首列基因 ID(=树叶名),其余列=样本表达量(建议 log2 TPM z-score)",
|
|
59
|
+
["DATASET_HEATMAP"],
|
|
60
|
+
),
|
|
61
|
+
"domains": TemplateSpec(
|
|
62
|
+
"domains", "域结构联树(蛋白域示意,Pfam/hmmsearch 结果上树)",
|
|
63
|
+
"域表 tsv:gene<TAB>domain<TAB>start<TAB>end<TAB>protein_length(每行一个域命中,同基因多行)",
|
|
64
|
+
["DATASET_DOMAINS(骨架+域形状,官方原生类型)"],
|
|
65
|
+
),
|
|
66
|
+
"motif": TemplateSpec(
|
|
67
|
+
"motif", "motif 联树(MEME/FIMO 基序位置按域状结构上树)",
|
|
68
|
+
"motif 表 tsv:gene<TAB>motif_name<TAB>start<TAB>end<TAB>gene_length(MEME/FIMO 输出提取后整理)",
|
|
69
|
+
["DATASET_DOMAINS(motif 作色块;>8 motif 建议合并相似项防色板爆炸)"],
|
|
70
|
+
),
|
|
71
|
+
"selection": TemplateSpec(
|
|
72
|
+
"selection", "选择压力标注树(枝按 ω 蓝→白→红渐变,正选择枝高亮)",
|
|
73
|
+
"ω 表 tsv:node<TAB>omega(node=叶名或内部节点 LCA 写法 ID1|ID2;codeml/HyPhy 输出整理)",
|
|
74
|
+
["TREE_COLORS(branch 按 ω 发散色阶)"],
|
|
75
|
+
),
|
|
76
|
+
"connections": TemplateSpec(
|
|
77
|
+
"connections", "共线连线树 / tanglegram(两节点间连线,synteny anchors 上树)",
|
|
78
|
+
"配对表 tsv:node1<TAB>node2[<TAB>color][<TAB>label](color 缺省灰;宽度按 MAXIMUM_LINE_WIDTH 归一)",
|
|
79
|
+
["DATASET_CONNECTION"],
|
|
80
|
+
),
|
|
81
|
+
"timescale": TemplateSpec(
|
|
82
|
+
"timescale", "时间树地质年代轴(树下方色带时间标尺)",
|
|
83
|
+
"区间表 tsv:name<TAB>start<TAB>end[<TAB>color](地质年代或自定义区间,单位与树枝长一致如 Mya)",
|
|
84
|
+
["DATASET_TIMESCALE"],
|
|
85
|
+
),
|
|
86
|
+
"pav": TemplateSpec(
|
|
87
|
+
"pav", "泛基因组 PAV 联树(基因×基因组 0/1 有冇矩阵上树)",
|
|
88
|
+
"0/1 矩阵 tsv:首列基因 ID(=树叶名),其余列=基因组 0/1(建议按树序排列)",
|
|
89
|
+
["DATASET_BINARY"],
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def write_subfamily(groups_tsv: Path, outdir: Path, palette: list | None = None,
|
|
95
|
+
palette_name: str = "npg") -> list:
|
|
96
|
+
"""亚家族着色:分组 tsv → TREE_COLORS + COLORSTRIP 两个注解文件。"""
|
|
97
|
+
groups: dict = {}
|
|
98
|
+
order: list = []
|
|
99
|
+
for ln in Path(groups_tsv).read_text(encoding="utf-8").splitlines():
|
|
100
|
+
if not ln.strip() or ln.startswith("#"):
|
|
101
|
+
continue
|
|
102
|
+
nid, _, g = ln.partition("\t")
|
|
103
|
+
g = g.strip() or "ungrouped"
|
|
104
|
+
if g not in groups:
|
|
105
|
+
groups[g] = None # 占位,色板后定
|
|
106
|
+
order.append(g)
|
|
107
|
+
groups.setdefault("_rows", []).append((nid.strip(), g))
|
|
108
|
+
palette = palette or palette_for(len(order), palette_name)
|
|
109
|
+
for i, g in enumerate(order):
|
|
110
|
+
groups[g] = palette[i % len(palette)]
|
|
111
|
+
rows = groups["_rows"]
|
|
112
|
+
|
|
113
|
+
# TREE_COLORS:叶标签+枝按组着色
|
|
114
|
+
tc = ["TREE_COLORS", "SEPARATOR TAB", "DATA"]
|
|
115
|
+
for nid, g in rows:
|
|
116
|
+
c = groups[g]
|
|
117
|
+
tc.append(f"{nid}\tbranch\t{c}\tnormal\t1") # 枝着色
|
|
118
|
+
tc.append(f"{nid}\tlabel\t{c}") # 标签着色
|
|
119
|
+
# COLORSTRIP:外圈色带 + 图例
|
|
120
|
+
# 官方 MANDATORY SETTINGS:SEPARATOR / DATASET_LABEL / COLOR 必填(模板实证)
|
|
121
|
+
# 图例规范:每行 legend 需 shape+color+label 三件套
|
|
122
|
+
cs = ["DATASET_COLORSTRIP", "SEPARATOR TAB",
|
|
123
|
+
"DATASET_LABEL\tSubfamily",
|
|
124
|
+
"COLOR\t#ff0000",
|
|
125
|
+
"LEGEND_TITLE\tSubfamily",
|
|
126
|
+
"LEGEND_SHAPES\t" + "\t".join("1" for _ in order),
|
|
127
|
+
"LEGEND_COLORS\t" + "\t".join(groups[g] for g in order),
|
|
128
|
+
"LEGEND_LABELS\t" + "\t".join(order),
|
|
129
|
+
"SHOW_STRIP_LABELS\t0", "DATA"]
|
|
130
|
+
for nid, g in rows:
|
|
131
|
+
cs.append(f"{nid}\t{groups[g]}\t{g}")
|
|
132
|
+
|
|
133
|
+
outdir = Path(outdir)
|
|
134
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
p1 = outdir / "itol_tree_colors.txt"
|
|
136
|
+
p2 = outdir / "itol_colorstrip.txt"
|
|
137
|
+
p1.write_text("\n".join(tc) + "\n", encoding="utf-8")
|
|
138
|
+
p2.write_text("\n".join(cs) + "\n", encoding="utf-8")
|
|
139
|
+
return [p1, p2]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
BOOTSTRAP_EXPORT_PRESET = {
|
|
143
|
+
"bootstrap_display": "1",
|
|
144
|
+
"bootstrap_type": "1", # 1=Symbol
|
|
145
|
+
"bootstrap_symbol_min_size": "2",
|
|
146
|
+
"bootstrap_symbol_max_size": "6",
|
|
147
|
+
"bootstrap_slider_min": "70", # <70 隐藏(agent6 审美规则)
|
|
148
|
+
"bootstrap_slider_max": "100",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def write_domains(domains_tsv: Path, outdir: Path, label: str = "Domains",
|
|
153
|
+
palette: list | None = None, shape: str = "RE") -> list:
|
|
154
|
+
"""域结构/基序联树:域表 → DATASET_DOMAINS(官方原生类型)。
|
|
155
|
+
|
|
156
|
+
输入 tsv(每行一个域命中,同基因多行):
|
|
157
|
+
gene<TAB>domain<TAB>start<TAB>end<TAB>protein_length
|
|
158
|
+
输出 DATA 行(官方语法):
|
|
159
|
+
gene<SEP>length<SEP>SHAPE|START|END|COLOR|LABEL[<SEP>SHAPE|START|END|COLOR|LABEL...]
|
|
160
|
+
"""
|
|
161
|
+
genes: dict = {}
|
|
162
|
+
domain_names: list = []
|
|
163
|
+
for i, ln in enumerate(Path(domains_tsv).read_text(encoding="utf-8").splitlines()):
|
|
164
|
+
if not ln.strip() or ln.startswith("#"):
|
|
165
|
+
continue
|
|
166
|
+
parts = ln.split("\t")
|
|
167
|
+
if len(parts) < 5:
|
|
168
|
+
raise ValueError(f"第 {i + 1} 行列数不足(需 5 列:gene/domain/start/end/length):{ln!r}")
|
|
169
|
+
g, dom = parts[0].strip(), parts[1].strip()
|
|
170
|
+
try:
|
|
171
|
+
s, e, plen = int(parts[2]), int(parts[3]), int(parts[4])
|
|
172
|
+
except ValueError:
|
|
173
|
+
raise ValueError(f"第 {i + 1} 行 start/end/length 必须是整数:{ln!r}") from None
|
|
174
|
+
if s >= e:
|
|
175
|
+
raise ValueError(f"第 {i + 1} 行 start>=end:{ln!r}")
|
|
176
|
+
if e > plen:
|
|
177
|
+
raise ValueError(f"第 {i + 1} 行 end>protein_length:{ln!r}")
|
|
178
|
+
genes.setdefault(g, {"length": plen, "domains": []})["domains"].append((dom, s, e))
|
|
179
|
+
if dom not in domain_names:
|
|
180
|
+
domain_names.append(dom)
|
|
181
|
+
if not genes:
|
|
182
|
+
raise ValueError("输入无任何有效域记录")
|
|
183
|
+
palette = palette or palette_for(len(domain_names))
|
|
184
|
+
colors = {d: palette[i % len(palette)] for i, d in enumerate(domain_names)}
|
|
185
|
+
|
|
186
|
+
out = ["DATASET_DOMAINS", "SEPARATOR TAB",
|
|
187
|
+
f"DATASET_LABEL\t{label}", "COLOR\t#ff0000",
|
|
188
|
+
"BACKBONE_COLOR\t#aaaaaa", "BACKBONE_HEIGHT\t10", "BORDER_WIDTH\t1",
|
|
189
|
+
f"LEGEND_TITLE\t{label}",
|
|
190
|
+
"LEGEND_SHAPES\t" + "\t".join(shape for _ in domain_names),
|
|
191
|
+
"LEGEND_COLORS\t" + "\t".join(colors[d] for d in domain_names),
|
|
192
|
+
"LEGEND_LABELS\t" + "\t".join(domain_names),
|
|
193
|
+
"DATA"]
|
|
194
|
+
for g, info in genes.items():
|
|
195
|
+
defs = "\t".join(f"{shape}|{s}|{e}|{colors[d]}|{d}" for d, s, e in info["domains"])
|
|
196
|
+
out.append(f"{g}\t{info['length']}\t{defs}")
|
|
197
|
+
outdir = Path(outdir)
|
|
198
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
p = outdir / "itol_domains.txt"
|
|
200
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
201
|
+
return [p]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _omega_color(omega: float) -> str:
|
|
205
|
+
"""ω 发散色阶:蓝(强纯化 #2166AC)→ 白(ω=1 #F7F7F7)→ 红(正选择 #B2182B,cap 3)。"""
|
|
206
|
+
def hex2rgb(h):
|
|
207
|
+
return tuple(int(h[i:i + 2], 16) for i in (1, 3, 5))
|
|
208
|
+
|
|
209
|
+
def rgb2hex(rgb):
|
|
210
|
+
return "#" + "".join(f"{max(0, min(255, round(c))):02X}" for c in rgb)
|
|
211
|
+
|
|
212
|
+
def lerp(a, b, t):
|
|
213
|
+
return tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
|
|
214
|
+
|
|
215
|
+
blue, white, red = hex2rgb("#2166AC"), hex2rgb("#F7F7F7"), hex2rgb("#B2182B")
|
|
216
|
+
w = max(0.0, min(omega, 3.0))
|
|
217
|
+
if w <= 1.0:
|
|
218
|
+
return rgb2hex(lerp(blue, white, w))
|
|
219
|
+
return rgb2hex(lerp(white, red, (w - 1.0) / 2.0))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def write_selection(omega_tsv: Path, outdir: Path) -> list:
|
|
223
|
+
"""选择压力标注树:ω 表 → TREE_COLORS(枝按 ω 蓝→白→红渐变)。
|
|
224
|
+
|
|
225
|
+
输入 tsv:node<TAB>omega(node=叶名或内部节点 LCA 写法 ID1|ID2)
|
|
226
|
+
"""
|
|
227
|
+
rows = []
|
|
228
|
+
for i, ln in enumerate(Path(omega_tsv).read_text(encoding="utf-8").splitlines()):
|
|
229
|
+
if not ln.strip() or ln.startswith("#"):
|
|
230
|
+
continue
|
|
231
|
+
nid, _, w_str = ln.partition("\t")
|
|
232
|
+
try:
|
|
233
|
+
w = float(w_str.strip())
|
|
234
|
+
except ValueError:
|
|
235
|
+
raise ValueError(f"第 {i + 1} 行 ω 不是数字:{ln!r}") from None
|
|
236
|
+
rows.append((nid.strip(), w))
|
|
237
|
+
if not rows:
|
|
238
|
+
raise ValueError("输入无任何有效 ω 记录")
|
|
239
|
+
|
|
240
|
+
out = ["TREE_COLORS", "SEPARATOR TAB",
|
|
241
|
+
"# ω 发散色阶:蓝(纯化)→白(ω=1)→红(正选择,cap 3)",
|
|
242
|
+
"DATA"]
|
|
243
|
+
for nid, w in rows:
|
|
244
|
+
out.append(f"{nid}\tbranch\t{_omega_color(w)}\tnormal\t2")
|
|
245
|
+
outdir = Path(outdir)
|
|
246
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
p = outdir / "itol_selection.txt"
|
|
248
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
249
|
+
return [p]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def write_connections(pairs_tsv: Path, outdir: Path, width: float = 1.0,
|
|
253
|
+
style: str = "normal") -> list:
|
|
254
|
+
"""共线连线树 / tanglegram:配对表 → DATASET_CONNECTION。
|
|
255
|
+
|
|
256
|
+
输入 tsv:node1<TAB>node2[<TAB>color][<TAB>label]
|
|
257
|
+
官方行:NODE1,NODE2,WIDTH,COLOR,STYLE,LABEL(宽度按 MAXIMUM_LINE_WIDTH 归一;STYLE normal/dashed)
|
|
258
|
+
"""
|
|
259
|
+
rows = []
|
|
260
|
+
for i, ln in enumerate(Path(pairs_tsv).read_text(encoding="utf-8").splitlines()):
|
|
261
|
+
if not ln.strip() or ln.startswith("#"):
|
|
262
|
+
continue
|
|
263
|
+
parts = [p.strip() for p in ln.split("\t")]
|
|
264
|
+
if len(parts) < 2:
|
|
265
|
+
raise ValueError(f"第 {i + 1} 行需 ≥2 列(node1 node2):{ln!r}")
|
|
266
|
+
color = parts[2] if len(parts) >= 3 and parts[2] else "#999999"
|
|
267
|
+
label = parts[3] if len(parts) >= 4 else ""
|
|
268
|
+
rows.append((parts[0], parts[1], color, label))
|
|
269
|
+
if not rows:
|
|
270
|
+
raise ValueError("输入无任何有效配对")
|
|
271
|
+
out = ["DATASET_CONNECTION", "SEPARATOR TAB",
|
|
272
|
+
"DATASET_LABEL\tConnections", "COLOR\t#999999",
|
|
273
|
+
"DRAW_ARROWS\t0", "MAXIMUM_LINE_WIDTH\t10", "CENTER_CURVES\t1",
|
|
274
|
+
"DATA"]
|
|
275
|
+
for n1, n2, c, lab in rows:
|
|
276
|
+
out.append(f"{n1}\t{n2}\t{width}\t{c}\t{style}\t{lab}")
|
|
277
|
+
outdir = Path(outdir)
|
|
278
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
p = outdir / "itol_connections.txt"
|
|
280
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
281
|
+
return [p]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def write_timescale(intervals_tsv: Path, outdir: Path, scaling: float = 1.0) -> list:
|
|
285
|
+
"""时间树地质年代轴:区间表 → DATASET_TIMESCALE(树下方色带时间标尺)。
|
|
286
|
+
|
|
287
|
+
输入 tsv:name<TAB>start<TAB>end[<TAB>color]
|
|
288
|
+
⚠️ iTOL 约定:零=树顶(现今),值向根递增——start(老,大值)> end(新,小值)。
|
|
289
|
+
官方行:RANGE_START,RANGE_END,START_LABEL,END_LABEL,FILL_COLOR,GRADIENT_COLOR,LABEL_TEXT,LABEL_STYLE
|
|
290
|
+
"""
|
|
291
|
+
rows = []
|
|
292
|
+
for i, ln in enumerate(Path(intervals_tsv).read_text(encoding="utf-8").splitlines()):
|
|
293
|
+
if not ln.strip() or ln.startswith("#"):
|
|
294
|
+
continue
|
|
295
|
+
parts = [p.strip() for p in ln.split("\t")]
|
|
296
|
+
if len(parts) < 3:
|
|
297
|
+
raise ValueError(f"第 {i + 1} 行需 ≥3 列(name start end):{ln!r}")
|
|
298
|
+
try:
|
|
299
|
+
start, end = float(parts[1]), float(parts[2])
|
|
300
|
+
except ValueError:
|
|
301
|
+
raise ValueError(f"第 {i + 1} 行 start/end 须为数字:{ln!r}") from None
|
|
302
|
+
if start <= end:
|
|
303
|
+
raise ValueError(f"第 {i + 1} 行 iTOL 约定 start(老)>end(新),此行 start={start} end={end}:{ln!r}")
|
|
304
|
+
color = parts[3] if len(parts) >= 4 and parts[3] else "#c8c8c8"
|
|
305
|
+
rows.append((parts[0], start, end, color))
|
|
306
|
+
if not rows:
|
|
307
|
+
raise ValueError("输入无任何有效区间")
|
|
308
|
+
out = ["DATASET_TIMESCALE", "SEPARATOR TAB",
|
|
309
|
+
"DATASET_LABEL\tTimescale", "COLOR\t#000000",
|
|
310
|
+
f"SCALING_FACTOR\t{scaling}", "AUTO_SCALE\t1", "SHOW_VALUE_LABELS\t1",
|
|
311
|
+
"DATA"]
|
|
312
|
+
|
|
313
|
+
def _num(x: float) -> str:
|
|
314
|
+
return str(int(x)) if float(x).is_integer() else str(x)
|
|
315
|
+
|
|
316
|
+
for name, start, end, color in rows:
|
|
317
|
+
out.append(f"{_num(start)}\t{_num(end)}\t\t\t{color}\t\t{name}\tnormal")
|
|
318
|
+
outdir = Path(outdir)
|
|
319
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
p = outdir / "itol_timescale.txt"
|
|
321
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
322
|
+
return [p]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def write_pav(matrix_tsv: Path, outdir: Path, tree_leaves: list | None = None) -> tuple[list, list]:
|
|
326
|
+
"""泛基因组 PAV 联树:0/1 矩阵 → DATASET_BINARY。
|
|
327
|
+
|
|
328
|
+
输入 tsv:首列基因 ID(=树叶名),其余列=基因组 0/1;与 heatmap 共享行序=树序校验。
|
|
329
|
+
"""
|
|
330
|
+
lines = Path(matrix_tsv).read_text(encoding="utf-8").splitlines()
|
|
331
|
+
header = lines[0].split("\t")
|
|
332
|
+
genomes = header[1:]
|
|
333
|
+
rows = {ln.split("\t")[0]: ln for ln in lines[1:] if ln.strip()}
|
|
334
|
+
# 校验 0/1 纯度
|
|
335
|
+
for ln in lines[1:]:
|
|
336
|
+
if not ln.strip():
|
|
337
|
+
continue
|
|
338
|
+
for v in ln.split("\t")[1:]:
|
|
339
|
+
if v.strip() not in ("0", "1"):
|
|
340
|
+
raise ValueError(f"PAV 矩阵只接受 0/1,发现:{v!r}(行 {ln[:40]!r})")
|
|
341
|
+
missing, ordered = [], []
|
|
342
|
+
if tree_leaves:
|
|
343
|
+
for leaf in tree_leaves:
|
|
344
|
+
if leaf in rows:
|
|
345
|
+
ordered.append(rows.pop(leaf))
|
|
346
|
+
else:
|
|
347
|
+
missing.append(leaf)
|
|
348
|
+
ordered.extend(rows.values())
|
|
349
|
+
else:
|
|
350
|
+
ordered = list(rows.values())
|
|
351
|
+
out = ["DATASET_BINARY", "SEPARATOR TAB",
|
|
352
|
+
"DATASET_LABEL\tPAV", "COLOR\t#E69F00",
|
|
353
|
+
"FIELD_LABELS\t" + ",".join(genomes),
|
|
354
|
+
"DATA"]
|
|
355
|
+
out.extend(ordered)
|
|
356
|
+
outdir = Path(outdir)
|
|
357
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
p = outdir / "itol_pav.txt"
|
|
359
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
360
|
+
return [p], missing
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def write_heatmap(matrix_tsv: Path, outdir: Path, tree_leaves: list | None = None) -> tuple[list, list]:
|
|
364
|
+
"""表达矩阵 → DATASET_HEATMAP。行序校验:给了 tree_leaves 就强制按树序重排+查漏。"""
|
|
365
|
+
lines = Path(matrix_tsv).read_text(encoding="utf-8").splitlines()
|
|
366
|
+
header = lines[0].split("\t")
|
|
367
|
+
samples = header[1:]
|
|
368
|
+
rows = {ln.split("\t")[0]: ln for ln in lines[1:] if ln.strip()}
|
|
369
|
+
missing, ordered = [], []
|
|
370
|
+
if tree_leaves:
|
|
371
|
+
for leaf in tree_leaves:
|
|
372
|
+
if leaf in rows:
|
|
373
|
+
ordered.append(rows.pop(leaf))
|
|
374
|
+
else:
|
|
375
|
+
missing.append(leaf)
|
|
376
|
+
ordered.extend(rows.values()) # 树外多余行挂尾(会告警)
|
|
377
|
+
else:
|
|
378
|
+
ordered = list(rows.values())
|
|
379
|
+
|
|
380
|
+
out = ["DATASET_HEATMAP", "SEPARATOR TAB",
|
|
381
|
+
"FIELD_LABELS\t" + ",".join(samples),
|
|
382
|
+
"COLOR_MIN\t#2166AC", "COLOR_MID\t#F7F7F7", "COLOR_MAX\t#B2182B",
|
|
383
|
+
"AUTO_SCALE\t1", "DATA"]
|
|
384
|
+
out.extend(ordered)
|
|
385
|
+
outdir = Path(outdir)
|
|
386
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
387
|
+
p = outdir / "itol_heatmap.txt"
|
|
388
|
+
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
389
|
+
return [p], missing
|