Skip to content

Add relaxProteomicsGreedy, shadow-price-ordered proteomics relaxation - #440

Merged
edkerk merged 3 commits into
develop4from
feat/relax-proteomics-greedy
Aug 28, 2026
Merged

Add relaxProteomicsGreedy, shadow-price-ordered proteomics relaxation#440
edkerk merged 3 commits into
develop4from
feat/relax-proteomics-greedy

Conversation

@edkerk

@edkerk edkerk commented Aug 28, 2026

Copy link
Copy Markdown
Member

Ports geckopy's relax_proteomics_greedy (limit_proteins/relax_proteomics_greedy.py) to MATLAB — itself ported from the legacy geckopy package (Carrasco et al., 2023, https://doi.org/10.1128/spectrum.01705-23, geckopy/experimental/relaxation.py). Tracked as SysBioChalmers/raven-gecko-parity#24.

What it does

An alternative to flexibilizeEnzConcs for reaching a target growth rate when proteomics constraints make the model infeasible or too slow. Greedy, shadow-price-ordered: while growth is below minimalGrowth, finds the still-proteomics-constrained enzyme (usage_prot_<id> upper bound below the shared-pool default) with the largest |shadow price|, fully relaxes it back to the default upper bound, re-solves, repeats. Returns which enzymes were relaxed (mapped to their original ec.concs value, so the caller can restore them), a step-by-step trace, and whether it converged.

One LP solve per step, no tighten-back pass — converges faster than flexibilizeEnzConcs when one or two enzymes dominate the infeasibility, at the cost of a looser (less proteomics-faithful) result.

Two distinct non-convergence outcomes, matching the Python side exactly: running out of eligible candidates before reaching the target returns normally with converged=false; exhausting maxIterations while candidates still remained raises an error instead.

Verification

Cross-checked against geckopy's relax_proteomics_greedy on an identical ecTestGEM-based fixture (two proteomics-constrained enzymes, one of which is the true growth bottleneck and one of which isn't) across all three code paths:

  • Converges: both relax only the true bottleneck (P5), in one step, growth 7.2 → 90, exact match on every trace field (before/after growth, shadow price).
  • Exhausts candidates (unreachable target): both relax {P5: 10, P4: 5} and report converged=False, final_growth=90.
  • maxIterations exceeded (target unreachable in the given budget): both raise, with the same final growth reported.

Added testRelaxProteomicsGreedyConverges_tc0024, testRelaxProteomicsGreedyExhaustsCandidates_tc0025, testRelaxProteomicsGreedyMaxIterationsRaises_tc0026 to geckoCoreFunctionTests.m. Full suite: 26 passed, 0 failed.

This closes out all three functions from #22/#23/#24.

…xation

Ports geckopy's relax_proteomics_greedy (limit_proteins/
relax_proteomics_greedy.py) to MATLAB: while growth is below a target,
finds the still-proteomics-constrained enzyme with the largest
absolute shadow price on its prot_<id> constraint, fully relaxes its
usage_prot_<id> upper bound back to the shared-pool default, re-solves,
and repeats. An alternative to flexibilizeEnzConcs: one LP solve per
step, no tighten-back pass, so it converges faster when one or two
enzymes dominate the infeasibility but the result is looser.

Two distinct non-convergence outcomes, matching the Python side
exactly: running out of eligible candidates before reaching the
target returns normally with converged=false; exhausting
maxIterations while candidates still remained raises an error
instead.

Cross-verified against geckopy's relax_proteomics_greedy on an
identical ecTestGEM-based fixture across all three code paths ---
normal convergence, exhausted-candidates return, and the
maxIterations error --- with an exact match on every value (growth
before/after, shadow price, which enzymes got relaxed, in what
order). Regression tests added to geckoCoreFunctionTests.m for all
three.
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Unit test results

28 tests   28 ✅  9s ⏱️
 1 suites   0 💤
 1 files     0 ❌

Results for commit 44bad94.

♻️ This comment has been updated with latest results.

edkerk added 2 commits August 28, 2026 10:29
testGetEnzymeBottlenecksRanksByShadowPrice_tc0024 (merged in #438) used
a fixture where R4 (a spontaneous, cost-free reaction) makes the whole
R2/R3 enzyme requirement optional, so the shadow price of the four
unused enzymes is genuinely LP-dual-degenerate: confirmed, across
repeated runs in separate MATLAB processes, that which one reports the
tied -0.72 (and which reports 0) is solver-path-dependent, not stable.
The test's hardcoded "-0.72 for all five" assertion happened to pass
in its own CI run and in this repo's local verification by chance of
solver path, not because that vertex is the only valid one.

Fixed by blocking R2 and R4 (leaving R3 as the sole route m1c->m2c),
the same fix enzyme_usage_ectestgem's own fixture already needed for
this exact degeneracy on this exact model. With R2/R4 blocked, P4 and
P5 are both genuinely, non-degenerately required in series, confirmed
stable across repeated runs in two separate fresh MATLAB processes.

This also surfaced a real, confirmed divergence from geckopy's own
get_enzyme_bottlenecks on the *unused* enzymes specifically: MATLAB's
gurobi interface reports a true 0 shadow price for them, while
geckopy's gurobipy-via-cobra stack instead propagates the shared
pool's own nonzero dual uniformly to every prot_<id> row regardless of
usage. Both are valid dual solutions to the same degenerate LP; the
test now asserts each side's own value rather than a false match, and
only asserts equality where both sides agree (the two genuinely
load-bearing enzymes, and every primal-derived value).
CI on this branch (Linux runner) failed: it reported the shared pool's
own -0.4 dual uniformly for every prot_<id> row, including the three
unused enzymes the previous commit's test asserted should be exactly
0 there (a value only ever observed on this repo's own local Windows
run). This isn't a stable MATLAB-vs-Python split as that commit
claimed -- the Linux MATLAB run here agrees with geckopy's Python run,
not with local Windows. It's genuine LP dual degeneracy for an enzyme
that carries no flux, resolved differently by different solver
builds/platforms/presolve paths, on both sides, unpredictably.

The test now asserts only what's actually stable: every primal-derived
value (flux, capUsage, upperBound), and that results are sorted by
non-increasing |shadowPrice| -- never a specific shadow-price value
for a zero-flux enzyme, on either side.
@edkerk
edkerk merged commit d512859 into develop4 Aug 28, 2026
2 checks passed
@edkerk
edkerk deleted the feat/relax-proteomics-greedy branch August 28, 2026 08:40
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