mptool 1.0.2__tar.gz → 1.2.0__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.
- {mptool-1.0.2 → mptool-1.2.0}/CHANGELOG.md +294 -0
- {mptool-1.0.2 → mptool-1.2.0}/CITATION.cff +2 -2
- {mptool-1.0.2 → mptool-1.2.0}/LICENSE +1 -1
- {mptool-1.0.2 → mptool-1.2.0}/PKG-INFO +99 -37
- {mptool-1.0.2 → mptool-1.2.0}/README.md +97 -35
- {mptool-1.0.2 → mptool-1.2.0}/mptool/__init__.py +5 -3
- {mptool-1.0.2 → mptool-1.2.0}/mptool/__main__.py +91 -37
- {mptool-1.0.2 → mptool-1.2.0}/mptool/consistency.py +58 -30
- {mptool-1.0.2 → mptool-1.2.0}/mptool/fva.py +21 -8
- {mptool-1.0.2 → mptool-1.2.0}/mptool/io.py +80 -33
- {mptool-1.0.2 → mptool-1.2.0}/mptool/mapping.py +23 -10
- {mptool-1.0.2 → mptool-1.2.0}/mptool/model.py +186 -19
- {mptool-1.0.2 → mptool-1.2.0}/mptool/mps.py +660 -196
- {mptool-1.0.2 → mptool-1.2.0}/mptool/preprocess.py +31 -15
- {mptool-1.0.2 → mptool-1.2.0}/mptool/reproduce.py +122 -24
- {mptool-1.0.2 → mptool-1.2.0}/mptool/validate.py +24 -17
- {mptool-1.0.2 → mptool-1.2.0}/mptool.egg-info/PKG-INFO +99 -37
- {mptool-1.0.2 → mptool-1.2.0}/mptool.egg-info/SOURCES.txt +1 -13
- {mptool-1.0.2 → mptool-1.2.0}/mptool.egg-info/requires.txt +1 -1
- {mptool-1.0.2 → mptool-1.2.0}/pyproject.toml +0 -1
- {mptool-1.0.2 → mptool-1.2.0}/setup.cfg +2 -9
- mptool-1.0.2/mptool.egg-info/zip-safe +0 -1
- {mptool-1.0.2 → mptool-1.2.0}/MANIFEST.in +0 -0
- {mptool-1.0.2 → mptool-1.2.0}/mptool.egg-info/dependency_links.txt +0 -0
- {mptool-1.0.2 → mptool-1.2.0}/mptool.egg-info/top_level.txt +0 -0
- {mptool-1.0.2 → mptool-1.2.0}/setup.py +0 -0
|
@@ -3,6 +3,300 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [1.2.0] — 2026-11-03
|
|
9
|
+
|
|
10
|
+
A minor release: `sample_mps` reports how many draws returned each MP and
|
|
11
|
+
takes a draw budget, sampling with single-reaction deletion is faster, and
|
|
12
|
+
preprocessing scales to large models. Two fixes can change results.
|
|
13
|
+
`fastercc` no longer reports some functional reactions as blocked, so
|
|
14
|
+
preprocessing keeps reactions that 1.1.0 removed, and runs on iML1515,
|
|
15
|
+
iCHOv1 or Recon3D under the affected bounds can find MPs that 1.1.0 could
|
|
16
|
+
not. With a `requirements` callback that returns zero-flux IDs, a reaction
|
|
17
|
+
the main LP needs is no longer deleted.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `sample_mps(return_counts=True)` returns `(mps, acceptance, counts)`,
|
|
22
|
+
where `counts[i]` is the number of completed draws that returned
|
|
23
|
+
`mps[i]`, so `sum(counts)` is the denominator of `acceptance`. With
|
|
24
|
+
`return_mapping=True` as well, the mapping stays last. The default return
|
|
25
|
+
is unchanged. Export bundles from `sample_mps` also write `counts.csv`,
|
|
26
|
+
one `count,reaction_id,...` line per MP in `mps.txt`'s order.
|
|
27
|
+
- `sample_mps(max_draws=N)` and `--max-draws N` stop a run after N completed
|
|
28
|
+
draws, duplicates included. A run stops at whichever of `n_samples`,
|
|
29
|
+
`max_draws` and `max_t` comes first, and `n_samples=0` is allowed with
|
|
30
|
+
`max_draws` alone. A seeded run stopped by `max_draws` is reproducible,
|
|
31
|
+
unlike one stopped by `max_t`. This gives runs where `n_samples` exceeds
|
|
32
|
+
the drawable MPs a stop other than `max_t` (#7). The docstring states
|
|
33
|
+
how many draws find every MP or reaction above a given draw frequency.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `sample_mps` with single-reaction deletion (`chunks=None`) is faster:
|
|
38
|
+
each deletion LP now weights the candidates' fluxes by their rank in the
|
|
39
|
+
draw's ordering instead of summing them, so more reactions are deleted
|
|
40
|
+
without a solve. On the 12 benchmark model and subset pairs a draw took
|
|
41
|
+
0.45–0.93 of its former time. A draw returns the same MP as before, so
|
|
42
|
+
seeded runs sample the same MPs.
|
|
43
|
+
- `make_forward_irreversible` on a Gurobi model, and so `preprocess_model`,
|
|
44
|
+
`enumerate_mps` and `sample_mps`, takes time linear in the model's size
|
|
45
|
+
instead of quadratic: it reads each reversed reaction's own column instead
|
|
46
|
+
of scanning every constraint for it. On a 77 643-reaction model it took
|
|
47
|
+
under 2 s instead of 1 011 s. The model it builds is unchanged.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- `fastercc` could report functional reactions as blocked, and so
|
|
52
|
+
`preprocess_model`, `enumerate_mps`, `sample_mps` and `fva` treated them
|
|
53
|
+
as blocked: preprocessing removed them, and FVA gave them bounds (0, 0).
|
|
54
|
+
Its second stage maximized and minimized the plain sum of the remaining
|
|
55
|
+
candidates' fluxes, which is 0 in every solution for reactions whose
|
|
56
|
+
fluxes cancel, such as an exchange and the transport reaction that is its
|
|
57
|
+
only partner (v1 = −v2). It also stopped trying a direction once that
|
|
58
|
+
direction had found nothing, even after the other direction shrank the
|
|
59
|
+
candidate set. It now uses random positive weights from a fixed seed, and
|
|
60
|
+
stops only when both directions find nothing for the same candidates.
|
|
61
|
+
`fastercc` now matches a max/min LP test of every reaction on all six
|
|
62
|
+
benchmark models. 1.1.0's `preprocess_model` removed 2 functional
|
|
63
|
+
reactions from iML1515 under the benchmark bounds, 4–6 from iCHOv1 and
|
|
64
|
+
10–12 from Recon3D, so MPs through them could not be found. `fastercc`
|
|
65
|
+
takes 0.7–1.2 of its former time.
|
|
66
|
+
- With a `requirements` callback, `enumerate_mps` and `sample_mps` deleted a
|
|
67
|
+
reaction without a solve whenever the callback listed it as zero-flux. The
|
|
68
|
+
callback gets an unsolved copy and may check other conditions than the
|
|
69
|
+
main LP, such as growth on another medium, so a reaction the main LP needs
|
|
70
|
+
could be deleted and the returned set was then not a pathway. A reaction is
|
|
71
|
+
now skipped only if it is in the callback's set *and* carries no flux in
|
|
72
|
+
mptool's own LP solution. Callbacks that return no zero-flux IDs, or that
|
|
73
|
+
re-solved the main problem to honour the old contract, sample the same
|
|
74
|
+
MPs; enumeration with such a callback may find them in another order.
|
|
75
|
+
|
|
76
|
+
## [1.1.0] — 2026-10-06
|
|
77
|
+
|
|
78
|
+
A minor release: a new rule for `adaptive=True`, compressed sampling that
|
|
79
|
+
draws MPs with the same distribution as uncompressed, and the fixes from a
|
|
80
|
+
pre-release review. Several fixes can change results, and several inputs
|
|
81
|
+
that were silently dropped now warn or raise. `conv_tol` and `conv_window`
|
|
82
|
+
are deprecated. See "Seeded runs and `export` bundles from 1.0.x" under
|
|
83
|
+
Changed for what replays differently.
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- `sample_mps` raises a `UserWarning` when no key of `weights` names a reaction
|
|
88
|
+
in the sampled subset. Keys that match nothing are still ignored, so one
|
|
89
|
+
weights table can cover several models, but when none matches every reaction
|
|
90
|
+
gets weight 0 and the weights silently do nothing. Typical causes are gene
|
|
91
|
+
IDs, or preprocessed IDs passed with `preprocess=True`.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- `adaptive=True` uses a new rule (#8). After each new distinct MP, a reaction
|
|
96
|
+
that occurs in at least one distinct MP found so far keeps its user weight,
|
|
97
|
+
and every other one gets N times it, N being the number of distinct MPs
|
|
98
|
+
found. The old rule gave N/count, which favours rare reactions. It made
|
|
99
|
+
the bias of sampled MPs worse on every measured instance with 30 or more
|
|
100
|
+
MPs, and it stalled the search for reactions not yet seen. The new rule
|
|
101
|
+
finds every reaction that occurs in some MP 2–5x faster than uniform
|
|
102
|
+
orderings at genome scale. In simulation with user weights, it was never
|
|
103
|
+
slower than the same weights alone. Once all such reactions have been seen,
|
|
104
|
+
it samples as `adaptive=False`. It does not reduce the bias toward MPs with
|
|
105
|
+
few reactions of their own. A reaction with weight 0, or missing from
|
|
106
|
+
`weights`, is never boosted.
|
|
107
|
+
- With compression, `sample_mps` now draws MPs with the same distribution as
|
|
108
|
+
without it, with or without `weights`, when it preprocesses the model
|
|
109
|
+
itself (`preprocess=True`, the default). Without compression, a chain of
|
|
110
|
+
fully coupled reactions is removed, or found to be needed, as soon as the
|
|
111
|
+
first of its members is tried. A merged cluster used to be ordered as one
|
|
112
|
+
reaction, and with `weights` it carried the sum of the weights of every
|
|
113
|
+
reaction merged into it, which held on to it harder the more reactions it
|
|
114
|
+
merged. Now each subset member draws its own key from its weight (every
|
|
115
|
+
member counts as 1 without `weights`), and the cluster is placed at the
|
|
116
|
+
earliest; reactions merged in from outside the subset do not count. On
|
|
117
|
+
e_coli_core the total variation between per-MP frequencies with and without
|
|
118
|
+
compression fell from 0.19 to 0.02 without weights, and is 0.015 with
|
|
119
|
+
log-normal weights, against 0.02 between seeds. Under the old sum, equal
|
|
120
|
+
weights and no weights differed by 0.24. A model compressed beforehand
|
|
121
|
+
and sampled with `preprocess=False` has no mapping, so each merged
|
|
122
|
+
reaction is still ordered as one reaction.
|
|
123
|
+
- Seeded runs and `export` bundles from 1.0.x that used compression (the
|
|
124
|
+
default) or `adaptive=True` can give different MPs when rerun with this
|
|
125
|
+
version. Runs with neither give the same MPs, except weighted runs whose
|
|
126
|
+
keys tied (see Fixed) and `method='direct'` runs that returned sets that
|
|
127
|
+
are not MPs (see Fixed).
|
|
128
|
+
|
|
129
|
+
- `load_bounds`, `load_weights` and `load_mapping` raise `ValueError`,
|
|
130
|
+
naming file and line, on a non-blank line that does not parse. Such
|
|
131
|
+
lines used to be skipped, so a file separated by `;` or tabs, as Excel
|
|
132
|
+
writes CSV under most European regional settings, loaded as `{}` without
|
|
133
|
+
a word. Blank lines are still skipped.
|
|
134
|
+
- `preprocess_model`, `enumerate_mps` and `sample_mps` name bounds keys
|
|
135
|
+
that match no reaction of the model in a `UserWarning`. They are still
|
|
136
|
+
ignored, so one bounds table can cover several models.
|
|
137
|
+
- `create_lp` raises `ValueError` on a COBRA model with constraints other
|
|
138
|
+
than metabolite balances, or variables other than reaction fluxes, such
|
|
139
|
+
as those added with `add_cons_vars`. It used to drop them, so mptool
|
|
140
|
+
solved a different model from cobrapy: with `PGI <= 2` added to
|
|
141
|
+
e_coli_core, FBA gave 0.8739 against cobrapy's 0.8676. Every model
|
|
142
|
+
shipped with the tests and benchmarks builds as before.
|
|
143
|
+
|
|
144
|
+
- The CLI refuses `--no-verbose` without `--export`: such a run printed
|
|
145
|
+
nothing and wrote nothing.
|
|
146
|
+
- `rerun` raises a `UserWarning` when the bundle was recorded with another
|
|
147
|
+
mptool version, since seeded results can differ between versions.
|
|
148
|
+
- `enumerate_mps`, `sample_mps` and the CLI reject an infinite `max_t`. 0
|
|
149
|
+
already means no limit, and `sample_mps` with `n_samples=0` accepted
|
|
150
|
+
`max_t=inf` as a stop criterion that never fires.
|
|
151
|
+
|
|
152
|
+
### Deprecated
|
|
153
|
+
|
|
154
|
+
- `sample_mps`'s `conv_tol` and `conv_window` (and the CLI's `--conv-tol` and
|
|
155
|
+
`--conv-window`) will be removed in a future major release, and
|
|
156
|
+
`conv_tol > 0` raises a `FutureWarning`. The change in reaction frequencies
|
|
157
|
+
they watch shrinks as 1/N by arithmetic, so the check stops at a roughly
|
|
158
|
+
fixed number of distinct MPs rather than when the estimates settle. At
|
|
159
|
+
`conv_tol=0.1` that was 71–98 distinct MPs on every genome-scale instance
|
|
160
|
+
measured, whether it had 146 MPs or 655. The check still works until it is
|
|
161
|
+
removed. Passing `conv_window` (or `--conv-window`) without `conv_tol`
|
|
162
|
+
raises the warning too; it was a silent no-op. The CLI words the warning
|
|
163
|
+
in terms of its own flags, and no longer passes `--conv-window`'s default
|
|
164
|
+
on every run.
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
|
|
168
|
+
- Weighted sampling lost the weights' effect for very small or very large
|
|
169
|
+
weights, so results depended on the scale of the weights. Keys were
|
|
170
|
+
computed as `u ** (1 / w)`, which rounds to 0 for small weights (from about
|
|
171
|
+
1e-2: 47% of keys at w = 1e-3, 99.9% at 1e-6) and to 1 for weights around
|
|
172
|
+
1e14 and up. Tied keys then fell back to column order. This affected, for
|
|
173
|
+
example, weights normalized to sum to 1 on a genome-scale model, where
|
|
174
|
+
about 70% of keys tied at w = 5e-4. With every weight near
|
|
175
|
+
1e-6, each draw had the same ordering, and a run could not find a second
|
|
176
|
+
MP. Keys are now `log(u) / w`, which orders the same without rounding, so
|
|
177
|
+
scaling every weight by 1e-6 or 1e6 gives the same seeded MPs. Seeded
|
|
178
|
+
runs whose keys did not tie are unchanged.
|
|
179
|
+
|
|
180
|
+
- With `preprocess=False`, `enumerate_mps` and `sample_mps` failed when the
|
|
181
|
+
model had a blocked reaction (bounds 0, 0) anywhere before the last subset
|
|
182
|
+
reaction in column order. Blocked reactions are removed before the search,
|
|
183
|
+
which renumbers the columns after them, but the internal sets of subset and
|
|
184
|
+
essential reactions kept the old numbering: gurobipy hashes a variable by its
|
|
185
|
+
column index. The search then usually stopped with `GurobiError:
|
|
186
|
+
Constraint has no bool value`. A lookup in such a set can also miss a
|
|
187
|
+
member without raising, so a wrong result without an error is possible too,
|
|
188
|
+
though none was observed. `preprocess=True` runs were not affected, because
|
|
189
|
+
preprocessing removes blocked reactions before these sets are built.
|
|
190
|
+
|
|
191
|
+
- `enumerate_mps` and `sample_mps` read a generator `subset` twice, so the
|
|
192
|
+
search got an empty subset and returned no MPs with `complete=True`. Any
|
|
193
|
+
iterable now works, and an empty generator raises like any empty subset.
|
|
194
|
+
- Subset IDs that name no reaction of the model were dropped silently, so a
|
|
195
|
+
typo, or original IDs passed with `preprocess=False` to a compressed model,
|
|
196
|
+
shrank the subset without a word: the latter gave 2 MPs instead of 33 on
|
|
197
|
+
e_coli_core, with `complete=True`. `enumerate_mps`, `sample_mps`,
|
|
198
|
+
`preprocess_model`, `validate_mps`, `fastcc`, `fastercc` and
|
|
199
|
+
`make_forward_irreversible` (on a Gurobi model) now name them in a
|
|
200
|
+
`UserWarning` and go on with the rest, and raise `ValueError` when none
|
|
201
|
+
names a reaction.
|
|
202
|
+
- A stop limit left on the caller's Gurobi model (`TimeLimit`,
|
|
203
|
+
`IterationLimit`, `WorkLimit`, `Cutoff` and the other termination
|
|
204
|
+
parameters except the memory limits) was copied onto every LP of a run.
|
|
205
|
+
A feasibility LP that stops early is read as infeasible, so such a limit
|
|
206
|
+
could turn a feasible model infeasible mid-search and give wrong MPs or
|
|
207
|
+
MCSs with `complete=True`. mptool now resets these parameters on its own
|
|
208
|
+
copies. `validate_mps`, which solves on the caller's model, applied them
|
|
209
|
+
too: with `TimeLimit=0` it judged every MP invalid. It now clears them for
|
|
210
|
+
its solves and restores every parameter it changes, and the objective
|
|
211
|
+
constant, which it used to leave at 0. It also flushes the caller's
|
|
212
|
+
pending changes before it reads anything, so it no longer reverts them.
|
|
213
|
+
- With `method='direct'`, the per-solve `TimeLimit` stayed on the solved
|
|
214
|
+
model, so an `export` bundle's `settings.prm` carried it and `rerun`
|
|
215
|
+
applied it to the replay.
|
|
216
|
+
- Compression could delete a real stoichiometric coefficient. An entry of a
|
|
217
|
+
merged reaction is the sum of its members' coefficients, each scaled by
|
|
218
|
+
the member's coupling ratio, and every entry below `merge_tol` (1e-6) was
|
|
219
|
+
zeroed as cancellation noise. An entry that only one member contributes
|
|
220
|
+
to cancels with nothing, but a small ratio can still push it below 1e-6
|
|
221
|
+
(a ratio of 1e-3 takes 5e-4 to 5e-7). Zeroing it decoupled that
|
|
222
|
+
metabolite from the cluster, and the MPs and MCSs changed. Only entries
|
|
223
|
+
with two or more contributing members are thresholded now. No such
|
|
224
|
+
entry was found on e_coli_core, iIT341, iMM904, iML1515, iCHOv1 or
|
|
225
|
+
Recon3D, so their compressed models are unchanged.
|
|
226
|
+
- `enumerate_mps(method='direct')` could return sets that are not MPs and
|
|
227
|
+
miss real ones, with `complete=True`. An indicator counted as off up to
|
|
228
|
+
Gurobi's integrality tolerance (1e-5), which let a reaction with upper
|
|
229
|
+
bound 1000 carry 0.01 of flux the MP did not list; its no-good cut then
|
|
230
|
+
excluded real MPs. On e_coli_core with biomass fixed at 0.01, 20 of the
|
|
231
|
+
33 returned sets were not MPs. The MILP now runs at the tightest
|
|
232
|
+
tolerance (1e-9), and every solution is checked before it is accepted.
|
|
233
|
+
One that fails is dropped, the run reports `complete=False`, and a
|
|
234
|
+
`UserWarning` says MPs may be missing. This still happens when the
|
|
235
|
+
fluxes an MP needs are small relative to its reactions' upper bounds (on
|
|
236
|
+
e_coli_core, from biomass 1e-4 down). The iterative method is exact.
|
|
237
|
+
- The MPs the CLI prints (and `verbose=True` prints from `enumerate_mps`
|
|
238
|
+
and `sample_mps`) lacked the essential reactions, which are added after
|
|
239
|
+
the search, so they were not the MPs the run returns. Without `--export`
|
|
240
|
+
the printed lines are the only output. They now include the essentials,
|
|
241
|
+
and sampling no longer says the essentials were added to "MPs and MCs".
|
|
242
|
+
- With `export=True`, a `requirements` lambda or closure was rejected only
|
|
243
|
+
when the bundle was written, after the whole search. It is now rejected
|
|
244
|
+
before the search starts.
|
|
245
|
+
- `export=True` failed after the whole search when the bounds held numpy
|
|
246
|
+
scalars such as `float32` or `int64`, and left a truncated
|
|
247
|
+
`manifest.json` that `rerun` took for a finished bundle. numpy scalars
|
|
248
|
+
are now written as numbers, and `manifest.json` is written under a
|
|
249
|
+
temporary name and moved into place, so a failed write leaves none.
|
|
250
|
+
- An export bundle's `settings.prm` recorded the exporter's Gurobi licence
|
|
251
|
+
ID, and any Compute Server setting on the caller's model. Licence and
|
|
252
|
+
connection parameters (licence ID, user name, WLS, Cloud, Compute Server
|
|
253
|
+
and token server) are now left out. Gurobi 12.0.2 already omitted the
|
|
254
|
+
secrets themselves.
|
|
255
|
+
- `translate_mps` and `translate_mcs` with a `subset` of COBRA reactions or
|
|
256
|
+
Gurobi variables, such as the README's `model.boundary`, returned empty
|
|
257
|
+
MPs and no cuts: the subset was compared with reaction IDs as given. They
|
|
258
|
+
now accept the same subset forms as `enumerate_mps`.
|
|
259
|
+
- The file readers (`load_set`, `load_sets`, `load_bounds`, `load_weights`,
|
|
260
|
+
`load_mapping`) kept the byte-order mark that Excel's "CSV UTF-8" puts at
|
|
261
|
+
the start of a file as part of the first ID, so that reaction silently
|
|
262
|
+
left the subset, or its bound or weight was not applied.
|
|
263
|
+
|
|
264
|
+
### Documentation
|
|
265
|
+
|
|
266
|
+
- The README, `sample_mps`'s docstring and the CLI help no longer claim that
|
|
267
|
+
adaptive weights reduce length bias, and the README cites [1] for the
|
|
268
|
+
uniform sampler only; nothing on weighted or adaptive sampling is
|
|
269
|
+
published. They now say what a weight means: relative, on any scale, 0 or
|
|
270
|
+
missing for "remove whenever possible" and never boosted, and the full
|
|
271
|
+
weight on each direction of a reversible reaction. An MP is never drawn
|
|
272
|
+
while another MP's weight-0 reactions are a proper subset of its own, so 0
|
|
273
|
+
excludes wherever an alternative exists while a small positive weight only
|
|
274
|
+
disfavours. They also say that sampled MPs favour MPs with few reactions
|
|
275
|
+
of their own, and that frequencies over the distinct MPs found approach the
|
|
276
|
+
true ones as more are found.
|
|
277
|
+
- `sample_mps`'s docstring said duplicates are discarded with a warning; they
|
|
278
|
+
are discarded silently, and the acceptance rate reports them.
|
|
279
|
+
- The README's back-translation example expanded `mapping[r]` by hand, which
|
|
280
|
+
drops the `_rev` naming of reversed reactions and keeps out-of-subset
|
|
281
|
+
reactions merged into clusters; it now uses `translate_mps` and
|
|
282
|
+
`translate_mcs` with the enumerated subset. It and the enumeration
|
|
283
|
+
example no longer pass `compress=True, tighten=True` as if they were
|
|
284
|
+
opt-in; both are defaults.
|
|
285
|
+
- `n_samples` counts distinct MPs, and a run without `max_t` never ends if
|
|
286
|
+
fewer can be drawn (for example when weights of 0 make some undrawable).
|
|
287
|
+
The docstring, the README and the CLI help now say so.
|
|
288
|
+
- Docstrings and messages that misstated the code:
|
|
289
|
+
- With `preprocess=False`, only subset reactions must have `lb >= 0`,
|
|
290
|
+
not every reaction of the model.
|
|
291
|
+
- The warning for a run cut short before its first MP no longer
|
|
292
|
+
suggests raising `max_mps`, which cannot help and which `sample_mps`
|
|
293
|
+
does not have.
|
|
294
|
+
- `--seed` gives identical results only without `--max-t`.
|
|
295
|
+
- `preprocess_model` is idempotent in its subset and mapping. With
|
|
296
|
+
`compress=True`, a second pass tightens the merged reactions' bounds,
|
|
297
|
+
without changing the feasible set.
|
|
298
|
+
- A compressed model's merged reactions keep structural bounds.
|
|
299
|
+
|
|
6
300
|
## [1.0.2] — 2026-09-29
|
|
7
301
|
|
|
8
302
|
A bug-fix release. No API change. Both fixes can change results: iterative
|
|
@@ -3,8 +3,8 @@ message: "If you use this software, please cite both the software and the paper
|
|
|
3
3
|
title: mptool
|
|
4
4
|
abstract: Enumeration and sampling of minimal pathways in metabolic (sub)networks.
|
|
5
5
|
type: software
|
|
6
|
-
version: 1.0
|
|
7
|
-
date-released: 2026-
|
|
6
|
+
version: 1.2.0
|
|
7
|
+
date-released: 2026-11-03
|
|
8
8
|
license: MIT
|
|
9
9
|
repository-code: "https://gitlab.com/csb.ethz/mptool"
|
|
10
10
|
url: "https://gitlab.com/csb.ethz/mptool"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mptool
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Enumeration and sampling of minimal pathways in metabolic (sub)networks.
|
|
5
5
|
Home-page: https://gitlab.com/csb.ethz/mptool
|
|
6
6
|
Author: Ove Øyås
|
|
@@ -24,7 +24,7 @@ Requires-Dist: networkx>=2.4
|
|
|
24
24
|
Requires-Dist: numpy>=1.21.0
|
|
25
25
|
Requires-Dist: scipy>=1.7.3
|
|
26
26
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
28
28
|
Provides-Extra: docs
|
|
29
29
|
Requires-Dist: sphinx; extra == "docs"
|
|
30
30
|
Requires-Dist: furo; extra == "docs"
|
|
@@ -45,7 +45,7 @@ This repository contains a Python package with implementations of methods for me
|
|
|
45
45
|
|
|
46
46
|
Minimal pathways (MPs) are minimal sets of reactions that need to be active (have non-zero flux) in a metabolic (sub)network to satisfy all constraints on the network as a whole [1]. They can also be defined as the set of *support-minimal* flux patterns from elementary flux vectors (EFVs) [2].
|
|
47
47
|
|
|
48
|
-
An MP can be found by direct minimization of a mixed-integer linear program (MILP) or by iterative minimization of multiple linear programs (LPs). Both approaches are implemented for enumeration (`enumerate_mps`): the iterative method (default) additionally enumerates minimal cut sets (MCSs), using either a binary integer program (`mcs_method='bip'`, default) [3] or a Berge minimal-hitting-set enumerator (`mcs_method='berge'`). Iterative enumeration can be accelerated by a graph defined by the known MPs to predict unknown MPs (`graph=True`) and by deleting reactions in multiple-reaction chunks rather than one at a time (`chunks`). Random sampling of MPs (`sample_mps`), for when complete enumeration is infeasible, is always iterative
|
|
48
|
+
An MP can be found by direct minimization of a mixed-integer linear program (MILP) or by iterative minimization of multiple linear programs (LPs). Both approaches are implemented for enumeration (`enumerate_mps`): the iterative method (default) additionally enumerates minimal cut sets (MCSs), using either a binary integer program (`mcs_method='bip'`, default) [3] or a Berge minimal-hitting-set enumerator (`mcs_method='berge'`). Iterative enumeration can be accelerated by a graph defined by the known MPs to predict unknown MPs (`graph=True`) and by deleting reactions in multiple-reaction chunks rather than one at a time (`chunks`). Random sampling of MPs (`sample_mps`), for when complete enumeration is infeasible, is always iterative: by default it draws uniform random orderings and deletes a single reaction at a time (`weights=None`, `chunks=None`) [1]. It can optionally bias the ordering with per-reaction weights, delete in chunks, and favour reactions not yet seen in a sampled MP (`adaptive=True`). Sampled MPs favour MPs with few reactions of their own; see [Sampling](#sampling). Sampling returns the sampled MPs together with an acceptance rate (the fraction of completed draws — those producing a valid MP — that yield a previously-unseen MP).
|
|
49
49
|
|
|
50
50
|
## Requirements
|
|
51
51
|
|
|
@@ -87,11 +87,13 @@ for the full option lists.
|
|
|
87
87
|
|
|
88
88
|
### Back-translating compressed CLI output
|
|
89
89
|
|
|
90
|
-
The
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
The CLI prints each MP as it is found, essential reactions included, so the
|
|
91
|
+
printed MPs are the ones the run returns. Without `--export` they are the only
|
|
92
|
+
output, which is why `--no-verbose` requires `--export`. Their reaction IDs are
|
|
93
|
+
in mptool's *preprocessed* namespace, not your input model's. With compression
|
|
94
|
+
on (the default) these include fused flux-coupled clusters (`A+B`); reversible
|
|
95
|
+
candidate reactions are also split into a forward half and a reverse half
|
|
96
|
+
(`X_rev`), regardless of compression.
|
|
95
97
|
|
|
96
98
|
To map these IDs back to your original model, pass `--export`: the run bundle
|
|
97
99
|
then contains `mapping.csv` — a headerless `new_id,orig_id,coef` table —
|
|
@@ -134,23 +136,29 @@ mps, mcs, complete = mpt.enumerate_mps(model, subset=subset,
|
|
|
134
136
|
mcs_method='berge')
|
|
135
137
|
|
|
136
138
|
# Direct MILP minimization: finds one MP per solve; no MCSs returned.
|
|
139
|
+
# Can miss MPs when the fluxes an MP needs are small relative to its
|
|
140
|
+
# reactions' upper bounds; it then warns and returns complete=False.
|
|
141
|
+
# The iterative method is exact.
|
|
137
142
|
mps, mcs, complete = mpt.enumerate_mps(model, subset=subset,
|
|
138
143
|
method='direct', graph=False)
|
|
139
144
|
|
|
140
|
-
# Lossless FVA-based compression
|
|
141
|
-
#
|
|
142
|
-
mps, mcs, complete
|
|
143
|
-
model, subset=subset,
|
|
144
|
-
preprocess_kwargs={'compress': True, 'tighten': True},
|
|
145
|
-
return_mapping=True,
|
|
146
|
-
)
|
|
145
|
+
# Lossless FVA-based compression is on by default, so the MPs above use
|
|
146
|
+
# compressed reaction IDs (see Back-translation below). Without it:
|
|
147
|
+
mps, mcs, complete = mpt.enumerate_mps(
|
|
148
|
+
model, subset=subset, preprocess_kwargs={'compress': False})
|
|
147
149
|
```
|
|
148
150
|
|
|
149
151
|
### Sampling
|
|
150
152
|
|
|
151
153
|
When complete enumeration is infeasible, random sampling is more practical.
|
|
152
|
-
`sample_mps` runs until
|
|
153
|
-
|
|
154
|
+
`sample_mps` runs until `n_samples` distinct MPs have been collected,
|
|
155
|
+
`max_draws` draws have completed, or `max_t` seconds have elapsed, whichever
|
|
156
|
+
comes first (at least one is required). Duplicates do not count toward
|
|
157
|
+
`n_samples`, so if the model has fewer distinct MPs than that — or fewer that
|
|
158
|
+
can be drawn: a reaction with weight 0 can make MPs undrawable — a run with
|
|
159
|
+
neither `max_t` nor `max_draws` never ends. Set one of them unless you know
|
|
160
|
+
there are enough; a seeded run stopped by `max_draws` is reproducible, one
|
|
161
|
+
stopped by `max_t` is not. Interrupting a run loses the MPs found so far. `max_t` budgets the sampling
|
|
154
162
|
loop only — preprocessing runs before the clock starts, so total runtime is
|
|
155
163
|
preprocessing plus `max_t`. The same holds for `max_t` in `enumerate_mps`. It
|
|
156
164
|
returns a 2-tuple
|
|
@@ -158,7 +166,9 @@ returns a 2-tuple
|
|
|
158
166
|
frequencies and convergence statistics can be recomputed directly from it)
|
|
159
167
|
and `acceptance` is the acceptance rate — the fraction of completed draws
|
|
160
168
|
that yielded a new MP rather than a duplicate (a low rate signals the MP
|
|
161
|
-
space is nearly exhausted; prefer `enumerate_mps` then).
|
|
169
|
+
space is nearly exhausted; prefer `enumerate_mps` then). With
|
|
170
|
+
`return_counts=True` it returns `(mps, acceptance, counts)`, where
|
|
171
|
+
`counts[i]` is the number of draws that returned `mps[i]`. The full model is
|
|
162
172
|
used here (no `subset` argument).
|
|
163
173
|
|
|
164
174
|
```python
|
|
@@ -176,30 +186,76 @@ mps, acceptance = mpt.sample_mps(model, n_samples=100, chunks=4)
|
|
|
176
186
|
weights = {r.id: random.random() for r in model.reactions}
|
|
177
187
|
mps, acceptance = mpt.sample_mps(model, n_samples=100, weights=weights)
|
|
178
188
|
|
|
179
|
-
# Adaptive sampler:
|
|
180
|
-
#
|
|
189
|
+
# Adaptive sampler: reactions not yet seen in a sampled MP are kept longer,
|
|
190
|
+
# so every reaction that occurs in some MP is found sooner.
|
|
181
191
|
mps, acceptance = mpt.sample_mps(model, n_samples=0, adaptive=True, max_t=60)
|
|
182
|
-
```
|
|
183
192
|
|
|
184
|
-
|
|
193
|
+
# A fixed number of draws, with how many of them returned each MP.
|
|
194
|
+
mps, acceptance, counts = mpt.sample_mps(model, n_samples=0, max_draws=1000,
|
|
195
|
+
return_counts=True)
|
|
196
|
+
```
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
Sampled MPs are not a uniform sample of the MP space. A draw is the MP that
|
|
199
|
+
avoids the earliest reactions it can along the ordering, so MPs with few
|
|
200
|
+
reactions of their own are drawn more often, and so, for example, are MPs
|
|
201
|
+
that share few reactions with others. Frequencies counted per draw are
|
|
202
|
+
therefore biased. `mps` holds each distinct MP once, and frequencies over the
|
|
203
|
+
distinct MPs found approach the true ones as more of them are found. Neither
|
|
204
|
+
weights nor `adaptive=True` removes this bias.
|
|
205
|
+
|
|
206
|
+
**Weights** are relative: reaction `r` is kept longer than `s` with odds
|
|
207
|
+
`w_r : w_s`, so any scale works (raw scores such as BLAST bit scores
|
|
208
|
+
included) and multiplying every weight by the same factor changes nothing. A
|
|
209
|
+
reaction with weight 0, or missing from `weights`, is tried before every
|
|
210
|
+
positive-weight reaction and removed whenever possible; list a reaction to
|
|
211
|
+
give it a say. An MP is never drawn while another MP's weight-0 reactions
|
|
212
|
+
are a proper subset of its own, and every other MP can be drawn, so 0
|
|
213
|
+
excludes wherever an alternative exists while a small positive weight only
|
|
214
|
+
disfavours. Each direction of a reversible reaction carries the full weight.
|
|
215
|
+
Keys that name no reaction in the subset are ignored, but a `UserWarning` is
|
|
216
|
+
raised when none of them does.
|
|
217
|
+
|
|
218
|
+
With `preprocess=True` (the default), compression does not change the
|
|
219
|
+
distribution of sampled MPs, though a given seed gives different MPs with and
|
|
220
|
+
without it. Without compression, a chain of fully coupled reactions is
|
|
221
|
+
removed, or found to be needed, as soon as the first of them is tried. With
|
|
222
|
+
it, each subset member of a merged cluster draws its own key from its weight
|
|
223
|
+
(every member counts as 1 without `weights`), and the cluster is placed at
|
|
224
|
+
the earliest. This needs the mapping that preprocessing returns, so a model
|
|
225
|
+
compressed beforehand and sampled with `preprocess=False` orders each merged
|
|
226
|
+
reaction as one reaction, and its sampled MPs are distributed differently.
|
|
227
|
+
|
|
228
|
+
**`adaptive=True`** multiplies the weight of every reaction that is in none
|
|
229
|
+
of the distinct MPs found so far by N, the number found, and leaves the
|
|
230
|
+
others at their weight (1 without `weights`; for a merged cluster, every
|
|
231
|
+
member's weight). The boost starts with the second distinct MP, and a weight
|
|
232
|
+
of 0 stays 0. Once every reaction that occurs in some MP has been seen,
|
|
233
|
+
sampling continues as without `adaptive`. `conv_tol` and `conv_window` are
|
|
234
|
+
deprecated: their check stops at a roughly fixed number of distinct MPs
|
|
235
|
+
rather than when estimates settle.
|
|
236
|
+
|
|
237
|
+
### Back-translation
|
|
238
|
+
|
|
239
|
+
The returned MPs and MCSs use preprocessed reaction IDs: with compression
|
|
240
|
+
(the default) a flux-coupled cluster is one reaction such as
|
|
241
|
+
`'ACONTa+ACONTb'`, and with or without it the reverse half of a reversible
|
|
242
|
+
reaction is `'X_rev'`. `return_mapping=True` appends the
|
|
243
|
+
preprocessed→original mapping to the return tuple. Each preprocessed reaction
|
|
244
|
+
maps to a `{original_id: coefficient}` dict; signs encode direction.
|
|
191
245
|
|
|
192
246
|
```python
|
|
193
247
|
mps, mcs, complete, mapping = mpt.enumerate_mps(
|
|
194
|
-
model, subset=subset,
|
|
195
|
-
preprocess_kwargs={'compress': True, 'tighten': True},
|
|
196
|
-
return_mapping=True,
|
|
197
|
-
)
|
|
248
|
+
model, subset=subset, return_mapping=True)
|
|
198
249
|
|
|
199
|
-
|
|
200
|
-
|
|
250
|
+
orig_mps = mpt.translate_mps(mps, mapping, subset=subset)
|
|
251
|
+
orig_mcs = mpt.translate_mcs(mcs, mapping, subset=subset)
|
|
201
252
|
```
|
|
202
253
|
|
|
254
|
+
Pass the same `subset` you enumerated over. The translation then names
|
|
255
|
+
reversed reactions as `X_rev`, as in the preprocessed MPs, and leaves out
|
|
256
|
+
reactions outside the subset that compression merged into a cluster with
|
|
257
|
+
subset reactions. Expanding `mapping[r]` by hand does neither.
|
|
258
|
+
|
|
203
259
|
## Preprocessing
|
|
204
260
|
|
|
205
261
|
`enumerate_mps` and `sample_mps` preprocess the model internally — copying it,
|
|
@@ -221,7 +277,11 @@ mps, mcs, complete = mpt.enumerate_mps(
|
|
|
221
277
|
|
|
222
278
|
This is useful when the same model is reused across multiple
|
|
223
279
|
enumeration / sampling calls — preprocessing is the expensive step and
|
|
224
|
-
running it once amortises its cost.
|
|
280
|
+
running it once amortises its cost. For sampling, note that `sample_mps` with
|
|
281
|
+
`preprocess=False` has no mapping, so on a compressed model it orders each
|
|
282
|
+
merged reaction as one reaction, and the sampled MPs are not distributed as
|
|
283
|
+
without compression (see [Sampling](#sampling)). Pass `compress=False` to
|
|
284
|
+
`preprocess_model` if that matters.
|
|
225
285
|
|
|
226
286
|
## Writing requirements functions
|
|
227
287
|
|
|
@@ -239,9 +299,11 @@ the callback runs, so a callback that inspects solution values must call
|
|
|
239
299
|
`model.optimize()` itself. It returns a 2-tuple `(feasible, zero_flux_ids)`:
|
|
240
300
|
|
|
241
301
|
- `feasible` (`bool`): True if the active reactions satisfy the criterion.
|
|
242
|
-
- `zero_flux_ids` (`set[str]`):
|
|
243
|
-
|
|
244
|
-
|
|
302
|
+
- `zero_flux_ids` (`set[str]`): IDs of the reactions that carry no flux in
|
|
303
|
+
the callback's own checks. mptool deletes a reaction without a solve only
|
|
304
|
+
if it is in this set *and* carries no flux in mptool's own LP solution, so
|
|
305
|
+
the callback does not need to re-solve the main problem. Return an empty
|
|
306
|
+
set if no such information is available.
|
|
245
307
|
|
|
246
308
|
### The preprocessed namespace
|
|
247
309
|
|