Add relaxProteomicsGreedy, shadow-price-ordered proteomics relaxation - #440
Merged
Conversation
…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.
Unit test results28 tests 28 ✅ 9s ⏱️ Results for commit 44bad94. ♻️ This comment has been updated with latest results. |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
flexibilizeEnzConcsfor reaching a target growth rate when proteomics constraints make the model infeasible or too slow. Greedy, shadow-price-ordered: while growth is belowminimalGrowth, 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 originalec.concsvalue, 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
flexibilizeEnzConcswhen 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; exhaustingmaxIterationswhile candidates still remained raises an error instead.Verification
Cross-checked against geckopy's
relax_proteomics_greedyon 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:P5), in one step, growth7.2 → 90, exact match on every trace field (before/after growth, shadow price).{P5: 10, P4: 5}and reportconverged=False, final_growth=90.maxIterationsexceeded (target unreachable in the given budget): both raise, with the same final growth reported.Added
testRelaxProteomicsGreedyConverges_tc0024,testRelaxProteomicsGreedyExhaustsCandidates_tc0025,testRelaxProteomicsGreedyMaxIterationsRaises_tc0026togeckoCoreFunctionTests.m. Full suite: 26 passed, 0 failed.This closes out all three functions from #22/#23/#24.