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.
Files changed (55) hide show
  1. framework/AntimonyGen.py +48 -0
  2. framework/RxnDict_to_antimony.py +594 -0
  3. framework/TelluriumGen.py +16 -0
  4. framework/__init__.py +0 -0
  5. framework/antimony_utils.py +294 -0
  6. framework/cli.py +229 -0
  7. framework/data_interpolation.py +340 -0
  8. framework/isotopomer_tools.py +41 -0
  9. framework/model_generation.py +46 -0
  10. framework/models.py +189 -0
  11. framework/module_base.py +42 -0
  12. framework/pyantigen.py +51 -0
  13. framework/rate_laws.py +101 -0
  14. framework/reaction_creation.py +43 -0
  15. framework/template/Example/AntiGen_paths.py +23 -0
  16. framework/template/Example/Engine/Anchor_cache.py +193 -0
  17. framework/template/Example/Engine/Deadline.py +535 -0
  18. framework/template/Example/Engine/Evaluator.py +1176 -0
  19. framework/template/Example/Engine/Event_times.py +491 -0
  20. framework/template/Example/Engine/Fast_profile.py +701 -0
  21. framework/template/Example/Engine/Fit_cache.py +329 -0
  22. framework/template/Example/Engine/Identifiability.py +698 -0
  23. framework/template/Example/Engine/Model_optimize.py +1483 -0
  24. framework/template/Example/Engine/Model_simulate.py +124 -0
  25. framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
  26. framework/template/Example/Engine/Optimize.py +6862 -0
  27. framework/template/Example/Engine/Petab_export.py +398 -0
  28. framework/template/Example/Engine/Preequil_cache.py +361 -0
  29. framework/template/Example/Engine/Profile_checkpoint.py +399 -0
  30. framework/template/Example/Engine/Results.py +395 -0
  31. framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
  32. framework/template/Example/Engine/Simulate.py +617 -0
  33. framework/template/Example/Flipflop_reference.py +401 -0
  34. framework/template/Example/Model_generate.py +37 -0
  35. framework/template/Example/Model_run.py +261 -0
  36. framework/template/Example/Modules/Data.py +63 -0
  37. framework/template/Example/Modules/Events.py +14 -0
  38. framework/template/Example/Modules/Experiment.py +194 -0
  39. framework/template/Example/Modules/Loss_config.py +61 -0
  40. framework/template/Example/Modules/Observed_species.py +3 -0
  41. framework/template/Example/Modules/Optimizer_settings.py +258 -0
  42. framework/template/Example/Modules/Plots.py +89 -0
  43. framework/template/Example/Modules/Solver_settings.py +16 -0
  44. framework/template/Example/Modules/Update_opt_parameters.py +24 -0
  45. framework/template/Example/Modules/Update_parameters.py +49 -0
  46. framework/template/data/ADneg.csv +27 -0
  47. framework/template/data/ADpos.csv +27 -0
  48. framework/template/data/Flipflop.csv +29 -0
  49. framework/template/data/make_flipflop_data.py +174 -0
  50. pyantigen-1.0.9.dist-info/METADATA +129 -0
  51. pyantigen-1.0.9.dist-info/RECORD +55 -0
  52. pyantigen-1.0.9.dist-info/WHEEL +5 -0
  53. pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
  54. pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
  55. 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