From 7dd3d131f1c572f96fabae45f55624bf316edd3a Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Sun, 18 Oct 2026 00:37:41 +0200 Subject: [PATCH 1/3] docs: describe the current code, not its development history Docstrings, comments and maintainer scripts state what the code does and which RAVEN 3 function it matches, without develop3 branch names or references to SysBioChalmers/RAVEN pull requests. The artefact publishing link points to CompMetEng/raven-gecko-parity. --- .github/workflows/parity-nightly.yml | 12 ++++------ scripts/benchmark_deeploc.py | 6 ++--- scripts/build_binary_bundles.py | 24 +++++++++---------- scripts/build_transporter_data.py | 2 +- .../analysis/differential_sampling.py | 6 ++--- src/raven_toolbox/analysis/fseof.py | 4 ++-- src/raven_toolbox/analysis/min_flux_count.py | 9 ++----- src/raven_toolbox/gapfilling/leak.py | 3 +-- src/raven_toolbox/init/ftinit.py | 10 ++++---- src/raven_toolbox/manipulation/simplify.py | 5 ++-- tests/parity/README.md | 15 ++++-------- tests/parity/test_genome_scale.py | 3 +-- tests/test_analysis_differential_sampling.py | 3 +-- tests/test_init_ftinit.py | 3 +-- tests/test_utils_composition.py | 3 +-- validate_genome_scale.py | 2 +- 16 files changed, 43 insertions(+), 67 deletions(-) diff --git a/.github/workflows/parity-nightly.yml b/.github/workflows/parity-nightly.yml index c9e4e4fb..e15af307 100644 --- a/.github/workflows/parity-nightly.yml +++ b/.github/workflows/parity-nightly.yml @@ -35,14 +35,10 @@ jobs: # night for no reason. if: github.repository == 'CompMetEng/raven-toolbox' runs-on: ubuntu-latest - # Measured against the classic INIT MILP this job used to check (now - # ftINIT's run_ftinit, bounded to time_limit=600s -- see - # tests/parity/test_genome_scale.py): that extraction took ~68 minutes on - # a fast workstation, of which only ~2 were parsing and copying, and - # time_limit did not bound the call -- a 60-second limit still ran for - # well over 15 minutes -- so the real stop was pytest-timeout below. This - # budget is comfortably larger than ftINIT's bound so a runaway is still - # reported as a test failure instead of a killed job. + # The extraction is run_ftinit bounded to time_limit=600s (see + # tests/parity/test_genome_scale.py). This budget is comfortably larger + # than that bound, so a runaway is reported as a test failure by + # pytest-timeout below instead of a killed job. timeout-minutes: 150 steps: diff --git a/scripts/benchmark_deeploc.py b/scripts/benchmark_deeploc.py index 50fccec6..d2cf9546 100644 --- a/scripts/benchmark_deeploc.py +++ b/scripts/benchmark_deeploc.py @@ -243,9 +243,9 @@ def main() -> None: ctruth, crxn_genes = build_truth(cmodel) collapse_desc = ", ".join(f"{mem}->{lum}" for mem, lum in membrane_to_lumen.items()) out += [f"## 3. Collapsed membranes ({collapse_desc})", - "", "Collapsing also turns lumen/membrane-spanning reactions (previously " - "multi-compartment, so excluded) into single-compartment ones, so the truth set " - "grows.", ""] + "", "Collapsing also turns lumen/membrane-spanning reactions (multi-compartment, " + "so excluded, on the uncollapsed model) into single-compartment ones, so the " + "truth set grows.", ""] out += fmt_acc("Per-compartment accuracy on the organelle-collapsed model", organelle_accuracy(ctruth, crxn_genes, dl), cfg["comp_order"]) diff --git a/scripts/build_binary_bundles.py b/scripts/build_binary_bundles.py index b8733817..f7a25082 100644 --- a/scripts/build_binary_bundles.py +++ b/scripts/build_binary_bundles.py @@ -12,7 +12,7 @@ Sources (pinned for reproducibility): -* RAVEN ``develop3`` — BLAST+ 2.17.0, DIAMOND 2.1.17, HMMER 3.4.0 ``hmmsearch`` +* RAVEN 3 — BLAST+ 2.17.0, DIAMOND 2.1.17, HMMER 3.4.0 ``hmmsearch`` (Linux + macOS). ``.mac`` builds: ``hmmsearch`` is arm64, BLAST/DIAMOND are universal — so ``macos-arm64`` serves all three (Intel macOS → conda). * RAVEN ``v2.10.5`` — HMMER **3.3.2** native Windows ``hmmsearch.exe`` (+ the @@ -32,8 +32,8 @@ from urllib.request import urlopen RAW = "https://raw.githubusercontent.com/SysBioChalmers/RAVEN" -# Pinned RAVEN refs (commit SHA for the mutable branch; tag is already immutable). -DEV3 = "aa5e2c670f2f9cb7d040557974bb493a8ce0ba91" # develop3 HEAD at build time +# Pinned RAVEN refs: the commit carrying the RAVEN 3 binaries, and a release tag. +RAVEN3 = "aa5e2c670f2f9cb7d040557974bb493a8ce0ba91" V2105 = "v2.10.5" PROVIDES = {"blast": ["blastp", "makeblastdb"], "diamond": ["diamond"], "hmmer": ["hmmsearch"]} @@ -41,30 +41,30 @@ # One dict per published ZIP. members: (path under software/, arcname in ZIP, executable?) SPECS: list[dict] = [ # BLAST+ 2.17.0 - {"bundle": "blast", "version": "2.17.0", "plat": "linux-x86_64", "ref": DEV3, "members": [ + {"bundle": "blast", "version": "2.17.0", "plat": "linux-x86_64", "ref": RAVEN3, "members": [ ("blast+/blastp", "blastp", True), ("blast+/makeblastdb", "makeblastdb", True), ("blast+/LICENSE", "LICENSE", False)]}, - {"bundle": "blast", "version": "2.17.0", "plat": "macos-arm64", "ref": DEV3, "members": [ + {"bundle": "blast", "version": "2.17.0", "plat": "macos-arm64", "ref": RAVEN3, "members": [ ("blast+/blastp.mac", "blastp", True), ("blast+/makeblastdb.mac", "makeblastdb", True), ("blast+/LICENSE", "LICENSE", False)]}, - {"bundle": "blast", "version": "2.17.0", "plat": "windows-x86_64", "ref": DEV3, "members": [ + {"bundle": "blast", "version": "2.17.0", "plat": "windows-x86_64", "ref": RAVEN3, "members": [ ("blast+/blastp.exe", "blastp.exe", True), ("blast+/makeblastdb.exe", "makeblastdb.exe", True), ("blast+/nghttp2.dll", "nghttp2.dll", False), ("blast+/LICENSE", "LICENSE", False)]}, # DIAMOND 2.1.17 - {"bundle": "diamond", "version": "2.1.17", "plat": "linux-x86_64", "ref": DEV3, "members": [ + {"bundle": "diamond", "version": "2.1.17", "plat": "linux-x86_64", "ref": RAVEN3, "members": [ ("diamond/diamond", "diamond", True), ("diamond/LICENSE", "LICENSE", False)]}, - {"bundle": "diamond", "version": "2.1.17", "plat": "macos-arm64", "ref": DEV3, "members": [ + {"bundle": "diamond", "version": "2.1.17", "plat": "macos-arm64", "ref": RAVEN3, "members": [ ("diamond/diamond.mac", "diamond", True), ("diamond/LICENSE", "LICENSE", False)]}, - {"bundle": "diamond", "version": "2.1.17", "plat": "windows-x86_64", "ref": DEV3, "members": [ + {"bundle": "diamond", "version": "2.1.17", "plat": "windows-x86_64", "ref": RAVEN3, "members": [ ("diamond/diamond.exe", "diamond.exe", True), ("diamond/LICENSE", "LICENSE", False)]}, # HMMER — 3.4.0 (Linux/macOS), 3.3.2 native Windows from v2.10.5 - {"bundle": "hmmer", "version": "3.4.0", "plat": "linux-x86_64", "ref": DEV3, "members": [ + {"bundle": "hmmer", "version": "3.4.0", "plat": "linux-x86_64", "ref": RAVEN3, "members": [ ("hmmer/hmmsearch", "hmmsearch", True), ("hmmer/LICENSE", "LICENSE", False)]}, - {"bundle": "hmmer", "version": "3.4.0", "plat": "macos-arm64", "ref": DEV3, "members": [ + {"bundle": "hmmer", "version": "3.4.0", "plat": "macos-arm64", "ref": RAVEN3, "members": [ ("hmmer/hmmsearch.mac", "hmmsearch", True), ("hmmer/LICENSE", "LICENSE", False)]}, {"bundle": "hmmer", "version": "3.3.2", "plat": "windows-x86_64", "ref": V2105, "members": [ ("hmmer/hmmsearch.exe", "hmmsearch.exe", True), @@ -104,7 +104,7 @@ def build(out: Path, base_url: str) -> None: out.mkdir(parents=True, exist_ok=True) rows: list[tuple[str, str, int]] = [] - provenance: list[str] = [f"RAVEN develop3 @ {DEV3}", f"RAVEN tag {V2105}", ""] + provenance: list[str] = [f"RAVEN 3 @ {RAVEN3}", f"RAVEN tag {V2105}", ""] manifest: dict[str, dict] = {} for spec in SPECS: diff --git a/scripts/build_transporter_data.py b/scripts/build_transporter_data.py index dd172dd2..a119a86c 100644 --- a/scripts/build_transporter_data.py +++ b/scripts/build_transporter_data.py @@ -3,7 +3,7 @@ Produces four toolbox-assets artefacts (host them under a ``transporters-`` release, then run ``make_registry_snippet.py`` + ``publish_to_toolbox_assets.py`` per -https://github.com/SysBioChalmers/raven-gecko-parity/blob/main/docs/artefact_hosting.md): +https://github.com/CompMetEng/raven-gecko-parity/blob/develop/docs/publishing_artefacts.md): * ``transporter_pfam.hmm`` — the HMMER database of the transporter Pfam families in :data:`raven_toolbox.localization.transporter_tables.PFAM_TRANSPORTERS`, fetched one HMM at a time diff --git a/src/raven_toolbox/analysis/differential_sampling.py b/src/raven_toolbox/analysis/differential_sampling.py index f5b65e4f..c870ffb6 100644 --- a/src/raven_toolbox/analysis/differential_sampling.py +++ b/src/raven_toolbox/analysis/differential_sampling.py @@ -64,10 +64,8 @@ def get_flux_z(solutions_a: pd.DataFrame, solutions_b: pd.DataFrame) -> pd.Serie Notes ----- - Fixes a real sign bug in the zero-variance branch of ``getFluxZ.m`` as it - stands on RAVEN's ``develop3`` (a flux *decrease* with no variance in - either condition scored ``+100``, backwards relative to the general - formula): SysBioChalmers/RAVEN#742 makes the same correction upstream. + A flux *decrease* with no variance in either condition scores ``-100``, + the same sign the general formula gives, as in RAVEN's ``getFluxZ``. """ reaction_ids = _check_same_reactions(solutions_a, solutions_b) diff --git a/src/raven_toolbox/analysis/fseof.py b/src/raven_toolbox/analysis/fseof.py index 95e2febd..23c2d86b 100644 --- a/src/raven_toolbox/analysis/fseof.py +++ b/src/raven_toolbox/analysis/fseof.py @@ -9,8 +9,8 @@ * **Robust trend, not strict monotonicity.** Each reaction's flux is regressed against the enforced product flux across the scan; the **slope** is the response and the **correlation** (|r|) is a quality score. A reaction is a target if it tracks the - product cleanly (|r| ≥ ``correlation_threshold``) — one noisy step from LP - alternative optima no longer discards it (and pFBA per step keeps the scan stable). + product cleanly (|r| ≥ ``correlation_threshold``), so one noisy step from LP + alternative optima does not discard it (and pFBA per step keeps the scan stable). * **Direction classification RAVEN lacks.** Targets are labelled ``amplify`` (|flux| rises with the product → over-express), ``knockdown`` (|flux| falls), or ``knockout`` (|flux| → ~0 → delete). RAVEN only ever reports the amplification targets. diff --git a/src/raven_toolbox/analysis/min_flux_count.py b/src/raven_toolbox/analysis/min_flux_count.py index 43ffdada..cbdd85b4 100644 --- a/src/raven_toolbox/analysis/min_flux_count.py +++ b/src/raven_toolbox/analysis/min_flux_count.py @@ -92,13 +92,8 @@ def get_min_nr_fluxes( negative = more discouraged); non-negative entries are clamped up to the largest (least negative) negative score present, matching MATLAB's "positive scores are not possible" rule — or to 0 if - ``scores`` has no negative entries at all (MATLAB used to crash - outright here: ``scores(scores>=0)=max(scores(scores<0))`` evaluates - the right side to an empty array when nothing is negative, and - unlike the literal ``[]`` MATLAB treats as element deletion, an empty - array computed by ``max()`` just errors on the resulting size - mismatch instead of deleting anything — fixed upstream too, see - SysBioChalmers/RAVEN#729). Defaults to an implicit -1 for every + ``scores`` has no negative entries at all, as RAVEN's + ``getMinNrFluxes`` does. Defaults to an implicit -1 for every reaction (plain unweighted cardinality minimization). big_m: Override the big-M constant. Defaults to an estimate from the diff --git a/src/raven_toolbox/gapfilling/leak.py b/src/raven_toolbox/gapfilling/leak.py index 7dbd9848..502a4e71 100644 --- a/src/raven_toolbox/gapfilling/leak.py +++ b/src/raven_toolbox/gapfilling/leak.py @@ -4,8 +4,7 @@ metabolite for free — a sign of a stoichiometric leak, futile cycle, or unconstrained exchange somewhere in the network. This is the unified replacement for RAVEN's ``makeSomething`` (``'produce'``) and -``consumeSomething`` (``'consume'``), both now deprecated wrappers around it -(SysBioChalmers/RAVEN#732). +``consumeSomething`` (``'consume'``). Works by grafting a throwaway exchange reaction onto every metabolite (an "any metabolite" probe), forcing at least one unit of combined flux through diff --git a/src/raven_toolbox/init/ftinit.py b/src/raven_toolbox/init/ftinit.py index c7052b2c..773dfb33 100644 --- a/src/raven_toolbox/init/ftinit.py +++ b/src/raven_toolbox/init/ftinit.py @@ -800,12 +800,10 @@ def ftinit( docstring for the MILP mechanics and its one caveat (the bonus checks a producer is "on", not that the metabolite is verifiably net-produced through it). A name that matches no metabolite in ``prep.ref_model`` warns rather than silently doing nothing. - Ported from RAVEN's ``ftINITInternalAlg`` on its ``develop3`` branch — the released - ``main`` branch has an unrelated, unrepeated defect here (a fixed big-M that - incorrectly caps some reversible/negative reactions above magnitude 100; raven-toolbox - never had this because its own ``big_m=100`` default relies on the model being - rescaled first, not on a per-reaction cap — see the `INIT parameter calibration study - `_). + Ported from RAVEN 3's ``ftINITInternalAlg``. The ``big_m=100`` default relies on the + model being rescaled first, not on a per-reaction cap — see the `INIT parameter + calibration study + `_. ``mip_gap``/``time_limit`` are forwarded to each :func:`run_ftinit` solve. On genome-scale models they are essential for tractability — see the `INIT parameter diff --git a/src/raven_toolbox/manipulation/simplify.py b/src/raven_toolbox/manipulation/simplify.py index 3f0a35af..00081927 100644 --- a/src/raven_toolbox/manipulation/simplify.py +++ b/src/raven_toolbox/manipulation/simplify.py @@ -55,9 +55,8 @@ def _can_produce_and_consume(met) -> tuple[bool, bool]: counts exactly when it is reversible (``lower_bound < 0``). This deliberately ignores ``upper_bound`` and does not check whether a reaction can actually carry flux — that bounds-aware check is ``remove_no_flux_reactions``'s job (RAVEN - ``deleteMinMax``, via FVA); mixing it into this cheap structural pass previously - made a reaction locked at zero flux (``lb == ub == 0``) silently drop out as a - producer/consumer here, diverging from RAVEN when this pass runs alone. + ``deleteMinMax``, via FVA). A reaction locked at zero flux (``lb == ub == 0``) + therefore still counts as a producer/consumer here, as in RAVEN. """ produce = consume = False for rxn in met.reactions: diff --git a/tests/parity/README.md b/tests/parity/README.md index 123d60dc..1bbd60c7 100644 --- a/tests/parity/README.md +++ b/tests/parity/README.md @@ -38,11 +38,9 @@ These problems have many optima of equal objective value, so a different answer is not a wrong answer. What can be checked is *drift*: today's result against the result that was last inspected and accepted. -Nothing currently records an ftINIT extraction baseline this way — the tier-2 -extraction check that used to live here compared the classic INIT MILP -(`run_init`) against a recorded baseline, and was removed along with the rest -of that MILP. An ftINIT equivalent (baseline + recording script, on the same -pattern) is open work; see `docs/reference/todo.md`. +Nothing records an ftINIT extraction baseline this way yet. An ftINIT +drift check (baseline + recording script) is open work; see +`docs/reference/todo.md`. ### Tier 3 — statistical @@ -111,8 +109,5 @@ Still only *reported*, not enforced: the genome-scale Human-GEM, yeast and multi-organism *comparisons against MATLAB RAVEN* in raven-gecko-parity and raven-docs (the nightly job above is a regression guard on this package, not a cross-language check — same distinction as tier 1's baseline), and any small-model -extraction drift check. A small-model tier-2 test used to cover the latter -(`test_set_level.py`, against the classic INIT MILP) and was removed along -with the rest of tINIT; an ftINIT equivalent is open, see -`docs/reference/todo.md`. Tier 3 has a stated contract and no tests yet for -the same reason. +extraction drift check; an ftINIT drift check is open, see +`docs/reference/todo.md`. Tier 3 has a stated contract and no tests yet. diff --git a/tests/parity/test_genome_scale.py b/tests/parity/test_genome_scale.py index af8b4309..33080939 100644 --- a/tests/parity/test_genome_scale.py +++ b/tests/parity/test_genome_scale.py @@ -18,8 +18,7 @@ than guessed now. This exercises ``run_ftinit`` directly (the single-step MILP, not the staged -``ftinit()`` pipeline) on the raw model with synthetic scores -- the same shape -of check the classic INIT MILP used to get here, before tINIT was removed. +``ftinit()`` pipeline) on the raw model with synthetic scores. ``time_limit``/``mip_gap`` are the genome-scale-staged values ``run_ftinit``'s own docstring recommends, since nothing here goes through ``prep_init_model``'s rescaling. diff --git a/tests/test_analysis_differential_sampling.py b/tests/test_analysis_differential_sampling.py index b99a05aa..8633a6c2 100644 --- a/tests/test_analysis_differential_sampling.py +++ b/tests/test_analysis_differential_sampling.py @@ -28,8 +28,7 @@ def test_zero_variance_increase_is_positive(): def test_zero_variance_decrease_is_negative(): - # The case RAVEN#742 fixes: a flux *decrease* with no variance in either - # condition must score -100, not +100. + # A flux *decrease* with no variance in either condition scores -100. a = _const({"R1": 9.0}) b = _const({"R1": 5.0}) assert get_flux_z(a, b)["R1"] == -100.0 diff --git a/tests/test_init_ftinit.py b/tests/test_init_ftinit.py index d6ddbad1..85ddd905 100644 --- a/tests/test_init_ftinit.py +++ b/tests/test_init_ftinit.py @@ -324,8 +324,7 @@ def optimize(self): # metabolomics (production-bonus for detected metabolites). # # Fixtures and expected outcomes are the same ones hand-verified end-to-end against -# RAVEN's own ftINITInternalAlg (develop3 branch, the corrected one -- see the -# reference_reactions postmortem for why not `main`) via a standalone MATLAB probe +# RAVEN 3's own ftINITInternalAlg via a standalone MATLAB probe # before porting: same toy topology, same objective values, same flux directions. # --------------------------------------------------------------------------- # def _irrev_producer_model(): diff --git a/tests/test_utils_composition.py b/tests/test_utils_composition.py index a1395ee8..521be4cd 100644 --- a/tests/test_utils_composition.py +++ b/tests/test_utils_composition.py @@ -11,8 +11,7 @@ def _met(mid, name, formula=None, compartment="c"): def test_accounts_for_own_stoichiometric_coefficient(): """A -> 2 B: B's own coefficient is 2, so its atom counts are half of - A's, not identical to A's (the bug this port fixes relative to RAVEN's - original guessComposition, see SysBioChalmers/RAVEN#743).""" + A's, not identical to A's, as in RAVEN's guessComposition.""" m = cobra.Model("t") a = _met("a_c", "A", formula="CH4") b = _met("b_c", "B") diff --git a/validate_genome_scale.py b/validate_genome_scale.py index c5edd621..36062a93 100644 --- a/validate_genome_scale.py +++ b/validate_genome_scale.py @@ -1,5 +1,5 @@ """Real ftINIT extraction against genome-scale Human-GEM, for cross-language parity -against RAVEN's validate_genome_scale.m (in a RAVEN develop3 checkout). +with the same pipeline (prepINITModel, then ftINIT) run in RAVEN. Not a committed test -- a script to run once per side of a comparison, using the full pipeline (prep_init_model with a real task list, then ftinit() with real RNA-seq From 06d8b2c0e61f67a2efb9aa7b66be1c5fcd86eaf3 Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Sun, 18 Oct 2026 00:38:08 +0200 Subject: [PATCH 2/3] scale_biomass: optional target_total for the balanced biomass total With balance_out, the balancing component brings the components to target_total g/gDW (default 1), matching RAVEN's scaleBiomassFraction targetTotal argument. --- src/raven_toolbox/biomass/scale.py | 18 +++++++++++------- tests/test_biomass.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/raven_toolbox/biomass/scale.py b/src/raven_toolbox/biomass/scale.py index 31c93aed..603d86f1 100644 --- a/src/raven_toolbox/biomass/scale.py +++ b/src/raven_toolbox/biomass/scale.py @@ -91,18 +91,22 @@ def scale_biomass( new_value: float, *, balance_out: str | None = None, + target_total: float = 1.0, ) -> None: """Rescale a component to a new mass fraction, optionally balancing - out a second component so the biomass total becomes 1 g/gDW. + out a second component so the biomass total becomes ``target_total`` + g/gDW. Ports ``scaleBioMass.m``. The rescaling factor is derived from :func:`sum_biomass` on the *current* model state, so call this with an in-place ``model`` mutation, not on a stale snapshot. With ``balance_out``, that component is rescaled so the components sum - to exactly 1 g/gDW, whatever the total was before. It must be large - enough to absorb the difference: a ``ValueError`` is raised, before the - model is changed, when its fraction is 0 or would become 0 or negative. + to exactly ``target_total`` g/gDW (default 1), whatever the total was + before. It must be large enough to absorb the difference: a + ``ValueError`` is raised, before the model is changed, when its + fraction is 0 or would become 0 or negative. ``target_total`` has no + effect without ``balance_out``. """ fractions = sum_biomass(model, config) current = fractions[component_name] @@ -120,12 +124,12 @@ def scale_biomass( ) # Fractions are linear in the substrate coefficients, so the total # after the first rescaling is known before the model is changed. - balance_new = balance_current + 1.0 - (fractions["total"] - current + new_value) + balance_new = balance_current + target_total - (fractions["total"] - current + new_value) if balance_new <= 0: raise ValueError( f"Cannot balance {component_name!r} = {new_value:g} with " f"{balance_out!r}: it would have to go from {balance_current:.4g} " - f"to {balance_new:.4g} g/gDW to bring the total to 1." + f"to {balance_new:.4g} g/gDW to bring the total to {target_total:g}." ) factor = new_value / current rescale_pseudoreaction(model, config, component_name, factor) @@ -135,7 +139,7 @@ def scale_biomass( fractions = sum_biomass(model, config) total = fractions["total"] balance_current = fractions[balance_out] - balance_factor = (balance_current + (1.0 - total)) / balance_current + balance_factor = (balance_current + (target_total - total)) / balance_current rescale_pseudoreaction(model, config, balance_out, balance_factor) diff --git a/tests/test_biomass.py b/tests/test_biomass.py index 288dc1ae..3633b7ba 100644 --- a/tests/test_biomass.py +++ b/tests/test_biomass.py @@ -196,6 +196,21 @@ def test_scale_biomass_with_balance_keeps_total_at_one(): assert out["total"] == pytest.approx(1.0, rel=1e-6) +def test_scale_biomass_balance_to_target_total(): + m, cfg = _toy_biomass_model() + scale_biomass(m, cfg, "protein", 0.4, balance_out="carbohydrate", target_total=0.9) + out = sum_biomass(m, cfg) + assert out["protein"] == pytest.approx(0.4, rel=1e-6) + assert out["total"] == pytest.approx(0.9, rel=1e-6) + + +def test_scale_biomass_negative_balance_uses_target_total(): + m, cfg = _toy_biomass_model() + # Protein 0.8 fits a total of 1 but not of 0.5. + with pytest.raises(ValueError, match="bring the total to 0.5"): + scale_biomass(m, cfg, "protein", 0.8, balance_out="carbohydrate", target_total=0.5) + + def test_scale_biomass_zero_current_raises(): m, cfg = _toy_biomass_model() # Build a config that references a missing component → 0 current From 2fbe216e42a931716a1a9286a2ef73ad50cffcd9 Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Sun, 18 Oct 2026 00:44:46 +0200 Subject: [PATCH 3/3] docs: gap_report closes the boundary as RAVEN's gapReport does --- src/raven_toolbox/gapfilling/report.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/raven_toolbox/gapfilling/report.py b/src/raven_toolbox/gapfilling/report.py index 66777645..5b2b348a 100644 --- a/src/raven_toolbox/gapfilling/report.py +++ b/src/raven_toolbox/gapfilling/report.py @@ -17,15 +17,10 @@ same way for the same reason, on top of the same probe-reaction technique :func:`~raven_toolbox.gapfilling.find_leak_metabolite` already uses. -``gapReport.m`` only runs its mass-balancing section -(``canProduceWithoutInput``/``canConsumeWithoutOutput``) when the model -carries RAVEN's legacy ``unconstrained`` metabolite marker — a v2-era way of -representing boundary metabolites that a ``cobra.Model`` has no equivalent -for (``simplify_model``'s own docstring notes the same gap for -``deleteUnconstrained``). This port always computes that section instead, by -temporarily closing every one of the model's own boundary reactions first — -strictly more informative, and well-defined for any model regardless of how -it was built. +The mass-balancing section (``can_produce_without_input`` / +``can_consume_without_output``) temporarily closes every one of the model's +own boundary reactions, as ``gapReport.m`` does with the reactions +``getExchangeRxns`` returns. """ from __future__ import annotations