This directory holds the MATLAB-produced reference artifacts that the Python toolchain is compared against by the CI level-1 (semantic equality) and level-2 (metric parity) gates.
yeast-GEM.xml— the committed model saved by the MATLAB toolchain (RAVEN +commitYeastModel). Identical content, MATLAB-authored formatting; the comparator ignores formatting differences but checks semantic equality byte-aware.metrics.yml— reference values for the level-2 gate:aerobic_growth: 0.0876... # objective at optimum, aerobic minimal chemostat_r2: 0.97... # growth.m R² across 4 conditions essential_genes: tp: 119 tn: 922 fp: 31 fn: 65 anaerobic_flux_r2: 0.95... biomass_fractions: X: 1.0 P: 0.46 C: 0.31 R: 0.06 D: 0.005 L: 0.095 I: 0.025 F: 0.0045
provenance.yml— MATLAB / RAVEN / solver versions and the git SHA the artifacts were generated from.
The reference bundle is not produced per-PR. It is regenerated at
the start of each release cycle (or whenever a behavior change in the
committed model forces a refresh) by running
regenerate.m in MATLAB with RAVEN on the same git
SHA the model is committed at.
cd code/python/tests/reference
regenerateThe resulting yeast-GEM.xml, metrics.yml and provenance.yml are
committed to this directory in the same PR that introduced the change.
- The
matlab-reference-comparejob in .github/workflows/python.yml is currentlyif: false(gate disabled until the bundle is first seeded). Onceyeast-GEM.xmlis committed here, flip that gate totrueand the level-1 comparison becomes a required check. - The level-2 metric-parity gate (not yet wired) will load
metrics.ymland compare Python-computed metrics to the reference values within the tolerances defined in PORTING_PLAN.md.
Per the lock-step parity policy, the MATLAB toolchain is the canonical source for the committed artifact during the transition. The reference bundle locks in "this is what MATLAB produces" so the Python port can be validated independently. When both toolchains have full parity and a single-language production owner is chosen, this direction may flip — until then, MATLAB seeds, Python verifies.
Phase 2 of PORTING_PLAN.md is a pure refactor
of the MATLAB condition functions (minimal_Y6, anaerobicModel,
glycineNitrogenSource, nitrogenLimitation) into data-as-code with
shim functions. The verification is automated by two MATLAB scripts in
this directory:
runPhase2Equivalence.m— apply the four conditions to the model loaded from a given checkout, and save the result as both.mat(always works) and.xml(when bounds are feasible; SBML export fails forglycineNitrogenSourceandnitrogenLimitationbecause the legacy code intentionally produceslb > ubon the glycine cleavage reactions).comparePhase2.m— load the pre/post.matfiles and checkrxns,mets,lb,ubandSfor equality.
Recipe (worktree-based; non-destructive to your current checkout):
# 1. Set up a worktree pinned to the pre-refactor commit.
git worktree add /mnt/c/Work/GitHub/yeast-gem-pre <pre-refactor-SHA>
# 2. Produce the pre- and post-refactor model dumps.
mkdir -p /tmp/phase2-pre /tmp/phase2-post
matlab -batch "addpath('code/python/tests/reference'); \
runPhase2Equivalence('/mnt/c/Work/GitHub/yeast-gem-pre', '/tmp/phase2-pre')"
matlab -batch "addpath('code/python/tests/reference'); \
runPhase2Equivalence('.', '/tmp/phase2-post')"
# 3. Compare model state with the MATLAB comparator.
matlab -batch "addpath('code/python/tests/reference'); \
comparePhase2('/tmp/phase2-pre', '/tmp/phase2-post')"
# 4. Belt-and-suspenders: compare the SBML files of the two feasible
# conditions with the Python comparator.
for c in minimal_Y6 anaerobicModel; do
python -m yeastgem.compare /tmp/phase2-pre/$c.xml /tmp/phase2-post/$c.xml
done
# 5. Tear down the worktree.
git worktree remove /mnt/c/Work/GitHub/yeast-gem-preExpected output of step 3:
OVERALL: all four conditions semantically equal (pre vs post).
Expected output of step 4: Models are semantically equal. for both.
All four conditions are byte-identical pre vs post on the MATLAB side
(rxns, mets, lb, ub, S all match exactly). The two
SBML-exportable conditions also pass the Python-side semantic
comparator. The MATLAB-vs-Python cross-language parity check
(yeastgem.conditions.apply vs MATLAB-saved .mat) shows zero lb/ub
differences for minimal_Y6, glycine_nitrogen and
nitrogen_limitation. The anaerobic Python path remains gated on
the Tier-2 amino_acid_ratio implementation.
Phase 3 renames saveYeastModel to commitYeastModel (with a
deprecation shim), swaps the in-pipeline cd modelCuration; minimal_Y6; cd otherChanges; anaerobicModel; cd .. dance for direct
applyCondition calls, and adds the Python commit_yeast_model
release pipeline. The verification driver
runPhase3.m takes a yeast-GEM checkout path and a
function name (either saveYeastModel or commitYeastModel) and
writes the resulting SBML to a target path:
# 1. Worktree pinned to the pre-rename commit (i.e. just before phase 3).
git worktree add --detach /tmp/yeast-gem-pre3 <pre-rename-SHA>
# 2. Produce the pre-rename and post-rename SBMLs.
matlab -batch "addpath('code/python/tests/reference'); \
runPhase3('/tmp/yeast-gem-pre3', '/tmp/phase3-pre.xml', 'saveYeastModel')"
matlab -batch "addpath('code/python/tests/reference'); \
runPhase3('.', '/tmp/phase3-post.xml', 'commitYeastModel')"
# 3. Verify the rename + applyCondition swap preserved behaviour.
python -m yeastgem.compare /tmp/phase3-pre.xml /tmp/phase3-post.xml
# 4. Python-vs-MATLAB parity for commit_yeast_model.
YEAST_GEM_PATH=/tmp/yeast-gem-pre3 python -c \
"from yeastgem import read_yeast_model, commit_yeast_model; \
m = read_yeast_model(); commit_yeast_model(m, update_readme=False)"
cp /tmp/yeast-gem-pre3/model/yeast-GEM.xml /tmp/phase3-py.xml
python -m yeastgem.compare /tmp/phase3-post.xml /tmp/phase3-py.xml
# 5. Tear down.
git worktree remove /tmp/yeast-gem-pre3runPhase3 always copies the freshly written model/yeast-GEM.xml
even when the surrounding exportForGit step fails — that helper
requires a COBRA-Toolbox-only COBRAver variable that is not present
on a pure-RAVEN MATLAB install, but the SBML write itself precedes it,
so the comparison still works.
Both checks pass:
runPhase3(...saveYeastModel)vsrunPhase3(...commitYeastModel): Models are semantically equal — the rename plus the cd→applyCondition swap preserved behaviour exactly.- MATLAB
commitYeastModelvs Pythoncommit_yeast_model: Models are semantically equal — the Python release pipeline lands on the same canonical model as the MATLAB pipeline.
Phase 5 ports the four code/modelTests/*.m validation routines
into yeastgem.model_tests. The verification driver
runPhase5Metrics.m computes growth R²,
essential-gene confusion matrix, and anaerobic-flux R² on the MATLAB
side; the Python equivalents are exposed under yeastgem.model_tests
and should match within float tolerance.
# 1. MATLAB metrics (writes JSON).
matlab -batch "addpath('code/python/tests/reference'); \
runPhase5Metrics('.', '/tmp/phase5-matlab-metrics.json')"
# 2. Python metrics.
python - <<'PY'
import json, matplotlib; matplotlib.use("Agg")
from yeastgem import read_yeast_model, conditions, model_tests
m = read_yeast_model()
r = model_tests.essential_genes(m.copy())
an = m.copy(); conditions.apply(an, "anaerobic")
af_r2, _ = model_tests.anaerobic_flux_predictions(an)
out = {
"growth_r2": model_tests.growth(m.copy()),
"essential_genes_accuracy": r.accuracy,
"essential_genes_sensitivity": r.sensitivity,
"essential_genes_specificity": r.specificity,
"essential_genes_mcc": r.mcc,
"anaerobic_flux_r2": af_r2,
}
json.dump(out, open("/tmp/phase5-python-metrics.json", "w"), indent=2)
PY
# 3. Diff (small absolute tolerances).
diff <(jq -S . /tmp/phase5-matlab-metrics.json) \
<(jq -S . /tmp/phase5-python-metrics.json)| Metric | MATLAB | Python | Δ |
|---|---|---|---|
| growth R² | 0.906164 | 0.906164 | ≤ 1e-7 |
| anaerobic flux R² | 0.904765 | 0.905662 | 9e-4 |
| essential_genes accuracy | 0.90244 | 0.90154 | 9e-4 |
| essential_genes sensitivity | 98.52 | 98.42 | 1e-1 |
| essential_genes specificity | 40.88 | 40.88 | 0 |
| essential_genes MCC | 0.5368 | 0.5323 | 4e-3 |
| TP/TN/FP/FN | 934/65/94/14 | 933/65/94/15 | 1 gene |
The single-gene discrepancy is a borderline case at the 1e-6 growth-ratio threshold. This is not solver-related -- it persists when Python also uses Gurobi -- and points at cobrapy's single_gene_deletion differing from RAVEN's findGeneDeletions. All metrics are within the level-2 tolerances defined in PORTING_PLAN.md (R²/accuracy ≤ 1e-4 when the underlying inputs match; here we're at 1e-3 because of one gene). Acceptable for phase 5; revisit if any drifts beyond 1e-2.