PyAntiGen 2.0.2__tar.gz → 2.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.
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PKG-INFO +1 -1
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/PKG-INFO +1 -1
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/SOURCES.txt +2 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/scm_file_list.json +9 -0
- pyantigen-2.1.0/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/_version.py +3 -3
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Evaluator.py +33 -4
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Fit_cache.py +62 -2
- pyantigen-2.1.0/pyantigen/engine/Model_cache.py +75 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Model_optimize.py +15 -5
- pyantigen-2.1.0/pyantigen/engine/Multistart.py +502 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Optimize.py +836 -27
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Preequil_cache.py +5 -2
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Results.py +38 -4
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Simulate.py +141 -18
- pyantigen-2.0.2/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-2.0.2 → pyantigen-2.1.0}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/.gitignore +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/.vscode/launch.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/LICENSE +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/MANIFEST.in +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/README.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/docs/MULTISTART.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/.gitignore +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/cli.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Fast_profile.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Identifiability.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Profile_checkpoint.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Profile_plan.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/rate_laws.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/design.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/lower_v1.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/optimization.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/params.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Model_run.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/pyproject.toml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.1.0}/setup.cfg +0 -0
|
@@ -32,8 +32,10 @@ pyantigen/engine/Event_times.py
|
|
|
32
32
|
pyantigen/engine/Fast_profile.py
|
|
33
33
|
pyantigen/engine/Fit_cache.py
|
|
34
34
|
pyantigen/engine/Identifiability.py
|
|
35
|
+
pyantigen/engine/Model_cache.py
|
|
35
36
|
pyantigen/engine/Model_optimize.py
|
|
36
37
|
pyantigen/engine/Model_simulate.py
|
|
38
|
+
pyantigen/engine/Multistart.py
|
|
37
39
|
pyantigen/engine/Nelder_mead.py
|
|
38
40
|
pyantigen/engine/Noise_floor.py
|
|
39
41
|
pyantigen/engine/Optimize.py
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
"pyantigen/engine/Fast_profile.py",
|
|
25
25
|
"pyantigen/engine/Fit_cache.py",
|
|
26
26
|
"pyantigen/engine/Identifiability.py",
|
|
27
|
+
"pyantigen/engine/Model_cache.py",
|
|
27
28
|
"pyantigen/engine/Model_optimize.py",
|
|
28
29
|
"pyantigen/engine/Model_simulate.py",
|
|
30
|
+
"pyantigen/engine/Multistart.py",
|
|
29
31
|
"pyantigen/engine/Nelder_mead.py",
|
|
30
32
|
"pyantigen/engine/Noise_floor.py",
|
|
31
33
|
"pyantigen/engine/Optimize.py",
|
|
@@ -104,13 +106,20 @@
|
|
|
104
106
|
"tests/engine/test_fast_profile.py",
|
|
105
107
|
"tests/engine/test_fit_cache.py",
|
|
106
108
|
"tests/engine/test_hessian_steps.py",
|
|
109
|
+
"tests/engine/test_model_cache.py",
|
|
110
|
+
"tests/engine/test_multistart_candidates.py",
|
|
111
|
+
"tests/engine/test_multistart_gradient.py",
|
|
112
|
+
"tests/engine/test_multistart_resume.py",
|
|
107
113
|
"tests/engine/test_multistart_sampler.py",
|
|
114
|
+
"tests/engine/test_multistart_stage.py",
|
|
115
|
+
"tests/engine/test_multistart_triage.py",
|
|
108
116
|
"tests/engine/test_nelder_mead.py",
|
|
109
117
|
"tests/engine/test_point_identity.py",
|
|
110
118
|
"tests/engine/test_profile_grid_open.py",
|
|
111
119
|
"tests/engine/test_profile_quadratic.py",
|
|
112
120
|
"tests/engine/test_profile_records.py",
|
|
113
121
|
"tests/engine/test_profile_stream.py",
|
|
122
|
+
"tests/engine/test_results_csv_policy.py",
|
|
114
123
|
"tests/engine/test_search_mode.py",
|
|
115
124
|
"tests/engine/test_sigma_floor.py",
|
|
116
125
|
"tests/engine/test_slice_screen.py",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g664f039a1'
|
|
@@ -239,14 +239,40 @@ def _worker_nll(x, frozen_sigmas=None):
|
|
|
239
239
|
)
|
|
240
240
|
|
|
241
241
|
|
|
242
|
-
def
|
|
242
|
+
def _eval_context(eval_mode):
|
|
243
|
+
"""Context for one task: the solver limits an ``eval_mode`` asks for.
|
|
244
|
+
|
|
245
|
+
``eval_mode`` is a plain dict so it pickles cheaply:
|
|
246
|
+
|
|
247
|
+
{"retry_limits": (max_attempts, max_depth), "relax": factor}
|
|
248
|
+
|
|
249
|
+
Applied around ONE task and undone after it, never at worker start-up: both
|
|
250
|
+
settings are process-global, and the same worker goes on to serve profile
|
|
251
|
+
points and diagnostics that must run exactly as configured. Absent or empty,
|
|
252
|
+
this does nothing.
|
|
253
|
+
"""
|
|
254
|
+
from contextlib import ExitStack
|
|
255
|
+
|
|
256
|
+
stack = ExitStack()
|
|
257
|
+
if eval_mode:
|
|
258
|
+
from pyantigen.engine.Simulate import relaxed_tolerances, retry_limits
|
|
259
|
+
limits = eval_mode.get("retry_limits")
|
|
260
|
+
if limits:
|
|
261
|
+
stack.enter_context(retry_limits(limits[0], limits[1]))
|
|
262
|
+
if eval_mode.get("relax"):
|
|
263
|
+
stack.enter_context(relaxed_tolerances(eval_mode["relax"]))
|
|
264
|
+
return stack
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _eval_task(x, frozen_sigmas=None, eval_mode=None):
|
|
243
268
|
"""Evaluate one parameter vector. Never raises across the pool boundary."""
|
|
244
269
|
if _WORKER["spec"] is None:
|
|
245
270
|
return (FAILURE_VALUE, "worker-not-initialized", 0.0)
|
|
246
271
|
|
|
247
272
|
t0 = time.time()
|
|
248
273
|
try:
|
|
249
|
-
|
|
274
|
+
with _eval_context(eval_mode):
|
|
275
|
+
val = _worker_nll(x, frozen_sigmas=frozen_sigmas)
|
|
250
276
|
_WORKER["n_evals"] += 1
|
|
251
277
|
status = "ok" if np.isfinite(val) and val < FAILURE_VALUE else "sentinel"
|
|
252
278
|
return (float(val), status, time.time() - t0)
|
|
@@ -791,9 +817,12 @@ class ParallelEvaluator:
|
|
|
791
817
|
# -- evaluation --------------------------------------------------------
|
|
792
818
|
|
|
793
819
|
def evaluate_batch(self, xs, label=None, heartbeat_s=_HEARTBEAT_SECONDS,
|
|
794
|
-
frozen_sigmas=None):
|
|
820
|
+
frozen_sigmas=None, eval_mode=None):
|
|
795
821
|
"""Evaluate every parameter vector in *xs*; return losses in input order.
|
|
796
822
|
|
|
823
|
+
``eval_mode`` ({"retry_limits": (attempts, depth), "relax": factor})
|
|
824
|
+
runs each evaluation under those solver limits; see :func:`_eval_context`.
|
|
825
|
+
|
|
797
826
|
Uses submit/wait, not map -- see profile_batch's docstring for the
|
|
798
827
|
general reasoning. map() (the previous implementation here) returns
|
|
799
828
|
nothing until the WHOLE batch is done, so one slow straggler among
|
|
@@ -833,7 +862,7 @@ class ParallelEvaluator:
|
|
|
833
862
|
flush=True)
|
|
834
863
|
|
|
835
864
|
try:
|
|
836
|
-
futures = {self._pool.submit(_eval_task, x, frozen_sigmas): i
|
|
865
|
+
futures = {self._pool.submit(_eval_task, x, frozen_sigmas, eval_mode): i
|
|
837
866
|
for i, x in enumerate(xs)}
|
|
838
867
|
except RuntimeError as exc:
|
|
839
868
|
if "bootstrapping phase" in str(exc):
|
|
@@ -156,6 +156,13 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
156
156
|
for k in ("profile_method", "profile_optimizer_kwargs", "profile_grid",
|
|
157
157
|
"profile_without_opt"):
|
|
158
158
|
kwargs.pop(k, None)
|
|
159
|
+
# The multi-start stage decides where the fit starts, which matters only
|
|
160
|
+
# when there is more than one start. Dropped otherwise so that giving every
|
|
161
|
+
# spec a default stage does not invalidate the fits already cached.
|
|
162
|
+
if int(n_starts or 1) <= 1:
|
|
163
|
+
for k in ("multistart_method", "multistart_optimizer_kwargs",
|
|
164
|
+
"multistart_limits", "multistart_triage", "multistart_gradient"):
|
|
165
|
+
kwargs.pop(k, None)
|
|
159
166
|
|
|
160
167
|
key = {
|
|
161
168
|
"format": _FORMAT,
|
|
@@ -175,8 +182,9 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
175
182
|
"data": data_hash,
|
|
176
183
|
}
|
|
177
184
|
# Left out at the default so fits cached before the sampler existed keep
|
|
178
|
-
# their hash
|
|
179
|
-
|
|
185
|
+
# their hash, and left out at one start, where nothing is sampled, so that
|
|
186
|
+
# a project-wide default of "sobol" does not invalidate single-start fits.
|
|
187
|
+
if int(n_starts or 1) > 1 and (start_sampler or "lhs") != "lhs":
|
|
180
188
|
key["start_sampler"] = start_sampler
|
|
181
189
|
blob = json.dumps(key, sort_keys=True, default=_json_default)
|
|
182
190
|
fit_hash = hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
|
|
@@ -379,6 +387,58 @@ class FitCache:
|
|
|
379
387
|
data["complete"] = block
|
|
380
388
|
return self._write(data)
|
|
381
389
|
|
|
390
|
+
def save_stage(self, state):
|
|
391
|
+
"""Checkpoint of the multi-start stage in progress.
|
|
392
|
+
|
|
393
|
+
Written after every candidate scored and every local fit finished, so a
|
|
394
|
+
kill costs the evaluation or fit in flight and nothing before it. Kept
|
|
395
|
+
under its own key: the stage is not the fit, and the fit's partial must
|
|
396
|
+
not appear until the stage is over (see ``_persist_stage_result``). Must
|
|
397
|
+
already be plain JSON data.
|
|
398
|
+
"""
|
|
399
|
+
if not self.enabled:
|
|
400
|
+
return False
|
|
401
|
+
data = self._read() or {}
|
|
402
|
+
data["multistart_stage"] = state
|
|
403
|
+
return self._write(data)
|
|
404
|
+
|
|
405
|
+
def load_stage(self):
|
|
406
|
+
"""The stage checkpoint :meth:`save_stage` kept, or None."""
|
|
407
|
+
data = self._read()
|
|
408
|
+
block = data.get("multistart_stage") if data else None
|
|
409
|
+
return block if isinstance(block, dict) else None
|
|
410
|
+
|
|
411
|
+
def clear_stage(self):
|
|
412
|
+
"""Drop the stage checkpoint, once the stage's result is on record."""
|
|
413
|
+
if not self.enabled:
|
|
414
|
+
return False
|
|
415
|
+
data = self._read()
|
|
416
|
+
if not data or "multistart_stage" not in data:
|
|
417
|
+
return False
|
|
418
|
+
data.pop("multistart_stage")
|
|
419
|
+
return self._write(data)
|
|
420
|
+
|
|
421
|
+
def save_multistart(self, report):
|
|
422
|
+
"""Keep the multi-start stage's report with the fit it produced.
|
|
423
|
+
|
|
424
|
+
Stored beside the ``complete``/``partial`` records, not inside them, so it
|
|
425
|
+
survives both: a relaunch that resumes the standard fit skips the stage,
|
|
426
|
+
and one that reuses a finished fit skips both, yet the result written at
|
|
427
|
+
the end should still say how the starting point was found. *report* must
|
|
428
|
+
already be plain JSON data (see ``Multistart.json_safe``).
|
|
429
|
+
"""
|
|
430
|
+
if not self.enabled:
|
|
431
|
+
return False
|
|
432
|
+
data = self._read() or {}
|
|
433
|
+
data["multistart"] = report
|
|
434
|
+
return self._write(data)
|
|
435
|
+
|
|
436
|
+
def load_multistart(self):
|
|
437
|
+
"""The report :meth:`save_multistart` kept, or None."""
|
|
438
|
+
data = self._read()
|
|
439
|
+
block = data.get("multistart") if data else None
|
|
440
|
+
return block if isinstance(block, dict) else None
|
|
441
|
+
|
|
382
442
|
def save_partial(self, x_lin, fun, n_evals=None, force=False, nm_state=None,
|
|
383
443
|
interval=None, de_state=None, cal_x_lin=None):
|
|
384
444
|
"""Record the best point so far, at most once per *interval*.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Per-model caches, stored on the RoadRunner instance without touching ``__getattr__``.
|
|
2
|
+
|
|
3
|
+
The engine keeps a few values on each RoadRunner so they are computed once per
|
|
4
|
+
model rather than once per evaluation (the state keys, the available symbols,
|
|
5
|
+
the parameter baseline, the configured tolerance). The obvious way to read one
|
|
6
|
+
back -- ``try: r._x`` / ``except AttributeError``, or ``getattr(r, "_x", None)`` --
|
|
7
|
+
is a trap, because RoadRunner defines::
|
|
8
|
+
|
|
9
|
+
def __getattr__(self, name):
|
|
10
|
+
if name != "this" and name in self._getIds(SelectionRecord_ALL):
|
|
11
|
+
return self[name]
|
|
12
|
+
raise AttributeError(name)
|
|
13
|
+
|
|
14
|
+
``__getattr__`` runs only after normal lookup has failed, so every *miss* builds
|
|
15
|
+
the list of every selection id the model has, to learn that the name is not one
|
|
16
|
+
of them. That list is roughly quadratic in model size: about a millisecond on a
|
|
17
|
+
25-species model, over a second on 400 species, and on a model with ~9,000 ODEs
|
|
18
|
+
~70 million ids, about 18 minutes and 16 GB per miss. A model pays it once per
|
|
19
|
+
cache slot per instance, which is why small models never notice and a large one
|
|
20
|
+
spends an hour before its first result.
|
|
21
|
+
|
|
22
|
+
Reading through the instance ``__dict__`` never reaches ``__getattr__``, so a
|
|
23
|
+
miss costs a dictionary lookup whatever the model size. Writing needs no care:
|
|
24
|
+
RoadRunner defines no ``__setattr__``, so ``r._x = v`` is an ordinary
|
|
25
|
+
instance-dict write.
|
|
26
|
+
|
|
27
|
+
Use :func:`get` and :func:`put` for every engine-private attribute on a
|
|
28
|
+
RoadRunner. ``tests/engine/test_model_cache.py`` fails if any of the engine's own
|
|
29
|
+
cache reads causes a ``_getIds`` call.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _target(r):
|
|
34
|
+
"""The object whose ``__dict__`` holds the caches.
|
|
35
|
+
|
|
36
|
+
``OptRoadRunnerProxy`` wraps a RoadRunner in ``_r`` and forwards attribute
|
|
37
|
+
writes to it, so a cache written through the proxy lives on the RoadRunner.
|
|
38
|
+
Reading ``vars(proxy)`` would look in the proxy's own dict instead, which is
|
|
39
|
+
never where the value is.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
d = vars(r)
|
|
43
|
+
except TypeError: # no __dict__ (slots, builtins): nothing cached
|
|
44
|
+
return None
|
|
45
|
+
inner = d.get("_r")
|
|
46
|
+
if inner is not None and "_opt_param_names" in d:
|
|
47
|
+
return inner
|
|
48
|
+
return r
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get(r, name, default=None):
|
|
52
|
+
"""The value cached on *r* under *name*, or *default*. Never calls ``__getattr__``."""
|
|
53
|
+
t = _target(r)
|
|
54
|
+
if t is None:
|
|
55
|
+
return default
|
|
56
|
+
try:
|
|
57
|
+
return vars(t).get(name, default)
|
|
58
|
+
except TypeError:
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def put(r, name, value):
|
|
63
|
+
"""Cache *value* on *r* under *name*. Returns whether it was stored.
|
|
64
|
+
|
|
65
|
+
A target that refuses attributes is tolerated, as the call sites always did:
|
|
66
|
+
the value is then recomputed next time instead of reused.
|
|
67
|
+
"""
|
|
68
|
+
t = _target(r)
|
|
69
|
+
if t is None:
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
vars(t)[name] = value
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
@@ -973,12 +973,20 @@ def _slice_dnll_at_optimum(param_vals, dnll, opt_val, rtol=1e-6):
|
|
|
973
973
|
return float(y[i])
|
|
974
974
|
|
|
975
975
|
|
|
976
|
-
def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="ALL"
|
|
977
|
-
|
|
976
|
+
def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="ALL",
|
|
977
|
+
slice_settings=None):
|
|
978
|
+
"""Run likelihood slice once per parameter and save plot.
|
|
979
|
+
|
|
980
|
+
slice_settings is the spec's ``slice_settings`` ({"n_points", "range_factor"});
|
|
981
|
+
absent keys keep the values that used to be fixed here, 20 and 2.0.
|
|
982
|
+
"""
|
|
978
983
|
from datetime import datetime
|
|
979
984
|
import numpy as np
|
|
980
985
|
import matplotlib.pyplot as plt
|
|
981
986
|
|
|
987
|
+
n_points = int((slice_settings or {}).get("n_points", 20))
|
|
988
|
+
range_factor = float((slice_settings or {}).get("range_factor", 2.0))
|
|
989
|
+
|
|
982
990
|
slice_func = opt.get("stats", {}).get("likelihood_slice")
|
|
983
991
|
if not slice_func:
|
|
984
992
|
print("Warning: no likelihood_slice closure in opt['stats'] — skipping plot.")
|
|
@@ -1004,7 +1012,7 @@ def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="AL
|
|
|
1004
1012
|
results = None
|
|
1005
1013
|
if slice_all is not None:
|
|
1006
1014
|
try:
|
|
1007
|
-
results = slice_all(n_points=
|
|
1015
|
+
results = slice_all(n_points=n_points, range_factor=range_factor)
|
|
1008
1016
|
except Exception as exc:
|
|
1009
1017
|
print(f" batched slice failed ({exc}); falling back per parameter.")
|
|
1010
1018
|
|
|
@@ -1014,7 +1022,7 @@ def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="AL
|
|
|
1014
1022
|
if results is not None and pname in results:
|
|
1015
1023
|
pv, nr = results[pname]
|
|
1016
1024
|
else:
|
|
1017
|
-
pv, nr = slice_func(i, n_points=
|
|
1025
|
+
pv, nr = slice_func(i, n_points=n_points, range_factor=range_factor)
|
|
1018
1026
|
cache[i] = (pv, nr)
|
|
1019
1027
|
slice_traces[pname] = {"x": np.asarray(pv).tolist(),
|
|
1020
1028
|
"y": np.asarray(nr).tolist()}
|
|
@@ -1233,7 +1241,9 @@ def setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_d
|
|
|
1233
1241
|
|
|
1234
1242
|
if settings.get("slice_analysis") and opt.get("stats", {}).get("likelihood_slice"):
|
|
1235
1243
|
_save_likelihood_slice_plot(opt, optimization_settings.param_names, paths["plot_path"],
|
|
1236
|
-
MODEL_NAME, tag=groups_tag
|
|
1244
|
+
MODEL_NAME, tag=groups_tag,
|
|
1245
|
+
slice_settings=getattr(optimization_settings,
|
|
1246
|
+
"slice_settings", None))
|
|
1237
1247
|
if (settings.get("profile_likelihood_analysis") or settings.get("fast_profile_likelihood_analysis")) and opt.get("stats", {}).get("profile_likelihood"):
|
|
1238
1248
|
_save_profile_likelihood_plot(
|
|
1239
1249
|
opt, optimization_settings.param_names, paths["plot_path"],
|