pymodest 0.1.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.
- pymodest-0.1.0/LICENSE +21 -0
- pymodest-0.1.0/PKG-INFO +398 -0
- pymodest-0.1.0/README.md +378 -0
- pymodest-0.1.0/pyproject.toml +43 -0
- pymodest-0.1.0/setup.cfg +4 -0
- pymodest-0.1.0/src/pymodest/__init__.py +50 -0
- pymodest-0.1.0/src/pymodest/cli.py +306 -0
- pymodest-0.1.0/src/pymodest/config.py +738 -0
- pymodest-0.1.0/src/pymodest/data.py +255 -0
- pymodest-0.1.0/src/pymodest/estimator.py +299 -0
- pymodest-0.1.0/src/pymodest/model.py +335 -0
- pymodest-0.1.0/src/pymodest/objective.py +391 -0
- pymodest-0.1.0/src/pymodest/optimizers/__init__.py +35 -0
- pymodest-0.1.0/src/pymodest/optimizers/base.py +122 -0
- pymodest-0.1.0/src/pymodest/optimizers/scatter.py +202 -0
- pymodest-0.1.0/src/pymodest/optimizers/scipy_backends.py +160 -0
- pymodest-0.1.0/src/pymodest/optimizers/swarm.py +113 -0
- pymodest-0.1.0/src/pymodest/result.py +211 -0
- pymodest-0.1.0/src/pymodest.egg-info/PKG-INFO +398 -0
- pymodest-0.1.0/src/pymodest.egg-info/SOURCES.txt +28 -0
- pymodest-0.1.0/src/pymodest.egg-info/dependency_links.txt +1 -0
- pymodest-0.1.0/src/pymodest.egg-info/entry_points.txt +2 -0
- pymodest-0.1.0/src/pymodest.egg-info/requires.txt +5 -0
- pymodest-0.1.0/src/pymodest.egg-info/top_level.txt +1 -0
- pymodest-0.1.0/tests/test_cli_and_example.py +134 -0
- pymodest-0.1.0/tests/test_config.py +224 -0
- pymodest-0.1.0/tests/test_estimator.py +264 -0
- pymodest-0.1.0/tests/test_model_and_data.py +211 -0
- pymodest-0.1.0/tests/test_objective.py +181 -0
- pymodest-0.1.0/tests/test_optimizers.py +116 -0
pymodest-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Song Feng
|
|
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.
|
pymodest-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymodest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Divide-and-conquer, module-wise parameter estimation for biological models written in Antimony
|
|
5
|
+
Author: Song Feng
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: systems biology,parameter estimation,antimony,sbml,roadrunner
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.22
|
|
15
|
+
Requires-Dist: scipy>=1.9
|
|
16
|
+
Requires-Dist: pandas>=1.4
|
|
17
|
+
Requires-Dist: antimony>=2.13
|
|
18
|
+
Requires-Dist: libroadrunner>=2.4
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# pyModEst
|
|
22
|
+
|
|
23
|
+
Divide-and-conquer parameter estimation for biological models.
|
|
24
|
+
|
|
25
|
+
pyModEst reads models written in [Antimony](https://tellurium.readthedocs.io/en/latest/antimony.html),
|
|
26
|
+
a declaration of which parameters to fit, and one or more experimental datasets,
|
|
27
|
+
then estimates the parameters **module by module**: each module's parameters are
|
|
28
|
+
optimized against only that module's measured variables, while every other
|
|
29
|
+
parameter is held fixed. The procedure cycles through the modules for a finite
|
|
30
|
+
number of loops.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
theta <- initial values
|
|
34
|
+
repeat up to max_loops times:
|
|
35
|
+
for each module m:
|
|
36
|
+
theta[m] <- argmin cost_m(theta[m] ; theta[not m] held fixed)
|
|
37
|
+
stop when the total cost stops improving
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
One shared parameter set can be constrained by **several models** and **several
|
|
41
|
+
datasets** at once, which is the usual situation when wild-type and mutant
|
|
42
|
+
strains, or different experimental conditions, are described by related models.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
Full reference in [`docs/`](docs/):
|
|
49
|
+
|
|
50
|
+
| | |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| [Concepts](docs/concepts.md) | what module-wise fitting is, when it helps, how it fails |
|
|
53
|
+
| [Configuration](docs/configuration.md) | the complete TOML reference |
|
|
54
|
+
| [Capabilities](docs/capabilities.md) | what the package does, each pinned by a test |
|
|
55
|
+
| [Optimizers](docs/optimizers.md) | the five backends, choosing one, adding one |
|
|
56
|
+
| [Python API](docs/api.md) | using pyModEst as a library |
|
|
57
|
+
| [Troubleshooting](docs/troubleshooting.md) | what goes wrong and what it means |
|
|
58
|
+
| [Examples](docs/examples/) | runnable scripts behind the documented output |
|
|
59
|
+
|
|
60
|
+
## Why fit in modules
|
|
61
|
+
|
|
62
|
+
A joint fit over thirty parameters searches a thirty-dimensional space; a
|
|
63
|
+
module-wise fit searches several small ones in turn. Each module fit is small
|
|
64
|
+
enough for a global optimizer to solve reliably, and the biology usually
|
|
65
|
+
suggests the partition already: uptake parameters are constrained by upstream
|
|
66
|
+
metabolites, feedback constants by the species that does the inhibiting.
|
|
67
|
+
|
|
68
|
+
The trade-off is real and worth stating plainly. Because each module minimizes
|
|
69
|
+
its own objective rather than the joint one, the total cost is **not guaranteed
|
|
70
|
+
to decrease every loop** — modules are coupled through the shared model. pyModEst
|
|
71
|
+
handles this by tracking the best parameter set across the whole run and
|
|
72
|
+
returning that, not whatever the last loop happened to produce. If you want a
|
|
73
|
+
strictly monotone total, set `accept = "total"`, but read the warning under
|
|
74
|
+
[Acceptance policy](#acceptance-policy) first.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
The project is managed with [uv](https://docs.astral.sh/uv/). From a clone:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv sync # create .venv and install from uv.lock, dev group included
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`uv sync` installs the exact versions recorded in `uv.lock`, so everyone gets
|
|
85
|
+
the same environment. Commands then run through `uv run`, which keeps the
|
|
86
|
+
environment up to date for you — no manual activation needed:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run pymodest --version
|
|
90
|
+
uv run pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To add or change a dependency, edit `pyproject.toml` and run `uv lock` (or
|
|
94
|
+
`uv add <package>`, which does both), then commit the updated `uv.lock`.
|
|
95
|
+
|
|
96
|
+
Plain pip works too, if you would rather not use uv:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install -e . # runtime only
|
|
100
|
+
pip install -e . --group dev # with pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Requires Python 3.11+ (the oldest version `libroadrunner` publishes wheels
|
|
104
|
+
for), and pulls in `antimony`, `libroadrunner`, `numpy`, `scipy` and `pandas`.
|
|
105
|
+
|
|
106
|
+
## Quick start
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run pymodest template --out study.toml # a commented starter configuration
|
|
110
|
+
uv run pymodest validate study.toml # check models, data and modules agree
|
|
111
|
+
uv run pymodest fit study.toml # run the estimation
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Drop the `uv run` prefix if you have activated the environment yourself
|
|
115
|
+
(`source .venv/bin/activate`) or installed with pip.
|
|
116
|
+
|
|
117
|
+
Or from Python:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from pymodest import load_config, fit
|
|
121
|
+
|
|
122
|
+
config = load_config("study.toml")
|
|
123
|
+
result = fit(config)
|
|
124
|
+
|
|
125
|
+
print(result.parameter_table())
|
|
126
|
+
print(result.loop_summary())
|
|
127
|
+
result.save(config.output_dir)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## The configuration file
|
|
131
|
+
|
|
132
|
+
One TOML file describes the whole study. Every path in it is resolved relative
|
|
133
|
+
to the file itself.
|
|
134
|
+
|
|
135
|
+
### Models
|
|
136
|
+
|
|
137
|
+
Several models may share one fitted parameter set. A parameter that exists in
|
|
138
|
+
only some of them is written only to those, and is therefore identified only by
|
|
139
|
+
their datasets.
|
|
140
|
+
|
|
141
|
+
```toml
|
|
142
|
+
[[models]]
|
|
143
|
+
id = "wt"
|
|
144
|
+
antimony_file = "models/wt.ant"
|
|
145
|
+
|
|
146
|
+
[models.observables] # derived quantities, usable as module variables
|
|
147
|
+
Total = "A + B + C"
|
|
148
|
+
|
|
149
|
+
[[models]]
|
|
150
|
+
id = "feedback"
|
|
151
|
+
antimony_file = "models/feedback.ant" # adds Ki; shares everything else
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`antimony = """..."""` may be given inline instead of `antimony_file`.
|
|
155
|
+
`[models.overrides]` pins values for one model only.
|
|
156
|
+
|
|
157
|
+
### Datasets
|
|
158
|
+
|
|
159
|
+
Each dataset is measured on one model, under its own conditions.
|
|
160
|
+
|
|
161
|
+
```toml
|
|
162
|
+
[[datasets]]
|
|
163
|
+
id = "wt_low"
|
|
164
|
+
model = "wt"
|
|
165
|
+
file = "data/wt_low.csv"
|
|
166
|
+
format = "wide" # wide: time,A,B,... long: time,variable,value
|
|
167
|
+
weight = 1.0
|
|
168
|
+
|
|
169
|
+
[datasets.conditions] # parameters set for this experiment
|
|
170
|
+
inducer = 0.0
|
|
171
|
+
|
|
172
|
+
[datasets.initial_conditions] # species starting values
|
|
173
|
+
S = 3.0
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Wide** files have one column per variable; a matching `A_sigma` (or `_sd`,
|
|
177
|
+
`_std`, `_err`) column supplies measurement errors. **Long** files have
|
|
178
|
+
`time, variable, value` and an optional `sigma`. Missing values are dropped per
|
|
179
|
+
variable, so variables need not share a measurement schedule. Data may also be
|
|
180
|
+
given inline under `[datasets.data]`.
|
|
181
|
+
|
|
182
|
+
### Modules
|
|
183
|
+
|
|
184
|
+
A module declares which parameters are fitted together and which measured
|
|
185
|
+
variables score them. **The modules must partition the parameter set** — every
|
|
186
|
+
fitted parameter belongs to exactly one module — which pyModEst checks at load
|
|
187
|
+
time.
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
[[modules]]
|
|
191
|
+
id = "upstream"
|
|
192
|
+
variables = ["A", "B"] # what this module is scored on
|
|
193
|
+
# datasets = ["wt_low"] # optional: restrict to particular experiments
|
|
194
|
+
# [modules.weights] # optional: per-variable weights
|
|
195
|
+
# A = 2.0
|
|
196
|
+
|
|
197
|
+
[[modules.parameters]]
|
|
198
|
+
name = "Vmax1"
|
|
199
|
+
lower = 0.05
|
|
200
|
+
upper = 50.0
|
|
201
|
+
init = 1.0
|
|
202
|
+
scale = "log" # log | linear; log searches log10(value)
|
|
203
|
+
# fixed = true # hold at init and exclude from the search
|
|
204
|
+
|
|
205
|
+
[modules.optimizer] # optional per-module optimizer
|
|
206
|
+
name = "scatter_search"
|
|
207
|
+
maxiter = 25
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use `scale = "log"` for rate constants and affinities spanning orders of
|
|
211
|
+
magnitude — it is usually the difference between a fit that converges and one
|
|
212
|
+
that does not.
|
|
213
|
+
|
|
214
|
+
### Fitting
|
|
215
|
+
|
|
216
|
+
```toml
|
|
217
|
+
[fitting]
|
|
218
|
+
max_loops = 8
|
|
219
|
+
module_order = "as_listed" # or a list of ids, "random", "round_robin_reversed"
|
|
220
|
+
tol = 1e-3 # relative improvement counting as progress
|
|
221
|
+
atol = 1e-12 # absolute floor, so a cost heading to zero terminates
|
|
222
|
+
patience = 2 # loops without progress before stopping
|
|
223
|
+
accept = "module" # module | total
|
|
224
|
+
seed = 7
|
|
225
|
+
|
|
226
|
+
[fitting.optimizer]
|
|
227
|
+
name = "differential_evolution"
|
|
228
|
+
maxiter = 40
|
|
229
|
+
popsize = 12
|
|
230
|
+
|
|
231
|
+
[fitting.objective]
|
|
232
|
+
scaling = "relative" # relative | absolute | sigma | max_normalized
|
|
233
|
+
aggregation = "mean" # mean | sum
|
|
234
|
+
epsilon = 1e-3
|
|
235
|
+
|
|
236
|
+
[fitting.simulation]
|
|
237
|
+
integrator = "cvode"
|
|
238
|
+
relative_tolerance = 1e-8
|
|
239
|
+
absolute_tolerance = 1e-10
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Residual scaling
|
|
243
|
+
|
|
244
|
+
Every residual is `simulated - observed`, then scaled:
|
|
245
|
+
|
|
246
|
+
| `scaling` | residual | use when |
|
|
247
|
+
| ----------------- | ---------------------------- | -------- |
|
|
248
|
+
| `relative` | `diff / (abs(obs) + epsilon)` | variables differ in magnitude (**default**) |
|
|
249
|
+
| `absolute` | `diff` | all variables share units and scale |
|
|
250
|
+
| `sigma` | `diff / sigma` | you measured errors — gives chi-square residuals |
|
|
251
|
+
| `max_normalized` | `diff / max(abs(obs))` | relative scaling is unstable near zero |
|
|
252
|
+
|
|
253
|
+
With `relative`, raise `epsilon` above the noise level of near-zero
|
|
254
|
+
measurements; otherwise an observation of 0.001 dominates the objective.
|
|
255
|
+
Residuals are then multiplied by `sqrt(dataset weight x variable weight)` and
|
|
256
|
+
aggregated by mean (default) or sum of squares.
|
|
257
|
+
|
|
258
|
+
## Optimizers
|
|
259
|
+
|
|
260
|
+
Named in `[fitting.optimizer]` or per module in `[modules.optimizer]`. Any key
|
|
261
|
+
other than `name` is passed straight to the backend.
|
|
262
|
+
|
|
263
|
+
| name | what it is | notable options |
|
|
264
|
+
| ---- | ---------- | --------------- |
|
|
265
|
+
| `differential_evolution` | bounded global search (SciPy) — robust default | `maxiter`, `popsize`, `polish`, `workers` |
|
|
266
|
+
| `scatter_search` | population search with reference-set update and local refinement | `refset_size`, `maxiter`, `max_nfev`, `local_search` |
|
|
267
|
+
| `particle_swarm` | swarm with inertia damping and reflecting bounds | `n_particles`, `maxiter`, `inertia`, `cognitive`, `social` |
|
|
268
|
+
| `least_squares` | local Trust Region Reflective on the residual vector | `max_nfev`, `loss`, `diff_step` |
|
|
269
|
+
| `minimize` | SciPy local minimizers | `method` (`L-BFGS-B`, `Nelder-Mead`, `Powell`, ...) |
|
|
270
|
+
|
|
271
|
+
Aliases (`de`, `pso`, `ss`, `trf`, `nelder_mead`) work too. Register your own:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from pymodest.optimizers import register, OptimizerResult
|
|
275
|
+
|
|
276
|
+
@register("my_method")
|
|
277
|
+
def my_method(objective, x0, bounds, rng, **options):
|
|
278
|
+
... # objective(x) -> cost; objective.residuals(x) -> vector
|
|
279
|
+
return OptimizerResult(x=best_x, fun=best_cost, nfev=n)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The local methods (`least_squares`, `minimize`) only refine where they start.
|
|
283
|
+
Use them for a module whose parameters are already close, or after a global
|
|
284
|
+
pass; on their own they will sit in whatever basin the initial values fall in.
|
|
285
|
+
|
|
286
|
+
### Acceptance policy
|
|
287
|
+
|
|
288
|
+
`accept = "module"` (default) keeps a module fit whenever that module's own cost
|
|
289
|
+
improved — the plain divide-and-conquer rule. `accept = "total"` additionally
|
|
290
|
+
requires that the total cost across all modules did not rise.
|
|
291
|
+
|
|
292
|
+
Monotone sounds safer, but it can stall the search: an early step that improves
|
|
293
|
+
one module while temporarily worsening another is often exactly the step needed
|
|
294
|
+
to escape a poor starting region. In the shipped example, `"total"` gets stuck
|
|
295
|
+
at a cost of 0.86 while `"module"` reaches 0.060. Prefer the default unless you
|
|
296
|
+
have a specific reason.
|
|
297
|
+
|
|
298
|
+
## What a run produces
|
|
299
|
+
|
|
300
|
+
`result.save(directory)` writes:
|
|
301
|
+
|
|
302
|
+
| file | contents |
|
|
303
|
+
| ---- | -------- |
|
|
304
|
+
| `best_parameters.toml` | the estimates, ready to feed back to `pymodest simulate` |
|
|
305
|
+
| `parameters.csv` | initial value beside final estimate |
|
|
306
|
+
| `history.csv` | one row per module fit: costs before/after, evaluations, timing |
|
|
307
|
+
| `loop_summary.csv` | one row per loop: total and per-module costs |
|
|
308
|
+
| `fit_report.json` | the complete record, including every step |
|
|
309
|
+
| `predictions_<id>.csv` | simulated trace beside the observations, per dataset |
|
|
310
|
+
|
|
311
|
+
In Python, `FitResult` exposes `parameters`, `cost`, `module_costs`, `loops`,
|
|
312
|
+
`converged`, `stop_reason`, and the DataFrames `history()`, `loop_summary()`
|
|
313
|
+
and `parameter_table()`.
|
|
314
|
+
|
|
315
|
+
## Worked example
|
|
316
|
+
|
|
317
|
+
`examples/two_module_pathway/` fits a linear pathway
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
S --J1--> A --J2--> B --J3--> C --J4--> out
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
with **two models sharing one parameter set** — the wild type, and a strain in
|
|
324
|
+
which C inhibits its own uptake (adding `Ki`, which only that strain's data can
|
|
325
|
+
identify) — against **three datasets** at two substrate concentrations.
|
|
326
|
+
Parameters split into an upstream module (`Vmax1, Km1, k2`, scored on A and B)
|
|
327
|
+
and a downstream module (`Vmax3, Km3, k4, Ki`, scored on C).
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
cd examples/two_module_pathway
|
|
331
|
+
uv run python generate_data.py # regenerate the synthetic measurements
|
|
332
|
+
uv run pymodest fit config.toml
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The data carry 4% proportional noise. Evaluated at the generating parameters the
|
|
336
|
+
total cost is **0.0469** — the noise floor, the best any fit could do. Starting
|
|
337
|
+
from cost 237, the run converges in about 7 seconds to:
|
|
338
|
+
|
|
339
|
+
| | cost | mean abs. log10 error |
|
|
340
|
+
| --- | --- | --- |
|
|
341
|
+
| `differential_evolution` | 0.0604 | 0.066 |
|
|
342
|
+
| `scatter_search` | 0.0604 | 0.067 |
|
|
343
|
+
| `particle_swarm` | 0.0605 | 0.066 |
|
|
344
|
+
|
|
345
|
+
All three independent optimizers land on the same answer, which is good evidence
|
|
346
|
+
the estimator finds the true optimum of the module-wise objective. The small gap
|
|
347
|
+
to the noise floor is inherent to the method, not a failure to converge: each
|
|
348
|
+
module minimizes its own objective, so the fixed point differs slightly from the
|
|
349
|
+
joint least-squares optimum. `Ki` and `k4` are recovered closely; the `Vmax`/`Km`
|
|
350
|
+
pairs remain correlated at this noise level, as they would in any fit.
|
|
351
|
+
|
|
352
|
+
## Practical notes
|
|
353
|
+
|
|
354
|
+
- **Choose modules by what the data constrain.** A module's variables should
|
|
355
|
+
respond to its parameters more strongly than to the rest. If two parameters
|
|
356
|
+
are only identifiable together, put them in the same module.
|
|
357
|
+
- **Coupled modules need more loops.** The example's feedback strain makes the
|
|
358
|
+
upstream variables depend on a downstream parameter; that coupling is what the
|
|
359
|
+
repeated loops resolve.
|
|
360
|
+
- **Watch `loop_summary.csv`.** A total cost that oscillates rather than settles
|
|
361
|
+
means the partition is fighting itself — merge the modules that trade against
|
|
362
|
+
each other, or widen the variables scoring one of them.
|
|
363
|
+
- **A failed integration is scored, not raised.** Parameter sets that make the
|
|
364
|
+
ODEs unsolvable get a large finite cost, so a global search can step over them.
|
|
365
|
+
- **Set `seed`** in `[fitting]` for reproducible runs; every backend derives its
|
|
366
|
+
randomness from it.
|
|
367
|
+
|
|
368
|
+
## Development
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
uv sync
|
|
372
|
+
uv run pytest # 118 tests
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`uv.lock` is committed so results are reproducible. It resolves for every
|
|
376
|
+
platform at once; the pinned `antimony` and `libroadrunner` wheels cover macOS
|
|
377
|
+
(Intel and Apple silicon), Windows, and Linux on x86-64. Linux on arm64 is not
|
|
378
|
+
covered by the current `antimony` release, so on that platform install without
|
|
379
|
+
the lock (`uv pip install -e .`), which falls back to the newest version that
|
|
380
|
+
does publish an arm64 wheel.
|
|
381
|
+
|
|
382
|
+
Layout:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
src/pymodest/
|
|
386
|
+
config.py TOML schema, dataclasses, validation
|
|
387
|
+
model.py Antimony -> SBML -> roadrunner, with result caching
|
|
388
|
+
data.py wide/long experiment tables
|
|
389
|
+
objective.py residual assembly, scaling, per-module objectives
|
|
390
|
+
estimator.py the divide-and-conquer loop
|
|
391
|
+
result.py records, DataFrames, on-disk report
|
|
392
|
+
cli.py the pymodest command
|
|
393
|
+
optimizers/ registry, SciPy backends, particle swarm, scatter search
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
MIT.
|