@dsh-bio/dsh-bio-gem 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.md +116 -0
- package/docs/DECISIONS-2026-08-29.md +37 -0
- package/docs/DECISIONS-/351/230/266/346/256/265A.md +67 -0
- package/docs/DECISIONS-/351/230/266/346/256/265E.md +56 -0
- package/index.js +5 -0
- package/package.json +50 -0
- package/python/annotate.py +208 -0
- package/python/benchmark.py +591 -0
- package/python/biomass_tools.py +329 -0
- package/python/budget.py +53 -0
- package/python/build.py +343 -0
- package/python/build_whitelist.py +127 -0
- package/python/double_knockout.py +201 -0
- package/python/enrichment.py +182 -0
- package/python/essential_scan.py +195 -0
- package/python/fluxscan.py +302 -0
- package/python/gapfill.py +176 -0
- package/python/gapfind.py +397 -0
- package/python/gapseq_wsl.py +251 -0
- package/python/gem_ops.py +520 -0
- package/python/l3_fix.py +641 -0
- package/python/ledger.py +581 -0
- package/python/model_card.py +248 -0
- package/python/phenotype_fix.py +115 -0
- package/python/roundtrip_check.py +45 -0
- package/python/secretion.py +179 -0
- package/python/sensitivity.py +484 -0
- package/python/silentio.py +28 -0
- package/python/targets.py +151 -0
- package/python/validate.py +393 -0
- package/skills/gem-expert.md +88 -0
- package/src/index.js +19 -0
- package/src/jobs.js +152 -0
- package/src/python.js +64 -0
- package/src/skills.js +29 -0
- package/src/tools.js +545 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""dsh-bio-gem Python 操作层 — JSON 协议分发器(GEM 构建/验证/补洞/报告)。
|
|
2
|
+
|
|
3
|
+
协议与 dsh-bio-genie 的 bio_ops.py 同族:
|
|
4
|
+
TS 侧通过 stdin 发送 {"op": "...", "args": {...}},
|
|
5
|
+
本脚本执行后将 {"ok": true, "result": ...} 或 {"ok": false, "error": "..."} 写到 stdout。
|
|
6
|
+
|
|
7
|
+
契约(bridge 层继承):
|
|
8
|
+
- 捕获所有代码异常后恒返回 ok:true,traceback 写 stderr(带 "Traceback (most recent call last)" 头)——
|
|
9
|
+
代码级失败判定必须在 TS 侧检测该头 → needs_repair=true。
|
|
10
|
+
- 输出前 _sanitize_json 递归规范化(-0.0→0.0, NaN/inf→null),规避 dsh snapshot 校验。
|
|
11
|
+
|
|
12
|
+
op 一览(M1 · 定稿 2026-08-29):
|
|
13
|
+
model_info 读 SBML 输出模型摘要(gem_report 的底层)
|
|
14
|
+
validate 五道验证关卡 G1 加载 / G2 元素平衡 / G3 生长真实性 / G4 表型(条件) / G5 必需性抽检(条件)
|
|
15
|
+
gapfind 缺口分级诊断 L1 缺 exchange / L2 缺转运 / L3 内部路径
|
|
16
|
+
gapfill 规则级补洞(L1/L2 自动,逐条打标 provenance)
|
|
17
|
+
build CarveMe 构建(后台长任务)+ 注释输入支持
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import traceback
|
|
23
|
+
|
|
24
|
+
# Windows 下 sys.stdin/stdout 默认按 GBK(locale)编解码,而 Node 侧以 UTF-8 写入/读取。
|
|
25
|
+
# 不显式重配置会导致中文参数/结果损坏。强制 UTF-8。
|
|
26
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
27
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
# Python -I isolated 模式下脚本目录不进 sys.path——显式插入以导入同目录模块
|
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
+
|
|
32
|
+
_MODEL_DIR = os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "models")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _sanitize_json(obj):
|
|
36
|
+
"""递归规范化:-0.0 -> 0.0, NaN/inf -> None(dsh snapshotToolValue 只接受 lossless JSON)。"""
|
|
37
|
+
if isinstance(obj, float):
|
|
38
|
+
if obj != obj or obj in (float("inf"), float("-inf")):
|
|
39
|
+
return None
|
|
40
|
+
if obj == 0.0 and str(obj).startswith("-"):
|
|
41
|
+
return 0.0
|
|
42
|
+
return obj
|
|
43
|
+
if isinstance(obj, dict):
|
|
44
|
+
return {k: _sanitize_json(v) for k, v in obj.items()}
|
|
45
|
+
if isinstance(obj, (list, tuple)):
|
|
46
|
+
return [_sanitize_json(v) for v in obj]
|
|
47
|
+
return obj
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# op: model_info — SBML 摘要(gem_report 底层)
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
def op_model_info(args):
|
|
54
|
+
"""读 SBML:返回基因/反应/代谢物统计 + 复制子统计(多染色体/多质粒)。"""
|
|
55
|
+
import cobra
|
|
56
|
+
path = args.get("model")
|
|
57
|
+
if not path or not os.path.exists(path):
|
|
58
|
+
return {"ok": False, "error": f"model file not found: {path}"}
|
|
59
|
+
m = cobra.io.read_sbml_model(path)
|
|
60
|
+
n_ex = sum(1 for r in m.reactions if r.id.startswith("EX_"))
|
|
61
|
+
n_dm = sum(1 for r in m.reactions if r.id.startswith("DM_"))
|
|
62
|
+
n_boundary = sum(1 for r in m.reactions if r.boundary)
|
|
63
|
+
# 复制子统计:模型基因 ID 前缀 NC_XXXX_N -> 复制子
|
|
64
|
+
from collections import Counter
|
|
65
|
+
repl = Counter()
|
|
66
|
+
for g in m.genes:
|
|
67
|
+
parts = g.id.split("_")
|
|
68
|
+
if len(parts) >= 3 and parts[0] == "NC":
|
|
69
|
+
repl["_".join(parts[:2])] += 1
|
|
70
|
+
else:
|
|
71
|
+
repl["other"] += 1
|
|
72
|
+
# 阶段A-M3: prediction ledger 基率摘要(文件不存在 -> {total: 0};ledger_path 可覆盖默认账本)
|
|
73
|
+
# P1-3:传 model=path 让摘要给 by_model 分布 + own_model_entries(防「本模型账本 N 条」误报)
|
|
74
|
+
ledger_summary, ledger_context = None, None
|
|
75
|
+
try:
|
|
76
|
+
import ledger as _ledger
|
|
77
|
+
# 2026-08-31:一个模型一个账本——缺省(无 ledger_path)定位到该模型自己的账本
|
|
78
|
+
ledger_summary = _ledger.ledger_summary(path=args.get("ledger_path"), model=path)
|
|
79
|
+
n_unverified = ledger_summary["by_status"].get("unverified", 0)
|
|
80
|
+
own = ledger_summary.get("own_model_entries")
|
|
81
|
+
if ledger_summary["total"]:
|
|
82
|
+
ledger_context = (f"预测账本:本模型 {own} 条(一个模型一个账本,账本文件按模型名分),"
|
|
83
|
+
f"{n_unverified} 条 unverified:"
|
|
84
|
+
"全部为模型推导预测(essentiality/phenotype 等),实验或文献兑现前不应当作事实引用;"
|
|
85
|
+
"状态分布即预测可信度基率,回填后 by_status 向 literature_supported/"
|
|
86
|
+
"experimentally_verified 迁移。")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
ledger_summary = {"error": str(e)[:120]}
|
|
89
|
+
return {"ok": True, "result": {
|
|
90
|
+
"path": path,
|
|
91
|
+
"genes": len(m.genes),
|
|
92
|
+
"reactions": len(m.reactions),
|
|
93
|
+
"metabolites": len(m.metabolites),
|
|
94
|
+
"compartments": list(m.compartments.values()) or list(m.compartments.keys()),
|
|
95
|
+
"exchanges": n_ex,
|
|
96
|
+
"demands": n_dm,
|
|
97
|
+
"boundary": n_boundary,
|
|
98
|
+
"replicons": dict(repl),
|
|
99
|
+
"objective": m.objective.name or m.objective.expression is not None and "set" or "None",
|
|
100
|
+
"ledger_summary": ledger_summary,
|
|
101
|
+
**({"ledger_context": ledger_context} if ledger_context else {}),
|
|
102
|
+
}}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# op: validate — 五道验证关卡(G1-G5)
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
def op_validate(args):
|
|
109
|
+
from validate import validate_model
|
|
110
|
+
model = args.get("model")
|
|
111
|
+
if not model or not os.path.exists(model):
|
|
112
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
113
|
+
rep = validate_model(
|
|
114
|
+
model,
|
|
115
|
+
medium=args.get("medium"),
|
|
116
|
+
phenotype_table=args.get("phenotype_table"),
|
|
117
|
+
essential_test=args.get("essential_test"),
|
|
118
|
+
reference_growth=args.get("reference_growth"),
|
|
119
|
+
reference_essential=args.get("reference_essential"),
|
|
120
|
+
carbon_mode=args.get("carbon_mode", "supplement"),
|
|
121
|
+
)
|
|
122
|
+
return {"ok": True, "result": rep}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# op: gapfind — 缺口分级诊断(L1/L2/L3)
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
def op_gapfind(args):
|
|
129
|
+
from gapfind import find_gaps
|
|
130
|
+
model = args.get("model")
|
|
131
|
+
if not model or not os.path.exists(model):
|
|
132
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
133
|
+
return {"ok": True, "result": find_gaps(model, medium=args.get("medium"),
|
|
134
|
+
substrates=args.get("substrates"))}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# op: gapfill — 规则级补洞(L1/L2,provenance 打标)
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
def op_gapfill(args):
|
|
141
|
+
from gapfill import apply_fixes
|
|
142
|
+
model = args.get("model")
|
|
143
|
+
if not model or not os.path.exists(model):
|
|
144
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
145
|
+
if not (args.get("medium") or args.get("substrates")):
|
|
146
|
+
return {"ok": False, "error": "need medium and/or substrates to drive gapfill"}
|
|
147
|
+
return {"ok": True, "result": apply_fixes(
|
|
148
|
+
model, medium=args.get("medium"), substrates=args.get("substrates"),
|
|
149
|
+
max_add=args.get("max_add", 20), out=args.get("out"),
|
|
150
|
+
confirm_budget=args.get("confirm_budget", False))}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# op: gapseq — 原子步骤(setup/launch/status/fetch),agent 编排长任务
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
def op_gapseq(args):
|
|
157
|
+
from gapseq_wsl import probe, launch_gapseq, status_gapseq, fetch_gapseq
|
|
158
|
+
action = args.get("action", "setup")
|
|
159
|
+
if action == "setup":
|
|
160
|
+
return {"ok": True, "result": probe()}
|
|
161
|
+
if action == "launch":
|
|
162
|
+
if not args.get("input"):
|
|
163
|
+
return {"ok": False, "error": "launch 需要 input(核苷酸 .fna 绝对路径)"}
|
|
164
|
+
r = launch_gapseq(args["input"], name=args.get("name", "model"),
|
|
165
|
+
work_win=args.get("out_dir"))
|
|
166
|
+
return {"ok": True, "result": r}
|
|
167
|
+
if action == "status":
|
|
168
|
+
return {"ok": True, "result": status_gapseq()}
|
|
169
|
+
if action == "fetch":
|
|
170
|
+
r = fetch_gapseq(args.get("out_dir"), name=args.get("name", "model"))
|
|
171
|
+
return {"ok": True, "result": r}
|
|
172
|
+
return {"ok": False, "error": f"unknown gapseq action: {action}(setup|launch|status|fetch)"}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# op: phenotype_fix — 表型回填迭代(A3)
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
def op_phenotype_fix(args):
|
|
179
|
+
from phenotype_fix import phenotype_fix
|
|
180
|
+
model = args.get("model")
|
|
181
|
+
if not model or not os.path.exists(model):
|
|
182
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
183
|
+
r = phenotype_fix(model, phenotype_table=args.get("phenotype_table"),
|
|
184
|
+
medium=args.get("medium"), max_add=args.get("max_add", 20),
|
|
185
|
+
out=args.get("out"), ledger_path=args.get("ledger_path"))
|
|
186
|
+
return {"ok": True, "result": r}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# op: essential_scan — G5 全量必需基因扫描(路线 P0)
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
def op_essential_scan(args):
|
|
193
|
+
from essential_scan import essential_scan
|
|
194
|
+
model = args.get("model")
|
|
195
|
+
if not model or not os.path.exists(model):
|
|
196
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
197
|
+
r = essential_scan(model, medium=args.get("medium"), gene_subset=args.get("gene_subset"),
|
|
198
|
+
ledger_path=args.get("ledger_path"), gene_table=args.get("gene_table"))
|
|
199
|
+
return {"ok": True, "result": r}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# op: annotate — 基因组注释(官方优先 + pyrodigal 兜底)
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
def op_annotate(args):
|
|
206
|
+
from annotate import nucleotide_to_protein
|
|
207
|
+
fna = args.get("fna")
|
|
208
|
+
if not fna or not os.path.exists(fna):
|
|
209
|
+
return {"ok": False, "error": f"fna file not found: {fna}"}
|
|
210
|
+
faa, src, stats = nucleotide_to_protein(fna, args.get("out"))
|
|
211
|
+
result = {"faa": faa, "source": src, "stats": stats}
|
|
212
|
+
# P1-4:GFF 路径产出 gene_table.tsv(坐标ID→locus_tag/product),供 gem_essentiality 解读必需基因功能
|
|
213
|
+
if stats.get("gene_table"):
|
|
214
|
+
result["gene_table"] = stats["gene_table"]
|
|
215
|
+
return {"ok": True, "result": result}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# op: media_resolve — 跨引擎介质解析 RPC(genie 消费侧统一走此;防介质语义漂移)
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
def op_media_resolve(args):
|
|
222
|
+
from gapfind import expand_medium, resolve_medium, build_ex_index, ex_index_is_boundary, ex_display_name
|
|
223
|
+
from silentio import silent_read_sbml
|
|
224
|
+
model = args.get("model")
|
|
225
|
+
if not model or not os.path.exists(model):
|
|
226
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
227
|
+
m = silent_read_sbml(model)
|
|
228
|
+
med, preset = expand_medium(args.get("medium"))
|
|
229
|
+
resolved, unresolved = resolve_medium(m, med)
|
|
230
|
+
idx = build_ex_index(m)
|
|
231
|
+
boundary_style = ex_index_is_boundary(idx)
|
|
232
|
+
result = {
|
|
233
|
+
"resolved_exchanges": sorted(resolved),
|
|
234
|
+
"medium_preset": preset,
|
|
235
|
+
"unresolved": unresolved,
|
|
236
|
+
"model": model,
|
|
237
|
+
# 阶段B-B1 附加(只增):两级策略②启用标注 + 规范展示名
|
|
238
|
+
"boundary_style": boundary_style,
|
|
239
|
+
}
|
|
240
|
+
if boundary_style:
|
|
241
|
+
result["resolved_display"] = [ex_display_name(m, rid) for rid in sorted(resolved)]
|
|
242
|
+
return {"ok": True, "result": result}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# op: l3_fix — B' 后半:L3 内部路径补洞(L3a 模型内连通性 + L3b 白名单/BiGG 反应式)
|
|
247
|
+
# 证据分级 EVIDENCE_sequence/math;防过补第五闸门(budget.py);补后 G1-G6 重验 + G6 失败回滚
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
def op_l3_fix(args):
|
|
250
|
+
from l3_fix import l3_fix
|
|
251
|
+
model = args.get("model")
|
|
252
|
+
if not model or not os.path.exists(model):
|
|
253
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
254
|
+
if not (args.get("medium") or args.get("substrates")):
|
|
255
|
+
return {"ok": False, "error": "need medium and substrates to drive L3 diagnosis"}
|
|
256
|
+
return {"ok": True, "result": l3_fix(
|
|
257
|
+
model, medium=args.get("medium"), substrates=args.get("substrates"),
|
|
258
|
+
out=args.get("out"), allow_math=args.get("allow_math", False),
|
|
259
|
+
confirm_budget=args.get("confirm_budget", False),
|
|
260
|
+
whitelist=args.get("whitelist"), faa=args.get("faa"),
|
|
261
|
+
species=args.get("species"), max_iter=args.get("max_iter", 1),
|
|
262
|
+
universal_path=args.get("universal_path"))}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# op: fluxscan — 阶段A-M1 通量区间制(FVA 区间 + pFBA 点值 + 条件对区间分离判定)
|
|
267
|
+
# 语义 bsp 锁稿:overlap = 求解器伪影禁止引用;判定公式见 fluxscan.judge_interval(单测锁定)
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
def op_fluxscan(args):
|
|
270
|
+
from fluxscan import fluxscan, DEFAULT_FRACTION, DEFAULT_TOL
|
|
271
|
+
model = args.get("model")
|
|
272
|
+
if not model or not os.path.exists(model):
|
|
273
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
274
|
+
if not args.get("conditions"):
|
|
275
|
+
return {"ok": False, "error": "conditions required(非空数组,每项 {name, medium, substrates?, carbon_mode?},name 唯一)"}
|
|
276
|
+
return {"ok": True, "result": fluxscan(
|
|
277
|
+
model, args.get("conditions"), reactions=args.get("reactions"),
|
|
278
|
+
pairs=args.get("pairs"),
|
|
279
|
+
fraction_of_optimum=args.get("fraction_of_optimum", DEFAULT_FRACTION),
|
|
280
|
+
tolerance=args.get("tolerance", DEFAULT_TOL),
|
|
281
|
+
only_diff=args.get("only_diff", False), export_csv=args.get("export_csv"))}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# op: sensitivity — 阶段A-M2 结构性灵敏度(GAM×biomass 网格 22 组合 + 必需性重扫 + 单组分漂移)
|
|
286
|
+
# action=probe 秒级只读(GAM 载体定位/组分计数);缺省 full=22 组合全量(约 35-45min,长任务)
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
def op_sensitivity(args):
|
|
289
|
+
from sensitivity import sensitivity, find_biomass_gam
|
|
290
|
+
model = args.get("model")
|
|
291
|
+
if not model or not os.path.exists(model):
|
|
292
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
293
|
+
if args.get("action") == "probe":
|
|
294
|
+
from silentio import silent_read_sbml
|
|
295
|
+
m = silent_read_sbml(model)
|
|
296
|
+
info = find_biomass_gam(m)
|
|
297
|
+
info["genes"] = len(m.genes)
|
|
298
|
+
info["reactions"] = len(m.reactions)
|
|
299
|
+
return {"ok": True, "result": info}
|
|
300
|
+
baseline_check = None
|
|
301
|
+
if args.get("baseline_check_path"):
|
|
302
|
+
with open(args["baseline_check_path"], encoding="utf-8") as f:
|
|
303
|
+
baseline_check = json.load(f)
|
|
304
|
+
return {"ok": True, "result": sensitivity(
|
|
305
|
+
model, medium=args.get("medium"),
|
|
306
|
+
biomass_scales=args.get("biomass_scales"), gam_grid=args.get("gam_grid"),
|
|
307
|
+
run_component_sensitivity=args.get("run_component_sensitivity", True),
|
|
308
|
+
run_drift=args.get("run_drift", True), top_n=args.get("top_n", 10),
|
|
309
|
+
export_csv=args.get("export_csv"), baseline_check=baseline_check)}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# 分发器
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
OPS = {
|
|
316
|
+
"model_info": op_model_info,
|
|
317
|
+
"validate": op_validate,
|
|
318
|
+
"gapfind": op_gapfind,
|
|
319
|
+
"gapfill": op_gapfill,
|
|
320
|
+
"gapseq": op_gapseq,
|
|
321
|
+
"phenotype_fix": op_phenotype_fix,
|
|
322
|
+
"essential_scan": op_essential_scan,
|
|
323
|
+
"annotate": op_annotate,
|
|
324
|
+
"media_resolve": op_media_resolve,
|
|
325
|
+
"l3_fix": op_l3_fix,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# op: biomass_inspect / biomass_apply — Q2 任务一:biomass 精修(可选 profile,不默认替换)
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
def op_biomass_inspect(args):
|
|
333
|
+
from biomass_tools import inspect_biomass
|
|
334
|
+
model = args.get("model")
|
|
335
|
+
if not model or not os.path.exists(model):
|
|
336
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
337
|
+
r = inspect_biomass(model, reference=args.get("reference"),
|
|
338
|
+
universal_path=args.get("universal_path"), inx_path=args.get("inx_path"))
|
|
339
|
+
if r.get("ok"):
|
|
340
|
+
return {"ok": True, "result": r["result"]}
|
|
341
|
+
return {"ok": False, "error": r.get("error") or "inspect failed"}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def op_biomass_apply(args):
|
|
345
|
+
from biomass_tools import apply_biomass
|
|
346
|
+
model = args.get("model")
|
|
347
|
+
if not model or not os.path.exists(model):
|
|
348
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
349
|
+
if not args.get("biomass_profile"):
|
|
350
|
+
return {"ok": False, "error": "provide biomass_profile(显式覆盖表 [{met_id, coeff, op: set|add|remove}]);"
|
|
351
|
+
"默认不应用任何 profile,只读诊断请用 action=inspect"}
|
|
352
|
+
r = apply_biomass(model, args.get("biomass_profile"), medium=args.get("medium"),
|
|
353
|
+
phenotype_table=args.get("phenotype_table"), out=args.get("out"),
|
|
354
|
+
essential_sample=args.get("essential_sample", 40), note=args.get("note"))
|
|
355
|
+
if r.get("ok"):
|
|
356
|
+
return {"ok": True, "result": r["result"]}
|
|
357
|
+
return {"ok": False, "error": r.get("error") or "apply failed",
|
|
358
|
+
"skipped": r.get("skipped")}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# OPS 引用上面的 op 函数——biomass 两 op 定义在 main 前补充注册(避免前向引用 NameError)
|
|
362
|
+
OPS["biomass_inspect"] = op_biomass_inspect
|
|
363
|
+
OPS["biomass_apply"] = op_biomass_apply
|
|
364
|
+
OPS["fluxscan"] = op_fluxscan
|
|
365
|
+
OPS["sensitivity"] = op_sensitivity
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# op: ledger — 阶段A-M3 prediction ledger(list/query/update;只读/追加/更新,不删行)
|
|
370
|
+
# 默认账本 ~/.dsh/dsh-bio-gem/ledger/predictions.jsonl;ledger_path 可覆盖(测试用临时路径)
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
def op_ledger(args):
|
|
373
|
+
import ledger as _ledger
|
|
374
|
+
action = args.get("action", "list")
|
|
375
|
+
lp = args.get("ledger_path")
|
|
376
|
+
if action == "list":
|
|
377
|
+
return {"ok": True, "result": _ledger.query_ledger(
|
|
378
|
+
limit=args.get("limit"), offset=args.get("offset", 0), path=lp)}
|
|
379
|
+
if action == "query":
|
|
380
|
+
return {"ok": True, "result": _ledger.query_ledger(
|
|
381
|
+
rtype=args.get("type"), status=args.get("status"), condition=args.get("condition"),
|
|
382
|
+
model=args.get("model"), limit=args.get("limit"), offset=args.get("offset", 0),
|
|
383
|
+
deprecated=args.get("deprecated"), path=lp)}
|
|
384
|
+
if action == "update":
|
|
385
|
+
r = _ledger.update_row(args.get("prediction_id"), status=args.get("status"),
|
|
386
|
+
source_refs=args.get("source_refs"),
|
|
387
|
+
comparison_refs=args.get("comparison_refs"), path=lp)
|
|
388
|
+
if r.get("ok"):
|
|
389
|
+
return {"ok": True, "result": r}
|
|
390
|
+
return {"ok": False, "error": r.get("error") or "update failed"}
|
|
391
|
+
return {"ok": False, "error": f"unknown ledger action: {action}(list|query|update)"}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
# op: benchmark — 阶段B-B1 通用基准对比(六关并列/生长/含边界介质回退/biomass 探针/必需性对比
|
|
396
|
+
# 含退化护栏/表型/可复现性/账本 comparison_refs 回填;md 落盘可选)
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
def op_benchmark(args):
|
|
399
|
+
from benchmark import benchmark
|
|
400
|
+
model_a, model_b = args.get("model_a"), args.get("model_b")
|
|
401
|
+
for k, v in (("model_a", model_a), ("model_b", model_b)):
|
|
402
|
+
if not v:
|
|
403
|
+
return {"ok": False, "error": f"{k} required"}
|
|
404
|
+
if not v.startswith("bigg:") and not os.path.exists(v):
|
|
405
|
+
# bigg:<id> URI 由 benchmark 层下载解析(B3);本地路径仍要求存在
|
|
406
|
+
return {"ok": False, "error": f"{k} file not found: {v}"}
|
|
407
|
+
return {"ok": True, "result": benchmark(
|
|
408
|
+
model_a, model_b, medium=args.get("medium"),
|
|
409
|
+
phenotype_table=args.get("phenotype_table"),
|
|
410
|
+
reference_essential=args.get("reference_essential"),
|
|
411
|
+
essential_full=args.get("essential_full", False),
|
|
412
|
+
ledger_refs=args.get("ledger_refs", True),
|
|
413
|
+
export_md=args.get("export_md"), ledger_path=args.get("ledger_path"))}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
OPS["ledger"] = op_ledger
|
|
417
|
+
OPS["benchmark"] = op_benchmark
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# op: secretion — 阶段C-C1 可分泌代谢物谱(production envelope 扫描;纯拓扑边界声明内置;
|
|
422
|
+
# wt<=EPS 退化护栏不登记;type=secretion 账本登记幂等)
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
def op_secretion(args):
|
|
425
|
+
from secretion import secretion
|
|
426
|
+
model = args.get("model")
|
|
427
|
+
if not model or not os.path.exists(model):
|
|
428
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
429
|
+
return {"ok": True, "result": secretion(
|
|
430
|
+
model, medium=args.get("medium"), fractions=args.get("fractions"),
|
|
431
|
+
export_csv=args.get("export_csv"), ledger_refs=args.get("ledger_refs", True),
|
|
432
|
+
ledger_path=args.get("ledger_path"),
|
|
433
|
+
# P0-1:默认 summary(防大输出被引擎省略截断);全量用 mode=full 或 export_csv 落盘
|
|
434
|
+
mode=args.get("mode", "summary"), summary_top=args.get("summary_top", 20))}
|
|
435
|
+
|
|
436
|
+
OPS["secretion"] = op_secretion
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# op: double_knockout — 阶段C-C2 双敲 v1(合成致死;GPR 穷尽先验 + FVA 预筛全扫,max_pairs 预算;
|
|
441
|
+
# 假设声明内置;wt<=EPS 退化护栏不登记;type=synthetic_lethal 账本登记幂等)
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
def op_double_knockout(args):
|
|
444
|
+
from double_knockout import double_knockout
|
|
445
|
+
model = args.get("model")
|
|
446
|
+
if not model or not os.path.exists(model):
|
|
447
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
448
|
+
return {"ok": True, "result": double_knockout(
|
|
449
|
+
model, medium=args.get("medium"), max_pairs=args.get("max_pairs", 5000),
|
|
450
|
+
export_csv=args.get("export_csv"), ledger_refs=args.get("ledger_refs", True),
|
|
451
|
+
ledger_path=args.get("ledger_path"))}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
OPS["double_knockout"] = op_double_knockout
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
# op: enrichment — 阶段C-C3 必需基因通路富集(超几何单侧 + BH FDR;通路源=SBML groups
|
|
459
|
+
# [gapseq MetaCyc PWY];无注释模型按契约 annotation_unavailable 兜底;不登记账本)
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
def op_enrichment(args):
|
|
462
|
+
from enrichment import enrichment
|
|
463
|
+
model = args.get("model")
|
|
464
|
+
if not model or not os.path.exists(model):
|
|
465
|
+
return {"ok": False, "error": f"model file not found: {model}"}
|
|
466
|
+
return {"ok": True, "result": enrichment(
|
|
467
|
+
model, gene_list=args.get("gene_list"), pathway_source=args.get("pathway_source"),
|
|
468
|
+
ledger_path=args.get("ledger_path"), export_csv=args.get("export_csv"))}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
OPS["enrichment"] = op_enrichment
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# op: targets — 阶段C-C4 靶点清单规范导出(账本三类预测 -> 锁定 schema;供下游引物/编辑
|
|
476
|
+
# 工具直接输入;与账本计数闭合;引物/质粒设计本身不做)
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
def op_targets(args):
|
|
479
|
+
from targets import targets
|
|
480
|
+
return {"ok": True, "result": targets(
|
|
481
|
+
model_path=args.get("model"), types=args.get("types"), condition=args.get("condition"),
|
|
482
|
+
ledger_path=args.get("ledger_path"), export_format=args.get("export_format", "csv"),
|
|
483
|
+
export_path=args.get("export_path"))}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
OPS["targets"] = op_targets
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def main():
|
|
491
|
+
line = sys.stdin.read()
|
|
492
|
+
try:
|
|
493
|
+
req = json.loads(line)
|
|
494
|
+
op = req.get("op", "")
|
|
495
|
+
args = req.get("args", {}) or {}
|
|
496
|
+
except Exception:
|
|
497
|
+
# 协议级失败:返回 ok:false + 说明
|
|
498
|
+
print(json.dumps({"ok": False, "error": "protocol error: invalid JSON on stdin"}))
|
|
499
|
+
return
|
|
500
|
+
fn = OPS.get(op)
|
|
501
|
+
if fn is None:
|
|
502
|
+
print(json.dumps({"ok": False, "error": f"unknown op: {op}"}))
|
|
503
|
+
return
|
|
504
|
+
try:
|
|
505
|
+
out = fn(args)
|
|
506
|
+
if isinstance(out, dict) and "ok" in out:
|
|
507
|
+
print(json.dumps(_sanitize_json(out), ensure_ascii=False))
|
|
508
|
+
else:
|
|
509
|
+
print(json.dumps(_sanitize_json({"ok": True, "result": out}), ensure_ascii=False))
|
|
510
|
+
except Exception as e:
|
|
511
|
+
# 捕获所有异常,恒返回 ok:true(traceback 写 stderr 头,TS 侧检测)
|
|
512
|
+
sys.stderr.write("Traceback (most recent call last):\n")
|
|
513
|
+
traceback.print_exc(file=sys.stderr)
|
|
514
|
+
print(json.dumps({"ok": True, "result": None,
|
|
515
|
+
"error_hint": f"op {op} failed: {type(e).__name__}: {e}"},
|
|
516
|
+
ensure_ascii=False))
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == "__main__":
|
|
520
|
+
main()
|