pymodest 0.1.0__py3-none-any.whl
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/__init__.py +50 -0
- pymodest/cli.py +306 -0
- pymodest/config.py +738 -0
- pymodest/data.py +255 -0
- pymodest/estimator.py +299 -0
- pymodest/model.py +335 -0
- pymodest/objective.py +391 -0
- pymodest/optimizers/__init__.py +35 -0
- pymodest/optimizers/base.py +122 -0
- pymodest/optimizers/scatter.py +202 -0
- pymodest/optimizers/scipy_backends.py +160 -0
- pymodest/optimizers/swarm.py +113 -0
- pymodest/result.py +211 -0
- pymodest-0.1.0.dist-info/METADATA +398 -0
- pymodest-0.1.0.dist-info/RECORD +19 -0
- pymodest-0.1.0.dist-info/WHEEL +5 -0
- pymodest-0.1.0.dist-info/entry_points.txt +2 -0
- pymodest-0.1.0.dist-info/licenses/LICENSE +21 -0
- pymodest-0.1.0.dist-info/top_level.txt +1 -0
pymodest/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""pyModEst -- divide-and-conquer parameter estimation for biological models.
|
|
2
|
+
|
|
3
|
+
Models are written in Antimony, simulated with roadrunner, and fitted against
|
|
4
|
+
one or more experimental datasets. Parameters are partitioned into *modules*;
|
|
5
|
+
each module is fitted against its own measured variables while the rest of the
|
|
6
|
+
parameter set is held fixed, and the procedure cycles through the modules for a
|
|
7
|
+
finite number of loops.
|
|
8
|
+
|
|
9
|
+
Typical use::
|
|
10
|
+
|
|
11
|
+
from pymodest import load_config, fit
|
|
12
|
+
|
|
13
|
+
config = load_config("study.toml")
|
|
14
|
+
result = fit(config)
|
|
15
|
+
print(result.parameters)
|
|
16
|
+
result.save(config.output_dir)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .config import ( # noqa: F401
|
|
20
|
+
ConfigError,
|
|
21
|
+
DatasetSpec,
|
|
22
|
+
FittingSpec,
|
|
23
|
+
ModelSpec,
|
|
24
|
+
ModuleSpec,
|
|
25
|
+
ObjectiveSpec,
|
|
26
|
+
OptimizerSpec,
|
|
27
|
+
ParameterSpec,
|
|
28
|
+
SimulationSpec,
|
|
29
|
+
StudyConfig,
|
|
30
|
+
load_config,
|
|
31
|
+
)
|
|
32
|
+
from .data import DataError, ExperimentData, Measurement, load_dataset, load_datasets # noqa: F401
|
|
33
|
+
from .estimator import ModularEstimator, fit, fit_from_file # noqa: F401
|
|
34
|
+
from .model import ModelError, SimulationFailure, SimulationModel, build_models # noqa: F401
|
|
35
|
+
from .objective import ModuleObjective, Problem, ProblemError # noqa: F401
|
|
36
|
+
from .optimizers import OptimizerResult, available as available_optimizers, register # noqa: F401
|
|
37
|
+
from .result import FitResult, LoopRecord, ModuleStep # noqa: F401
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ConfigError", "DataError", "DatasetSpec", "ExperimentData", "FitResult",
|
|
43
|
+
"FittingSpec", "LoopRecord", "Measurement", "ModelError", "ModelSpec",
|
|
44
|
+
"ModularEstimator", "ModuleObjective", "ModuleSpec", "ModuleStep",
|
|
45
|
+
"ObjectiveSpec", "OptimizerResult", "OptimizerSpec", "ParameterSpec",
|
|
46
|
+
"Problem", "ProblemError", "SimulationFailure", "SimulationModel",
|
|
47
|
+
"SimulationSpec", "StudyConfig", "available_optimizers", "build_models",
|
|
48
|
+
"fit", "fit_from_file", "load_config", "load_dataset", "load_datasets",
|
|
49
|
+
"register", "__version__",
|
|
50
|
+
]
|
pymodest/cli.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Command line interface: ``pymodest <command> ...``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import __version__, optimizers
|
|
12
|
+
from .config import ConfigError, StudyConfig, load_config
|
|
13
|
+
from .data import DataError
|
|
14
|
+
from .estimator import ModularEstimator
|
|
15
|
+
from .objective import Problem, ProblemError
|
|
16
|
+
|
|
17
|
+
LOG_FORMAT = "%(message)s"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------
|
|
21
|
+
# helpers
|
|
22
|
+
# --------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def _configure_logging(verbosity: int) -> None:
|
|
25
|
+
level = logging.WARNING if verbosity < 0 else (
|
|
26
|
+
logging.DEBUG if verbosity > 0 else logging.INFO
|
|
27
|
+
)
|
|
28
|
+
logging.basicConfig(level=level, format=LOG_FORMAT, stream=sys.stdout, force=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load(path: str) -> StudyConfig:
|
|
32
|
+
return load_config(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _describe(config: StudyConfig) -> str:
|
|
36
|
+
lines = [f"study: {config.name}", ""]
|
|
37
|
+
lines.append(f"models ({len(config.models)}):")
|
|
38
|
+
for m in config.models:
|
|
39
|
+
source = m.source.name if m.source else "inline"
|
|
40
|
+
lines.append(f" - {m.id} [{source}]" + (f" {m.description}" if m.description else ""))
|
|
41
|
+
lines.append("")
|
|
42
|
+
lines.append(f"datasets ({len(config.datasets)}):")
|
|
43
|
+
for d in config.datasets:
|
|
44
|
+
source = d.file.name if d.file else "inline"
|
|
45
|
+
lines.append(f" - {d.id} model={d.model} [{source}] weight={d.weight:g}")
|
|
46
|
+
lines.append("")
|
|
47
|
+
lines.append(f"modules ({len(config.modules)}):")
|
|
48
|
+
for mod in config.modules:
|
|
49
|
+
opt = config.optimizer_for(mod)
|
|
50
|
+
lines.append(f" - {mod.id} optimizer={opt.name}")
|
|
51
|
+
lines.append(f" variables: {', '.join(mod.variables)}")
|
|
52
|
+
for p in mod.parameters:
|
|
53
|
+
flag = " (fixed)" if p.fixed else ""
|
|
54
|
+
lines.append(
|
|
55
|
+
f" parameter: {p.name:<12} [{p.lower:g}, {p.upper:g}] "
|
|
56
|
+
f"{p.scale} init={p.initial_value:g}{flag}"
|
|
57
|
+
)
|
|
58
|
+
lines.append("")
|
|
59
|
+
fitting = config.fitting
|
|
60
|
+
lines.append(
|
|
61
|
+
f"fitting: max_loops={fitting.max_loops} order={fitting.module_order} "
|
|
62
|
+
f"tol={fitting.tol:g} patience={fitting.patience}"
|
|
63
|
+
)
|
|
64
|
+
lines.append(
|
|
65
|
+
f"objective: scaling={fitting.objective.scaling} "
|
|
66
|
+
f"aggregation={fitting.objective.aggregation}"
|
|
67
|
+
)
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --------------------------------------------------------------------------
|
|
72
|
+
# commands
|
|
73
|
+
# --------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
76
|
+
config = _load(args.config)
|
|
77
|
+
print(_describe(config))
|
|
78
|
+
problem = Problem(config)
|
|
79
|
+
print("")
|
|
80
|
+
print("consistency check: models, datasets and modules agree")
|
|
81
|
+
total = problem.total_cost()
|
|
82
|
+
print(f"cost at initial parameter values: {total:.6g}")
|
|
83
|
+
for module in config.modules:
|
|
84
|
+
print(f" {module.id:<20} {problem.module_cost(module):.6g}")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_fit(args: argparse.Namespace) -> int:
|
|
89
|
+
config = _load(args.config)
|
|
90
|
+
if args.out:
|
|
91
|
+
config = config.with_output_dir(Path(args.out).resolve())
|
|
92
|
+
if args.optimizer:
|
|
93
|
+
from dataclasses import replace
|
|
94
|
+
from .config import OptimizerSpec
|
|
95
|
+
|
|
96
|
+
fitting = replace(config.fitting, optimizer=OptimizerSpec(name=args.optimizer))
|
|
97
|
+
config = replace(config, fitting=fitting)
|
|
98
|
+
if args.seed is not None:
|
|
99
|
+
from dataclasses import replace
|
|
100
|
+
|
|
101
|
+
config = replace(config, fitting=replace(config.fitting, seed=args.seed))
|
|
102
|
+
|
|
103
|
+
estimator = ModularEstimator(config)
|
|
104
|
+
result = estimator.run(max_loops=args.loops)
|
|
105
|
+
|
|
106
|
+
print("")
|
|
107
|
+
print(result.parameter_table().to_string(index=False))
|
|
108
|
+
print("")
|
|
109
|
+
print(f"total cost: {result.initial_cost:.6g} -> {result.cost:.6g}")
|
|
110
|
+
print(f"stopped because: {result.stop_reason}")
|
|
111
|
+
|
|
112
|
+
written = result.save(config.output_dir)
|
|
113
|
+
if not args.no_predictions:
|
|
114
|
+
written_predictions = estimator.write_predictions(
|
|
115
|
+
config.output_dir, result.parameters
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
written_predictions = []
|
|
119
|
+
print("")
|
|
120
|
+
print(f"results written to {config.output_dir}")
|
|
121
|
+
for path in list(written.values()) + written_predictions:
|
|
122
|
+
print(f" {path.name}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_simulate(args: argparse.Namespace) -> int:
|
|
127
|
+
config = _load(args.config)
|
|
128
|
+
if args.out:
|
|
129
|
+
config = config.with_output_dir(Path(args.out).resolve())
|
|
130
|
+
estimator = ModularEstimator(config)
|
|
131
|
+
|
|
132
|
+
values = None
|
|
133
|
+
if args.parameters:
|
|
134
|
+
try:
|
|
135
|
+
import tomllib as _toml
|
|
136
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
137
|
+
import tomli as _toml # type: ignore
|
|
138
|
+
with open(args.parameters, "rb") as handle:
|
|
139
|
+
table = _toml.load(handle)
|
|
140
|
+
values = {k: float(v) for k, v in table.get("parameters", table).items()}
|
|
141
|
+
estimator.problem.set_values(
|
|
142
|
+
{k: v for k, v in values.items() if k in estimator.problem.parameter_specs}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
paths = estimator.write_predictions(config.output_dir, estimator.problem.snapshot())
|
|
146
|
+
print(f"cost at these parameters: {estimator.problem.total_cost():.6g}")
|
|
147
|
+
print(f"predictions written to {config.output_dir}")
|
|
148
|
+
for path in paths:
|
|
149
|
+
print(f" {path.name}")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def cmd_optimizers(_: argparse.Namespace) -> int:
|
|
154
|
+
print("available optimizer backends:")
|
|
155
|
+
for name in optimizers.available():
|
|
156
|
+
print(f" {name}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
TEMPLATE = '''# pyModEst study configuration
|
|
161
|
+
#
|
|
162
|
+
# Fitting proceeds module by module: each module's parameters are optimized
|
|
163
|
+
# against that module's variables while all other parameters stay fixed, and
|
|
164
|
+
# the loop repeats until the total cost stops improving.
|
|
165
|
+
|
|
166
|
+
[study]
|
|
167
|
+
name = "my-study"
|
|
168
|
+
output_dir = "results"
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------- models ---
|
|
171
|
+
# Several models may share one fitted parameter set.
|
|
172
|
+
[[models]]
|
|
173
|
+
id = "wt"
|
|
174
|
+
antimony_file = "models/wt.ant"
|
|
175
|
+
# [models.overrides] # values pinned for this model only
|
|
176
|
+
# [models.observables] # derived quantities, e.g. Total = "A + B"
|
|
177
|
+
|
|
178
|
+
# -------------------------------------------------------------- datasets ---
|
|
179
|
+
# Each dataset is measured on one model, under its own conditions.
|
|
180
|
+
[[datasets]]
|
|
181
|
+
id = "exp1"
|
|
182
|
+
model = "wt"
|
|
183
|
+
file = "data/exp1.csv"
|
|
184
|
+
format = "wide" # wide: time,A,B,... long: time,variable,value
|
|
185
|
+
weight = 1.0
|
|
186
|
+
# [datasets.conditions] # parameters set for this experiment
|
|
187
|
+
# [datasets.initial_conditions] # species starting values
|
|
188
|
+
|
|
189
|
+
# --------------------------------------------------------------- modules ---
|
|
190
|
+
[[modules]]
|
|
191
|
+
id = "module_one"
|
|
192
|
+
variables = ["A", "B"] # measured variables scoring this module
|
|
193
|
+
|
|
194
|
+
[[modules.parameters]]
|
|
195
|
+
name = "k1"
|
|
196
|
+
lower = 1e-3
|
|
197
|
+
upper = 1e2
|
|
198
|
+
scale = "log"
|
|
199
|
+
|
|
200
|
+
[[modules.parameters]]
|
|
201
|
+
name = "Km1"
|
|
202
|
+
lower = 1e-2
|
|
203
|
+
upper = 1e3
|
|
204
|
+
scale = "log"
|
|
205
|
+
|
|
206
|
+
# --------------------------------------------------------------- fitting ---
|
|
207
|
+
[fitting]
|
|
208
|
+
max_loops = 12
|
|
209
|
+
module_order = "as_listed" # or a list of module ids, "random", "round_robin_reversed"
|
|
210
|
+
tol = 1e-4 # relative improvement that counts as progress
|
|
211
|
+
atol = 1e-12 # absolute floor, so a cost heading to zero terminates
|
|
212
|
+
patience = 2 # loops without progress before stopping
|
|
213
|
+
accept = "module" # "module" keeps a step that helps its own module;
|
|
214
|
+
# "total" also requires the overall cost not to rise
|
|
215
|
+
seed = 0
|
|
216
|
+
|
|
217
|
+
[fitting.optimizer]
|
|
218
|
+
name = "differential_evolution"
|
|
219
|
+
maxiter = 60
|
|
220
|
+
popsize = 15
|
|
221
|
+
|
|
222
|
+
[fitting.objective]
|
|
223
|
+
scaling = "relative" # relative | absolute | sigma | max_normalized
|
|
224
|
+
aggregation = "mean"
|
|
225
|
+
|
|
226
|
+
[fitting.simulation]
|
|
227
|
+
integrator = "cvode"
|
|
228
|
+
relative_tolerance = 1e-8
|
|
229
|
+
absolute_tolerance = 1e-10
|
|
230
|
+
'''
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cmd_template(args: argparse.Namespace) -> int:
|
|
234
|
+
if args.out:
|
|
235
|
+
path = Path(args.out)
|
|
236
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
path.write_text(TEMPLATE)
|
|
238
|
+
print(f"wrote {path}")
|
|
239
|
+
else:
|
|
240
|
+
print(TEMPLATE)
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# --------------------------------------------------------------------------
|
|
245
|
+
# argument parsing
|
|
246
|
+
# --------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
249
|
+
parser = argparse.ArgumentParser(
|
|
250
|
+
prog="pymodest",
|
|
251
|
+
description="Divide-and-conquer parameter estimation for biological models.",
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument("--version", action="version", version=f"pymodest {__version__}")
|
|
254
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="only warnings and errors")
|
|
255
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="debug logging")
|
|
256
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
257
|
+
|
|
258
|
+
p = sub.add_parser("validate", help="load a study and report what it contains")
|
|
259
|
+
p.add_argument("config", help="path to the study TOML file")
|
|
260
|
+
p.set_defaults(func=cmd_validate)
|
|
261
|
+
|
|
262
|
+
p = sub.add_parser("fit", help="run the module-wise parameter estimation")
|
|
263
|
+
p.add_argument("config", help="path to the study TOML file")
|
|
264
|
+
p.add_argument("--loops", type=int, default=None, help="override max_loops")
|
|
265
|
+
p.add_argument("--out", default=None, help="override the output directory")
|
|
266
|
+
p.add_argument("--optimizer", default=None, help="override the default optimizer")
|
|
267
|
+
p.add_argument("--seed", type=int, default=None, help="override the random seed")
|
|
268
|
+
p.add_argument(
|
|
269
|
+
"--no-predictions", action="store_true", help="skip writing simulated traces"
|
|
270
|
+
)
|
|
271
|
+
p.set_defaults(func=cmd_fit)
|
|
272
|
+
|
|
273
|
+
p = sub.add_parser("simulate", help="simulate the study at given parameter values")
|
|
274
|
+
p.add_argument("config", help="path to the study TOML file")
|
|
275
|
+
p.add_argument(
|
|
276
|
+
"--parameters", default=None, help="TOML file with a [parameters] table"
|
|
277
|
+
)
|
|
278
|
+
p.add_argument("--out", default=None, help="override the output directory")
|
|
279
|
+
p.set_defaults(func=cmd_simulate)
|
|
280
|
+
|
|
281
|
+
p = sub.add_parser("optimizers", help="list the registered optimizer backends")
|
|
282
|
+
p.set_defaults(func=cmd_optimizers)
|
|
283
|
+
|
|
284
|
+
p = sub.add_parser("template", help="print a commented starter configuration")
|
|
285
|
+
p.add_argument("--out", default=None, help="write to this file instead of stdout")
|
|
286
|
+
p.set_defaults(func=cmd_template)
|
|
287
|
+
|
|
288
|
+
return parser
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
292
|
+
parser = build_parser()
|
|
293
|
+
args = parser.parse_args(argv)
|
|
294
|
+
_configure_logging(-1 if args.quiet else (1 if args.verbose else 0))
|
|
295
|
+
try:
|
|
296
|
+
return int(args.func(args))
|
|
297
|
+
except (ConfigError, DataError, ProblemError) as exc:
|
|
298
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
299
|
+
return 2
|
|
300
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
301
|
+
print("interrupted", file=sys.stderr)
|
|
302
|
+
return 130
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__": # pragma: no cover
|
|
306
|
+
raise SystemExit(main())
|