MaxTiC-Next 0.1.1__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.
- maxtic_next/__init__.py +28 -0
- maxtic_next/__main__.py +9 -0
- maxtic_next/api.py +553 -0
- maxtic_next/benchmarks/__init__.py +9 -0
- maxtic_next/benchmarks/suite.py +1133 -0
- maxtic_next/cli.py +538 -0
- maxtic_next/config.py +91 -0
- maxtic_next/constraints/__init__.py +7 -0
- maxtic_next/constraints/adapters/__init__.py +122 -0
- maxtic_next/constraints/adapters/_diagnostics.py +513 -0
- maxtic_next/constraints/adapters/_recon_report.py +457 -0
- maxtic_next/constraints/adapters/_species.py +218 -0
- maxtic_next/constraints/adapters/ale.py +906 -0
- maxtic_next/constraints/adapters/alerax.py +405 -0
- maxtic_next/constraints/adapters/artra.py +350 -0
- maxtic_next/constraints/adapters/eccetera.py +674 -0
- maxtic_next/constraints/adapters/ranger_dtl.py +329 -0
- maxtic_next/constraints/adapters/registry.py +545 -0
- maxtic_next/constraints/constraint.py +112 -0
- maxtic_next/constraints/filter.py +37 -0
- maxtic_next/constraints/parsers.py +116 -0
- maxtic_next/dry_run.py +247 -0
- maxtic_next/io/__init__.py +17 -0
- maxtic_next/io/compression.py +471 -0
- maxtic_next/io/output.py +275 -0
- maxtic_next/io/parsing.py +195 -0
- maxtic_next/prune.py +117 -0
- maxtic_next/random_.py +76 -0
- maxtic_next/ranking/__init__.py +23 -0
- maxtic_next/ranking/checkpoint.py +166 -0
- maxtic_next/ranking/edge.py +107 -0
- maxtic_next/ranking/greedy.py +42 -0
- maxtic_next/ranking/local_search.py +242 -0
- maxtic_next/ranking/mixing.py +132 -0
- maxtic_next/ranking/path.py +29 -0
- maxtic_next/ranking/ranker.py +1373 -0
- maxtic_next/ranking/reachability.py +127 -0
- maxtic_next/ranking/value.py +176 -0
- maxtic_next/report/__init__.py +14 -0
- maxtic_next/report/html.py +341 -0
- maxtic_next/report/plot.py +399 -0
- maxtic_next/report/templates/partials/robustness.j2 +1 -0
- maxtic_next/report/templates/partials/summary.j2 +20 -0
- maxtic_next/report/templates/partials/tree_view.j2 +1 -0
- maxtic_next/report/templates/report.html.j2 +53 -0
- maxtic_next/robustness/__init__.py +13 -0
- maxtic_next/robustness/mcmc.py +943 -0
- maxtic_next/robustness/sensitivity.py +239 -0
- maxtic_next/tree/__init__.py +13 -0
- maxtic_next/tree/cache.py +122 -0
- maxtic_next/tree/node.py +36 -0
- maxtic_next/tree/tree.py +551 -0
- maxtic_next-0.1.1.dist-info/METADATA +378 -0
- maxtic_next-0.1.1.dist-info/RECORD +58 -0
- maxtic_next-0.1.1.dist-info/WHEEL +5 -0
- maxtic_next-0.1.1.dist-info/entry_points.txt +2 -0
- maxtic_next-0.1.1.dist-info/licenses/LICENSE +541 -0
- maxtic_next-0.1.1.dist-info/top_level.txt +1 -0
maxtic_next/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""MaxTiC-Next —— MaxTiC 的 Python 3 重写版本。
|
|
2
|
+
|
|
3
|
+
本包把原版 ``MaxTiC.py``(Eric Tannier, Inria)的核心算法重构为模块化、可测试、
|
|
4
|
+
可复现的 Python 3 代码。
|
|
5
|
+
|
|
6
|
+
**关于"等价"的准确表述**:核心启发式(贪婪 / mixing / 局部搜索 / 目标函数)为忠实
|
|
7
|
+
移植,并在固定 ``--seed`` 下与 ``tests/baselines/`` 中**由本版本自产**的快照逐字节
|
|
8
|
+
一致(回归基线,防漂移)。相对原版的**已记录偏离**(输入守卫、统计口径、
|
|
9
|
+
哨兵阈值、局部搜索后回写、置换检验 p 值校正等)见
|
|
10
|
+
``maxtic_next.ranking.ranker`` 模块文档与 ``tests/test_reference_equivalence.py``;
|
|
11
|
+
这些偏离使输出**不再**与原版逐字节相同,但每一项都对应原版的一处缺陷。
|
|
12
|
+
|
|
13
|
+
原版作者:Eric Tannier
|
|
14
|
+
引用:MaxTiC: Fast ranking of a phylogenetic tree by Maximum Time Consistency
|
|
15
|
+
with lateral gene transfers, Biorxiv doi.org/10.1101/127548
|
|
16
|
+
许可证:CeCILL 2.1(继承自原版)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.1"
|
|
20
|
+
__author__ = "Zichao Zeng (曾子超, ORCID 0000-0001-6553-970X) — Python 3 rewrite; Eric Tannier — original MaxTiC"
|
|
21
|
+
__license__ = "CECILL-2.1"
|
|
22
|
+
__original_doi__ = "doi.org/10.1101/127548"
|
|
23
|
+
|
|
24
|
+
__all__ = ["__version__", "rank", "build_constraints", "Tree", "Ranker"]
|
|
25
|
+
|
|
26
|
+
from maxtic_next.tree.tree import Tree
|
|
27
|
+
from maxtic_next.ranking.ranker import Ranker
|
|
28
|
+
from maxtic_next.api import rank, build_constraints
|
maxtic_next/__main__.py
ADDED
maxtic_next/api.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""Python API:``rank`` / ``build_constraints``。
|
|
2
|
+
|
|
3
|
+
本模块是 ``rank`` 的统一编排入口,也是各可选插入点(约束转换 / 目标类群剪裁 /
|
|
4
|
+
预检 / HTML 报告)的挂载处。所有可选特性**默认关闭,关闭时不影响核心排序结果**:
|
|
5
|
+
|
|
6
|
+
* ``from_ale`` / ``from_tool``:把上游工具输出经适配器转为约束;
|
|
7
|
+
* ``target_clade`` / ``ancestor_map``:目标类群剪裁;
|
|
8
|
+
* ``dry_run``:正式排序前的静态预检;预检结论同时以**结构化字段**
|
|
9
|
+
``Result.run_metadata["dry_run_ok"]`` 回传(CLI 退出码不得靠子串匹配);
|
|
10
|
+
* ``html_report``:交互式 HTML 报告;
|
|
11
|
+
* ``force``:允许覆盖既有产物(,CLI ``--force``)。
|
|
12
|
+
|
|
13
|
+
本轮(第二遍)新增的三个可选项,**默认值都保持历史行为**:
|
|
14
|
+
|
|
15
|
+
* ``top_k``(CLI ``--near-optimal-top-k``):近优解收集器容量 —— 稳健性/敏感性摘要
|
|
16
|
+
能看过多少个去重近优排序(,默认 50);
|
|
17
|
+
* ``stop_check``:协作式取消谓词,在局部搜索 / MCMC 的**迭代边界**真正打断计算
|
|
18
|
+
(Studio 的「取消」按钮);
|
|
19
|
+
* 压缩输入:``.gz`` 单文件与上游官方 ``.tgz`` 示例(如 ALE 的
|
|
20
|
+
``examples/reconciliations.tgz``)都可直接作为输入,无需先手工解压。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Callable, Dict, List, Optional, Tuple, Union
|
|
26
|
+
|
|
27
|
+
from maxtic_next.config import (
|
|
28
|
+
DEFAULT_MIN_TRANSFER_DIST,
|
|
29
|
+
DEFAULT_NEAR_OPTIMAL_TOP_K,
|
|
30
|
+
DEFAULT_OUTPUT_STYLE,
|
|
31
|
+
DEFAULT_RANDOM_TREES,
|
|
32
|
+
DEFAULT_RANDOM_TYPE,
|
|
33
|
+
DEFAULT_SEED,
|
|
34
|
+
DEFAULT_TEMPERATURE,
|
|
35
|
+
DEFAULT_THRESHOLD_CONSTRAINTS,
|
|
36
|
+
DEFAULT_TIME_FOR_SEARCH,
|
|
37
|
+
MCMC_TEMPERATURE_AUTO,
|
|
38
|
+
DEFAULT_MCMC_ITERS,
|
|
39
|
+
)
|
|
40
|
+
from maxtic_next.constraints.constraint import ConstraintSet
|
|
41
|
+
from maxtic_next.ranking.ranker import Ranker, Result
|
|
42
|
+
from maxtic_next.random_ import RandomWrapper
|
|
43
|
+
|
|
44
|
+
#: 适配器端点命中率告警阈值的"未指定"哨兵:实际默认值由适配器层
|
|
45
|
+
#: (``adapters._diagnostics.DEFAULT_MIN_ENDPOINT_HIT_RATE``) 提供,延迟导入以保持
|
|
46
|
+
#: ``import maxtic_next.api`` 轻量(适配器及其可选依赖只在真正用到时加载)。
|
|
47
|
+
ADAPTER_HIT_RATE_UNSPECIFIED = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_constraints(constraints_path: str) -> ConstraintSet:
|
|
51
|
+
"""读取并解析约束文件,返回 ``ConstraintSet``。
|
|
52
|
+
|
|
53
|
+
注:按距离过滤由 ``ConstraintSet.filter_by_distance`` 在排序阶段执行,
|
|
54
|
+
此处仅做解析。
|
|
55
|
+
"""
|
|
56
|
+
from maxtic_next.io.parsing import read_constraints_file
|
|
57
|
+
|
|
58
|
+
return read_constraints_file(constraints_path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _expand_archive_inputs(
|
|
62
|
+
cons_list: List[str], tool: Optional[str]
|
|
63
|
+
) -> Tuple[List[str], Dict[str, List[str]]]:
|
|
64
|
+
"""把输入里的 **tar 归档**(上游官方 ``.tgz`` / ``.tar.gz`` 示例)展开为成员输入。
|
|
65
|
+
|
|
66
|
+
:MaxTiC 的 ``examples/reconciliations.tgz`` 里是 1000 个
|
|
67
|
+
``*.uml_rec``,旧实现要求用户先手工 ``tar -zxvf`` 且**不给任何提示**。这里只处理
|
|
68
|
+
"一个归档里有多份上游输出";**单文件** gzip(``.gz`` / gzip 流)由读取层
|
|
69
|
+
(``io.compression.open_text``)透明解压,不经过本函数。
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
``(展开后的输入列表, {归档路径: 展开得到的输入列表})``;第二个返回值供 stderr
|
|
73
|
+
提示与 ``run_metadata["archives_expanded"]`` 留痕(成员活在临时目录里)。
|
|
74
|
+
"""
|
|
75
|
+
from maxtic_next.io.compression import expand_archive_inputs
|
|
76
|
+
|
|
77
|
+
return expand_archive_inputs(cons_list, tool)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _archive_input_notes(archive_expansions: Dict[str, List[str]]) -> List[str]:
|
|
81
|
+
"""为每个被展开的归档生成一条 stderr 提示(含临时解包位置,便于复核)。"""
|
|
82
|
+
notes: List[str] = []
|
|
83
|
+
for archive, inputs in archive_expansions.items():
|
|
84
|
+
where = inputs[0] if len(inputs) == 1 else os.path.dirname(inputs[0])
|
|
85
|
+
notes.append(
|
|
86
|
+
f"NOTE: 输入 {archive} 是 tar 归档,已自动展开为 {len(inputs)} 项输入"
|
|
87
|
+
f"(解包于 {where})。三输出文件与 HTML 报告的前缀取第一项输入;如需为"
|
|
88
|
+
"每个成员分别留存产物,请显式指定 -p/--output-prefix 或分别运行。"
|
|
89
|
+
"单文件 gzip(.gz)无需解包,读取层已透明解压;含多个成员的归档若被当作"
|
|
90
|
+
"**一个**文本输入读取,会给出含 tar 解包命令的提示而非 traceback。"
|
|
91
|
+
)
|
|
92
|
+
return notes
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rank(
|
|
96
|
+
species_tree_path: str,
|
|
97
|
+
constraints_path: Union[str, List[str]],
|
|
98
|
+
seed: int = DEFAULT_SEED,
|
|
99
|
+
local_search: float = DEFAULT_TIME_FOR_SEARCH,
|
|
100
|
+
temperature: float = DEFAULT_TEMPERATURE,
|
|
101
|
+
random_type: int = DEFAULT_RANDOM_TYPE,
|
|
102
|
+
min_transfer_distance: float = DEFAULT_MIN_TRANSFER_DIST,
|
|
103
|
+
threshold_constraints: float = DEFAULT_THRESHOLD_CONSTRAINTS,
|
|
104
|
+
random_trees: int = DEFAULT_RANDOM_TREES,
|
|
105
|
+
output_prefix: Optional[str] = None,
|
|
106
|
+
print_summary: bool = True,
|
|
107
|
+
*,
|
|
108
|
+
from_ale: bool = False,
|
|
109
|
+
from_tool: Optional[str] = None,
|
|
110
|
+
target_clade: Optional[str] = None,
|
|
111
|
+
ancestor_map: bool = False,
|
|
112
|
+
dry_run: bool = False,
|
|
113
|
+
html_report: bool = True,
|
|
114
|
+
ale_min_support: float = 0.05,
|
|
115
|
+
ale_min_family_size: int = 5,
|
|
116
|
+
ale_cache_dir: Optional[str] = None,
|
|
117
|
+
ale_source: str = "trf",
|
|
118
|
+
ale_parallel: str = "process",
|
|
119
|
+
artra_transfer_kind: str = "all",
|
|
120
|
+
adapter_min_endpoint_hit_rate: Optional[float] = ADAPTER_HIT_RATE_UNSPECIFIED,
|
|
121
|
+
adapter_quiet: bool = False,
|
|
122
|
+
mcmc: bool = False,
|
|
123
|
+
mcmc_iters: int = DEFAULT_MCMC_ITERS,
|
|
124
|
+
mcmc_temperature: float = MCMC_TEMPERATURE_AUTO,
|
|
125
|
+
mcmc_burn_in: Optional[int] = None,
|
|
126
|
+
mcmc_thin: Optional[int] = None,
|
|
127
|
+
incremental: bool = True,
|
|
128
|
+
checkpoint_path: Optional[str] = None,
|
|
129
|
+
checkpoint_interval: float = 60.0,
|
|
130
|
+
constraints_out: Optional[str] = None,
|
|
131
|
+
output_style: str = DEFAULT_OUTPUT_STYLE,
|
|
132
|
+
force: bool = False,
|
|
133
|
+
local_search_max_iterations: int = 0,
|
|
134
|
+
top_k: int = DEFAULT_NEAR_OPTIMAL_TOP_K,
|
|
135
|
+
stop_check: Optional[Callable[[], bool]] = None,
|
|
136
|
+
) -> Result:
|
|
137
|
+
"""运行 MaxTiC 排序流程,返回 ``Result``。
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
species_tree_path: 物种树(Newick)路径。
|
|
141
|
+
constraints_path: 约束文件路径(文本双格式),或上游工具输出文件/目录列表。
|
|
142
|
+
seed: 随机种子(驱动 mix 平局与局部搜索),默认 42。
|
|
143
|
+
local_search: 局部搜索时长(秒),0 表示关闭。
|
|
144
|
+
temperature: Metropolis 温度(``local_search > 0`` 时必须 > 0)。
|
|
145
|
+
random_type: 随机化类型,只接受 ``0`` / ``1`` / ``2``。
|
|
146
|
+
min_transfer_distance: 最小转移距离阈值(按 phylogenetic distance 列过滤)。
|
|
147
|
+
threshold_constraints: 约束权重阈值比例,取值域 ``[0, 1]``。
|
|
148
|
+
random_trees: 随机树采样数量,0 表示关闭。
|
|
149
|
+
output_prefix: 输出文件前缀,默认使用**第一个**约束文件路径(多文件时会在
|
|
150
|
+
``run_metadata["constraint_files_merged"]`` 与 stdout 中显式记录)。
|
|
151
|
+
print_summary: 是否打印 stdout 摘要。
|
|
152
|
+
from_ale: 是否使用 ALE 适配器模式(``constraints_path`` 视为 ``.uml_rec`` 列表)。
|
|
153
|
+
向后兼容开关,等价 ``from_tool="ale"``。
|
|
154
|
+
from_tool: 上游工具适配器名(``"ale"`` / ``"ranger"`` / ``"eccetera"`` /
|
|
155
|
+
``"artra"`` / ``"alerax"`` / ``"auto"``)。设置后 ``constraints_path`` 视为
|
|
156
|
+
该工具输出文件/目录列表,经注册表分发到对应适配器转为统一约束;
|
|
157
|
+
``"auto"`` 时按内容/扩展名自动检测。优先级高于 ``from_ale``。
|
|
158
|
+
target_clade: 目标类群根节点标签;``None`` 表示不剪裁。
|
|
159
|
+
ancestor_map: 是否开启"外部祖先映射到根"的保守剪裁规则(需配合 ``target_clade``)。
|
|
160
|
+
dry_run: 仅做静态预检并打印报告,不运行正式排序。预检结论同时写入
|
|
161
|
+
``Result.run_metadata`` 的 ``dry_run_ok`` / ``dry_run_report``,供调用方
|
|
162
|
+
以**结构化字段**判定退出码。适配器模式下 ``dry_run`` 不再只是
|
|
163
|
+
"文件可读"检查:会真正解析上游输出并报告端点命中率 / 零约束问题。
|
|
164
|
+
html_report: 是否生成交互式 HTML 报告(默认 True;``--no-html`` 对应 False)。
|
|
165
|
+
ale_min_support: ALE 适配器单家族内最小支持度阈值。
|
|
166
|
+
ale_min_family_size: ALE 适配器最小基因家族规模。
|
|
167
|
+
ale_cache_dir: ALE 适配器文件级缓存目录(断点续传)。
|
|
168
|
+
ale_source: ALE 约束来源 ``"trf"``(**默认**)/ ``"rec"``。默认取 ``trf``,
|
|
169
|
+
即 ALE 官方 MaxTiC 集成(``maxtic/constraints_from_reconciliations.py``
|
|
170
|
+
的 ``constraints_from_transfers``:供体的父节点须早于受体的子节点)所
|
|
171
|
+
规定的口径;``rec``(按调和事件)仍可选,但两者产出的约束集
|
|
172
|
+
**规模与权重量级都不同**,切换即换掉整个输入。
|
|
173
|
+
adapter_min_endpoint_hit_rate: 适配器端点命中率告警阈值:
|
|
174
|
+
低于该比例时向 stderr 告警,命中率为 0 时直接报错(不再产出"静默的
|
|
175
|
+
空约束集")。``None`` 表示沿用适配器层默认(0.5)。
|
|
176
|
+
adapter_quiet: 抑制适配器在 stderr 上的例行摘要与告警。
|
|
177
|
+
**只影响打印**:诊断量仍完整写入
|
|
178
|
+
``Result.run_metadata["adapter_diagnostics"]``;程序性错误(0 命中、
|
|
179
|
+
上游 XML 解析失败)依然抛出。
|
|
180
|
+
ale_parallel: ALE 并行解析模式 ``"process"``(默认)/ ``"thread"``。
|
|
181
|
+
artra_transfer_kind: ARTra 转移类别。
|
|
182
|
+
mcmc: 是否开启线性扩展上的 Metropolis–Hastings 采样(默认 False)。
|
|
183
|
+
mcmc_iters: MH 链步数(必须 > 0)。
|
|
184
|
+
mcmc_temperature: Boltzmann 温度;``0.0``(默认)表示 ``auto``,
|
|
185
|
+
有效值 ``max(total_weight, 1) / 100``,会被打印并记入
|
|
186
|
+
``run_metadata["params"]``。
|
|
187
|
+
mcmc_burn_in: burn-in 步数;``None`` 表示取 ``mcmc_iters`` 的一半。
|
|
188
|
+
mcmc_thin: 抽稀间隔;``None`` 表示 ``max(1, mcmc_iters // 100)``。
|
|
189
|
+
incremental: 是否启用增量 scoring。**默认开启**:
|
|
190
|
+
增量与全量在逐字节等价性扫描(4 组输入 × 5 个种子 × 7 个产物字段全一致)
|
|
191
|
+
下等价,由 tests/test_output_equivalence.py 的成对用例与 tests/test_delivery_guards.py 看守;
|
|
192
|
+
传 False(CLI ``--no-incremental``)可退回全量重算口径。
|
|
193
|
+
checkpoint_path: 检查点文件路径;``None``(默认)禁用。
|
|
194
|
+
checkpoint_interval: 检查点自动保存间隔(秒),默认 60 秒。
|
|
195
|
+
constraints_out: 仅适配器模式有效。写出统一约束集后提前返回;
|
|
196
|
+
``dry_run=True`` 时同样会写出该文件(不再静默忽略 ``-o``)。
|
|
197
|
+
output_style: 三输出文件命名风格,``"short"``(默认)或 ``"legacy"``。
|
|
198
|
+
force: 允许覆盖已存在的产物文件(对应 CLI ``--force``)。
|
|
199
|
+
local_search_max_iterations: 局部搜索迭代上限(0 = 仅受时长约束)。
|
|
200
|
+
用于把"固定 seed 可复现"的承诺扩展到 ``--ls > 0`` 路径。
|
|
201
|
+
top_k: **近优解收集容量**:局部搜索期间为"稳健性/敏感性摘要"
|
|
202
|
+
保留多少个**去重**近优排序;必须为 ``>= 1`` 的整数,默认 ``50``
|
|
203
|
+
(``config.DEFAULT_NEAR_OPTIMAL_TOP_K``,与历史行为一致)。它同时是摘要能
|
|
204
|
+
看到多少个近优序的**硬上限**,因此 ``local_search > 0`` 时才有效果,
|
|
205
|
+
生效值记入 ``run_metadata["params"]`` 与
|
|
206
|
+
``run_metadata["near_optimal_top_k"]``。对应 CLI
|
|
207
|
+
``--near-optimal-top-k``。
|
|
208
|
+
stop_check: **协作式取消回调**(``() -> bool``):在局部搜索与
|
|
209
|
+
MCMC 链的**迭代边界**打断计算,返回"截至取消点"的结果而不是抛异常。
|
|
210
|
+
Studio(独立仓库)的「取消」按钮经其 ``cancellation.StopToken`` 注入该参数。
|
|
211
|
+
默认 ``None``(不检查);是否真的打断见 ``run_metadata["cancelled"]``。
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
包含排序结果与输出文件路径的 ``Result``(dry_run 时含 ``dry_run_report``)。
|
|
215
|
+
"""
|
|
216
|
+
from maxtic_next.io.parsing import read_constraints_file, read_newick_file
|
|
217
|
+
|
|
218
|
+
# ---- 参数即校验:dry-run 路径也必须拒绝非法取值 ----
|
|
219
|
+
Ranker._validate_params(
|
|
220
|
+
min_transfer_distance=min_transfer_distance,
|
|
221
|
+
threshold_constraints=threshold_constraints,
|
|
222
|
+
temperature=temperature,
|
|
223
|
+
random_type=random_type,
|
|
224
|
+
time_for_search=local_search,
|
|
225
|
+
random_trees=random_trees,
|
|
226
|
+
mcmc_iters=mcmc_iters,
|
|
227
|
+
mcmc_temperature=mcmc_temperature,
|
|
228
|
+
mcmc_burn_in=mcmc_burn_in,
|
|
229
|
+
mcmc_thin=mcmc_thin,
|
|
230
|
+
local_search_max_iterations=local_search_max_iterations,
|
|
231
|
+
near_optimal_top_k=top_k,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# 规范化约束路径为列表
|
|
235
|
+
if isinstance(constraints_path, (list, tuple)):
|
|
236
|
+
cons_list = [str(p) for p in constraints_path]
|
|
237
|
+
else:
|
|
238
|
+
cons_list = [str(constraints_path)]
|
|
239
|
+
|
|
240
|
+
# ---- :tar 归档输入(上游官方示例 .tgz)自动展开为成员文件 ----
|
|
241
|
+
tree = read_newick_file(species_tree_path)
|
|
242
|
+
|
|
243
|
+
# 适配器名(优先级:--from > --from-ale)
|
|
244
|
+
tool = from_tool if from_tool else ("ale" if from_ale else None)
|
|
245
|
+
|
|
246
|
+
# 归档展开需要知道工具名(AleRax 的输出树归档要按目录约定取文件),故排在
|
|
247
|
+
# ``tool`` 判定之后、预检与解析之前:此后所有分支看到的都是"真正的输入文件"。
|
|
248
|
+
cons_list, archive_expansions = _expand_archive_inputs(cons_list, tool)
|
|
249
|
+
archive_notes = _archive_input_notes(archive_expansions)
|
|
250
|
+
for archive_note in archive_notes:
|
|
251
|
+
# 走 stderr:stdout 仍是可机读的原版口径摘要(与多文件前缀提示同一通道)
|
|
252
|
+
sys.stderr.write(archive_note + "\n")
|
|
253
|
+
sys.stderr.flush()
|
|
254
|
+
if tool is None and constraints_out is not None:
|
|
255
|
+
# -o/--constraints-out 仅在适配器模式下有意义(写"转换后的统一约束");
|
|
256
|
+
# 文本模式下静默忽略会误导用户,显式报错。
|
|
257
|
+
raise ValueError(
|
|
258
|
+
"constraints_out/-o 仅在适配器模式(--from <tool> / --from-ale)下有效;"
|
|
259
|
+
"文本约束模式请直接使用输入文件(已是统一格式)。"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# ---- 预检(可选插入点,早于正式排序)----
|
|
263
|
+
adapter_diagnostics: Dict = {}
|
|
264
|
+
if dry_run:
|
|
265
|
+
from maxtic_next.dry_run import dry_run_check, format_dry_run
|
|
266
|
+
|
|
267
|
+
report = dry_run_check(
|
|
268
|
+
tree,
|
|
269
|
+
cons_list,
|
|
270
|
+
from_ale=(tool is not None),
|
|
271
|
+
min_transfer_distance=min_transfer_distance,
|
|
272
|
+
target_clade=target_clade,
|
|
273
|
+
)
|
|
274
|
+
# :适配器模式的预检不再只做"文件可读"检查——真正解析上游输出,
|
|
275
|
+
# 并把"零约束 / 端点命中率过低"如实报为问题(旧实现对垃圾输入判"通过")。
|
|
276
|
+
if tool is not None:
|
|
277
|
+
issues = report["issues"]
|
|
278
|
+
try:
|
|
279
|
+
cset_dry = _convert_via_registry(
|
|
280
|
+
tool,
|
|
281
|
+
tree,
|
|
282
|
+
cons_list,
|
|
283
|
+
constraints_out=constraints_out,
|
|
284
|
+
ale_min_support=ale_min_support,
|
|
285
|
+
ale_min_family_size=ale_min_family_size,
|
|
286
|
+
ale_cache_dir=ale_cache_dir,
|
|
287
|
+
ale_parallel=ale_parallel,
|
|
288
|
+
ale_source=ale_source,
|
|
289
|
+
artra_transfer_kind=artra_transfer_kind,
|
|
290
|
+
min_endpoint_hit_rate=adapter_min_endpoint_hit_rate,
|
|
291
|
+
quiet=adapter_quiet,
|
|
292
|
+
)
|
|
293
|
+
resolved = cset_dry[0]
|
|
294
|
+
cset = cset_dry[1]
|
|
295
|
+
adapter_diagnostics = _adapter_diagnostics_of(cset)
|
|
296
|
+
except Exception as exc: # noqa: BLE001 - 预检须把异常转为报告项
|
|
297
|
+
issues.append(
|
|
298
|
+
{
|
|
299
|
+
"severity": "error",
|
|
300
|
+
"message": f"上游输出(格式 {tool})解析失败:{type(exc).__name__}: {exc}",
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
exc_diagnostics = getattr(exc, "diagnostics", None)
|
|
304
|
+
if exc_diagnostics:
|
|
305
|
+
adapter_diagnostics = _as_dict(exc_diagnostics)
|
|
306
|
+
else:
|
|
307
|
+
valid_labels = set(str(x) for x in tree.internal_node_labels()) | set(
|
|
308
|
+
tree.get_leaves_names()
|
|
309
|
+
)
|
|
310
|
+
hit = sum(
|
|
311
|
+
1
|
|
312
|
+
for c in cset.constraints
|
|
313
|
+
if c.donor in valid_labels and c.receptor in valid_labels
|
|
314
|
+
)
|
|
315
|
+
if not cset.constraints:
|
|
316
|
+
issues.append(
|
|
317
|
+
{
|
|
318
|
+
"severity": "error",
|
|
319
|
+
"message": f"上游输出(格式 {resolved})未产出任何约束:"
|
|
320
|
+
"正式排序将在 0 总权重上产出一个与数据无关的"
|
|
321
|
+
"「平凡最优」排序( 的实际后果)。"
|
|
322
|
+
"请检查工具输出格式、命名(类群名 vs 数字内部节点 ID)"
|
|
323
|
+
"与 --ale-min-support / --ale-min-family-size 阈值。",
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
elif hit == 0:
|
|
327
|
+
issues.append(
|
|
328
|
+
{
|
|
329
|
+
"severity": "error",
|
|
330
|
+
"message": f"上游输出(格式 {resolved})的 {len(cset.constraints)} 条约束"
|
|
331
|
+
"端点全部不在物种树中:正式排序将退化为无信息约束。"
|
|
332
|
+
"请核对物种树叶名与上游工具所用命名。",
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
elif hit < len(cset.constraints):
|
|
336
|
+
issues.append(
|
|
337
|
+
{
|
|
338
|
+
"severity": "warning",
|
|
339
|
+
"message": f"上游输出(格式 {resolved})的 {len(cset.constraints)} 条约束中"
|
|
340
|
+
f"仅 {hit} 条端点命中物种树标签"
|
|
341
|
+
f"(命中率 {hit * 100.0 / len(cset.constraints):.1f}%)。",
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
# 计数与 ``ok`` 必须在**两条**路径上重算:若只在正常路径重算,
|
|
345
|
+
# "适配器抛异常"那条路径追加了 error 项却不会更新 ``n_errors``/``ok``,
|
|
346
|
+
# 报告就会一边打印 ``[error]``、一边判定"通过"并以退出码 0 结束。
|
|
347
|
+
report["n_errors"] = sum(1 for it in issues if it["severity"] == "error")
|
|
348
|
+
report["n_warnings"] = sum(1 for it in issues if it["severity"] == "warning")
|
|
349
|
+
report["ok"] = report["n_errors"] == 0
|
|
350
|
+
report_str = format_dry_run(report)
|
|
351
|
+
if print_summary:
|
|
352
|
+
print(report_str)
|
|
353
|
+
return Result(
|
|
354
|
+
constraint_file=cons_list[0] if cons_list else species_tree_path,
|
|
355
|
+
# :退出码取自这个结构化字段,而非对 report_str 的子串匹配
|
|
356
|
+
run_metadata={
|
|
357
|
+
"dry_run": True,
|
|
358
|
+
"dry_run_ok": bool(report["ok"]),
|
|
359
|
+
"dry_run_n_errors": int(report["n_errors"]),
|
|
360
|
+
"dry_run_n_warnings": int(report["n_warnings"]),
|
|
361
|
+
# :适配器诊断随预检一并回传
|
|
362
|
+
"adapter_diagnostics": adapter_diagnostics,
|
|
363
|
+
# :归档输入展开为哪些成员(可追溯)
|
|
364
|
+
"archives_expanded": dict(archive_expansions),
|
|
365
|
+
"archive_notes": list(archive_notes),
|
|
366
|
+
},
|
|
367
|
+
dry_run_report=report_str,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# ---- 构建约束集:上游工具适配器(注册表分发)或文本解析 ----
|
|
371
|
+
constraint_files_merged = list(cons_list)
|
|
372
|
+
multi_file_note = ""
|
|
373
|
+
if tool is not None:
|
|
374
|
+
resolved, cset = _convert_via_registry(
|
|
375
|
+
tool,
|
|
376
|
+
tree,
|
|
377
|
+
cons_list,
|
|
378
|
+
constraints_out=constraints_out,
|
|
379
|
+
ale_min_support=ale_min_support,
|
|
380
|
+
ale_min_family_size=ale_min_family_size,
|
|
381
|
+
ale_cache_dir=ale_cache_dir,
|
|
382
|
+
ale_parallel=ale_parallel,
|
|
383
|
+
ale_source=ale_source,
|
|
384
|
+
artra_transfer_kind=artra_transfer_kind,
|
|
385
|
+
min_endpoint_hit_rate=adapter_min_endpoint_hit_rate,
|
|
386
|
+
quiet=adapter_quiet,
|
|
387
|
+
)
|
|
388
|
+
adapter_diagnostics = _adapter_diagnostics_of(cset)
|
|
389
|
+
adapter_diagnostics.setdefault("from_tool", resolved)
|
|
390
|
+
# 两阶段流程的"约束生成"阶段:仅写出约束文件即返回,不进入正式排序
|
|
391
|
+
if constraints_out is not None:
|
|
392
|
+
if print_summary:
|
|
393
|
+
print(f"[约束生成] {resolved} 约束已写出至:{constraints_out}")
|
|
394
|
+
return Result(
|
|
395
|
+
constraint_file=cons_list[0] if cons_list else species_tree_path,
|
|
396
|
+
run_metadata={
|
|
397
|
+
"from_tool": resolved,
|
|
398
|
+
"constraints_out": constraints_out,
|
|
399
|
+
"adapter_diagnostics": adapter_diagnostics,
|
|
400
|
+
"archives_expanded": dict(archive_expansions),
|
|
401
|
+
"archive_notes": list(archive_notes),
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
constraint_file = cons_list[0]
|
|
405
|
+
if len(cons_list) > 1 and output_prefix is None:
|
|
406
|
+
multi_file_note = _multi_file_prefix_note(cons_list)
|
|
407
|
+
else:
|
|
408
|
+
# 文本双格式:支持一个或多个约束文件(CLI synopsis "CONSTRAINTS [...]" 与
|
|
409
|
+
# 手册 03 的约定),全部解析后合并为单一 ConstraintSet(边键聚合在
|
|
410
|
+
# EdgeBuilder 中进行,与文件顺序无关)。
|
|
411
|
+
if not cons_list:
|
|
412
|
+
raise ValueError("未提供约束文件。")
|
|
413
|
+
csets = [read_constraints_file(p) for p in cons_list]
|
|
414
|
+
merged = ConstraintSet()
|
|
415
|
+
for cs in csets:
|
|
416
|
+
for c in cs:
|
|
417
|
+
merged.add(c)
|
|
418
|
+
cset = merged
|
|
419
|
+
constraint_file = cons_list[0]
|
|
420
|
+
if len(cons_list) > 1 and output_prefix is None:
|
|
421
|
+
# :产物前缀只体现第一个文件,必须显式告知而非静默
|
|
422
|
+
multi_file_note = _multi_file_prefix_note(cons_list)
|
|
423
|
+
if multi_file_note:
|
|
424
|
+
# 走 stderr:stdout 只承载"与原版逐字节对齐"的摘要,命名口径这类
|
|
425
|
+
# 运行说明不得污染可被机器解析的 stdout。
|
|
426
|
+
sys.stderr.write(multi_file_note + "\n")
|
|
427
|
+
sys.stderr.flush()
|
|
428
|
+
|
|
429
|
+
# ---- 目标类群剪裁(可选插入点,约束解析后、建边前)----
|
|
430
|
+
if target_clade is not None:
|
|
431
|
+
from maxtic_next.prune import prune_constraints
|
|
432
|
+
|
|
433
|
+
cset = prune_constraints(tree, cset, target_clade, ancestor_map=ancestor_map)
|
|
434
|
+
|
|
435
|
+
rng = RandomWrapper(seed)
|
|
436
|
+
ranker = Ranker(
|
|
437
|
+
tree=tree,
|
|
438
|
+
cset=cset,
|
|
439
|
+
rng=rng,
|
|
440
|
+
min_transfer_distance=min_transfer_distance,
|
|
441
|
+
threshold_constraints=threshold_constraints,
|
|
442
|
+
temperature=temperature,
|
|
443
|
+
random_type=random_type,
|
|
444
|
+
time_for_search=local_search,
|
|
445
|
+
random_trees=random_trees,
|
|
446
|
+
constraint_file=constraint_file,
|
|
447
|
+
output_prefix=output_prefix,
|
|
448
|
+
mcmc=mcmc,
|
|
449
|
+
mcmc_iters=mcmc_iters,
|
|
450
|
+
mcmc_temperature=mcmc_temperature,
|
|
451
|
+
mcmc_burn_in=mcmc_burn_in,
|
|
452
|
+
mcmc_thin=mcmc_thin,
|
|
453
|
+
incremental=incremental,
|
|
454
|
+
checkpoint_path=checkpoint_path,
|
|
455
|
+
checkpoint_interval=checkpoint_interval,
|
|
456
|
+
output_style=output_style,
|
|
457
|
+
force=force,
|
|
458
|
+
constraint_files_merged=constraint_files_merged,
|
|
459
|
+
local_search_max_iterations=local_search_max_iterations,
|
|
460
|
+
)
|
|
461
|
+
result = ranker.run(
|
|
462
|
+
print_summary=print_summary,
|
|
463
|
+
html_report=html_report,
|
|
464
|
+
seed=seed,
|
|
465
|
+
top_k=top_k,
|
|
466
|
+
stop_check=stop_check,
|
|
467
|
+
)
|
|
468
|
+
if multi_file_note:
|
|
469
|
+
# :stderr 已声明,同时结构化留痕,供 API 调用方自检
|
|
470
|
+
result.run_metadata["multi_file_prefix_note"] = multi_file_note
|
|
471
|
+
if archive_expansions:
|
|
472
|
+
# :归档输入被展开成哪些成员(成员活在临时目录里)必须可追溯
|
|
473
|
+
result.run_metadata["archives_expanded"] = dict(archive_expansions)
|
|
474
|
+
result.run_metadata["archive_notes"] = list(archive_notes)
|
|
475
|
+
return result
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _convert_via_registry(
|
|
479
|
+
tool: str,
|
|
480
|
+
tree,
|
|
481
|
+
cons_list: List[str],
|
|
482
|
+
*,
|
|
483
|
+
constraints_out: Optional[str],
|
|
484
|
+
ale_min_support: float,
|
|
485
|
+
ale_min_family_size: int,
|
|
486
|
+
ale_cache_dir: Optional[str],
|
|
487
|
+
ale_parallel: str,
|
|
488
|
+
ale_source: str,
|
|
489
|
+
artra_transfer_kind: str,
|
|
490
|
+
min_endpoint_hit_rate: Optional[float] = None,
|
|
491
|
+
quiet: bool = False,
|
|
492
|
+
):
|
|
493
|
+
"""经注册表把上游输出转为统一约束集,返回 ``(解析到的工具名, ConstraintSet)``。
|
|
494
|
+
|
|
495
|
+
:``auto`` 模式下只做**一次**格式嗅探(旧实现先 ``detect_formats`` 再
|
|
496
|
+
``convert_auto``,同一批文件被嗅探两遍,无扩展名的大目录代价翻倍)。
|
|
497
|
+
|
|
498
|
+
:``auto`` 遇到**混合格式**时不再只报错 —— 按各自工具的适配器分组
|
|
499
|
+
解析后经 ``merge_constraint_sets`` 合并,同时把"供体端点层级不同 / 权重口径不可
|
|
500
|
+
通约"这一事实打到 stderr 并记入 ``diagnostics``(``convention_conflict`` /
|
|
501
|
+
``semantics_conflict``),由调用方随 ``Result`` 上报,不再静默相加。
|
|
502
|
+
"""
|
|
503
|
+
from maxtic_next.constraints.adapters import registry
|
|
504
|
+
|
|
505
|
+
conv_kwargs = dict(
|
|
506
|
+
min_support=ale_min_support,
|
|
507
|
+
min_family_size=ale_min_family_size,
|
|
508
|
+
cache_dir=ale_cache_dir,
|
|
509
|
+
parallel=ale_parallel,
|
|
510
|
+
output_path=constraints_out,
|
|
511
|
+
ale_source=ale_source,
|
|
512
|
+
artra_transfer_kind=artra_transfer_kind,
|
|
513
|
+
min_endpoint_hit_rate=min_endpoint_hit_rate,
|
|
514
|
+
quiet=quiet,
|
|
515
|
+
)
|
|
516
|
+
if tool == "auto":
|
|
517
|
+
try:
|
|
518
|
+
resolved = registry.detect_formats(cons_list)
|
|
519
|
+
except registry.MixedFormatError as exc:
|
|
520
|
+
# 嗅探结果已在异常里(分组),不再二次读取文件
|
|
521
|
+
sets = [
|
|
522
|
+
registry.convert(t, tree, paths, **conv_kwargs)
|
|
523
|
+
for t, paths in sorted(exc.groups.items())
|
|
524
|
+
]
|
|
525
|
+
merged = registry.merge_constraint_sets(sets, quiet=bool(quiet))
|
|
526
|
+
return "auto+" + "+".join(sorted(exc.groups)), merged
|
|
527
|
+
if resolved is None:
|
|
528
|
+
raise ValueError(
|
|
529
|
+
"无法自动识别上游工具格式;请用 --from <tool> 显式指定"
|
|
530
|
+
f"(可用:{registry.REGISTRY.names()})。"
|
|
531
|
+
)
|
|
532
|
+
return resolved, registry.convert(resolved, tree, cons_list, **conv_kwargs)
|
|
533
|
+
return tool, registry.convert(tool, tree, cons_list, **conv_kwargs)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _as_dict(value) -> Dict:
|
|
537
|
+
"""把可能是 ``diagnostics`` 字典 / 异常携带值的东西规整为 dict。"""
|
|
538
|
+
return dict(value) if isinstance(value, dict) else {}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _adapter_diagnostics_of(cset) -> Dict:
|
|
542
|
+
"""取 ``ConstraintSet.diagnostics`` 的**浅拷贝**(不得只留在内存里)。"""
|
|
543
|
+
return _as_dict(getattr(cset, "diagnostics", None))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _multi_file_prefix_note(cons_list: List[str]) -> str:
|
|
547
|
+
"""多约束文件时的"产物前缀只体现第一个文件"声明。"""
|
|
548
|
+
return (
|
|
549
|
+
f"NOTE: 输入了 {len(cons_list)} 个约束文件,"
|
|
550
|
+
f"三输出文件与 HTML 报告均以第一个文件为前缀:{cons_list[0]}"
|
|
551
|
+
f"(其余 {len(cons_list) - 1} 个文件已参与计算:{cons_list[1:]})。"
|
|
552
|
+
"如需为不同来源分别留存产物,请对每次运行显式指定 -p/--output-prefix。"
|
|
553
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""性能基准子包。
|
|
2
|
+
|
|
3
|
+
提供合成大规模数据集(大 n 内部节点数 / 大 |E| 约束数)的生成与基准测试,测量 CPU
|
|
4
|
+
时间与峰值内存,并标出可处理上限。纯标准库实现,不依赖真实 ALE 或任何第三方包。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from maxtic_next.benchmarks.suite import run_benchmark, run_suite, make_synthetic_tree
|
|
8
|
+
|
|
9
|
+
__all__ = ["run_benchmark", "run_suite", "make_synthetic_tree"]
|