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.
@@ -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.2
7
- date-released: 2026-09-29
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
  MIT License
2
2
 
3
- Copyright (c) 2026 ETH Zurich, Ove Øyås
3
+ Copyright (c) 2019-2026 Ove Øyås, ETH Zurich
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mptool
3
- Version: 1.0.2
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>=4.6.9; extra == "test"
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; by default it draws uniform random orderings and deletes a single reaction at a time (`weights=None`, `chunks=None`), and can optionally bias the ordering with per-reaction weights, delete in chunks, and update the weights adaptively to reduce length bias [1]. 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).
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 reaction IDs the CLI prints are in mptool's *preprocessed* namespace, not
91
- your input model's. With compression on (the default) these include fused
92
- flux-coupled clusters (`A+B`); reversible candidate reactions are also split
93
- into a forward half and a reverse half (`X_rev`), regardless of compression.
94
- Bare stdout output is always in this preprocessed namespace.
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 applied before enumeration. Returned
141
- # MPs use compressed reaction IDs; back-translate via the mapping.
142
- mps, mcs, complete, mapping = mpt.enumerate_mps(
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 either `n_samples` MPs have been collected or `max_t`
153
- seconds have elapsed (at least one is required). `max_t` budgets the sampling
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). The full model is
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: weights are updated after each sample to reduce
180
- # length bias and improve coverage of the MP space.
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
- ### Back-translation when `compress=True`
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
- With `compress=True` the returned MPs use compressed reaction IDs (e.g.
187
- `'ACONTa+ACONTb'` for a flux-coupled cluster, or `'X_rev'` for the reverse
188
- half of a reversible reaction). `return_mapping=True` appends the
189
- preprocessed→original mapping to the return tuple. Each merged reaction maps
190
- to a `{original_id: coefficient}` dict; signs encode direction.
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
- # Expand a single MP back to original reaction IDs (union over members):
200
- original_ids = {orig for r in next(iter(mps)) for orig in mapping[r]}
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]`): Reaction ID strings known to carry zero flux
243
- under the current state; these can be pruned without re-solving the LP.
244
- Return an empty set if no such information is available.
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