PyAntiGen 2.1.0__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.1.0 → pyantigen-2.1.1}/PKG-INFO +1 -1
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/PKG-INFO +1 -1
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/SOURCES.txt +1 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/scm_file_list.json +2 -0
- pyantigen-2.1.1/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/_version.py +3 -3
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Evaluator.py +54 -1
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Optimize.py +34 -2
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Profile_plan.py +56 -15
- pyantigen-2.1.1/pyantigen/engine/Stop_rules.py +205 -0
- pyantigen-2.1.0/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-2.1.0 → pyantigen-2.1.1}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/.gitignore +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/.vscode/launch.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/LICENSE +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/MANIFEST.in +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/README.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/docs/MULTISTART.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.gitignore +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/__init__.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/cli.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Fast_profile.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Fit_cache.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Identifiability.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Model_cache.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Model_optimize.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Multistart.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Preequil_cache.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Profile_checkpoint.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Results.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/Simulate.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/rate_laws.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/design.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/lower_v1.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/optimization.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/params.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Model_run.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/pyproject.toml +0 -0
- {pyantigen-2.1.0 → pyantigen-2.1.1}/setup.cfg +0 -0
|
@@ -47,6 +47,7 @@ pyantigen/engine/README.md
|
|
|
47
47
|
pyantigen/engine/Results.py
|
|
48
48
|
pyantigen/engine/Sensitivity_analysis.py
|
|
49
49
|
pyantigen/engine/Simulate.py
|
|
50
|
+
pyantigen/engine/Stop_rules.py
|
|
50
51
|
pyantigen/engine/__init__.py
|
|
51
52
|
pyantigen/generate/AntimonyGen.py
|
|
52
53
|
pyantigen/generate/RxnDict_to_antimony.py
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"pyantigen/engine/Results.py",
|
|
40
40
|
"pyantigen/engine/Sensitivity_analysis.py",
|
|
41
41
|
"pyantigen/engine/Simulate.py",
|
|
42
|
+
"pyantigen/engine/Stop_rules.py",
|
|
42
43
|
"pyantigen/engine/__init__.py",
|
|
43
44
|
"pyantigen/generate/AntimonyGen.py",
|
|
44
45
|
"pyantigen/generate/RxnDict_to_antimony.py",
|
|
@@ -125,6 +126,7 @@
|
|
|
125
126
|
"tests/engine/test_slice_screen.py",
|
|
126
127
|
"tests/engine/test_slice_window.py",
|
|
127
128
|
"tests/engine/test_solver_fingerprint.py",
|
|
129
|
+
"tests/engine/test_stop_rules.py",
|
|
128
130
|
"tests/engine/test_tolerance_floor.py",
|
|
129
131
|
"tests/engine/test_wald_units.py",
|
|
130
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.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 1,
|
|
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'
|
|
@@ -353,6 +353,11 @@ def _profile_task(job):
|
|
|
353
353
|
out.pop("frozen_sigmas", None)
|
|
354
354
|
# A location on this machine's disk, meaningless in a stored record.
|
|
355
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)
|
|
356
361
|
out.update({"nll": None, "status": "ok", "n_evals": 0, "wall_s": 0.0,
|
|
357
362
|
"worker": os.getpid(), "converged": True, "nit": -1,
|
|
358
363
|
"nfev": -1, "opt_message": "", "interrupted": False})
|
|
@@ -384,6 +389,12 @@ def _profile_task(job):
|
|
|
384
389
|
# because a point stopped before its first simplex is complete has no
|
|
385
390
|
# sorted vertex to read it from.
|
|
386
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}
|
|
387
398
|
|
|
388
399
|
# Every profile point pins each floored block at its own sigma_used
|
|
389
400
|
# from the fit rather than letting it re-concentrate (see
|
|
@@ -404,6 +415,12 @@ def _profile_task(job):
|
|
|
404
415
|
if np.isfinite(v) and v < best["f"]:
|
|
405
416
|
best["f"] = float(v)
|
|
406
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
|
|
407
424
|
return v
|
|
408
425
|
|
|
409
426
|
def time_is_up():
|
|
@@ -419,7 +436,8 @@ def _profile_task(job):
|
|
|
419
436
|
# is just the objective at the fixed value -- exact by definition.
|
|
420
437
|
nll = raw_objective(x_start, x_fixed)
|
|
421
438
|
x_opt = x_start
|
|
422
|
-
out.update({"converged": True, "nit": 0, "nfev": 1
|
|
439
|
+
out.update({"converged": True, "nit": 0, "nfev": 1,
|
|
440
|
+
"stop_reason": "exact"})
|
|
423
441
|
eval_count["n"] = 1
|
|
424
442
|
else:
|
|
425
443
|
options, extra_kwargs = nuisance_options(
|
|
@@ -429,6 +447,19 @@ def _profile_task(job):
|
|
|
429
447
|
resumable = str(method).lower() == "nelder-mead" and can_run(
|
|
430
448
|
options, extra_kwargs)
|
|
431
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
|
+
|
|
432
463
|
res = None
|
|
433
464
|
outcome = "done"
|
|
434
465
|
if resumable:
|
|
@@ -457,6 +488,7 @@ def _profile_task(job):
|
|
|
457
488
|
# run, and nothing to save either.
|
|
458
489
|
outcome = "capped"
|
|
459
490
|
else:
|
|
491
|
+
start_shadow(nm_state["nfev"])
|
|
460
492
|
interval = float(job.get("state_interval_s",
|
|
461
493
|
POINT_STATE_INTERVAL_S))
|
|
462
494
|
last_save = [0.0]
|
|
@@ -490,6 +522,8 @@ def _profile_task(job):
|
|
|
490
522
|
if nfev_used >= max_fev or nit_used >= max_it:
|
|
491
523
|
outcome = "capped"
|
|
492
524
|
else:
|
|
525
|
+
start_shadow(nfev_used)
|
|
526
|
+
|
|
493
527
|
def guarded(x_nuisance, fixed_val):
|
|
494
528
|
v = nuisance_objective(x_nuisance, fixed_val)
|
|
495
529
|
if time_is_up():
|
|
@@ -549,6 +583,20 @@ def _profile_task(job):
|
|
|
549
583
|
out["nm_simplex"] = np.asarray(nm_state["sim"],
|
|
550
584
|
dtype=float).tolist()
|
|
551
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
|
+
|
|
552
600
|
if nm_state is not None:
|
|
553
601
|
nfev_total, nit_total = nm_state["nfev"], nm_state["nit"]
|
|
554
602
|
else:
|
|
@@ -565,6 +613,11 @@ def _profile_task(job):
|
|
|
565
613
|
"nit_total": nit_total,
|
|
566
614
|
"status": "ok" if np.isfinite(nll) and nll < FAILURE_VALUE else "sentinel",
|
|
567
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
|
|
568
621
|
except Exception as exc:
|
|
569
622
|
out.update({"status": f"error: {type(exc).__name__}: {exc}",
|
|
570
623
|
"nll": FAILURE_VALUE, "interrupted": False})
|
|
@@ -5166,7 +5166,8 @@ def _carry_resume_state(keep, res):
|
|
|
5166
5166
|
allowance never advancing and its optimizer state never moving.
|
|
5167
5167
|
"""
|
|
5168
5168
|
for field in ("interrupted", "converged", "nfev_total", "nit_total",
|
|
5169
|
-
"nm_simplex", "opt_message"
|
|
5169
|
+
"nm_simplex", "opt_message", "stop_reason", "trace",
|
|
5170
|
+
"shadow_stops"):
|
|
5170
5171
|
if field in res:
|
|
5171
5172
|
keep[field] = res[field]
|
|
5172
5173
|
return keep
|
|
@@ -5233,10 +5234,17 @@ def run_parallel_profile(
|
|
|
5233
5234
|
max_extend=8, extend_growth=2.0, bracket_rtol=0.05,
|
|
5234
5235
|
screen=None, open_decades=None, grid_spacing="linear",
|
|
5235
5236
|
adaptive_pass1=False, max_step_decades=0.7, n_workers=None,
|
|
5236
|
-
first_probe="max",
|
|
5237
|
+
first_probe="max", shadow_stop_rules=True,
|
|
5237
5238
|
):
|
|
5238
5239
|
"""Profile likelihood for every parameter as parallel batches.
|
|
5239
5240
|
|
|
5241
|
+
*shadow_stop_rules* attaches a watcher to every point (see
|
|
5242
|
+
:mod:`pyantigen.engine.Stop_rules`): a sparse trace of the best value reached
|
|
5243
|
+
and the evaluation at which each candidate stop rule would first have
|
|
5244
|
+
fired, written into the record, and summarised at the end of the run. It
|
|
5245
|
+
never stops anything. ``False`` turns it off; a dict overrides the rule
|
|
5246
|
+
constants.
|
|
5247
|
+
|
|
5240
5248
|
Five passes:
|
|
5241
5249
|
1. coarse grid, cold-started from the optimum, fully parallel;
|
|
5242
5250
|
2. outward extension: any side whose points all sit below the threshold
|
|
@@ -5369,6 +5377,15 @@ def run_parallel_profile(
|
|
|
5369
5377
|
# this one's travel into a rate. See _predicted_travel.
|
|
5370
5378
|
"x_step": x_step,
|
|
5371
5379
|
}
|
|
5380
|
+
if shadow_stop_rules:
|
|
5381
|
+
cfg = dict(shadow_stop_rules) if isinstance(shadow_stop_rules, dict) else {}
|
|
5382
|
+
cfg.setdefault("threshold", float(threshold))
|
|
5383
|
+
cfg["anchor"] = float(nll_at_optimum)
|
|
5384
|
+
job["stop_rules"] = cfg
|
|
5385
|
+
if resume_from is not None:
|
|
5386
|
+
# One history across launches of the same point.
|
|
5387
|
+
job["trace"] = resume_from.get("trace")
|
|
5388
|
+
job["shadow_stops"] = resume_from.get("shadow_stops")
|
|
5372
5389
|
# Every job carries an explicit starting simplex, from its neighbour
|
|
5373
5390
|
# where there is one and from the Wald SE otherwise. Leaving it to
|
|
5374
5391
|
# scipy is what the two constructors exist to avoid: its 5%-of-value
|
|
@@ -5765,6 +5782,21 @@ def run_parallel_profile(
|
|
|
5765
5782
|
param_names, res_x, meta, anchor, where, nll_at_optimum)
|
|
5766
5783
|
_print_profile_convergence(convergence)
|
|
5767
5784
|
_print_profile_reach(convergence["reach"])
|
|
5785
|
+
if shadow_stop_rules:
|
|
5786
|
+
try:
|
|
5787
|
+
from pyantigen.engine.Stop_rules import (
|
|
5788
|
+
format_shadow_summary, shadow_summary)
|
|
5789
|
+
cfg = (dict(shadow_stop_rules)
|
|
5790
|
+
if isinstance(shadow_stop_rules, dict) else {})
|
|
5791
|
+
cfg.setdefault("threshold", float(threshold))
|
|
5792
|
+
text = format_shadow_summary(shadow_summary(
|
|
5793
|
+
[r for pts in completed.values() for r in pts.values()],
|
|
5794
|
+
nll_at_optimum, cfg))
|
|
5795
|
+
if text:
|
|
5796
|
+
print(f"\n{text}", flush=True)
|
|
5797
|
+
except Exception as exc: # reporting must never end a run
|
|
5798
|
+
print(f"[profile] shadow stop rules: summary failed ({exc})",
|
|
5799
|
+
flush=True)
|
|
5768
5800
|
if convergence["incomplete"]:
|
|
5769
5801
|
parts = []
|
|
5770
5802
|
if still_unfinished:
|
|
@@ -21,9 +21,14 @@ What it reads, per side of each parameter, in the optimizer's own space:
|
|
|
21
21
|
* a side whose points are all below the threshold is extended outward by the
|
|
22
22
|
curve-fitted step (``_extension_growth``), capped at ``max_step_decades`` per
|
|
23
23
|
step so a flat curve cannot launch a jump of several decades;
|
|
24
|
-
* a side that has crossed only on
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
* a side that has crossed only on points that did not converge is narrowed, not
|
|
25
|
+
repeated: an unconverged dNLL is an upper bound, so a crossing read from one
|
|
26
|
+
is not a crossing, and re-running the same far point from the same start
|
|
27
|
+
reproduces it exactly. The probe goes between the innermost converged point
|
|
28
|
+
below the threshold and the innermost unconverged one above it (sqrt(dNLL)
|
|
29
|
+
interpolation), warm from the former, and waits while anything is still
|
|
30
|
+
running inside that bracket. Only once the bracket is tight is the outer
|
|
31
|
+
point itself re-run;
|
|
27
32
|
* a bracketed side is narrowed by sqrt(dNLL) interpolation until the bracket is
|
|
28
33
|
tight, then filled in toward ``n_grid`` points so the curve has a shape.
|
|
29
34
|
|
|
@@ -300,21 +305,57 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
300
305
|
continue
|
|
301
306
|
|
|
302
307
|
if above_t:
|
|
303
|
-
# Crossed only on points that did not converge.
|
|
304
|
-
#
|
|
308
|
+
# Crossed only on points that did not converge. Each is an
|
|
309
|
+
# upper bound, so the crossing lies somewhere inside the
|
|
310
|
+
# bracket (nearest converged-below point, innermost of them).
|
|
305
311
|
tgt = above_t[0]
|
|
306
|
-
|
|
307
|
-
d_t = _ext_distance(p_opt,
|
|
312
|
+
x_t = tgt["x_fixed"]
|
|
313
|
+
d_t = _ext_distance(p_opt, x_t, is_log)
|
|
308
314
|
inner_c = [r for r in below
|
|
309
315
|
if _ext_distance(p_opt, r["x_fixed"], is_log) < d_t]
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
inner = inner_c[-1] if inner_c else None
|
|
317
|
+
x_in = inner["x_fixed"] if inner else p_opt
|
|
318
|
+
# Anything still running between the optimum and the failed
|
|
319
|
+
# point is about to become a better inner neighbour, or to
|
|
320
|
+
# replace the point we would plan. Planning now would seed from
|
|
321
|
+
# whatever has landed so far -- often nothing -- and a cold
|
|
322
|
+
# restart of a deterministic optimizer from the same start
|
|
323
|
+
# reproduces the failed point to the last digit.
|
|
324
|
+
if any(_ext_distance(p_opt, f, is_log) < d_t for f in fl):
|
|
325
|
+
continue
|
|
326
|
+
tight = _bracket_is_tight(x_in, x_t, p_opt, is_log,
|
|
327
|
+
bracket_rtol)
|
|
328
|
+
if not tight:
|
|
329
|
+
# Narrow the bracket toward the threshold instead of
|
|
330
|
+
# repeating the far point: a probe between the inner
|
|
331
|
+
# neighbour and the failed point is a short, warm
|
|
332
|
+
# continuation and so can converge where the long one
|
|
333
|
+
# could not. dnll at the failed point is too high, which
|
|
334
|
+
# puts the probe on the near side of the crossing; the
|
|
335
|
+
# next round narrows from there.
|
|
336
|
+
d_in_nll = dn(inner) if inner else 0.0
|
|
337
|
+
r_in, r_out = np.sqrt(max(d_in_nll, 0.0)), np.sqrt(dn(tgt))
|
|
338
|
+
frac = ((np.sqrt(thr) - r_in) / (r_out - r_in)
|
|
339
|
+
if r_out > r_in + 1e-12 else 0.5)
|
|
340
|
+
frac = float(min(max(frac, 0.05), 0.95))
|
|
341
|
+
x = x_in + frac * (x_t - x_in)
|
|
342
|
+
if free(x, abs(x_t - p_opt)) and \
|
|
343
|
+
ProfileCheckpointKey(x) not in completed.get(name, {}):
|
|
344
|
+
cands.append({"i": i, "name": name, "x": float(x),
|
|
345
|
+
"seed": inner, "phase": 1,
|
|
346
|
+
"kind": "narrow", "tier": 1,
|
|
347
|
+
"sign": sign})
|
|
348
|
+
continue
|
|
349
|
+
# The bracket is tight and the outer end still has not
|
|
350
|
+
# converged: give it one more go, warm from the inner point.
|
|
351
|
+
# With no inner point and a cold original this would be the
|
|
352
|
+
# same computation again, so it is not planned.
|
|
353
|
+
key = (name, ProfileCheckpointKey(x_t))
|
|
354
|
+
if key not in attempted and (
|
|
355
|
+
inner is not None or tgt.get("warm_seeded")):
|
|
356
|
+
cands.append({"i": i, "name": name, "x": float(x_t),
|
|
357
|
+
"seed": inner, "phase": 3, "kind": "rerun",
|
|
358
|
+
"tier": 1, "sign": sign, "attempt_key": key})
|
|
318
359
|
continue
|
|
319
360
|
|
|
320
361
|
# ── below the threshold everywhere so far: step outward ────────
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Shadow stop rules for profile points: record what they would have done.
|
|
2
|
+
|
|
3
|
+
A profile point is a nuisance minimization, and its only use to the planner
|
|
4
|
+
(:mod:`pyantigen.engine.Profile_plan`) is a verdict on the value it reaches U, an
|
|
5
|
+
upper bound on the profile there, against the threshold. Most of the 1500
|
|
6
|
+
evaluations a point is allowed buy digits that do not change the verdict:
|
|
7
|
+
|
|
8
|
+
* U already below the threshold is a proof that the profile is, and more
|
|
9
|
+
evaluations only lower it;
|
|
10
|
+
* U that is creeping down from far above the threshold will not get there
|
|
11
|
+
within the cap, and its magnitude is not trusted either way;
|
|
12
|
+
* U that has stopped improving by more than a fraction of its distance from the
|
|
13
|
+
threshold has said everything it is going to say.
|
|
14
|
+
|
|
15
|
+
None of this has been calibrated against real trajectories, and loosening the
|
|
16
|
+
optimizer's tolerance blind has cost accuracy here before (see the TOLERANCES
|
|
17
|
+
note in Optimizer_settings). So this module only *watches*. For each point it
|
|
18
|
+
keeps a sparse trace of the best value reached, and the first evaluation at which
|
|
19
|
+
each rule would have fired. Nothing is ever stopped. The numbers are for reading
|
|
20
|
+
off a finished run (:func:`shadow_summary`) and tuning the constants below
|
|
21
|
+
before any of them is allowed to end a point.
|
|
22
|
+
|
|
23
|
+
The rules, with U = best nll reached - anchor and thr the profile threshold:
|
|
24
|
+
|
|
25
|
+
``below`` U < ``below_frac`` * thr. A proof, needs no minimum spend.
|
|
26
|
+
``hopeless`` U > ``u_trust`` and, extrapolating the log-rate of the last
|
|
27
|
+
window to the evaluation cap, U would still be above ``u_trust``.
|
|
28
|
+
The extrapolation is optimistic -- Nelder-Mead's descent usually
|
|
29
|
+
slows -- so a point it fires on is very unlikely to have got
|
|
30
|
+
anywhere useful.
|
|
31
|
+
``settled`` U improved by less than ``kappa`` * |U - thr| over the last
|
|
32
|
+
window: the tolerance scales with the distance from the decision,
|
|
33
|
+
so a U of 7.6 needs ~0.4 and a U of 0.04 needs almost nothing.
|
|
34
|
+
Measured on progress over a window, not on the simplex spread,
|
|
35
|
+
which fires on a collapsed simplex in a curved valley.
|
|
36
|
+
|
|
37
|
+
The two window rules wait for ``max(MIN_EVALS, MIN_EVALS_PER_DIM * (n + 1))``
|
|
38
|
+
evaluations plus one window, so the initial simplex has been built and the
|
|
39
|
+
descent has started.
|
|
40
|
+
"""
|
|
41
|
+
import math
|
|
42
|
+
|
|
43
|
+
DEFAULTS = {
|
|
44
|
+
"threshold": 1.9207,
|
|
45
|
+
"below_frac": 0.25,
|
|
46
|
+
"u_trust": 20.0,
|
|
47
|
+
"kappa": 0.05,
|
|
48
|
+
"window": 100,
|
|
49
|
+
"sample_every": 25,
|
|
50
|
+
}
|
|
51
|
+
MIN_EVALS = 60
|
|
52
|
+
MIN_EVALS_PER_DIM = 5
|
|
53
|
+
|
|
54
|
+
RULES = ("below", "hopeless", "settled")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PointMonitor:
|
|
58
|
+
"""Watches one point's best-so-far; never alters it.
|
|
59
|
+
|
|
60
|
+
*trace* and *fired* continue those of an earlier launch of the same point,
|
|
61
|
+
so a point resumed after the clock stopped keeps one history.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, anchor, max_fev, n_nuis, cfg=None, trace=None, fired=None):
|
|
65
|
+
c = dict(DEFAULTS)
|
|
66
|
+
c.update({k: v for k, v in (cfg or {}).items() if k in DEFAULTS})
|
|
67
|
+
self.cfg = c
|
|
68
|
+
self.anchor = float(anchor)
|
|
69
|
+
self.max_fev = (float(max_fev) if max_fev is not None
|
|
70
|
+
and math.isfinite(float(max_fev)) else None)
|
|
71
|
+
self.min_evals = max(MIN_EVALS, MIN_EVALS_PER_DIM * (int(n_nuis) + 1))
|
|
72
|
+
self.trace = [list(map(float, p)) for p in (trace or [])]
|
|
73
|
+
self.fired = {k: dict(v) for k, v in (fired or {}).items()}
|
|
74
|
+
self._last_n = int(self.trace[-1][0]) if self.trace else 0
|
|
75
|
+
self._best = self.trace[-1][1] if self.trace else math.inf
|
|
76
|
+
|
|
77
|
+
# -- feeding -----------------------------------------------------------
|
|
78
|
+
def update(self, nfev, best_nll):
|
|
79
|
+
"""Call after every evaluation with the running total and best so far."""
|
|
80
|
+
if best_nll is None or not math.isfinite(best_nll):
|
|
81
|
+
return
|
|
82
|
+
nfev = int(nfev)
|
|
83
|
+
if nfev <= self._last_n and self.trace:
|
|
84
|
+
return
|
|
85
|
+
self._best = float(best_nll)
|
|
86
|
+
self._last_n = nfev
|
|
87
|
+
u = self._best - self.anchor
|
|
88
|
+
if "below" not in self.fired and u < self.cfg["below_frac"] * self.cfg["threshold"]:
|
|
89
|
+
self.fired["below"] = {"nfev": nfev, "U": u}
|
|
90
|
+
if nfev == 1 or nfev % self.cfg["sample_every"] == 0:
|
|
91
|
+
self.trace.append([nfev, self._best])
|
|
92
|
+
self._window_rules(nfev, u)
|
|
93
|
+
|
|
94
|
+
def _reference(self, nfev):
|
|
95
|
+
"""The last sample at least one window back, or None."""
|
|
96
|
+
want = nfev - self.cfg["window"]
|
|
97
|
+
ref = None
|
|
98
|
+
for n, b in self.trace:
|
|
99
|
+
if n <= want:
|
|
100
|
+
ref = (n, b)
|
|
101
|
+
else:
|
|
102
|
+
break
|
|
103
|
+
return ref
|
|
104
|
+
|
|
105
|
+
def _window_rules(self, nfev, u):
|
|
106
|
+
if nfev < self.min_evals + self.cfg["window"]:
|
|
107
|
+
return
|
|
108
|
+
ref = self._reference(nfev)
|
|
109
|
+
if ref is None:
|
|
110
|
+
return
|
|
111
|
+
n_ref, b_ref = ref
|
|
112
|
+
u_ref = b_ref - self.anchor
|
|
113
|
+
thr, u_trust = self.cfg["threshold"], self.cfg["u_trust"]
|
|
114
|
+
gain = u_ref - u
|
|
115
|
+
|
|
116
|
+
if "settled" not in self.fired and gain < self.cfg["kappa"] * abs(u - thr):
|
|
117
|
+
self.fired["settled"] = {"nfev": nfev, "U": u, "gain": gain}
|
|
118
|
+
|
|
119
|
+
if ("hopeless" not in self.fired and u > u_trust and self.max_fev is not None
|
|
120
|
+
and u_ref > 0 and u > 0):
|
|
121
|
+
span = nfev - n_ref
|
|
122
|
+
rate = math.log(u_ref / u) / span if span > 0 else 0.0
|
|
123
|
+
u_cap = (u * math.exp(-rate * max(self.max_fev - nfev, 0.0))
|
|
124
|
+
if rate > 0 else u)
|
|
125
|
+
if u_cap > u_trust:
|
|
126
|
+
self.fired["hopeless"] = {"nfev": nfev, "U": u, "U_cap": u_cap}
|
|
127
|
+
|
|
128
|
+
# -- reporting ---------------------------------------------------------
|
|
129
|
+
def finish(self, nfev, best_nll):
|
|
130
|
+
"""The fields to merge into the point's record."""
|
|
131
|
+
if best_nll is not None and math.isfinite(best_nll):
|
|
132
|
+
if not self.trace or int(self.trace[-1][0]) != int(nfev):
|
|
133
|
+
self.trace.append([int(nfev), float(best_nll)])
|
|
134
|
+
return {"trace": self.trace, "shadow_stops": self.fired}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Reading a finished run
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _verdict_class(u, thr, u_trust):
|
|
142
|
+
if u < thr:
|
|
143
|
+
return "below"
|
|
144
|
+
return "mid" if u < u_trust else "high"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def shadow_summary(records, anchor, cfg=None):
|
|
148
|
+
"""What the rules would have saved, and what they would have got wrong.
|
|
149
|
+
|
|
150
|
+
*records* is any iterable of point records carrying ``shadow_stops``. For
|
|
151
|
+
each rule: how many points it fired on, the evaluations it would have
|
|
152
|
+
saved (the point's ``nfev_total`` minus the evaluation it fired at), and how
|
|
153
|
+
many of those points ended in a different class -- below the threshold,
|
|
154
|
+
between it and ``u_trust``, or above -- than the value the rule fired on.
|
|
155
|
+
A class change is the only thing that matters to the planner, so that count
|
|
156
|
+
is the cost of the rule, in the only currency that counts.
|
|
157
|
+
"""
|
|
158
|
+
c = dict(DEFAULTS)
|
|
159
|
+
c.update({k: v for k, v in (cfg or {}).items() if k in DEFAULTS})
|
|
160
|
+
thr, u_trust = c["threshold"], c["u_trust"]
|
|
161
|
+
out = {r: {"fired": 0, "saved": 0, "class_changed": 0, "examples": []}
|
|
162
|
+
for r in RULES}
|
|
163
|
+
n_points, spent = 0, 0
|
|
164
|
+
for rec in records:
|
|
165
|
+
stops = rec.get("shadow_stops")
|
|
166
|
+
nll, total = rec.get("nll"), rec.get("nfev_total")
|
|
167
|
+
if stops is None or nll is None or total is None:
|
|
168
|
+
continue
|
|
169
|
+
n_points += 1
|
|
170
|
+
spent += int(total)
|
|
171
|
+
u_final = float(nll) - float(anchor)
|
|
172
|
+
for rule, hit in stops.items():
|
|
173
|
+
if rule not in out:
|
|
174
|
+
continue
|
|
175
|
+
o = out[rule]
|
|
176
|
+
o["fired"] += 1
|
|
177
|
+
o["saved"] += max(int(total) - int(hit["nfev"]), 0)
|
|
178
|
+
if (_verdict_class(hit["U"], thr, u_trust)
|
|
179
|
+
!= _verdict_class(u_final, thr, u_trust)):
|
|
180
|
+
o["class_changed"] += 1
|
|
181
|
+
if len(o["examples"]) < 5:
|
|
182
|
+
o["examples"].append({
|
|
183
|
+
"param": rec.get("param_name"), "x": rec.get("x_fixed"),
|
|
184
|
+
"nfev": hit["nfev"], "U_at_fire": hit["U"],
|
|
185
|
+
"U_final": u_final})
|
|
186
|
+
return {"n_points": n_points, "evals_spent": spent, "rules": out}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def format_shadow_summary(summary):
|
|
190
|
+
"""The summary as log lines."""
|
|
191
|
+
s = summary
|
|
192
|
+
if not s["n_points"]:
|
|
193
|
+
return ""
|
|
194
|
+
lines = [f"[profile] shadow stop rules (nothing was stopped): "
|
|
195
|
+
f"{s['n_points']} point(s), {s['evals_spent']} evaluation(s) spent"]
|
|
196
|
+
for rule in RULES:
|
|
197
|
+
o = s["rules"][rule]
|
|
198
|
+
lines.append(f" {rule:<9} fired on {o['fired']:>3}, would have saved "
|
|
199
|
+
f"{o['saved']:>6} eval(s), class changed on "
|
|
200
|
+
f"{o['class_changed']}")
|
|
201
|
+
for e in o["examples"]:
|
|
202
|
+
lines.append(f" changed: {e['param']} at {e['x']:.6g}: "
|
|
203
|
+
f"U {e['U_at_fire']:.3g} at eval {e['nfev']} -> "
|
|
204
|
+
f"{e['U_final']:.3g} final")
|
|
205
|
+
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md
RENAMED
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Optimizer_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/Modules/Update_opt_parameters.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADneg.json
RENAMED
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example1_ADpos.json
RENAMED
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example3_joint.json
RENAMED
|
File without changes
|
{pyantigen-2.1.0 → pyantigen-2.1.1}/pyantigen/template/Example/optimizations/Example4_flipflop.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|