PyAntiGen 1.0.9__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.
- framework/AntimonyGen.py +48 -0
- framework/RxnDict_to_antimony.py +594 -0
- framework/TelluriumGen.py +16 -0
- framework/__init__.py +0 -0
- framework/antimony_utils.py +294 -0
- framework/cli.py +229 -0
- framework/data_interpolation.py +340 -0
- framework/isotopomer_tools.py +41 -0
- framework/model_generation.py +46 -0
- framework/models.py +189 -0
- framework/module_base.py +42 -0
- framework/pyantigen.py +51 -0
- framework/rate_laws.py +101 -0
- framework/reaction_creation.py +43 -0
- framework/template/Example/AntiGen_paths.py +23 -0
- framework/template/Example/Engine/Anchor_cache.py +193 -0
- framework/template/Example/Engine/Deadline.py +535 -0
- framework/template/Example/Engine/Evaluator.py +1176 -0
- framework/template/Example/Engine/Event_times.py +491 -0
- framework/template/Example/Engine/Fast_profile.py +701 -0
- framework/template/Example/Engine/Fit_cache.py +329 -0
- framework/template/Example/Engine/Identifiability.py +698 -0
- framework/template/Example/Engine/Model_optimize.py +1483 -0
- framework/template/Example/Engine/Model_simulate.py +124 -0
- framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
- framework/template/Example/Engine/Optimize.py +6862 -0
- framework/template/Example/Engine/Petab_export.py +398 -0
- framework/template/Example/Engine/Preequil_cache.py +361 -0
- framework/template/Example/Engine/Profile_checkpoint.py +399 -0
- framework/template/Example/Engine/Results.py +395 -0
- framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
- framework/template/Example/Engine/Simulate.py +617 -0
- framework/template/Example/Flipflop_reference.py +401 -0
- framework/template/Example/Model_generate.py +37 -0
- framework/template/Example/Model_run.py +261 -0
- framework/template/Example/Modules/Data.py +63 -0
- framework/template/Example/Modules/Events.py +14 -0
- framework/template/Example/Modules/Experiment.py +194 -0
- framework/template/Example/Modules/Loss_config.py +61 -0
- framework/template/Example/Modules/Observed_species.py +3 -0
- framework/template/Example/Modules/Optimizer_settings.py +258 -0
- framework/template/Example/Modules/Plots.py +89 -0
- framework/template/Example/Modules/Solver_settings.py +16 -0
- framework/template/Example/Modules/Update_opt_parameters.py +24 -0
- framework/template/Example/Modules/Update_parameters.py +49 -0
- framework/template/data/ADneg.csv +27 -0
- framework/template/data/ADpos.csv +27 -0
- framework/template/data/Flipflop.csv +29 -0
- framework/template/data/make_flipflop_data.py +174 -0
- pyantigen-1.0.9.dist-info/METADATA +129 -0
- pyantigen-1.0.9.dist-info/RECORD +55 -0
- pyantigen-1.0.9.dist-info/WHEEL +5 -0
- pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
- pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
- pyantigen-1.0.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""The fitted optimum, saved the moment the fit ends and reused on relaunch.
|
|
2
|
+
|
|
3
|
+
The profile is anchored on the fit that precedes it: every point is centred on
|
|
4
|
+
``res.x`` and every dNLL is measured against ``nll(res.x)``. Inside one process
|
|
5
|
+
that anchor is simply handed from the fit to the profile. The problem is the
|
|
6
|
+
process does not survive. On a preemptible partition the whole script is
|
|
7
|
+
restarted every 4-5 hours, and a restart with ``fit_mode="optimize"`` used to
|
|
8
|
+
re-run the fit from the spec's x0 -- hours of serial Nelder-Mead charged again
|
|
9
|
+
on every link, before a single profile point could start.
|
|
10
|
+
|
|
11
|
+
That refit is also what put the profile's checkpoint at risk. The checkpoint
|
|
12
|
+
directory is named from a hash that includes the optimum, so a refit that lands
|
|
13
|
+
even six significant figures away from the last one opens a *new* directory and
|
|
14
|
+
the profile starts over. Nelder-Mead from an identical start is deterministic,
|
|
15
|
+
so in practice the refit usually lands on the same point; but "usually" is not
|
|
16
|
+
a property a five-day run should rest on.
|
|
17
|
+
|
|
18
|
+
So the fit is cached. The key is the *problem* -- model text, parameter names,
|
|
19
|
+
the spec's x0, bounds, scaling, groups, method, the optimizer's own settings,
|
|
20
|
+
the multi-start configuration, the solver settings and the data -- and the
|
|
21
|
+
value is where the fit landed. A relaunch that poses the same problem gets the
|
|
22
|
+
same answer without paying for it; any change in the problem is a miss, and
|
|
23
|
+
the fit runs as it always has.
|
|
24
|
+
|
|
25
|
+
Two records live under one key:
|
|
26
|
+
|
|
27
|
+
* ``complete`` -- a fit that returned. Reused as the optimum outright.
|
|
28
|
+
* ``partial`` -- the best point seen so far by a fit that has not returned.
|
|
29
|
+
Written on a throttle from inside the objective, so that a fit killed at
|
|
30
|
+
hour three restarts from hour three's best point rather than from x0. The
|
|
31
|
+
simplex itself is not saved; Nelder-Mead rebuilds one around the point,
|
|
32
|
+
which costs n+1 evaluations rather than the hours already spent.
|
|
33
|
+
|
|
34
|
+
Layout::
|
|
35
|
+
|
|
36
|
+
results/<MODEL>/fits/<tag>_<model_hash>_<fit_hash>.json
|
|
37
|
+
|
|
38
|
+
beside the ``profiles/`` tree the checkpoint uses, and following the same
|
|
39
|
+
write-temp-then-rename discipline: a kill mid-write leaves a temp file, never
|
|
40
|
+
a truncated record.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import time
|
|
47
|
+
from datetime import datetime
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
from Engine.Profile_checkpoint import _safe_name, sweep_stale_temp_files
|
|
52
|
+
|
|
53
|
+
# Bumped when the meaning of a stored record changes, so old files miss.
|
|
54
|
+
_FORMAT = "fit-v1"
|
|
55
|
+
|
|
56
|
+
# How often the partial record may be rewritten. An evaluation costs tens of
|
|
57
|
+
# seconds on the specs this exists for, so once a minute is at most one write
|
|
58
|
+
# per few evaluations and negligible against them; on a toy problem that runs
|
|
59
|
+
# thousands of evaluations a second it stops the objective becoming a disk
|
|
60
|
+
# benchmark.
|
|
61
|
+
PARTIAL_SAVE_INTERVAL_S = 60.0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _json_default(obj):
|
|
65
|
+
"""Make optimizer settings hashable: arrays to lists, the rest to repr."""
|
|
66
|
+
if isinstance(obj, np.ndarray):
|
|
67
|
+
return obj.tolist()
|
|
68
|
+
if isinstance(obj, (np.floating, np.integer)):
|
|
69
|
+
return obj.item()
|
|
70
|
+
return repr(obj)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def data_fingerprint(models):
|
|
74
|
+
"""Hash of every data table the objective reads.
|
|
75
|
+
|
|
76
|
+
Nothing else in the fingerprint sees the data: the model text and the spec
|
|
77
|
+
say which files are read, not what is in them. Editing a data file changes
|
|
78
|
+
the objective without changing anything a hash of the code would notice,
|
|
79
|
+
and a cached optimum for the *old* data would then anchor a profile of the
|
|
80
|
+
new one. The tables are already loaded on every replicate by the time the
|
|
81
|
+
fit could be looked up, so hashing their contents is cheap.
|
|
82
|
+
|
|
83
|
+
Every value that cannot be hashed contributes its type name only. That is a
|
|
84
|
+
weaker key, not a broken one, and it is deterministic, which is what makes
|
|
85
|
+
a stale record a miss rather than a wrong hit.
|
|
86
|
+
"""
|
|
87
|
+
h = hashlib.sha256()
|
|
88
|
+
|
|
89
|
+
def _feed(v):
|
|
90
|
+
if v is None:
|
|
91
|
+
h.update(b"None")
|
|
92
|
+
elif hasattr(v, "to_csv"):
|
|
93
|
+
# pandas: the CSV text is a stable, complete rendering.
|
|
94
|
+
h.update(v.to_csv(index=True).encode("utf-8"))
|
|
95
|
+
elif isinstance(v, np.ndarray):
|
|
96
|
+
h.update(repr(v.shape).encode("utf-8"))
|
|
97
|
+
h.update(np.ascontiguousarray(v).tobytes())
|
|
98
|
+
elif isinstance(v, dict):
|
|
99
|
+
for k in sorted(v, key=str):
|
|
100
|
+
h.update(str(k).encode("utf-8"))
|
|
101
|
+
_feed(v[k])
|
|
102
|
+
elif isinstance(v, (list, tuple)):
|
|
103
|
+
for item in v:
|
|
104
|
+
_feed(item)
|
|
105
|
+
elif isinstance(v, (str, bytes, int, float, bool, np.generic)):
|
|
106
|
+
h.update(repr(v).encode("utf-8"))
|
|
107
|
+
else:
|
|
108
|
+
h.update(type(v).__name__.encode("utf-8"))
|
|
109
|
+
|
|
110
|
+
for sim_name in sorted(models or {}, key=str):
|
|
111
|
+
h.update(str(sim_name).encode("utf-8"))
|
|
112
|
+
try:
|
|
113
|
+
_feed((models[sim_name] or {}).get("df_dict"))
|
|
114
|
+
except Exception:
|
|
115
|
+
h.update(b"unhashable")
|
|
116
|
+
return h.hexdigest()[:16]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
120
|
+
method, optimizer_kwargs, n_starts=1, start_seed=None,
|
|
121
|
+
search_decades=None, solver_hash=None, data_hash=None):
|
|
122
|
+
"""Hashes identifying a fit *problem*, before it has been solved.
|
|
123
|
+
|
|
124
|
+
Deliberately distinct from :func:`Engine.Profile_checkpoint.spec_fingerprint`,
|
|
125
|
+
which hashes the *answer* (the optimum) and so cannot be used to look the
|
|
126
|
+
answer up. Everything that decides where Nelder-Mead lands from here is
|
|
127
|
+
included; nothing that only decides what is done with the result (the
|
|
128
|
+
profile's own settings, the diagnostics requested) is.
|
|
129
|
+
|
|
130
|
+
x0 is in the key on purpose. Two fits from different starting points are
|
|
131
|
+
two different fits -- Nelder-Mead is local -- and copying fitted values
|
|
132
|
+
into the registry, which is how this project moves a fit forward, changes
|
|
133
|
+
x0 and so asks for a fresh fit from there. That is the right behaviour;
|
|
134
|
+
the cache is for the *same* launch repeated, not for skipping fits.
|
|
135
|
+
"""
|
|
136
|
+
model_hash = hashlib.sha256((model_text or "").encode("utf-8")).hexdigest()[:16]
|
|
137
|
+
|
|
138
|
+
def _r(v):
|
|
139
|
+
try:
|
|
140
|
+
v = float(v)
|
|
141
|
+
except (TypeError, ValueError):
|
|
142
|
+
return None
|
|
143
|
+
return round(v, 12) if np.isfinite(v) else None
|
|
144
|
+
|
|
145
|
+
kwargs = dict(optimizer_kwargs or {})
|
|
146
|
+
# Profile-only keys configure what happens after the fit, not the fit.
|
|
147
|
+
for k in ("profile_method", "profile_optimizer_kwargs", "profile_grid",
|
|
148
|
+
"profile_without_opt"):
|
|
149
|
+
kwargs.pop(k, None)
|
|
150
|
+
|
|
151
|
+
blob = json.dumps(
|
|
152
|
+
{
|
|
153
|
+
"format": _FORMAT,
|
|
154
|
+
"param_names": list(param_names),
|
|
155
|
+
"x0": [_r(v) for v in np.atleast_1d(x0_lin)],
|
|
156
|
+
"bounds": None if bounds_lin is None else [
|
|
157
|
+
None if b is None else [_r(v) for v in b] for b in bounds_lin
|
|
158
|
+
],
|
|
159
|
+
"scales": list(scales),
|
|
160
|
+
"groups": sorted(groups.keys()) if hasattr(groups, "keys") else None,
|
|
161
|
+
"method": str(method).lower(),
|
|
162
|
+
"optimizer_kwargs": kwargs,
|
|
163
|
+
"n_starts": int(n_starts or 1),
|
|
164
|
+
"start_seed": start_seed,
|
|
165
|
+
"search_decades": search_decades,
|
|
166
|
+
"solver": solver_hash,
|
|
167
|
+
"data": data_hash,
|
|
168
|
+
},
|
|
169
|
+
sort_keys=True, default=_json_default,
|
|
170
|
+
)
|
|
171
|
+
fit_hash = hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
|
|
172
|
+
return model_hash, fit_hash
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FitCache:
|
|
176
|
+
"""Reads and writes the fitted optimum for one fit problem."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, root, tag, model_hash, fit_hash, n_params, enabled=True):
|
|
179
|
+
self.enabled = bool(enabled and root)
|
|
180
|
+
self.model_hash = model_hash
|
|
181
|
+
self.fit_hash = fit_hash
|
|
182
|
+
self.n_params = int(n_params)
|
|
183
|
+
self.dir = os.path.join(root, "fits") if root else None
|
|
184
|
+
self._name = f"{_safe_name(tag)}_{model_hash[:8]}_{fit_hash[:8]}.json"
|
|
185
|
+
self._last_partial_save = 0.0
|
|
186
|
+
if self.enabled and self.dir:
|
|
187
|
+
try:
|
|
188
|
+
os.makedirs(self.dir, exist_ok=True)
|
|
189
|
+
sweep_stale_temp_files(self.dir)
|
|
190
|
+
except OSError:
|
|
191
|
+
self.enabled = False
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def path(self):
|
|
195
|
+
return os.path.join(self.dir, self._name) if self.dir else None
|
|
196
|
+
|
|
197
|
+
# -- reading -----------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _read(self):
|
|
200
|
+
"""The record on disk, or None on any miss.
|
|
201
|
+
|
|
202
|
+
Every failure is a miss rather than an error: a corrupt or stale file
|
|
203
|
+
must cost the refit it was meant to save, never the run.
|
|
204
|
+
"""
|
|
205
|
+
if not (self.enabled and self.path and os.path.exists(self.path)):
|
|
206
|
+
return None
|
|
207
|
+
try:
|
|
208
|
+
with open(self.path, "r", encoding="utf-8") as fh:
|
|
209
|
+
data = json.load(fh)
|
|
210
|
+
except (OSError, ValueError):
|
|
211
|
+
return None
|
|
212
|
+
if (not isinstance(data, dict)
|
|
213
|
+
or data.get("format") != _FORMAT
|
|
214
|
+
or data.get("model_hash") != self.model_hash
|
|
215
|
+
or data.get("fit_hash") != self.fit_hash
|
|
216
|
+
or int(data.get("n_params") or -1) != self.n_params):
|
|
217
|
+
return None
|
|
218
|
+
return data
|
|
219
|
+
|
|
220
|
+
def _vector(self, block):
|
|
221
|
+
if not isinstance(block, dict):
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
x = np.asarray(block.get("x_lin"), dtype=float)
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return None
|
|
227
|
+
if x.shape != (self.n_params,) or not np.all(np.isfinite(x)):
|
|
228
|
+
return None
|
|
229
|
+
return x
|
|
230
|
+
|
|
231
|
+
def load_complete(self):
|
|
232
|
+
"""A finished fit for this problem: ``{"x_lin", "fun", ...}`` or None."""
|
|
233
|
+
data = self._read()
|
|
234
|
+
if data is None:
|
|
235
|
+
return None
|
|
236
|
+
block = data.get("complete")
|
|
237
|
+
x = self._vector(block)
|
|
238
|
+
if x is None:
|
|
239
|
+
return None
|
|
240
|
+
out = dict(block)
|
|
241
|
+
out["x_lin"] = x
|
|
242
|
+
out["path"] = self.path
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
def load_partial(self):
|
|
246
|
+
"""The best point of an unfinished fit, or None."""
|
|
247
|
+
data = self._read()
|
|
248
|
+
if data is None:
|
|
249
|
+
return None
|
|
250
|
+
block = data.get("partial")
|
|
251
|
+
x = self._vector(block)
|
|
252
|
+
if x is None:
|
|
253
|
+
return None
|
|
254
|
+
out = dict(block)
|
|
255
|
+
out["x_lin"] = x
|
|
256
|
+
out["path"] = self.path
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
# -- writing -----------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _write(self, data):
|
|
262
|
+
if not (self.enabled and self.path):
|
|
263
|
+
return False
|
|
264
|
+
data = dict(data)
|
|
265
|
+
data.update({
|
|
266
|
+
"format": _FORMAT,
|
|
267
|
+
"model_hash": self.model_hash,
|
|
268
|
+
"fit_hash": self.fit_hash,
|
|
269
|
+
"n_params": self.n_params,
|
|
270
|
+
})
|
|
271
|
+
tmp = f"{self.path}.{os.getpid()}.tmp"
|
|
272
|
+
try:
|
|
273
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
274
|
+
json.dump(data, fh, indent=2)
|
|
275
|
+
os.replace(tmp, self.path)
|
|
276
|
+
return True
|
|
277
|
+
except (OSError, TypeError, ValueError):
|
|
278
|
+
try:
|
|
279
|
+
os.unlink(tmp)
|
|
280
|
+
except OSError:
|
|
281
|
+
pass
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _block(x_lin, fun, **extra):
|
|
286
|
+
block = {
|
|
287
|
+
"x_lin": [float(v) for v in np.atleast_1d(x_lin)],
|
|
288
|
+
"fun": None if fun is None or not np.isfinite(fun) else float(fun),
|
|
289
|
+
"saved": datetime.now().isoformat(timespec="seconds"),
|
|
290
|
+
}
|
|
291
|
+
block.update(extra)
|
|
292
|
+
return block
|
|
293
|
+
|
|
294
|
+
def save_complete(self, x_lin, fun, param_names=None, **extra):
|
|
295
|
+
"""Record a finished fit. Clears any partial record: it is superseded."""
|
|
296
|
+
if not self.enabled:
|
|
297
|
+
return False
|
|
298
|
+
block = self._block(x_lin, fun, **extra)
|
|
299
|
+
if param_names is not None:
|
|
300
|
+
# Registry-shaped, so the file is readable by a person too.
|
|
301
|
+
block["parameters"] = {
|
|
302
|
+
str(n): float(v) for n, v in zip(param_names, np.atleast_1d(x_lin))
|
|
303
|
+
}
|
|
304
|
+
data = self._read() or {}
|
|
305
|
+
data.pop("partial", None)
|
|
306
|
+
data["complete"] = block
|
|
307
|
+
return self._write(data)
|
|
308
|
+
|
|
309
|
+
def save_partial(self, x_lin, fun, n_evals=None, force=False):
|
|
310
|
+
"""Record the best point so far, at most once per interval.
|
|
311
|
+
|
|
312
|
+
Never overwrites a ``complete`` record: a finished fit is strictly
|
|
313
|
+
better information than any point along the way to it, and a partial
|
|
314
|
+
from a *later* process (a relaunch that found no complete record yet,
|
|
315
|
+
then raced one that did) must not demote it.
|
|
316
|
+
"""
|
|
317
|
+
if not self.enabled:
|
|
318
|
+
return False
|
|
319
|
+
now = time.time()
|
|
320
|
+
if not force and now - self._last_partial_save < PARTIAL_SAVE_INTERVAL_S:
|
|
321
|
+
return False
|
|
322
|
+
data = self._read() or {}
|
|
323
|
+
if data.get("complete") is not None:
|
|
324
|
+
return False
|
|
325
|
+
data["partial"] = self._block(x_lin, fun, n_evals=n_evals)
|
|
326
|
+
ok = self._write(data)
|
|
327
|
+
if ok:
|
|
328
|
+
self._last_partial_save = now
|
|
329
|
+
return ok
|