Metadata-Version: 2.4
Name: gpugem
Version: 0.1.3
Summary: GPU-accelerated Flux Balance Analysis for genome-scale metabolic models
Author: Digital Metabolic Twin Centre
License-Expression: MIT
Project-URL: Homepage, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
Project-URL: Repository, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
Project-URL: Issues, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/issues
Keywords: flux balance analysis,FBA,metabolic modelling,GPU,PDLP,cuOpt,COBRA,systems biology
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Requires-Dist: cuopt-cu12>=26.6.0
Requires-Dist: cobra>=0.29
Provides-Extra: gurobi
Requires-Dist: gurobipy>=11; extra == "gurobi"
Provides-Extra: application
Requires-Dist: pandas>=2.0; extra == "application"
Requires-Dist: gurobipy>=11; extra == "application"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: pandas>=2.0; extra == "dev"
Requires-Dist: matplotlib; extra == "dev"
Dynamic: license-file

<div align="center">

# gpuGEM

[![CI](https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/actions/workflows/ci.yml/badge.svg)](https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
[![CUDA](https://img.shields.io/badge/CUDA-12%2B-green)](https://developer.nvidia.com/cuda-toolkit)
[![cuOpt](https://img.shields.io/badge/NVIDIA%20cuOpt-26.6%2B-76B900)](https://developer.nvidia.com/cuopt)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Status: Alpha](https://img.shields.io/badge/status-alpha-orange)]()

GPU-accelerated Flux Balance Analysis (FBA) for genome-scale metabolic models.

</div>

gpuGEM wraps [NVIDIA cuOpt](https://developer.nvidia.com/cuopt) PDLP solver with validated default settings derived from systematic benchmarking on whole-body metabolic models, including the [Harvey](https://www.vmh.life/) model and personalised microbiome whole-body models. It provides a minimal, COBRA-compatible API so existing modelling workflows can switch to GPU solving with one line of code.

---

## Why gpuGEM?

GPU first-order LP solvers (PDLP) offer substantial speedups on genome-scale FBA, but getting correct results requires non-obvious configuration. Out-of-the-box cuOpt settings produce large constraint violations or silently return wrong solutions on whole-body models. gpuGEM ships with settings that have been validated for correctness:

| Model | Reactions | Constraints | Time | Max stoich. residual |
|---|---|---|---|---|
| Harvey whole-body model | 81K | 160K | **0.76 s** | 1.8e-5 |
| Microbiome whole-body model | 789K | 1.66M | **27 s** | 8.4e-5 |

---

## Installation

```bash
pip install gpugem
```

This installs the full tool: numpy, scipy, NVIDIA cuOpt (the GPU solver) and COBRApy. gpuGEM is a
GPU solver, so solving needs an NVIDIA GPU with CUDA 12 and Python 3.11 or later. cuOpt is a large
download (about 1.4 GB, mostly `libcudf-cu12` and `libcuopt-cu12`). The `cuopt-cu12` entry on PyPI
is a stub that fetches the real wheels from NVIDIA's index. Behind a proxy that blocks
`pypi.nvidia.com` it can fail with a confusing build error; if that happens, name the index
explicitly:

```bash
pip install gpugem --extra-index-url https://pypi.nvidia.com
```

`import gpugem` works without a usable GPU; the GPU is checked when you call `solve`, which raises a
`DependencyError` explaining what is missing.

Optional extras:

```bash
pip install "gpugem[gurobi]"       # the Gurobi CPU baseline (needs a licence)
pip install "gpugem[application]"  # the candidate-gene application, see application/
pip install "gpugem[dev]"          # tests and linting
```

### Dependencies

| Package / tool | Role | Needed for | Install |
|---|---|---|---|
| `numpy`, `scipy` | required | every function | `pip install "numpy>=1.24"`, `pip install "scipy>=1.10"` |
| `cuopt` | required | the GPU solver; checked when you solve, not at `import gpugem` | `pip install "cuopt-cu12>=26.6.0" --extra-index-url https://pypi.nvidia.com` (other CUDA versions: see the [cuOpt install guide](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html)) |
| `cobra` | required | `solve_cobra`, `loaders.from_cobra` | `pip install "cobra>=0.29"` |
| `gurobipy` | benchmarks, application | the Gurobi baseline (needs a licence); also the minimum-norm QP in `application/` | `pip install "gpugem[gurobi]"` |
| `pandas` | application | result tables in `application/` | `pip install "gpugem[application]"` |
| `highspy` | benchmarks | the HiGHS CPU baseline | `pip install highspy` |
| `pandas`, `matplotlib` | benchmarks | result tables and figures | `pip install pandas`, `pip install matplotlib` |
| `pytest` | development | the test suite | `pip install -e ".[dev]"` |
| `nvidia-smi` | tool | GPU name in benchmark metadata; GPU diagnosis | install the NVIDIA driver (https://www.nvidia.com/drivers); nvidia-smi ships with it |

### Troubleshooting

A missing or unusable dependency raises `gpugem.DependencyError` (benchmark and example scripts
print the same message; benchmark scripts exit with status 3). The message always says what is wrong, why gpuGEM
needs it, and how to fix it. The original exception is kept as `__cause__`. `err.kind` is one of:

| `kind` | Meaning | What to do |
|---|---|---|
| `not_installed` | the package is absent | run the `pip install` command in the message |
| `broken` | installed but fails to import, or a model file cannot be read | reinstall; check that Python, the CUDA runtime and the driver match the build. For `.mat` files, re-save with `save(file, 'model', '-v7')` |
| `version` | installed version below the minimum | run the `pip install --upgrade` command in the message |
| `no_gpu` | no usable NVIDIA GPU or driver is detected | run `nvidia-smi`; install or update the driver; in a container, start it with GPU access |
| `license` | Gurobi is installed but has no valid licence for the model | obtain a licence and set `GRB_LICENSE_FILE`; check with `grbprobe` |
| `tool_missing` | an executable is not on `PATH` | install it or add it to `PATH` |
| `network` | a model download failed | download the file manually into `benchmarks/model_cache/` |

```python
try:
    result = gpugem.solve_cobra(model)
except gpugem.DependencyError as err:
    print(err.kind, err.dependency, err.remedy)
```

---

## Quick start

**From a COBRApy model:**

```python
import cobra
import gpugem

model = cobra.io.read_sbml_model("e_coli_core.xml")
result = gpugem.solve_cobra(model)

print(result)                       # FBAResult(status='Optimal', objective=0.8739, time=0.42s)
print(result.objective)
print(result.fluxes)                # numpy array, same order as model.reactions
```

**From a COBRA `.mat` file (MATLAB format):**

```python
import gpugem

arrays = gpugem.loaders.from_mat("Harvey_1_03c_reduced.mat")
result = gpugem.solve(**arrays)
```

**From raw matrices (scipy sparse):**

```python
import gpugem

result = gpugem.solve(
    S=S,           # stoichiometric matrix (sparse)
    b=b,           # RHS equalities
    lb=lb, ub=ub,  # variable bounds
    c=c,           # objective coefficients
    maximize=True,
)
```

**Whole-body models with coupling constraints:**

```python
result = gpugem.solve(S=S, b=b, lb=lb, ub=ub, c=c,
                      C=C, d_lb=d_lb, d_ub=d_ub,
                      maximize=True)
```

---

## Default settings

gpuGEM automatically selects solver settings based on model size:

**Small models (≤ 100K reactions) — e.g. Harvey whole-body model:**

```python
pdlp_precision             = 1      # mixed FP32/FP64 — double precision diverges on these models
absolute_primal_tolerance  = 1e-8   # tight tolerance: 0 stoich violations at 1e-6 max residual
relative_primal_tolerance  = 1e-8
absolute_dual_tolerance    = 1e-8
relative_dual_tolerance    = 1e-8
per_constraint_residual    = 1      # max-norm convergence check → better accuracy
```

**Large models (> 100K reactions) — e.g. personalised microbiome whole-body models:**

```python
presolve                   = 1      # PaPILO — required; default PSLP falsely reports Infeasible
per_constraint_residual    = 1      # max-norm convergence → 44% fewer violations vs default
absolute_primal_tolerance  = 1e-4   # cuOpt's internal default tolerance
relative_primal_tolerance  = 1e-4
absolute_dual_tolerance    = 1e-4
relative_dual_tolerance    = 1e-4
```

Any cuOpt parameter can be overridden:

```python
# Use Stable1 PDLP mode for maximum accuracy (slower)
result = gpugem.solve(**arrays, pdlp_solver_mode=0, time_limit=300)

# Disable presolve explicitly
result = gpugem.solve(**arrays, presolve=0)
```

### Time limit for large models

`time_limit` is the wall-clock budget in seconds and defaults to **60**. That is enough for
genome-scale models such as iML1515 (about 4 s) and the Harvey whole-body model (a few
seconds), but not for the million-variable personalised microbiome models, which took
roughly 25 to 880 s on an NVIDIA RTX A4500. For those, pass a larger limit, for example
`time_limit=900`. If the limit is reached, `result.status` is `"TimeLimit"` and the solution
has not converged (its residual can be far above the 1e-4 criterion), so check
`result.status` and `result.feasibility["stoich_max_residual"]` before using the fluxes.

---

## Stateful solver (repeated solves)

```python
solver = gpugem.FBASolver(S=S, b=b, lb=lb, ub=ub, c=c, maximize=True)

for patient_c in patient_objectives:
    result = solver.solve(c=patient_c)
    print(result.objective)
```

---

## Result object

```python
result.status       # 'Optimal', 'TimeLimit', 'Infeasible', ...
result.objective    # float
result.fluxes       # np.ndarray (n_vars,)
result.wall_time_s  # float
result.solver_settings  # dict of applied cuOpt parameters
result.feasibility  # dict: stoich_max_residual, stoich_rows_violated_1e6, ...
```

---

## Known limitations

- **Lifted, extreme-scale (`S84`/`S85`-class) models are not reliably solvable by every Gurobi
  entry point.** `benchmarks/run_matlab_python_lifting_comparison.py` found that Gurobi solves the
  lifted `S84`/`S85` systems to `Optimal` via COBRA Toolbox's `solveCobraLP` (MATLAB) but reports
  `Infeasible` for the *structurally identical* system (confirmed: identical auxiliary-variable
  and lifted-row counts) via a direct `gurobipy` call (Python). Checked and ruled out as causes:
  a Gurobi solver-settings mismatch (`Method`, `TimeLimit`, `FeasibilityTol`/`OptimalityTol` all
  confirmed identical), a Gurobi *version* mismatch (`pip install gurobipy` initially resolved a
  different major version, `13.0.3`, than MATLAB's licensed `11.0.3`; re-run with the versions
  pinned to match — `Infeasible` persisted), and a borderline numerical-tolerance flip (persisted
  after relaxing `FeasibilityTol` 100x). This reproduces, under a second independent solver
  pathway, the same `S84`/`S85` scale boundary already found for lifted cuOpt solves (see "Version
  x lifting runtime comparison" above) — lifting's mathematical correctness is not in question
  (structural fidelity is exact), but its practical solvability at this scale is
  entry-point-dependent, for a reason not yet isolated. See
  `specs/015-matlab-python-lifting-comparison/`.

- **GPU/driver-absent detection is a heuristic, checked on one kind of machine.** gpuGEM reports
  `DependencyError(kind="no_gpu")` only when cuOpt raises *and* there is positive evidence of a GPU
  problem (`nvidia-smi` fails or lists no GPU, or it is absent and no `/dev/nvidia*` device exists).
  Observed with cuOpt 26.8.0 on a machine with no NVIDIA driver and no `nvidia-smi`: `import cuopt`
  succeeds and the first solve raises `RuntimeError: CUDA error ... cudaErrorInsufficientDriver`,
  which gpuGEM reports as `no_gpu`. A machine with a working `nvidia-smi` but a broken CUDA setup
  has not been tested, and its failure may surface as cuOpt's own exception. The Gurobi licence
  error numbers (10009, 10010) used for `kind="license"` are unverified.
  See `specs/016-missing-dependency-errors/`.
- **The dependency inventory is static.** `tests/test_dependency_inventory.py` finds imports and
  `subprocess` / `shutil.which` calls with literal names; a module or executable whose name is built
  at run time is not seen (the test prints those it cannot analyse).

---

## Running the tests

```bash
pip install -e ".[dev]"
pytest -q -rs
```

No GPU is required to run the suite. On a CPU-only machine the tests that need
cuOpt or a Gurobi licence are reported as **skipped** rather than failed, so a
clean run looks like:

```
206 passed, 10 skipped
```

If you see failures rather than skips, that is a genuine problem and we would
like to hear about it. The 10 skips become real tests on a machine with an
NVIDIA GPU (CUDA 12) and `pip install gpugem`.

CI runs this suite on Python 3.11 and 3.12 on every push. Because GitHub's
hosted runners have no GPU, a green badge means "everything checkable without a
GPU is correct"; it does not assert that the GPU solving path was exercised.
That path is covered by the benchmark runs reported in the paper.

---

## Support and maintenance

gpuGEM is maintained by the Digital Metabolic Twin Centre at the University of
Galway. We commit to keeping the software available and to responding to issues
for at least two years following publication. Please report problems through
the [issue tracker](https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/issues).

Every released version is archived on Zenodo with a permanent DOI, so the exact
code behind a published result stays retrievable even if this repository moves.

---

## Citation

If you use gpuGEM in your research, please cite:

> *gpuGEM: validated GPU solving of genome-scale metabolic LPs*  
> Digital Metabolic Twin Centre, 2026. https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM

---

## License

MIT © 2026 Digital Metabolic Twin Centre
