Skip to content

Load drain_list in MSTemplateBuilder.from_dict - #164

Open
jplfaria wants to merge 1 commit into
ModelSEED:devfrom
jplfaria:fix/template-from-dict-drain-list
Open

jplfaria wants to merge 1 commit into
ModelSEED:devfrom
jplfaria:fix/template-from-dict-drain-list

Conversation

@jplfaria

@jplfaria jplfaria commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Heads-up: this changes gap-fill picks in real builds. With the standard templates, the set of sinks stays the same
for models that lack cpd22811. But the sinks are now created in the template's drain_list order instead of
DEFAULT_SINKS order. That changes the column order of the gap-fill LP, and with it which of two near-equal solutions
the solver picks. In two genome-scale builds, one or two gap-filled reactions swapped for an alternative, and growth
changed by less than 1e-4. With drain_list reordered to match DEFAULT_SINKS, the builds are identical to dev
(details below).

Why this matters in our workflow

We load templates from JSON with MSTemplateBuilder.from_dict(...).build(), and we edit them. A template's
drain_list never reaches the model: dev ignores it and builds DEFAULT_SINKS instead. cobrakbase's template loader
(NewModelTemplateBuilder.from_dict) already reads drain_list, so templates loaded through KBase carry their
drains, while the same JSON loaded through ModelSEEDpy does not.

What happens

  • MSTemplateBuilder.from_dict
    (mstemplate.py L1510-L1531 at 062714b)
    copies every top-level field except drain_list.
  • MSTemplateBuilder.build adds self.drains to the template
    (L1641-L1642),
    and MSTemplate.get_data writes them back as drain_list
    (L1431-L1432).
    Only the reader drops the field, so a template saved and loaded again loses its drains.
  • MSBuilder.build_drains falls back to DEFAULT_SINKS when template.drains is empty
    (msbuilder.py L324-L368).
  • Every GramNeg, GramPos and Archaea template in ModelSEEDTemplates v5.0 to v7.0 lists six sinks:
    DEFAULT_SINKS's five plus cpd22811 ((2R,4S)-2-methyl-2,4-dihydroxydihydrofuran-3-one, whose only template
    reaction is rxn20806). On dev the cpd22811 sink is never built, and any drain a curator adds is dropped.

How to reproduce

d = json.load(open("tests/test_data/template_core_bigg.json"))
d["drain_list"] = {"pyr_c": [0, 1000], "glc__D_e": [-1000, 0]}
MSTemplateBuilder.from_dict(d).build().drains   # dev: None

The fix

         builder.biomasses = d["biomasses"]
+        builder.drains = d.get("drain_list", {})
  • A template without the field behaves as before: no drains, so DEFAULT_SINKS is used.
  • In every ModelSEEDTemplates JSON, each drain_list compound exists in its template's compcompounds. So build()'s
    add_drain, which raises ValueError for an unknown compound, accepts all of them. The Core templates have no
    drain_list.
  • The only code that reads template.drains is MSBuilder.build_drains, build_sinks and build_demands.
  • I sent an earlier version of this fix to cshenry/ModelSEEDpy (Unit-tests and ameliorated package codes #25, d5f50c7). That version also turned every drain
    into a DM_ reaction in build_demands, and its message said FBA returns 0 growth without DM_cpd11416_c0. That
    claim was wrong: on dev the SK_ sinks already give biomass an outlet, and builds grow without any DM_ reaction.
    So this PR does not include that change.

Tests

tests/core/test_mstemplate.py::test_template_builder_from_dict_drain_list: from_dict loads a drain_list (a sink
and a demand on template_core_bigg.json), and get_data followed by from_dict keeps it.

Against dev's code:

E       AttributeError: 'NoneType' object has no attribute 'items'
tests/core/test_mstemplate.py:131: AttributeError
1 failed

With the fix, it passes. Full suite (Python 3.10, cobra 0.32.1, CPLEX 22.1.1): tests/core goes from 56 to 57
passed, and tests/ from 58 to 59. black 22.8.0 and the other pre-commit hooks pass.

Real-model check

We ran MSBuilder.build() on two genome-scale builds (E. coli K-12 and a Pantoea strain, from RAST annotations,
GramNeg template v7.1 with Core-V6.1, minimal glucose) three ways: on dev, on this branch, and on this branch with a
template copy whose drain_list keys are in DEFAULT_SINKS order (control).

Genome dev this branch control
E. coli 1,836 reactions; growth 0.545127 1,835; growth 0.545119; gap-fill picks rxn00599 instead of rxn00181 + rxn02990 identical to dev
Pantoea 1,185 reactions; growth 0.241407 1,184; growth 0.241428; gap-fill picks rxn00957 + rxn12239 instead of rxn00966 + rxn12376 (+ an exchange) identical to dev

All three builds have the same sink reactions, because neither model has cpd22811; only their order differs. ATP
produced from nothing and from water + H+ + each of 7 electron acceptors stays 0.

Related

🤖 Generated with Claude Code

MSTemplate.get_data writes the template drains as "drain_list", and
MSTemplateBuilder.build adds self.drains to the template, but from_dict
never read the field. Templates loaded from JSON therefore had no drains,
and MSBuilder.build_drains fell back to DEFAULT_SINKS. The ModelSEED
templates list six drains; DEFAULT_SINKS has five (cpd22811 is missing),
and any other drain a template defines was ignored.

Read drain_list in from_dict (empty when absent, as before).

Test: from_dict loads a drain_list, and get_data -> from_dict keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant