Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .github/workflows/parity-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,10 @@ jobs:
# night for no reason.
if: github.repository == 'CompMetEng/raven-toolbox'
runs-on: ubuntu-latest
# Measured against the classic INIT MILP this job used to check (now
# ftINIT's run_ftinit, bounded to time_limit=600s -- see
# tests/parity/test_genome_scale.py): that extraction took ~68 minutes on
# a fast workstation, of which only ~2 were parsing and copying, and
# time_limit did not bound the call -- a 60-second limit still ran for
# well over 15 minutes -- so the real stop was pytest-timeout below. This
# budget is comfortably larger than ftINIT's bound so a runaway is still
# reported as a test failure instead of a killed job.
# The extraction is run_ftinit bounded to time_limit=600s (see
# tests/parity/test_genome_scale.py). This budget is comfortably larger
# than that bound, so a runaway is reported as a test failure by
# pytest-timeout below instead of a killed job.
timeout-minutes: 150

steps:
Expand Down
6 changes: 3 additions & 3 deletions scripts/benchmark_deeploc.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ def main() -> None:
ctruth, crxn_genes = build_truth(cmodel)
collapse_desc = ", ".join(f"{mem}->{lum}" for mem, lum in membrane_to_lumen.items())
out += [f"## 3. Collapsed membranes ({collapse_desc})",
"", "Collapsing also turns lumen/membrane-spanning reactions (previously "
"multi-compartment, so excluded) into single-compartment ones, so the truth set "
"grows.", ""]
"", "Collapsing also turns lumen/membrane-spanning reactions (multi-compartment, "
"so excluded, on the uncollapsed model) into single-compartment ones, so the "
"truth set grows.", ""]
out += fmt_acc("Per-compartment accuracy on the organelle-collapsed model",
organelle_accuracy(ctruth, crxn_genes, dl), cfg["comp_order"])

Expand Down
24 changes: 12 additions & 12 deletions scripts/build_binary_bundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

Sources (pinned for reproducibility):

* RAVEN ``develop3`` — BLAST+ 2.17.0, DIAMOND 2.1.17, HMMER 3.4.0 ``hmmsearch``
* RAVEN 3 — BLAST+ 2.17.0, DIAMOND 2.1.17, HMMER 3.4.0 ``hmmsearch``
(Linux + macOS). ``.mac`` builds: ``hmmsearch`` is arm64, BLAST/DIAMOND are
universal — so ``macos-arm64`` serves all three (Intel macOS → conda).
* RAVEN ``v2.10.5`` — HMMER **3.3.2** native Windows ``hmmsearch.exe`` (+ the
Expand All @@ -32,39 +32,39 @@
from urllib.request import urlopen

RAW = "https://raw.githubusercontent.com/SysBioChalmers/RAVEN"
# Pinned RAVEN refs (commit SHA for the mutable branch; tag is already immutable).
DEV3 = "aa5e2c670f2f9cb7d040557974bb493a8ce0ba91" # develop3 HEAD at build time
# Pinned RAVEN refs: the commit carrying the RAVEN 3 binaries, and a release tag.
RAVEN3 = "aa5e2c670f2f9cb7d040557974bb493a8ce0ba91"
V2105 = "v2.10.5"

PROVIDES = {"blast": ["blastp", "makeblastdb"], "diamond": ["diamond"], "hmmer": ["hmmsearch"]}

# One dict per published ZIP. members: (path under software/, arcname in ZIP, executable?)
SPECS: list[dict] = [
# BLAST+ 2.17.0
{"bundle": "blast", "version": "2.17.0", "plat": "linux-x86_64", "ref": DEV3, "members": [
{"bundle": "blast", "version": "2.17.0", "plat": "linux-x86_64", "ref": RAVEN3, "members": [
("blast+/blastp", "blastp", True),
("blast+/makeblastdb", "makeblastdb", True),
("blast+/LICENSE", "LICENSE", False)]},
{"bundle": "blast", "version": "2.17.0", "plat": "macos-arm64", "ref": DEV3, "members": [
{"bundle": "blast", "version": "2.17.0", "plat": "macos-arm64", "ref": RAVEN3, "members": [
("blast+/blastp.mac", "blastp", True),
("blast+/makeblastdb.mac", "makeblastdb", True),
("blast+/LICENSE", "LICENSE", False)]},
{"bundle": "blast", "version": "2.17.0", "plat": "windows-x86_64", "ref": DEV3, "members": [
{"bundle": "blast", "version": "2.17.0", "plat": "windows-x86_64", "ref": RAVEN3, "members": [
("blast+/blastp.exe", "blastp.exe", True),
("blast+/makeblastdb.exe", "makeblastdb.exe", True),
("blast+/nghttp2.dll", "nghttp2.dll", False),
("blast+/LICENSE", "LICENSE", False)]},
# DIAMOND 2.1.17
{"bundle": "diamond", "version": "2.1.17", "plat": "linux-x86_64", "ref": DEV3, "members": [
{"bundle": "diamond", "version": "2.1.17", "plat": "linux-x86_64", "ref": RAVEN3, "members": [
("diamond/diamond", "diamond", True), ("diamond/LICENSE", "LICENSE", False)]},
{"bundle": "diamond", "version": "2.1.17", "plat": "macos-arm64", "ref": DEV3, "members": [
{"bundle": "diamond", "version": "2.1.17", "plat": "macos-arm64", "ref": RAVEN3, "members": [
("diamond/diamond.mac", "diamond", True), ("diamond/LICENSE", "LICENSE", False)]},
{"bundle": "diamond", "version": "2.1.17", "plat": "windows-x86_64", "ref": DEV3, "members": [
{"bundle": "diamond", "version": "2.1.17", "plat": "windows-x86_64", "ref": RAVEN3, "members": [
("diamond/diamond.exe", "diamond.exe", True), ("diamond/LICENSE", "LICENSE", False)]},
# HMMER — 3.4.0 (Linux/macOS), 3.3.2 native Windows from v2.10.5
{"bundle": "hmmer", "version": "3.4.0", "plat": "linux-x86_64", "ref": DEV3, "members": [
{"bundle": "hmmer", "version": "3.4.0", "plat": "linux-x86_64", "ref": RAVEN3, "members": [
("hmmer/hmmsearch", "hmmsearch", True), ("hmmer/LICENSE", "LICENSE", False)]},
{"bundle": "hmmer", "version": "3.4.0", "plat": "macos-arm64", "ref": DEV3, "members": [
{"bundle": "hmmer", "version": "3.4.0", "plat": "macos-arm64", "ref": RAVEN3, "members": [
("hmmer/hmmsearch.mac", "hmmsearch", True), ("hmmer/LICENSE", "LICENSE", False)]},
{"bundle": "hmmer", "version": "3.3.2", "plat": "windows-x86_64", "ref": V2105, "members": [
("hmmer/hmmsearch.exe", "hmmsearch.exe", True),
Expand Down Expand Up @@ -104,7 +104,7 @@ def build(out: Path, base_url: str) -> None:
out.mkdir(parents=True, exist_ok=True)

rows: list[tuple[str, str, int]] = []
provenance: list[str] = [f"RAVEN develop3 @ {DEV3}", f"RAVEN tag {V2105}", ""]
provenance: list[str] = [f"RAVEN 3 @ {RAVEN3}", f"RAVEN tag {V2105}", ""]
manifest: dict[str, dict] = {}

for spec in SPECS:
Expand Down
2 changes: 1 addition & 1 deletion scripts/build_transporter_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Produces four toolbox-assets artefacts (host them under a ``transporters-<date>`` release, then run
``make_registry_snippet.py`` + ``publish_to_toolbox_assets.py`` per
https://github.com/SysBioChalmers/raven-gecko-parity/blob/main/docs/artefact_hosting.md):
https://github.com/CompMetEng/raven-gecko-parity/blob/develop/docs/publishing_artefacts.md):

* ``transporter_pfam.hmm`` — the HMMER database of the transporter Pfam families in
:data:`raven_toolbox.localization.transporter_tables.PFAM_TRANSPORTERS`, fetched one HMM at a time
Expand Down
6 changes: 2 additions & 4 deletions src/raven_toolbox/analysis/differential_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,8 @@ def get_flux_z(solutions_a: pd.DataFrame, solutions_b: pd.DataFrame) -> pd.Serie

Notes
-----
Fixes a real sign bug in the zero-variance branch of ``getFluxZ.m`` as it
stands on RAVEN's ``develop3`` (a flux *decrease* with no variance in
either condition scored ``+100``, backwards relative to the general
formula): SysBioChalmers/RAVEN#742 makes the same correction upstream.
A flux *decrease* with no variance in either condition scores ``-100``,
the same sign the general formula gives, as in RAVEN's ``getFluxZ``.
"""
reaction_ids = _check_same_reactions(solutions_a, solutions_b)

Expand Down
4 changes: 2 additions & 2 deletions src/raven_toolbox/analysis/fseof.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* **Robust trend, not strict monotonicity.** Each reaction's flux is regressed against
the enforced product flux across the scan; the **slope** is the response and the
**correlation** (|r|) is a quality score. A reaction is a target if it tracks the
product cleanly (|r| ≥ ``correlation_threshold``) — one noisy step from LP
alternative optima no longer discards it (and pFBA per step keeps the scan stable).
product cleanly (|r| ≥ ``correlation_threshold``), so one noisy step from LP
alternative optima does not discard it (and pFBA per step keeps the scan stable).
* **Direction classification RAVEN lacks.** Targets are labelled ``amplify`` (|flux|
rises with the product → over-express), ``knockdown`` (|flux| falls), or ``knockout``
(|flux| → ~0 → delete). RAVEN only ever reports the amplification targets.
Expand Down
9 changes: 2 additions & 7 deletions src/raven_toolbox/analysis/min_flux_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,8 @@ def get_min_nr_fluxes(
negative = more discouraged); non-negative entries are clamped up to
the largest (least negative) negative score present, matching
MATLAB's "positive scores are not possible" rule — or to 0 if
``scores`` has no negative entries at all (MATLAB used to crash
outright here: ``scores(scores>=0)=max(scores(scores<0))`` evaluates
the right side to an empty array when nothing is negative, and
unlike the literal ``[]`` MATLAB treats as element deletion, an empty
array computed by ``max()`` just errors on the resulting size
mismatch instead of deleting anything — fixed upstream too, see
SysBioChalmers/RAVEN#729). Defaults to an implicit -1 for every
``scores`` has no negative entries at all, as RAVEN's
``getMinNrFluxes`` does. Defaults to an implicit -1 for every
reaction (plain unweighted cardinality minimization).
big_m:
Override the big-M constant. Defaults to an estimate from the
Expand Down
18 changes: 11 additions & 7 deletions src/raven_toolbox/biomass/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,22 @@ def scale_biomass(
new_value: float,
*,
balance_out: str | None = None,
target_total: float = 1.0,
) -> None:
"""Rescale a component to a new mass fraction, optionally balancing
out a second component so the biomass total becomes 1 g/gDW.
out a second component so the biomass total becomes ``target_total``
g/gDW.

Ports ``scaleBioMass.m``. The rescaling factor is derived from
:func:`sum_biomass` on the *current* model state, so call this with
an in-place ``model`` mutation, not on a stale snapshot.

With ``balance_out``, that component is rescaled so the components sum
to exactly 1 g/gDW, whatever the total was before. It must be large
enough to absorb the difference: a ``ValueError`` is raised, before the
model is changed, when its fraction is 0 or would become 0 or negative.
to exactly ``target_total`` g/gDW (default 1), whatever the total was
before. It must be large enough to absorb the difference: a
``ValueError`` is raised, before the model is changed, when its
fraction is 0 or would become 0 or negative. ``target_total`` has no
effect without ``balance_out``.
"""
fractions = sum_biomass(model, config)
current = fractions[component_name]
Expand All @@ -120,12 +124,12 @@ def scale_biomass(
)
# Fractions are linear in the substrate coefficients, so the total
# after the first rescaling is known before the model is changed.
balance_new = balance_current + 1.0 - (fractions["total"] - current + new_value)
balance_new = balance_current + target_total - (fractions["total"] - current + new_value)
if balance_new <= 0:
raise ValueError(
f"Cannot balance {component_name!r} = {new_value:g} with "
f"{balance_out!r}: it would have to go from {balance_current:.4g} "
f"to {balance_new:.4g} g/gDW to bring the total to 1."
f"to {balance_new:.4g} g/gDW to bring the total to {target_total:g}."
)
factor = new_value / current
rescale_pseudoreaction(model, config, component_name, factor)
Expand All @@ -135,7 +139,7 @@ def scale_biomass(
fractions = sum_biomass(model, config)
total = fractions["total"]
balance_current = fractions[balance_out]
balance_factor = (balance_current + (1.0 - total)) / balance_current
balance_factor = (balance_current + (target_total - total)) / balance_current
rescale_pseudoreaction(model, config, balance_out, balance_factor)


Expand Down
3 changes: 1 addition & 2 deletions src/raven_toolbox/gapfilling/leak.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
metabolite for free — a sign of a stoichiometric leak, futile cycle, or
unconstrained exchange somewhere in the network. This is the unified
replacement for RAVEN's ``makeSomething`` (``'produce'``) and
``consumeSomething`` (``'consume'``), both now deprecated wrappers around it
(SysBioChalmers/RAVEN#732).
``consumeSomething`` (``'consume'``).

Works by grafting a throwaway exchange reaction onto every metabolite (an
"any metabolite" probe), forcing at least one unit of combined flux through
Expand Down
13 changes: 4 additions & 9 deletions src/raven_toolbox/gapfilling/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@
same way for the same reason, on top of the same probe-reaction technique
:func:`~raven_toolbox.gapfilling.find_leak_metabolite` already uses.

``gapReport.m`` only runs its mass-balancing section
(``canProduceWithoutInput``/``canConsumeWithoutOutput``) when the model
carries RAVEN's legacy ``unconstrained`` metabolite marker — a v2-era way of
representing boundary metabolites that a ``cobra.Model`` has no equivalent
for (``simplify_model``'s own docstring notes the same gap for
``deleteUnconstrained``). This port always computes that section instead, by
temporarily closing every one of the model's own boundary reactions first —
strictly more informative, and well-defined for any model regardless of how
it was built.
The mass-balancing section (``can_produce_without_input`` /
``can_consume_without_output``) temporarily closes every one of the model's
own boundary reactions, as ``gapReport.m`` does with the reactions
``getExchangeRxns`` returns.
"""
from __future__ import annotations

Expand Down
10 changes: 4 additions & 6 deletions src/raven_toolbox/init/ftinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,12 +800,10 @@ def ftinit(
docstring for the MILP mechanics and its one caveat (the bonus checks a producer is
"on", not that the metabolite is verifiably net-produced through it). A name that
matches no metabolite in ``prep.ref_model`` warns rather than silently doing nothing.
Ported from RAVEN's ``ftINITInternalAlg`` on its ``develop3`` branch — the released
``main`` branch has an unrelated, unrepeated defect here (a fixed big-M that
incorrectly caps some reversible/negative reactions above magnitude 100; raven-toolbox
never had this because its own ``big_m=100`` default relies on the model being
rescaled first, not on a per-reaction cap — see the `INIT parameter calibration study
<https://github.com/CompMetEng/raven-docs/blob/main/docs/parameter-tuning/studies/init-param-calibration.md>`_).
Ported from RAVEN 3's ``ftINITInternalAlg``. The ``big_m=100`` default relies on the
model being rescaled first, not on a per-reaction cap — see the `INIT parameter
calibration study
<https://github.com/CompMetEng/raven-docs/blob/main/docs/parameter-tuning/studies/init-param-calibration.md>`_.

``mip_gap``/``time_limit`` are forwarded to each :func:`run_ftinit` solve. On
genome-scale models they are essential for tractability — see the `INIT parameter
Expand Down
5 changes: 2 additions & 3 deletions src/raven_toolbox/manipulation/simplify.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ def _can_produce_and_consume(met) -> tuple[bool, bool]:
counts exactly when it is reversible (``lower_bound < 0``). This deliberately
ignores ``upper_bound`` and does not check whether a reaction can actually carry
flux — that bounds-aware check is ``remove_no_flux_reactions``'s job (RAVEN
``deleteMinMax``, via FVA); mixing it into this cheap structural pass previously
made a reaction locked at zero flux (``lb == ub == 0``) silently drop out as a
producer/consumer here, diverging from RAVEN when this pass runs alone.
``deleteMinMax``, via FVA). A reaction locked at zero flux (``lb == ub == 0``)
therefore still counts as a producer/consumer here, as in RAVEN.
"""
produce = consume = False
for rxn in met.reactions:
Expand Down
15 changes: 5 additions & 10 deletions tests/parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ These problems have many optima of equal objective value, so a different
answer is not a wrong answer. What can be checked is *drift*: today's result
against the result that was last inspected and accepted.

Nothing currently records an ftINIT extraction baseline this way — the tier-2
extraction check that used to live here compared the classic INIT MILP
(`run_init`) against a recorded baseline, and was removed along with the rest
of that MILP. An ftINIT equivalent (baseline + recording script, on the same
pattern) is open work; see `docs/reference/todo.md`.
Nothing records an ftINIT extraction baseline this way yet. An ftINIT
drift check (baseline + recording script) is open work; see
`docs/reference/todo.md`.

### Tier 3 — statistical

Expand Down Expand Up @@ -111,8 +109,5 @@ Still only *reported*, not enforced: the genome-scale Human-GEM, yeast and
multi-organism *comparisons against MATLAB RAVEN* in raven-gecko-parity and
raven-docs (the nightly job above is a regression guard on this package, not a
cross-language check — same distinction as tier 1's baseline), and any small-model
extraction drift check. A small-model tier-2 test used to cover the latter
(`test_set_level.py`, against the classic INIT MILP) and was removed along
with the rest of tINIT; an ftINIT equivalent is open, see
`docs/reference/todo.md`. Tier 3 has a stated contract and no tests yet for
the same reason.
extraction drift check; an ftINIT drift check is open, see
`docs/reference/todo.md`. Tier 3 has a stated contract and no tests yet.
3 changes: 1 addition & 2 deletions tests/parity/test_genome_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
than guessed now.

This exercises ``run_ftinit`` directly (the single-step MILP, not the staged
``ftinit()`` pipeline) on the raw model with synthetic scores -- the same shape
of check the classic INIT MILP used to get here, before tINIT was removed.
``ftinit()`` pipeline) on the raw model with synthetic scores.
``time_limit``/``mip_gap`` are the genome-scale-staged values ``run_ftinit``'s
own docstring recommends, since nothing here goes through ``prep_init_model``'s
rescaling.
Expand Down
3 changes: 1 addition & 2 deletions tests/test_analysis_differential_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ def test_zero_variance_increase_is_positive():


def test_zero_variance_decrease_is_negative():
# The case RAVEN#742 fixes: a flux *decrease* with no variance in either
# condition must score -100, not +100.
# A flux *decrease* with no variance in either condition scores -100.
a = _const({"R1": 9.0})
b = _const({"R1": 5.0})
assert get_flux_z(a, b)["R1"] == -100.0
Expand Down
15 changes: 15 additions & 0 deletions tests/test_biomass.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ def test_scale_biomass_with_balance_keeps_total_at_one():
assert out["total"] == pytest.approx(1.0, rel=1e-6)


def test_scale_biomass_balance_to_target_total():
m, cfg = _toy_biomass_model()
scale_biomass(m, cfg, "protein", 0.4, balance_out="carbohydrate", target_total=0.9)
out = sum_biomass(m, cfg)
assert out["protein"] == pytest.approx(0.4, rel=1e-6)
assert out["total"] == pytest.approx(0.9, rel=1e-6)


def test_scale_biomass_negative_balance_uses_target_total():
m, cfg = _toy_biomass_model()
# Protein 0.8 fits a total of 1 but not of 0.5.
with pytest.raises(ValueError, match="bring the total to 0.5"):
scale_biomass(m, cfg, "protein", 0.8, balance_out="carbohydrate", target_total=0.5)


def test_scale_biomass_zero_current_raises():
m, cfg = _toy_biomass_model()
# Build a config that references a missing component → 0 current
Expand Down
3 changes: 1 addition & 2 deletions tests/test_init_ftinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,7 @@ def optimize(self):
# metabolomics (production-bonus for detected metabolites).
#
# Fixtures and expected outcomes are the same ones hand-verified end-to-end against
# RAVEN's own ftINITInternalAlg (develop3 branch, the corrected one -- see the
# reference_reactions postmortem for why not `main`) via a standalone MATLAB probe
# RAVEN 3's own ftINITInternalAlg via a standalone MATLAB probe
# before porting: same toy topology, same objective values, same flux directions.
# --------------------------------------------------------------------------- #
def _irrev_producer_model():
Expand Down
3 changes: 1 addition & 2 deletions tests/test_utils_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ def _met(mid, name, formula=None, compartment="c"):

def test_accounts_for_own_stoichiometric_coefficient():
"""A -> 2 B: B's own coefficient is 2, so its atom counts are half of
A's, not identical to A's (the bug this port fixes relative to RAVEN's
original guessComposition, see SysBioChalmers/RAVEN#743)."""
A's, not identical to A's, as in RAVEN's guessComposition."""
m = cobra.Model("t")
a = _met("a_c", "A", formula="CH4")
b = _met("b_c", "B")
Expand Down
Loading
Loading