gpugem 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. gpugem-0.1.2/LICENSE +21 -0
  2. gpugem-0.1.2/PKG-INFO +328 -0
  3. gpugem-0.1.2/README.md +292 -0
  4. gpugem-0.1.2/gpugem/__init__.py +32 -0
  5. gpugem-0.1.2/gpugem/_defaults.py +80 -0
  6. gpugem-0.1.2/gpugem/_deps.py +271 -0
  7. gpugem-0.1.2/gpugem/lifting.py +320 -0
  8. gpugem-0.1.2/gpugem/loaders.py +205 -0
  9. gpugem-0.1.2/gpugem/result.py +50 -0
  10. gpugem-0.1.2/gpugem/scaling.py +257 -0
  11. gpugem-0.1.2/gpugem/solve_cobra.py +29 -0
  12. gpugem-0.1.2/gpugem/solver.py +410 -0
  13. gpugem-0.1.2/gpugem.egg-info/PKG-INFO +328 -0
  14. gpugem-0.1.2/gpugem.egg-info/SOURCES.txt +34 -0
  15. gpugem-0.1.2/gpugem.egg-info/dependency_links.txt +1 -0
  16. gpugem-0.1.2/gpugem.egg-info/requires.txt +18 -0
  17. gpugem-0.1.2/gpugem.egg-info/top_level.txt +1 -0
  18. gpugem-0.1.2/pyproject.toml +72 -0
  19. gpugem-0.1.2/setup.cfg +4 -0
  20. gpugem-0.1.2/tests/test_block_residuals.py +171 -0
  21. gpugem-0.1.2/tests/test_cuopt_tuning.py +277 -0
  22. gpugem-0.1.2/tests/test_dependency_errors.py +481 -0
  23. gpugem-0.1.2/tests/test_dependency_inventory.py +295 -0
  24. gpugem-0.1.2/tests/test_gurobi_default_benchmark.py +236 -0
  25. gpugem-0.1.2/tests/test_lifting.py +279 -0
  26. gpugem-0.1.2/tests/test_matlab_python_lifting_comparison.py +112 -0
  27. gpugem-0.1.2/tests/test_model_lifting_validation.py +65 -0
  28. gpugem-0.1.2/tests/test_objective_panel.py +249 -0
  29. gpugem-0.1.2/tests/test_ordinal_ramp.py +32 -0
  30. gpugem-0.1.2/tests/test_residual_tradeoff.py +118 -0
  31. gpugem-0.1.2/tests/test_s85_objectives.py +123 -0
  32. gpugem-0.1.2/tests/test_scaling.py +66 -0
  33. gpugem-0.1.2/tests/test_solver.py +87 -0
  34. gpugem-0.1.2/tests/test_solver_mode_variants.py +133 -0
  35. gpugem-0.1.2/tests/test_version_lifting_comparison.py +143 -0
  36. gpugem-0.1.2/tests/test_violation_histogram.py +176 -0
gpugem-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digital Metabolic Twin Centre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gpugem-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,328 @@
1
+ Metadata-Version: 2.4
2
+ Name: gpugem
3
+ Version: 0.1.2
4
+ Summary: GPU-accelerated Flux Balance Analysis for genome-scale metabolic models
5
+ Author: Digital Metabolic Twin Centre
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
8
+ Project-URL: Repository, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
9
+ Project-URL: Issues, https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/issues
10
+ Keywords: flux balance analysis,FBA,metabolic modelling,GPU,PDLP,cuOpt,COBRA,systems biology
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: cuopt-cu12>=26.6.0
23
+ Requires-Dist: cobra>=0.29
24
+ Provides-Extra: gurobi
25
+ Requires-Dist: gurobipy>=11; extra == "gurobi"
26
+ Provides-Extra: application
27
+ Requires-Dist: pandas>=2.0; extra == "application"
28
+ Requires-Dist: gurobipy>=11; extra == "application"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7; extra == "dev"
31
+ Requires-Dist: pytest-cov; extra == "dev"
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Requires-Dist: pandas>=2.0; extra == "dev"
34
+ Requires-Dist: matplotlib; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ <div align="center">
38
+
39
+ # gpuGEM
40
+
41
+ [![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)
42
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
43
+ [![CUDA](https://img.shields.io/badge/CUDA-12%2B-green)](https://developer.nvidia.com/cuda-toolkit)
44
+ [![cuOpt](https://img.shields.io/badge/NVIDIA%20cuOpt-26.6%2B-76B900)](https://developer.nvidia.com/cuopt)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
46
+ [![Status: Alpha](https://img.shields.io/badge/status-alpha-orange)]()
47
+
48
+ GPU-accelerated Flux Balance Analysis (FBA) for genome-scale metabolic models.
49
+
50
+ </div>
51
+
52
+ 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.
53
+
54
+ ---
55
+
56
+ ## Why gpuGEM?
57
+
58
+ 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:
59
+
60
+ | Model | Reactions | Constraints | Time | Max stoich. residual |
61
+ |---|---|---|---|---|
62
+ | Harvey whole-body model | 81K | 160K | **0.76 s** | 1.8e-5 |
63
+ | Microbiome whole-body model | 789K | 1.66M | **27 s** | 8.4e-5 |
64
+
65
+ ---
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install gpugem
71
+ ```
72
+
73
+ This installs the full tool: numpy, scipy, NVIDIA cuOpt (the GPU solver) and COBRApy. gpuGEM is a
74
+ GPU solver, so solving needs an NVIDIA GPU with CUDA 12 and Python 3.11 or later. cuOpt is a large
75
+ download (about 1.4 GB, mostly `libcudf-cu12` and `libcuopt-cu12`). The `cuopt-cu12` entry on PyPI
76
+ is a stub that fetches the real wheels from NVIDIA's index. Behind a proxy that blocks
77
+ `pypi.nvidia.com` it can fail with a confusing build error; if that happens, name the index
78
+ explicitly:
79
+
80
+ ```bash
81
+ pip install gpugem --extra-index-url https://pypi.nvidia.com
82
+ ```
83
+
84
+ `import gpugem` works without a usable GPU; the GPU is checked when you call `solve`, which raises a
85
+ `DependencyError` explaining what is missing.
86
+
87
+ Optional extras:
88
+
89
+ ```bash
90
+ pip install "gpugem[gurobi]" # the Gurobi CPU baseline (needs a licence)
91
+ pip install "gpugem[application]" # the candidate-gene application, see application/
92
+ pip install "gpugem[dev]" # tests and linting
93
+ ```
94
+
95
+ ### Dependencies
96
+
97
+ | Package / tool | Role | Needed for | Install |
98
+ |---|---|---|---|
99
+ | `numpy`, `scipy` | required | every function | `pip install "numpy>=1.24"`, `pip install "scipy>=1.10"` |
100
+ | `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)) |
101
+ | `cobra` | required | `solve_cobra`, `loaders.from_cobra` | `pip install "cobra>=0.29"` |
102
+ | `gurobipy` | benchmarks, application | the Gurobi baseline (needs a licence); also the minimum-norm QP in `application/` | `pip install "gpugem[gurobi]"` |
103
+ | `pandas` | application | result tables in `application/` | `pip install "gpugem[application]"` |
104
+ | `highspy` | benchmarks | the HiGHS CPU baseline | `pip install highspy` |
105
+ | `pandas`, `matplotlib` | benchmarks | result tables and figures | `pip install pandas`, `pip install matplotlib` |
106
+ | `pytest` | development | the test suite | `pip install -e ".[dev]"` |
107
+ | `nvidia-smi` | tool | GPU name in benchmark metadata; GPU diagnosis | install the NVIDIA driver (https://www.nvidia.com/drivers); nvidia-smi ships with it |
108
+
109
+ ### Troubleshooting
110
+
111
+ A missing or unusable dependency raises `gpugem.DependencyError` (benchmark and example scripts
112
+ print the same message; benchmark scripts exit with status 3). The message always says what is wrong, why gpuGEM
113
+ needs it, and how to fix it. The original exception is kept as `__cause__`. `err.kind` is one of:
114
+
115
+ | `kind` | Meaning | What to do |
116
+ |---|---|---|
117
+ | `not_installed` | the package is absent | run the `pip install` command in the message |
118
+ | `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')` |
119
+ | `version` | installed version below the minimum | run the `pip install --upgrade` command in the message |
120
+ | `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 |
121
+ | `license` | Gurobi is installed but has no valid licence for the model | obtain a licence and set `GRB_LICENSE_FILE`; check with `grbprobe` |
122
+ | `tool_missing` | an executable is not on `PATH` | install it or add it to `PATH` |
123
+ | `network` | a model download failed | download the file manually into `benchmarks/model_cache/` |
124
+
125
+ ```python
126
+ try:
127
+ result = gpugem.solve_cobra(model)
128
+ except gpugem.DependencyError as err:
129
+ print(err.kind, err.dependency, err.remedy)
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Quick start
135
+
136
+ **From a COBRApy model:**
137
+
138
+ ```python
139
+ import cobra
140
+ import gpugem
141
+
142
+ model = cobra.io.read_sbml_model("e_coli_core.xml")
143
+ result = gpugem.solve_cobra(model)
144
+
145
+ print(result) # FBAResult(status='Optimal', objective=0.8739, time=0.42s)
146
+ print(result.objective)
147
+ print(result.fluxes) # numpy array, same order as model.reactions
148
+ ```
149
+
150
+ **From a COBRA `.mat` file (MATLAB format):**
151
+
152
+ ```python
153
+ import gpugem
154
+
155
+ arrays = gpugem.loaders.from_mat("Harvey_1_03c_reduced.mat")
156
+ result = gpugem.solve(**arrays)
157
+ ```
158
+
159
+ **From raw matrices (scipy sparse):**
160
+
161
+ ```python
162
+ import gpugem
163
+
164
+ result = gpugem.solve(
165
+ S=S, # stoichiometric matrix (sparse)
166
+ b=b, # RHS equalities
167
+ lb=lb, ub=ub, # variable bounds
168
+ c=c, # objective coefficients
169
+ maximize=True,
170
+ )
171
+ ```
172
+
173
+ **Whole-body models with coupling constraints:**
174
+
175
+ ```python
176
+ result = gpugem.solve(S=S, b=b, lb=lb, ub=ub, c=c,
177
+ C=C, d_lb=d_lb, d_ub=d_ub,
178
+ maximize=True)
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Default settings
184
+
185
+ gpuGEM automatically selects solver settings based on model size:
186
+
187
+ **Small models (≤ 100K reactions) — e.g. Harvey whole-body model:**
188
+
189
+ ```python
190
+ pdlp_precision = 1 # mixed FP32/FP64 — double precision diverges on these models
191
+ absolute_primal_tolerance = 1e-8 # tight tolerance: 0 stoich violations at 1e-6 max residual
192
+ relative_primal_tolerance = 1e-8
193
+ absolute_dual_tolerance = 1e-8
194
+ relative_dual_tolerance = 1e-8
195
+ per_constraint_residual = 1 # max-norm convergence check → better accuracy
196
+ ```
197
+
198
+ **Large models (> 100K reactions) — e.g. personalised microbiome whole-body models:**
199
+
200
+ ```python
201
+ presolve = 1 # PaPILO — required; default PSLP falsely reports Infeasible
202
+ per_constraint_residual = 1 # max-norm convergence → 44% fewer violations vs default
203
+ absolute_primal_tolerance = 1e-4 # cuOpt's internal default tolerance
204
+ relative_primal_tolerance = 1e-4
205
+ absolute_dual_tolerance = 1e-4
206
+ relative_dual_tolerance = 1e-4
207
+ ```
208
+
209
+ Any cuOpt parameter can be overridden:
210
+
211
+ ```python
212
+ # Use Stable1 PDLP mode for maximum accuracy (slower)
213
+ result = gpugem.solve(**arrays, pdlp_solver_mode=0, time_limit=300)
214
+
215
+ # Disable presolve explicitly
216
+ result = gpugem.solve(**arrays, presolve=0)
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Stateful solver (repeated solves)
222
+
223
+ ```python
224
+ solver = gpugem.FBASolver(S=S, b=b, lb=lb, ub=ub, c=c, maximize=True)
225
+
226
+ for patient_c in patient_objectives:
227
+ result = solver.solve(c=patient_c)
228
+ print(result.objective)
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Result object
234
+
235
+ ```python
236
+ result.status # 'Optimal', 'TimeLimit', 'Infeasible', ...
237
+ result.objective # float
238
+ result.fluxes # np.ndarray (n_vars,)
239
+ result.wall_time_s # float
240
+ result.solver_settings # dict of applied cuOpt parameters
241
+ result.feasibility # dict: stoich_max_residual, stoich_rows_violated_1e6, ...
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Known limitations
247
+
248
+ - **Lifted, extreme-scale (`S84`/`S85`-class) models are not reliably solvable by every Gurobi
249
+ entry point.** `benchmarks/run_matlab_python_lifting_comparison.py` found that Gurobi solves the
250
+ lifted `S84`/`S85` systems to `Optimal` via COBRA Toolbox's `solveCobraLP` (MATLAB) but reports
251
+ `Infeasible` for the *structurally identical* system (confirmed: identical auxiliary-variable
252
+ and lifted-row counts) via a direct `gurobipy` call (Python). Checked and ruled out as causes:
253
+ a Gurobi solver-settings mismatch (`Method`, `TimeLimit`, `FeasibilityTol`/`OptimalityTol` all
254
+ confirmed identical), a Gurobi *version* mismatch (`pip install gurobipy` initially resolved a
255
+ different major version, `13.0.3`, than MATLAB's licensed `11.0.3`; re-run with the versions
256
+ pinned to match — `Infeasible` persisted), and a borderline numerical-tolerance flip (persisted
257
+ after relaxing `FeasibilityTol` 100x). This reproduces, under a second independent solver
258
+ pathway, the same `S84`/`S85` scale boundary already found for lifted cuOpt solves (see "Version
259
+ x lifting runtime comparison" above) — lifting's mathematical correctness is not in question
260
+ (structural fidelity is exact), but its practical solvability at this scale is
261
+ entry-point-dependent, for a reason not yet isolated. See
262
+ `specs/015-matlab-python-lifting-comparison/`.
263
+
264
+ - **GPU/driver-absent detection is a heuristic, checked on one kind of machine.** gpuGEM reports
265
+ `DependencyError(kind="no_gpu")` only when cuOpt raises *and* there is positive evidence of a GPU
266
+ problem (`nvidia-smi` fails or lists no GPU, or it is absent and no `/dev/nvidia*` device exists).
267
+ Observed with cuOpt 26.8.0 on a machine with no NVIDIA driver and no `nvidia-smi`: `import cuopt`
268
+ succeeds and the first solve raises `RuntimeError: CUDA error ... cudaErrorInsufficientDriver`,
269
+ which gpuGEM reports as `no_gpu`. A machine with a working `nvidia-smi` but a broken CUDA setup
270
+ has not been tested, and its failure may surface as cuOpt's own exception. The Gurobi licence
271
+ error numbers (10009, 10010) used for `kind="license"` are unverified.
272
+ See `specs/016-missing-dependency-errors/`.
273
+ - **The dependency inventory is static.** `tests/test_dependency_inventory.py` finds imports and
274
+ `subprocess` / `shutil.which` calls with literal names; a module or executable whose name is built
275
+ at run time is not seen (the test prints those it cannot analyse).
276
+
277
+ ---
278
+
279
+ ## Running the tests
280
+
281
+ ```bash
282
+ pip install -e ".[dev]"
283
+ pytest -q -rs
284
+ ```
285
+
286
+ No GPU is required to run the suite. On a CPU-only machine the tests that need
287
+ cuOpt or a Gurobi licence are reported as **skipped** rather than failed, so a
288
+ clean run looks like:
289
+
290
+ ```
291
+ 190 passed, 10 skipped
292
+ ```
293
+
294
+ If you see failures rather than skips, that is a genuine problem and we would
295
+ like to hear about it. The 10 skips become real tests on a machine with an
296
+ NVIDIA GPU (CUDA 12) and `pip install gpugem`.
297
+
298
+ CI runs this suite on Python 3.11 and 3.12 on every push. Because GitHub's
299
+ hosted runners have no GPU, a green badge means "everything checkable without a
300
+ GPU is correct"; it does not assert that the GPU solving path was exercised.
301
+ That path is covered by the benchmark runs reported in the paper.
302
+
303
+ ---
304
+
305
+ ## Support and maintenance
306
+
307
+ gpuGEM is maintained by the Digital Metabolic Twin Centre at the University of
308
+ Galway. We commit to keeping the software available and to responding to issues
309
+ for at least two years following publication. Please report problems through
310
+ the [issue tracker](https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/issues).
311
+
312
+ Every released version is archived on Zenodo with a permanent DOI, so the exact
313
+ code behind a published result stays retrievable even if this repository moves.
314
+
315
+ ---
316
+
317
+ ## Citation
318
+
319
+ If you use gpuGEM in your research, please cite:
320
+
321
+ > *gpuGEM: validated GPU solving of genome-scale metabolic LPs*
322
+ > Digital Metabolic Twin Centre, 2026. https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
323
+
324
+ ---
325
+
326
+ ## License
327
+
328
+ MIT © 2026 Digital Metabolic Twin Centre
gpugem-0.1.2/README.md ADDED
@@ -0,0 +1,292 @@
1
+ <div align="center">
2
+
3
+ # gpuGEM
4
+
5
+ [![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)
6
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
7
+ [![CUDA](https://img.shields.io/badge/CUDA-12%2B-green)](https://developer.nvidia.com/cuda-toolkit)
8
+ [![cuOpt](https://img.shields.io/badge/NVIDIA%20cuOpt-26.6%2B-76B900)](https://developer.nvidia.com/cuopt)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
+ [![Status: Alpha](https://img.shields.io/badge/status-alpha-orange)]()
11
+
12
+ GPU-accelerated Flux Balance Analysis (FBA) for genome-scale metabolic models.
13
+
14
+ </div>
15
+
16
+ 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.
17
+
18
+ ---
19
+
20
+ ## Why gpuGEM?
21
+
22
+ 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:
23
+
24
+ | Model | Reactions | Constraints | Time | Max stoich. residual |
25
+ |---|---|---|---|---|
26
+ | Harvey whole-body model | 81K | 160K | **0.76 s** | 1.8e-5 |
27
+ | Microbiome whole-body model | 789K | 1.66M | **27 s** | 8.4e-5 |
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install gpugem
35
+ ```
36
+
37
+ This installs the full tool: numpy, scipy, NVIDIA cuOpt (the GPU solver) and COBRApy. gpuGEM is a
38
+ GPU solver, so solving needs an NVIDIA GPU with CUDA 12 and Python 3.11 or later. cuOpt is a large
39
+ download (about 1.4 GB, mostly `libcudf-cu12` and `libcuopt-cu12`). The `cuopt-cu12` entry on PyPI
40
+ is a stub that fetches the real wheels from NVIDIA's index. Behind a proxy that blocks
41
+ `pypi.nvidia.com` it can fail with a confusing build error; if that happens, name the index
42
+ explicitly:
43
+
44
+ ```bash
45
+ pip install gpugem --extra-index-url https://pypi.nvidia.com
46
+ ```
47
+
48
+ `import gpugem` works without a usable GPU; the GPU is checked when you call `solve`, which raises a
49
+ `DependencyError` explaining what is missing.
50
+
51
+ Optional extras:
52
+
53
+ ```bash
54
+ pip install "gpugem[gurobi]" # the Gurobi CPU baseline (needs a licence)
55
+ pip install "gpugem[application]" # the candidate-gene application, see application/
56
+ pip install "gpugem[dev]" # tests and linting
57
+ ```
58
+
59
+ ### Dependencies
60
+
61
+ | Package / tool | Role | Needed for | Install |
62
+ |---|---|---|---|
63
+ | `numpy`, `scipy` | required | every function | `pip install "numpy>=1.24"`, `pip install "scipy>=1.10"` |
64
+ | `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)) |
65
+ | `cobra` | required | `solve_cobra`, `loaders.from_cobra` | `pip install "cobra>=0.29"` |
66
+ | `gurobipy` | benchmarks, application | the Gurobi baseline (needs a licence); also the minimum-norm QP in `application/` | `pip install "gpugem[gurobi]"` |
67
+ | `pandas` | application | result tables in `application/` | `pip install "gpugem[application]"` |
68
+ | `highspy` | benchmarks | the HiGHS CPU baseline | `pip install highspy` |
69
+ | `pandas`, `matplotlib` | benchmarks | result tables and figures | `pip install pandas`, `pip install matplotlib` |
70
+ | `pytest` | development | the test suite | `pip install -e ".[dev]"` |
71
+ | `nvidia-smi` | tool | GPU name in benchmark metadata; GPU diagnosis | install the NVIDIA driver (https://www.nvidia.com/drivers); nvidia-smi ships with it |
72
+
73
+ ### Troubleshooting
74
+
75
+ A missing or unusable dependency raises `gpugem.DependencyError` (benchmark and example scripts
76
+ print the same message; benchmark scripts exit with status 3). The message always says what is wrong, why gpuGEM
77
+ needs it, and how to fix it. The original exception is kept as `__cause__`. `err.kind` is one of:
78
+
79
+ | `kind` | Meaning | What to do |
80
+ |---|---|---|
81
+ | `not_installed` | the package is absent | run the `pip install` command in the message |
82
+ | `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')` |
83
+ | `version` | installed version below the minimum | run the `pip install --upgrade` command in the message |
84
+ | `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 |
85
+ | `license` | Gurobi is installed but has no valid licence for the model | obtain a licence and set `GRB_LICENSE_FILE`; check with `grbprobe` |
86
+ | `tool_missing` | an executable is not on `PATH` | install it or add it to `PATH` |
87
+ | `network` | a model download failed | download the file manually into `benchmarks/model_cache/` |
88
+
89
+ ```python
90
+ try:
91
+ result = gpugem.solve_cobra(model)
92
+ except gpugem.DependencyError as err:
93
+ print(err.kind, err.dependency, err.remedy)
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Quick start
99
+
100
+ **From a COBRApy model:**
101
+
102
+ ```python
103
+ import cobra
104
+ import gpugem
105
+
106
+ model = cobra.io.read_sbml_model("e_coli_core.xml")
107
+ result = gpugem.solve_cobra(model)
108
+
109
+ print(result) # FBAResult(status='Optimal', objective=0.8739, time=0.42s)
110
+ print(result.objective)
111
+ print(result.fluxes) # numpy array, same order as model.reactions
112
+ ```
113
+
114
+ **From a COBRA `.mat` file (MATLAB format):**
115
+
116
+ ```python
117
+ import gpugem
118
+
119
+ arrays = gpugem.loaders.from_mat("Harvey_1_03c_reduced.mat")
120
+ result = gpugem.solve(**arrays)
121
+ ```
122
+
123
+ **From raw matrices (scipy sparse):**
124
+
125
+ ```python
126
+ import gpugem
127
+
128
+ result = gpugem.solve(
129
+ S=S, # stoichiometric matrix (sparse)
130
+ b=b, # RHS equalities
131
+ lb=lb, ub=ub, # variable bounds
132
+ c=c, # objective coefficients
133
+ maximize=True,
134
+ )
135
+ ```
136
+
137
+ **Whole-body models with coupling constraints:**
138
+
139
+ ```python
140
+ result = gpugem.solve(S=S, b=b, lb=lb, ub=ub, c=c,
141
+ C=C, d_lb=d_lb, d_ub=d_ub,
142
+ maximize=True)
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Default settings
148
+
149
+ gpuGEM automatically selects solver settings based on model size:
150
+
151
+ **Small models (≤ 100K reactions) — e.g. Harvey whole-body model:**
152
+
153
+ ```python
154
+ pdlp_precision = 1 # mixed FP32/FP64 — double precision diverges on these models
155
+ absolute_primal_tolerance = 1e-8 # tight tolerance: 0 stoich violations at 1e-6 max residual
156
+ relative_primal_tolerance = 1e-8
157
+ absolute_dual_tolerance = 1e-8
158
+ relative_dual_tolerance = 1e-8
159
+ per_constraint_residual = 1 # max-norm convergence check → better accuracy
160
+ ```
161
+
162
+ **Large models (> 100K reactions) — e.g. personalised microbiome whole-body models:**
163
+
164
+ ```python
165
+ presolve = 1 # PaPILO — required; default PSLP falsely reports Infeasible
166
+ per_constraint_residual = 1 # max-norm convergence → 44% fewer violations vs default
167
+ absolute_primal_tolerance = 1e-4 # cuOpt's internal default tolerance
168
+ relative_primal_tolerance = 1e-4
169
+ absolute_dual_tolerance = 1e-4
170
+ relative_dual_tolerance = 1e-4
171
+ ```
172
+
173
+ Any cuOpt parameter can be overridden:
174
+
175
+ ```python
176
+ # Use Stable1 PDLP mode for maximum accuracy (slower)
177
+ result = gpugem.solve(**arrays, pdlp_solver_mode=0, time_limit=300)
178
+
179
+ # Disable presolve explicitly
180
+ result = gpugem.solve(**arrays, presolve=0)
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Stateful solver (repeated solves)
186
+
187
+ ```python
188
+ solver = gpugem.FBASolver(S=S, b=b, lb=lb, ub=ub, c=c, maximize=True)
189
+
190
+ for patient_c in patient_objectives:
191
+ result = solver.solve(c=patient_c)
192
+ print(result.objective)
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Result object
198
+
199
+ ```python
200
+ result.status # 'Optimal', 'TimeLimit', 'Infeasible', ...
201
+ result.objective # float
202
+ result.fluxes # np.ndarray (n_vars,)
203
+ result.wall_time_s # float
204
+ result.solver_settings # dict of applied cuOpt parameters
205
+ result.feasibility # dict: stoich_max_residual, stoich_rows_violated_1e6, ...
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Known limitations
211
+
212
+ - **Lifted, extreme-scale (`S84`/`S85`-class) models are not reliably solvable by every Gurobi
213
+ entry point.** `benchmarks/run_matlab_python_lifting_comparison.py` found that Gurobi solves the
214
+ lifted `S84`/`S85` systems to `Optimal` via COBRA Toolbox's `solveCobraLP` (MATLAB) but reports
215
+ `Infeasible` for the *structurally identical* system (confirmed: identical auxiliary-variable
216
+ and lifted-row counts) via a direct `gurobipy` call (Python). Checked and ruled out as causes:
217
+ a Gurobi solver-settings mismatch (`Method`, `TimeLimit`, `FeasibilityTol`/`OptimalityTol` all
218
+ confirmed identical), a Gurobi *version* mismatch (`pip install gurobipy` initially resolved a
219
+ different major version, `13.0.3`, than MATLAB's licensed `11.0.3`; re-run with the versions
220
+ pinned to match — `Infeasible` persisted), and a borderline numerical-tolerance flip (persisted
221
+ after relaxing `FeasibilityTol` 100x). This reproduces, under a second independent solver
222
+ pathway, the same `S84`/`S85` scale boundary already found for lifted cuOpt solves (see "Version
223
+ x lifting runtime comparison" above) — lifting's mathematical correctness is not in question
224
+ (structural fidelity is exact), but its practical solvability at this scale is
225
+ entry-point-dependent, for a reason not yet isolated. See
226
+ `specs/015-matlab-python-lifting-comparison/`.
227
+
228
+ - **GPU/driver-absent detection is a heuristic, checked on one kind of machine.** gpuGEM reports
229
+ `DependencyError(kind="no_gpu")` only when cuOpt raises *and* there is positive evidence of a GPU
230
+ problem (`nvidia-smi` fails or lists no GPU, or it is absent and no `/dev/nvidia*` device exists).
231
+ Observed with cuOpt 26.8.0 on a machine with no NVIDIA driver and no `nvidia-smi`: `import cuopt`
232
+ succeeds and the first solve raises `RuntimeError: CUDA error ... cudaErrorInsufficientDriver`,
233
+ which gpuGEM reports as `no_gpu`. A machine with a working `nvidia-smi` but a broken CUDA setup
234
+ has not been tested, and its failure may surface as cuOpt's own exception. The Gurobi licence
235
+ error numbers (10009, 10010) used for `kind="license"` are unverified.
236
+ See `specs/016-missing-dependency-errors/`.
237
+ - **The dependency inventory is static.** `tests/test_dependency_inventory.py` finds imports and
238
+ `subprocess` / `shutil.which` calls with literal names; a module or executable whose name is built
239
+ at run time is not seen (the test prints those it cannot analyse).
240
+
241
+ ---
242
+
243
+ ## Running the tests
244
+
245
+ ```bash
246
+ pip install -e ".[dev]"
247
+ pytest -q -rs
248
+ ```
249
+
250
+ No GPU is required to run the suite. On a CPU-only machine the tests that need
251
+ cuOpt or a Gurobi licence are reported as **skipped** rather than failed, so a
252
+ clean run looks like:
253
+
254
+ ```
255
+ 190 passed, 10 skipped
256
+ ```
257
+
258
+ If you see failures rather than skips, that is a genuine problem and we would
259
+ like to hear about it. The 10 skips become real tests on a machine with an
260
+ NVIDIA GPU (CUDA 12) and `pip install gpugem`.
261
+
262
+ CI runs this suite on Python 3.11 and 3.12 on every push. Because GitHub's
263
+ hosted runners have no GPU, a green badge means "everything checkable without a
264
+ GPU is correct"; it does not assert that the GPU solving path was exercised.
265
+ That path is covered by the benchmark runs reported in the paper.
266
+
267
+ ---
268
+
269
+ ## Support and maintenance
270
+
271
+ gpuGEM is maintained by the Digital Metabolic Twin Centre at the University of
272
+ Galway. We commit to keeping the software available and to responding to issues
273
+ for at least two years following publication. Please report problems through
274
+ the [issue tracker](https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM/issues).
275
+
276
+ Every released version is archived on Zenodo with a permanent DOI, so the exact
277
+ code behind a published result stays retrievable even if this repository moves.
278
+
279
+ ---
280
+
281
+ ## Citation
282
+
283
+ If you use gpuGEM in your research, please cite:
284
+
285
+ > *gpuGEM: validated GPU solving of genome-scale metabolic LPs*
286
+ > Digital Metabolic Twin Centre, 2026. https://github.com/Digital-Metabolic-Twin-Centre/gpuGEM
287
+
288
+ ---
289
+
290
+ ## License
291
+
292
+ MIT © 2026 Digital Metabolic Twin Centre
@@ -0,0 +1,32 @@
1
+ """gpuGEM — GPU-accelerated Flux Balance Analysis for genome-scale metabolic models."""
2
+
3
+ from gpugem._deps import DependencyError, require as _require
4
+
5
+ # numpy/scipy are hard dependencies; fail here with guidance, not with a bare ImportError below.
6
+ # cuOpt is deliberately NOT checked at import: it is required only when a solve is requested.
7
+ _require("numpy")
8
+ _require("scipy")
9
+
10
+ from gpugem.solver import FBASolver, solve # noqa: E402
11
+ from gpugem.solve_cobra import solve_cobra # noqa: E402
12
+ from gpugem.result import FBAResult # noqa: E402
13
+ from gpugem.scaling import CoefficientScalingMapping, remap_fluxes, scale_model # noqa: E402
14
+ from gpugem.lifting import LiftingMapping, lift_coupling, lift_mass_balance, map_back # noqa: E402
15
+ from gpugem import loaders # noqa: E402
16
+
17
+ __version__ = "0.1.2"
18
+ __all__ = [
19
+ "DependencyError",
20
+ "FBASolver",
21
+ "solve",
22
+ "solve_cobra",
23
+ "FBAResult",
24
+ "CoefficientScalingMapping",
25
+ "scale_model",
26
+ "remap_fluxes",
27
+ "LiftingMapping",
28
+ "lift_mass_balance",
29
+ "lift_coupling",
30
+ "map_back",
31
+ "loaders",
32
+ ]