Metadata-Version: 2.4
Name: equilibrator-pathway-core
Version: 0.1.1
Summary: MDF and enzyme cost minimization for fully specified pathway models, with no compound database
Author-email: Elad Noor <elad.noor@weizmann.ac.il>
License-Expression: MIT
Project-URL: repository, https://gitlab.com/equilibrator/equilibrator-pathway-core/
Keywords: eQuilibrator,pathway analysis,max-min driving force,enzyme cost minimization
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Chemistry
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.26
Requires-Dist: scipy>=1.13
Requires-Dist: cvxpy>=1.6
Requires-Dist: sbtabpy>=1.1.1
Dynamic: license-file

# equilibrator-pathway-core

Max-min driving force (MDF) and enzyme cost minimization (ECM) for **fully
specified** pathway models, with no compound database.

This is the lower layer of
[equilibrator-pathway](https://gitlab.com/equilibrator/equilibrator-pathway).
It analyses model SBtab files that already carry everything the optimisation
needs -- stoichiometry, concentration bounds, fluxes and standard Gibbs energies
-- and it never predicts or looks anything up. That is what lets it install in
a few megabytes of Python on top of numpy, including in the browser via
[Pyodide](https://pyodide.org).

| | this package | equilibrator-pathway |
| --- | --- | --- |
| MDF, ECM on a model SBtab | yes | yes (re-exported) |
| network SBtabs, formula search, identifier resolution | -- | yes |
| dG'0 prediction (Component Contribution) | -- | yes |
| dG'0 uncertainty, MDMC | -- | yes |
| pint `Q_` public API | -- | yes |
| compound database, sqlalchemy, pint | never | yes |

## Usage

```sh
pip install equilibrator-pathway-core
```

A model SBtab names its analysis in the `algorithm` option of its
`Configuration` table. `load_model` reads the file, applies any Configuration
edits, and returns the document, the model, the algorithm and a list of the
edits it made:

```python
import numpy as np

from equilibrator_pathway_core import mdf_analysis
from equilibrator_pathway_core.ecm_model import EnzymeCostModel
from equilibrator_pathway_core.model import load_model
from equilibrator_pathway_core.solution import EcmSolutionReport, MdfSolutionReport

with open("model.tsv") as f:
    document, model, algorithm, _ = load_model(f.read(), "model.tsv")

if algorithm == "MDF":
    result = mdf_analysis(
        model.S,
        model.standard_dg_primes,  # kJ/mol
        np.diag(model.I_dir),  # the flux directions
        model.ln_conc_mu,
        model.ln_conc_sigma,
        model.ln_conc_confidence,
    )
    report = MdfSolutionReport(
        model,
        result.score,
        result.ln_conc,
        result.reaction_prices,
        result.compound_prices,
    )
    print(f"MDF = {result.score:.2f} kJ/mol")
else:
    ecm = EnzymeCostModel.from_sbtab(document, model)
    score, ln_conc = ecm.optimize_ecm()
    report = EcmSolutionReport(model, ecm.ecf, score, ln_conc)
    print(f"enzyme cost = {score:.4g}")

with open("report.tsv", "w") as f:
    f.write(report.to_sbtab().to_str())
```

The report is the SBtab eQuilibrator's pathway page offers for download, byte
for byte. `mdf_analysis` takes plain arrays, so it also runs on a model built
some other way; `load_model(text, filename, overrides={"version": "2"})` edits
the Configuration before anything is read (see `configuration.OPTION_SCHEMA`
for the editable options).

Errors in the model file raise `ModelError` (a `ValueError`); a missing table
or column raises sbtabpy's `SBtabError`, and an unsupported unit
`units.UnitError` (also a `ValueError`). A solver failure raises
`SolverFailure`; an infeasible model (no concentrations within the bounds let
every reaction run in the direction of its flux) raises a plain
`RuntimeError`.

For the network-only SBtabs of eQuilibrator's "Build pathway model" step, or to
predict dG'0 with Component Contribution, use
[equilibrator-pathway](https://gitlab.com/equilibrator/equilibrator-pathway).

## Units

Everything here works on plain floats in one canonical set of units -- M,
kJ/mol, K, 1/s, Da -- documented in `equilibrator_pathway_core.constants`.
Units are attached at the boundary, by equilibrator-pathway's pint adapter or by
the browser client's string parser.

## Staying small

Importing this package must not pull in the equilibrator database stack. That is
tested directly, in a fresh interpreter, by `tests/test_isolation.py`, which
fails if `equilibrator_api`, `equilibrator_cache`, `component_contribution`,
`sqlalchemy`, `pint` or `matplotlib` appear in `sys.modules`. A dependency added
to `pyproject.toml` without the code that needs it is equally unwelcome.

## How this reader differs from eQuilibrator's

eQuilibrator's server reads model SBtabs with equilibrator-api's
`StoichiometricModel` (via equilibrator-pathway), which resolves every compound
against the compound database. This package reads the same files on its own.
The two agree on the example models to the last bit -- the golden files in the
ECM webapp repository hold them to it -- but they are separate code, and on
other files they can differ. The known differences, checked on 2026-09-24
against equilibrator-api 0.8 by feeding both readers the same broken models:

**Standard Gibbs energies.** Both take dG'0 from the `Thermodynamics` table
(`reaction gibbs energy` or `equilibrium constant` rows). Without that table the
server *predicts* dG'0 with Component Contribution; this package cannot, so it
falls back on `equilibrium constant` rows in the `Parameter` table, and refuses
the model if there are none. The server never reads energies from the
`Parameter` table.

**Water.** Water is held at activity 1 and left out of the physiological dG'
correction, so misidentifying it shifts every dG' involving it. Both readers
honour a Compound table `IsWater` column first (a column that marks nothing
means "no water"). Without it, the server asks the compound database, which
knows water under 61 accessions; this package matches a short list of
accessions (`model.WATER_ACCESSIONS`) and then the names in `model.WATER_NAMES`.
Tests on both sides pin this.

**The physiological dG' correction** (every reactant at 1 mM). The server
computes it from each compound's *phase*, as the database records it: liquid
water and solid sulfur contribute nothing, and a compound in the gas phase
would count at 1 mbar rather than 1 mM (O2, CO2 and the other gases default to
the aqueous phase, so this needs a model that asks for the gas). This package
counts every compound at 1 mM except water and compounds with a
`RedoxPotential`. So a model with elemental sulfur gets a different correction;
how the server treats `RedoxPotential` compounds here was not compared.

**Units.** The server parses units with pint, which accepts anything pint
understands. This package accepts a fixed list per dimension (see `units.py`),
matched case-insensitively -- so `mM` and `MM` both mean millimolar -- and
raises `UnitError` for anything else, e.g. `mol/m^3`.

**Missing concentration bounds.** A compound with no row in the
`ConcentrationConstraint` table gets the server's default bounds (1 µM to
10 mM) silently; this package refuses the model and names the compound.

**A `Thermodynamics` table without a `Compound` column** cannot be read by the
server (`KeyError: 'Compound'`), although reaction energies never use that
column. This package reads it.

**Error types.** For the same mistakes the server raises a mix --
`ValueError` for a missing table, `AssertionError` for a missing Flux table,
unknown compounds or reactions, inverted bounds or a standard concentration
other than 1 M, and pyparsing's `ParseException` for a reaction formula with no
arrow. This package raises `ModelError` for all of these, and `SBtabError` for
a missing table, with messages that mostly match the server's word for word.

## Status

In place:

| module | what | needs sbtabpy |
| --- | --- | --- |
| `constants.py` | R, T, standard concentrations | -- |
| `ecm.py` | the enzyme cost function and ECM | -- |
| `mdf.py` | the MDF linear program | -- |
| `errors.py` | `ModelError`, `SolverFailure`, `ConfigurationError` | -- |
| `units.py` | unit strings to canonical floats | -- |
| `model.py` | model SBtab to arrays: S, bounds, fluxes, dG'0, water | yes |
| `ecm_model.py` | an ECM model's Parameter table | yes |
| `solution.py` | result tables and the SBtab report | yes |
| `configuration.py` | the editable Configuration options, their schema, and applying edits | yes |

Both equilibrator-pathway and the browser client run MDF and ECM through this
package; the browser also reads models (`model.load_model`), edits their
Configuration (`configuration.py`) and writes reports with it.

The SBtab modules use sbtabpy directly, and need sbtabpy **1.1.1 or later**.
1.1.0 was the first release with no required dependencies (earlier ones
declared pandas, pyarrow, python-libsbml and openpyxl, which its parser never
imports); 1.1.1 added the dictionary rows, attribute defaults and pandas-free
`SBtabTable.from_rows` that replaced the core's own adapter module.
`tests/test_isolation.py` still forbids all four: nothing installs them by
default now, but the test is what guarantees the core never imports them, and
so stays loadable in the browser.

The extraction plan is in
`enzyme-cost-minimization-webapp/CORE_EXTRACTION_PLAN.txt`. Correctness is held
to the golden results frozen in that repository (`tests/golden/`), which record
what equilibrator-pathway and the browser port each produced while they were
still independent implementations. After the move, both consumers reproduce
every one of those 482 numbers **bit for bit**, not merely within the checker's
1e-6 tolerance.

Where the two replaced copies disagreed, see the docstring of `ecm.py` for
which behaviour was kept and why. In short: the port's `get_volumes` (the
server's zeroed the last metabolite when a model had no water), and working
versions of `get_fluxes` and `is_feasible`, which raised on every call in
equilibrator-pathway without any test noticing.

```sh
PYTHONPATH=src pytest tests/
```

## Changelog

### 0.1.1 (2026-09-25)

- A Compound table can declare its water compound in an `IsWater` column,
  which then decides on its own; a column that marks nothing means the model
  has no water. Before, water was recognised only by accession or name.
- `sabiork.compound:34` is no longer taken for water -- it is ATP; water is
  `sabiork.compound:40`. A model identifying ATP by that accession had ATP
  pinned to 1 M.
- The Configuration schema's notes no longer assume the browser (the
  `solver` and `dg_confidence` options).
- The version is taken from the git tag at build time, as in the other
  equilibrator packages; `__version__` reads it from the installed metadata.

### 0.1.0 (2026-09-24)

First release: the database-free layer of equilibrator-pathway, shared with the
in-browser ECM app.

- MDF (`mdf.py`) and ECM (`ecm.py`) on plain arrays in canonical units.
- The model SBtab reader (`model.py`), the ECM Parameter table
  (`ecm_model.py`), the result tables and SBtab report (`solution.py`), and the
  editable Configuration options (`configuration.py`).
- Requires numpy, scipy, cvxpy and sbtabpy >= 1.1.1; importing it loads no
  pandas, pint or compound database.
