PyAntiGen 2.0.3__tar.gz → 2.1.1__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.3 → pyantigen-2.1.1}/PKG-INFO +1 -1
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/PKG-INFO +1 -1
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/SOURCES.txt +3 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/scm_file_list.json +10 -0
- pyantigen-2.1.1/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/_version.py +3 -3
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Evaluator.py +87 -5
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Fit_cache.py +53 -1
- pyantigen-2.1.1/pyantigen/engine/Model_cache.py +75 -0
- pyantigen-2.1.1/pyantigen/engine/Multistart.py +502 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Optimize.py +754 -51
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Preequil_cache.py +5 -2
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Profile_plan.py +56 -15
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Results.py +38 -4
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Simulate.py +83 -16
- pyantigen-2.1.1/pyantigen/engine/Stop_rules.py +205 -0
- pyantigen-2.0.3/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-2.0.3 → pyantigen-2.1.1}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/.gitignore +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/.vscode/launch.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/LICENSE +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/MANIFEST.in +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/README.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/docs/MULTISTART.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/.gitignore +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/__init__.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/cli.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Fast_profile.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Identifiability.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Model_optimize.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Profile_checkpoint.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/rate_laws.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/design.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/lower_v1.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/optimization.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/params.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Model_run.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/pyproject.toml +0 -0
- {pyantigen-2.0.3 → pyantigen-2.1.1}/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
|
|
@@ -45,6 +47,7 @@ pyantigen/engine/README.md
|
|
|
45
47
|
pyantigen/engine/Results.py
|
|
46
48
|
pyantigen/engine/Sensitivity_analysis.py
|
|
47
49
|
pyantigen/engine/Simulate.py
|
|
50
|
+
pyantigen/engine/Stop_rules.py
|
|
48
51
|
pyantigen/engine/__init__.py
|
|
49
52
|
pyantigen/generate/AntimonyGen.py
|
|
50
53
|
pyantigen/generate/RxnDict_to_antimony.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",
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
"pyantigen/engine/Results.py",
|
|
38
40
|
"pyantigen/engine/Sensitivity_analysis.py",
|
|
39
41
|
"pyantigen/engine/Simulate.py",
|
|
42
|
+
"pyantigen/engine/Stop_rules.py",
|
|
40
43
|
"pyantigen/engine/__init__.py",
|
|
41
44
|
"pyantigen/generate/AntimonyGen.py",
|
|
42
45
|
"pyantigen/generate/RxnDict_to_antimony.py",
|
|
@@ -104,19 +107,26 @@
|
|
|
104
107
|
"tests/engine/test_fast_profile.py",
|
|
105
108
|
"tests/engine/test_fit_cache.py",
|
|
106
109
|
"tests/engine/test_hessian_steps.py",
|
|
110
|
+
"tests/engine/test_model_cache.py",
|
|
111
|
+
"tests/engine/test_multistart_candidates.py",
|
|
112
|
+
"tests/engine/test_multistart_gradient.py",
|
|
113
|
+
"tests/engine/test_multistart_resume.py",
|
|
107
114
|
"tests/engine/test_multistart_sampler.py",
|
|
108
115
|
"tests/engine/test_multistart_stage.py",
|
|
116
|
+
"tests/engine/test_multistart_triage.py",
|
|
109
117
|
"tests/engine/test_nelder_mead.py",
|
|
110
118
|
"tests/engine/test_point_identity.py",
|
|
111
119
|
"tests/engine/test_profile_grid_open.py",
|
|
112
120
|
"tests/engine/test_profile_quadratic.py",
|
|
113
121
|
"tests/engine/test_profile_records.py",
|
|
114
122
|
"tests/engine/test_profile_stream.py",
|
|
123
|
+
"tests/engine/test_results_csv_policy.py",
|
|
115
124
|
"tests/engine/test_search_mode.py",
|
|
116
125
|
"tests/engine/test_sigma_floor.py",
|
|
117
126
|
"tests/engine/test_slice_screen.py",
|
|
118
127
|
"tests/engine/test_slice_window.py",
|
|
119
128
|
"tests/engine/test_solver_fingerprint.py",
|
|
129
|
+
"tests/engine/test_stop_rules.py",
|
|
120
130
|
"tests/engine/test_tolerance_floor.py",
|
|
121
131
|
"tests/engine/test_wald_units.py",
|
|
122
132
|
"tests/generate/test_gen_antimony_utils.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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6282b0d51'
|
|
@@ -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)
|
|
@@ -327,6 +353,11 @@ def _profile_task(job):
|
|
|
327
353
|
out.pop("frozen_sigmas", None)
|
|
328
354
|
# A location on this machine's disk, meaningless in a stored record.
|
|
329
355
|
out.pop("state_path", None)
|
|
356
|
+
# An input as well: the settings for the shadow stop rules. What they
|
|
357
|
+
# observed comes back as `trace`, `shadow_stops` and `stop_reason` below;
|
|
358
|
+
# `trace` and `shadow_stops` are also inputs when a point is resumed, and
|
|
359
|
+
# are overwritten with the continued history.
|
|
360
|
+
stop_rules = out.pop("stop_rules", None)
|
|
330
361
|
out.update({"nll": None, "status": "ok", "n_evals": 0, "wall_s": 0.0,
|
|
331
362
|
"worker": os.getpid(), "converged": True, "nit": -1,
|
|
332
363
|
"nfev": -1, "opt_message": "", "interrupted": False})
|
|
@@ -358,6 +389,12 @@ def _profile_task(job):
|
|
|
358
389
|
# because a point stopped before its first simplex is complete has no
|
|
359
390
|
# sorted vertex to read it from.
|
|
360
391
|
best = {"f": float("inf"), "x": x_start}
|
|
392
|
+
# The shadow stop rules' watcher (pyantigen.engine.Stop_rules). It only
|
|
393
|
+
# observes -- nothing it computes feeds back into the optimizer -- and
|
|
394
|
+
# is None unless the job asked for it. `base` is the point's evaluation
|
|
395
|
+
# count when this job began, so a resumed point numbers on from where
|
|
396
|
+
# it stopped.
|
|
397
|
+
shadow = {"mon": None, "base": 0}
|
|
361
398
|
|
|
362
399
|
# Every profile point pins each floored block at its own sigma_used
|
|
363
400
|
# from the fit rather than letting it re-concentrate (see
|
|
@@ -378,6 +415,12 @@ def _profile_task(job):
|
|
|
378
415
|
if np.isfinite(v) and v < best["f"]:
|
|
379
416
|
best["f"] = float(v)
|
|
380
417
|
best["x"] = x_arr.copy()
|
|
418
|
+
if shadow["mon"] is not None:
|
|
419
|
+
try:
|
|
420
|
+
shadow["mon"].update(shadow["base"] + eval_count["n"],
|
|
421
|
+
best["f"])
|
|
422
|
+
except Exception: # a watcher must never fail a point
|
|
423
|
+
shadow["mon"] = None
|
|
381
424
|
return v
|
|
382
425
|
|
|
383
426
|
def time_is_up():
|
|
@@ -393,7 +436,8 @@ def _profile_task(job):
|
|
|
393
436
|
# is just the objective at the fixed value -- exact by definition.
|
|
394
437
|
nll = raw_objective(x_start, x_fixed)
|
|
395
438
|
x_opt = x_start
|
|
396
|
-
out.update({"converged": True, "nit": 0, "nfev": 1
|
|
439
|
+
out.update({"converged": True, "nit": 0, "nfev": 1,
|
|
440
|
+
"stop_reason": "exact"})
|
|
397
441
|
eval_count["n"] = 1
|
|
398
442
|
else:
|
|
399
443
|
options, extra_kwargs = nuisance_options(
|
|
@@ -403,6 +447,19 @@ def _profile_task(job):
|
|
|
403
447
|
resumable = str(method).lower() == "nelder-mead" and can_run(
|
|
404
448
|
options, extra_kwargs)
|
|
405
449
|
|
|
450
|
+
def start_shadow(base):
|
|
451
|
+
if not stop_rules or stop_rules.get("anchor") is None:
|
|
452
|
+
return
|
|
453
|
+
try:
|
|
454
|
+
from pyantigen.engine.Stop_rules import PointMonitor
|
|
455
|
+
shadow["mon"] = PointMonitor(
|
|
456
|
+
stop_rules["anchor"], max_fev, x_start.size,
|
|
457
|
+
cfg=stop_rules, trace=job.get("trace"),
|
|
458
|
+
fired=job.get("shadow_stops"))
|
|
459
|
+
shadow["base"] = int(base)
|
|
460
|
+
except Exception: # a watcher must never fail a point
|
|
461
|
+
shadow["mon"] = None
|
|
462
|
+
|
|
406
463
|
res = None
|
|
407
464
|
outcome = "done"
|
|
408
465
|
if resumable:
|
|
@@ -431,6 +488,7 @@ def _profile_task(job):
|
|
|
431
488
|
# run, and nothing to save either.
|
|
432
489
|
outcome = "capped"
|
|
433
490
|
else:
|
|
491
|
+
start_shadow(nm_state["nfev"])
|
|
434
492
|
interval = float(job.get("state_interval_s",
|
|
435
493
|
POINT_STATE_INTERVAL_S))
|
|
436
494
|
last_save = [0.0]
|
|
@@ -464,6 +522,8 @@ def _profile_task(job):
|
|
|
464
522
|
if nfev_used >= max_fev or nit_used >= max_it:
|
|
465
523
|
outcome = "capped"
|
|
466
524
|
else:
|
|
525
|
+
start_shadow(nfev_used)
|
|
526
|
+
|
|
467
527
|
def guarded(x_nuisance, fixed_val):
|
|
468
528
|
v = nuisance_objective(x_nuisance, fixed_val)
|
|
469
529
|
if time_is_up():
|
|
@@ -523,6 +583,20 @@ def _profile_task(job):
|
|
|
523
583
|
out["nm_simplex"] = np.asarray(nm_state["sim"],
|
|
524
584
|
dtype=float).tolist()
|
|
525
585
|
|
|
586
|
+
# Why this job's optimization ended, for reading a finished run:
|
|
587
|
+
# `converged` alone cannot tell a point that met its tolerance from
|
|
588
|
+
# one that spent its cap and happened to be at the minimum, and at
|
|
589
|
+
# a 1500 cap most points are the second kind.
|
|
590
|
+
if outcome == "interrupted":
|
|
591
|
+
out["stop_reason"] = "interrupted"
|
|
592
|
+
elif res is None or not np.isfinite(getattr(res, "fun", np.inf)):
|
|
593
|
+
out["stop_reason"] = "budget-spent"
|
|
594
|
+
elif outcome == "capped" or not getattr(res, "success", True):
|
|
595
|
+
out["stop_reason"] = ("maxiter" if getattr(res, "status", None) == 2
|
|
596
|
+
else "maxfev")
|
|
597
|
+
else:
|
|
598
|
+
out["stop_reason"] = "tolerance"
|
|
599
|
+
|
|
526
600
|
if nm_state is not None:
|
|
527
601
|
nfev_total, nit_total = nm_state["nfev"], nm_state["nit"]
|
|
528
602
|
else:
|
|
@@ -539,6 +613,11 @@ def _profile_task(job):
|
|
|
539
613
|
"nit_total": nit_total,
|
|
540
614
|
"status": "ok" if np.isfinite(nll) and nll < FAILURE_VALUE else "sentinel",
|
|
541
615
|
})
|
|
616
|
+
if shadow["mon"] is not None:
|
|
617
|
+
try:
|
|
618
|
+
out.update(shadow["mon"].finish(nfev_total, best["f"]))
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
542
621
|
except Exception as exc:
|
|
543
622
|
out.update({"status": f"error: {type(exc).__name__}: {exc}",
|
|
544
623
|
"nll": FAILURE_VALUE, "interrupted": False})
|
|
@@ -791,9 +870,12 @@ class ParallelEvaluator:
|
|
|
791
870
|
# -- evaluation --------------------------------------------------------
|
|
792
871
|
|
|
793
872
|
def evaluate_batch(self, xs, label=None, heartbeat_s=_HEARTBEAT_SECONDS,
|
|
794
|
-
frozen_sigmas=None):
|
|
873
|
+
frozen_sigmas=None, eval_mode=None):
|
|
795
874
|
"""Evaluate every parameter vector in *xs*; return losses in input order.
|
|
796
875
|
|
|
876
|
+
``eval_mode`` ({"retry_limits": (attempts, depth), "relax": factor})
|
|
877
|
+
runs each evaluation under those solver limits; see :func:`_eval_context`.
|
|
878
|
+
|
|
797
879
|
Uses submit/wait, not map -- see profile_batch's docstring for the
|
|
798
880
|
general reasoning. map() (the previous implementation here) returns
|
|
799
881
|
nothing until the WHOLE batch is done, so one slow straggler among
|
|
@@ -833,7 +915,7 @@ class ParallelEvaluator:
|
|
|
833
915
|
flush=True)
|
|
834
916
|
|
|
835
917
|
try:
|
|
836
|
-
futures = {self._pool.submit(_eval_task, x, frozen_sigmas): i
|
|
918
|
+
futures = {self._pool.submit(_eval_task, x, frozen_sigmas, eval_mode): i
|
|
837
919
|
for i, x in enumerate(xs)}
|
|
838
920
|
except RuntimeError as exc:
|
|
839
921
|
if "bootstrapping phase" in str(exc):
|
|
@@ -161,7 +161,7 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
161
161
|
# spec a default stage does not invalidate the fits already cached.
|
|
162
162
|
if int(n_starts or 1) <= 1:
|
|
163
163
|
for k in ("multistart_method", "multistart_optimizer_kwargs",
|
|
164
|
-
"multistart_limits"):
|
|
164
|
+
"multistart_limits", "multistart_triage", "multistart_gradient"):
|
|
165
165
|
kwargs.pop(k, None)
|
|
166
166
|
|
|
167
167
|
key = {
|
|
@@ -387,6 +387,58 @@ class FitCache:
|
|
|
387
387
|
data["complete"] = block
|
|
388
388
|
return self._write(data)
|
|
389
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
|
+
|
|
390
442
|
def save_partial(self, x_lin, fun, n_evals=None, force=False, nm_state=None,
|
|
391
443
|
interval=None, de_state=None, cal_x_lin=None):
|
|
392
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
|