Skip to content

feat(ftinit): opt-in deterministic extraction (strict gap + canonical optimum) - #85

Draft
edkerk wants to merge 2 commits into
developfrom
feat/ftinit-deterministic-extraction
Draft

feat(ftinit): opt-in deterministic extraction (strict gap + canonical optimum)#85
edkerk wants to merge 2 commits into
developfrom
feat/ftinit-deterministic-extraction

Conversation

@edkerk

@edkerk edkerk commented Jul 20, 2026

Copy link
Copy Markdown
Member

Opt-in deterministic ftINIT extraction

The ftINIT extraction MILP is highly degenerate: many reaction subsets reach the same
score optimum, and the solver returns an arbitrary one. Threads=1 + a fixed Seed make
that choice reproducible for a fixed solver build, but not unique: a different Gurobi
version or platform lands on a different optimum. Downstream that surfaces as
gene-essentiality predictions that drift for reasons unrelated to the model.

Measured on Human-GEM v2.0.0 / DLD1 (Gurobi 13, Threads=1): changing only the solver
Seed from 1234 to 7 moves 125 of ~7766 kept reactions and flips 5 of ~281
essential-gene calls
(PC, ACADVL, DUT, ALDH3A2, RDH5).

What this adds

Two opt-in flags on ftinit() (and the underlying run_ftinit / fill_tasks),
both default off, so the existing behaviour is byte-for-byte unchanged:

  • strict_gap — one solve per step proven to a fixed absolute gap (0.05, below the
    0.1 reaction-score granularity), replacing the loose relative-gap escalation whose
    final near-zero-objective run otherwise accepts an arbitrary within-gap incumbent. A
    relative gap is meaningless on that step; an absolute one is optimal at any scale.
  • canonical — a best-effort lexicographic phase 2, applied to both each extraction
    step and the task gap-fill: hold the primary objective at its optimum, then minimise the
    count of kept/added removable reactions (parsimony), then their summed id rank. Over the
    binary (removable) reactions only, so the objectives are integer and provable with a
    cheap absolute gap below 1. Selects a stable, near-unique optimum instead of a solver
    tie-break.

Both are guarded by a solution-availability check (_has_solution): a solve that ends at
the time limit with no incumbent now raises a clear error (or, for the canonical phase,
falls back to the primary optimum) rather than throwing an opaque AttributeError on a
null primal.

Tests

tests/test_init_ftinit.py, test_init_pipeline.py, test_init_taskfill.py: a
degenerate-tie model (two interchangeable reactions) is resolved to the lower-id one
deterministically; the flags are no-ops on the unique-optimum oracle (T0001) and are
run-to-run identical. Full suite green (852 passed).

Empirical results (Human-GEM v2.0.0 / DLD1) — and an important caveat

Measured seed 1234 vs seed 7:

Metric Baseline (no flags) Canonical only Canonical + strict
Reaction seed-swing 125 64 34
Essential-gene seed-swing 5 (not measured) 19
Build time ~12 min ~25 min ~34-80 min

The flags reduce the reaction-level swing as intended, but on this benchmark they did
not improve — and actually worsened — the gene-essentiality determinism (5 -> 19
flips). Two compounding reasons: (1) the 34 residual reactions are all GPR-less (mostly
transport), so the reaction metric is a poor proxy, and (2) canonical minimises the kept
set, producing sparser models with more essential genes (283/296 vs the baseline
281/278) that are more sensitive to the residual alternative-pathway degeneracy, which
propagates to enzymatic genes' essentiality through connectivity. strict_gap also fails
to prove the genome-scale optimum in reasonable time (a build hit ~67 min of timeouts and
fell back to arbitrary incumbents), adding noise.

Conclusion: these are a correct, tested, opt-in tool for a more parsimonious and
reproducible extraction
, but they are not a fix for gene-essentiality determinism and
can make it worse. For run-to-run essentiality reproducibility the reliable lever is
pinning the solver stack (raven-toolbox commit + gurobipy version) so a fixed stack
gives bit-identical results. Posting these numbers so the trade-off is on the record;
marking the PR draft pending that discussion.

… optimum)

The ftINIT MILP is degenerate: many reaction subsets reach the same score
optimum and the solver returns an arbitrary one. This is reproducible for a
fixed solver build + seed but fragile to the Gurobi version or platform, so a
downstream metric such as gene essentiality can drift on solver noise rather
than on the model. Measured on Human-GEM/DLD1: changing only the solver seed
moves ~125 of 7766 kept reactions and flips 5 essential-gene calls.

Add two opt-in flags (default off, exact RAVEN behaviour preserved):

- strict_gap: one near-proven-optimal solve per step instead of RAVEN's loose
  absolute-gap escalation, whose final near-zero-objective run accepts an
  arbitrary within-gap incumbent.
- canonical: a lexicographic phase 2 that, holding the score objective at its
  optimum, minimises the id-ordered count of kept reactions, selecting the
  unique sparsest optimum instead of an arbitrary tie-break.

Threaded through run_ftinit / _solve_step / ftinit. Tests cover degenerate-tie
resolution, no regression on the unique-optimum oracle, and staged stability.
@edkerk
edkerk marked this pull request as draft July 21, 2026 01:58
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