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 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())