Fix two knock-in defects - This allows computation of EFVs with StrainDesign - #78
Conversation
`essential_kis` was built from `cmp_ki_cost[er]`, which yields the intervention
cost of each essential reaction rather than the reaction identifier. On
e_coli_core with every reaction made a knock-in candidate the set came out as
{1.0, 2.0, 3.0} -- the distinct lump costs.
`SDProblem.__init__` then tested `r in self.essential_kis` with `r` a cobra
Reaction, so the membership check could never succeed: 0 of 56 binaries were
pinned, and reactions essential to a module stayed optional. Knock-in problems
could therefore return solutions that omit a reaction the module cannot do
without.
Collect identifiers instead and compare against `r.id`. Measured on e_coli_core
with a PROTECT module and all reactions as knock-in candidates: forced-on
binaries go from 0 to 11.
The default knockout path is unaffected -- `essential_kis` is only populated
from reactions carrying a knock-in cost, so it stays empty when `ki_cost` is
unused. MCS on e_coli_core (growth >= 0.001, max_cost 3) returns the identical
353 designs before and after, and the test suite passes (368 passed, 2 skipped,
performance tests deselected).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtof9nZXFvCNoXz19po8C
|
Added a third commit addressing the underlying verification defect rather than only its symptom.
|
d8f39bd to
ae174f5
Compare
|
Reworked the third commit (force-pushed) after review: column removal is now kept as the default. Full removal is the stronger oracle — an absent variable owes nothing to feasibility tolerances, whereas an interval of # zeroing a variable whose bounds exclude zero contradicts that bound
if any(self.cont_MILP.lb[j] > 0.0 or self.cont_MILP.ub[j] < 0.0 for j in inactive_vars):
valid[i] = False
continue
# otherwise drop the columns outright, exactly as beforeFor a zero-exclusive variable the intersected interval is empty, so the region is infeasible whatever the remaining system does and no LP has to be built. Every other variable is removed exactly as before, so the tolerance-free property of the original check is preserved in the common case. Re-validated after the rework:
|
…n verify_sd
verify_sd rebuilds the continuous part of the problem as a plain LP and decides
feasibility from it. Variables an intervention touches are removed by dropping
their columns, which equals v=0 only while every constraint that should still
bite survives.
That does not hold in general. prevent_boundary_knockouts turns a bound that
excludes zero, such as ATPM >= 3.15, into a row with no z-mapping precisely so a
knockout contradicts it rather than deleting it. For PROTECT modules,
reassign_lb_ub_from_ineq then folds single-variable rows back into variable
bounds, and those bounds disappear together with the column. The check saw an
empty system and reported it feasible, which also made the "strain already meets
the requirements" shortcut fire on knock-in problems and skip the MILP entirely.
Test that case before building the LP: zeroing a variable whose bounds exclude
zero contradicts the bound, so the region is empty whatever the rest of the
system does. Column removal is kept for every other variable, since absence owes
nothing to feasibility tolerances and is a stronger statement than an interval
of [0, 0].
e_coli_core MCS (growth >= 0.001, max_cost 3) returns the identical 353 designs
and still reports {ATPM} as a size-1 MCS. EFV enumeration via a PROTECT module
with all reactions as knock-in candidates (max_cost 45) returns 607 designs, all
members of the minimal-support antichain, with the two size-26 supports
set-identical to those obtained independently as the Berge dual of the model's
minimal cut sets. Test suite: 368 passed, 2 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFtof9nZXFvCNoXz19po8C
ae174f5 to
91ac88e
Compare
|
Simplified to two commits (force-pushed). The pre-check guard turned out to be redundant once The shortcut in Verified by reverting the guards and re-running:
Final diff is 17 insertions across three files:
|
Two related defects make knock-in problems return solutions that omit reactions the modules cannot do without. Both are specific to knock-ins; the knockout path is untouched.
1.
essential_kiscollected costs instead of identifierscompute_strain_designs.pyThis takes the intervention cost of each essential reaction rather than its identifier. On e_coli_core with every reaction made a knock-in candidate, the set came out as
{1.0, 2.0, 3.0}— the distinct lump costs.SDProblem.__init__then didtesting a cobra
Reactionfor membership in a set of floats, which can never succeed. Measured: 0 of 56 binaries pinned, so reactions essential to a module stayed optional. Fixed by collecting identifiers and comparing againstr.id; forced-on binaries go from 0 to 11 on that setup.The same variable also feeds the "targetable reactions" log line, which was correspondingly wrong.
2. The no-interventions-needed shortcut misreads an all-zero knock-in vector
compute_optimal,compute_anyandenumerateeach start by checking whether the unmodified strain already satisfies the setup, viaverify_sdon an all-zeroz. For knockouts that is the wild type and the check is right.For knock-ins an all-zero
zmeans those reactions are absent.verify_sddeactivates a variable together with the constraints acting on it, so the check evaluates an empty system, which passes vacuously instead of failing. The run returned "no interventions are needed" and one empty design without ever solving the MILP.The shortcut is now skipped whenever any intervention binary is inverted.
Validation
With both fixes, a PROTECT module plus knock-in candidates enumerates minimal reaction sets that support the protected region — elementary flux vectors.
On e_coli_core (
growth >= 0.001, all reactions as knock-in candidates,max_cost 45), checked against the minimal-support antichain obtained independently as the Berge dual of the model's own minimal cut sets:Knockout behaviour is unchanged: MCS on e_coli_core (
growth >= 0.001,max_cost 3) returns the identical 353 designs before and after.essential_kisis only populated from reactions carrying a knock-in cost, so it stays empty whenki_costis unused.Test suite: 368 passed, 2 skipped (performance tests deselected).
🤖 Generated with Claude Code
https://claude.ai/code/session_01RFtof9nZXFvCNoXz19po8C