PyAntiGen 2.1.1__tar.gz → 2.1.2__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.1 → pyantigen-2.1.2}/PKG-INFO +1 -1
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/PKG-INFO +1 -1
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/SOURCES.txt +2 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/scm_file_list.json +5 -0
- pyantigen-2.1.2/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/_version.py +3 -3
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Optimize.py +21 -2
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Preequil_cache.py +9 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Profile_plan.py +187 -99
- pyantigen-2.1.2/pyantigen/engine/Segmented.py +157 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Simulate.py +58 -7
- pyantigen-2.1.2/pyantigen/generate/input_schedule.py +624 -0
- pyantigen-2.1.1/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-2.1.1 → pyantigen-2.1.2}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/.gitignore +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/.vscode/launch.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/LICENSE +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/MANIFEST.in +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/README.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/docs/MULTISTART.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/.gitignore +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/__init__.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/cli.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Evaluator.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Fast_profile.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Fit_cache.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Identifiability.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Model_cache.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Model_optimize.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Multistart.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Profile_checkpoint.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Results.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/Stop_rules.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/rate_laws.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/design.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/lower_v1.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/optimization.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/params.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Model_run.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/pyproject.toml +0 -0
- {pyantigen-2.1.1 → pyantigen-2.1.2}/setup.cfg +0 -0
|
@@ -45,6 +45,7 @@ pyantigen/engine/Profile_checkpoint.py
|
|
|
45
45
|
pyantigen/engine/Profile_plan.py
|
|
46
46
|
pyantigen/engine/README.md
|
|
47
47
|
pyantigen/engine/Results.py
|
|
48
|
+
pyantigen/engine/Segmented.py
|
|
48
49
|
pyantigen/engine/Sensitivity_analysis.py
|
|
49
50
|
pyantigen/engine/Simulate.py
|
|
50
51
|
pyantigen/engine/Stop_rules.py
|
|
@@ -55,6 +56,7 @@ pyantigen/generate/TelluriumGen.py
|
|
|
55
56
|
pyantigen/generate/__init__.py
|
|
56
57
|
pyantigen/generate/antimony_utils.py
|
|
57
58
|
pyantigen/generate/data_interpolation.py
|
|
59
|
+
pyantigen/generate/input_schedule.py
|
|
58
60
|
pyantigen/generate/isotopomer_tools.py
|
|
59
61
|
pyantigen/generate/model_generation.py
|
|
60
62
|
pyantigen/generate/models.py
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"pyantigen/engine/Profile_plan.py",
|
|
38
38
|
"pyantigen/engine/README.md",
|
|
39
39
|
"pyantigen/engine/Results.py",
|
|
40
|
+
"pyantigen/engine/Segmented.py",
|
|
40
41
|
"pyantigen/engine/Sensitivity_analysis.py",
|
|
41
42
|
"pyantigen/engine/Simulate.py",
|
|
42
43
|
"pyantigen/engine/Stop_rules.py",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"pyantigen/generate/__init__.py",
|
|
48
49
|
"pyantigen/generate/antimony_utils.py",
|
|
49
50
|
"pyantigen/generate/data_interpolation.py",
|
|
51
|
+
"pyantigen/generate/input_schedule.py",
|
|
50
52
|
"pyantigen/generate/isotopomer_tools.py",
|
|
51
53
|
"pyantigen/generate/model_generation.py",
|
|
52
54
|
"pyantigen/generate/models.py",
|
|
@@ -122,6 +124,8 @@
|
|
|
122
124
|
"tests/engine/test_profile_stream.py",
|
|
123
125
|
"tests/engine/test_results_csv_policy.py",
|
|
124
126
|
"tests/engine/test_search_mode.py",
|
|
127
|
+
"tests/engine/test_segment_sensitivity.py",
|
|
128
|
+
"tests/engine/test_segmented.py",
|
|
125
129
|
"tests/engine/test_sigma_floor.py",
|
|
126
130
|
"tests/engine/test_slice_screen.py",
|
|
127
131
|
"tests/engine/test_slice_window.py",
|
|
@@ -131,6 +135,7 @@
|
|
|
131
135
|
"tests/engine/test_wald_units.py",
|
|
132
136
|
"tests/generate/test_gen_antimony_utils.py",
|
|
133
137
|
"tests/generate/test_gen_data_interpolation.py",
|
|
138
|
+
"tests/generate/test_gen_input_schedule.py",
|
|
134
139
|
"tests/generate/test_gen_models.py",
|
|
135
140
|
"tests/generate/test_gen_pyantigen_api.py",
|
|
136
141
|
"tests/generate/test_gen_rxndict_to_antimony.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.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6b3300aa1'
|
|
@@ -5483,6 +5483,14 @@ def run_parallel_profile(
|
|
|
5483
5483
|
key = ProfileCheckpointKey(res["x_fixed"])
|
|
5484
5484
|
prev = completed.get(name, {}).get(key)
|
|
5485
5485
|
keep = res
|
|
5486
|
+
# A planner restart (see Profile_plan) that ran to its end is counted on
|
|
5487
|
+
# the point, so a relaunch does not restart it again: the planner reads
|
|
5488
|
+
# the count to decide whether a value past the threshold can be trusted.
|
|
5489
|
+
finished_rerun = (str(res.get("pass_label", "")).endswith("rerun")
|
|
5490
|
+
and res.get("status") == "ok"
|
|
5491
|
+
and not res.get("interrupted"))
|
|
5492
|
+
if prev is not None and finished_rerun:
|
|
5493
|
+
res["reruns_tried"] = int(prev.get("reruns_tried") or 0) + 1
|
|
5486
5494
|
# Two evaluations at the same fixed value are both upper bounds on the
|
|
5487
5495
|
# profile, so the lower one is the better estimate. Nothing in passes 1,
|
|
5488
5496
|
# 2 or 4 re-visits a point, but keeping the rule here means the store
|
|
@@ -5494,8 +5502,17 @@ def run_parallel_profile(
|
|
|
5494
5502
|
# Dropping it would resume the point forever against a budget that
|
|
5495
5503
|
# never moves.
|
|
5496
5504
|
if not (prev.get("interrupted") and res.get("status") == "ok"):
|
|
5497
|
-
|
|
5498
|
-
|
|
5505
|
+
if not finished_rerun:
|
|
5506
|
+
return
|
|
5507
|
+
# A restart that did not lower the point: the value stands, but
|
|
5508
|
+
# the point has had its restart. Written as a tie, which the
|
|
5509
|
+
# store resolves to the later record.
|
|
5510
|
+
keep = dict(prev)
|
|
5511
|
+
keep["reruns_tried"] = res["reruns_tried"]
|
|
5512
|
+
else:
|
|
5513
|
+
keep = _carry_resume_state(dict(prev), res)
|
|
5514
|
+
if finished_rerun:
|
|
5515
|
+
keep["reruns_tried"] = res["reruns_tried"]
|
|
5499
5516
|
if checkpoint is not None and keep.get("status") == "ok":
|
|
5500
5517
|
checkpoint.append(keep)
|
|
5501
5518
|
completed.setdefault(name, {})[key] = keep
|
|
@@ -5972,6 +5989,8 @@ def _run_warm_passes(nll_batch_profile, completed, param_names, res_x, meta,
|
|
|
5972
5989
|
# probe) and is counted by the refinement budget, so it must
|
|
5973
5990
|
# survive a warm re-run regardless of which value won.
|
|
5974
5991
|
keep["phase"] = prev.get("phase", res.get("phase", 1))
|
|
5992
|
+
if prev.get("reruns_tried"):
|
|
5993
|
+
keep["reruns_tried"] = prev["reruns_tried"]
|
|
5975
5994
|
keep["warm_refined"] = int(
|
|
5976
5995
|
(prev or {}).get("warm_refined", 0)) + 1
|
|
5977
5996
|
# How much re-running this point warm actually lowered it. This is
|
|
@@ -128,6 +128,15 @@ class PreequilCache:
|
|
|
128
128
|
if block is None:
|
|
129
129
|
return solver_settings
|
|
130
130
|
|
|
131
|
+
# The cached block is integrated on its own, without the schedule's
|
|
132
|
+
# steps (see Segmented). A schedule that switches inside it cannot be
|
|
133
|
+
# replayed from a cached end state.
|
|
134
|
+
schedule = solver_settings.get("schedule")
|
|
135
|
+
if schedule is not None:
|
|
136
|
+
from pyantigen.engine.Segmented import switches_before
|
|
137
|
+
if switches_before(r, schedule, float(block["end"])):
|
|
138
|
+
return solver_settings
|
|
139
|
+
|
|
131
140
|
configure_integrator(r, solver_settings)
|
|
132
141
|
key = _digest(save_model_state(r), block, solver_settings)
|
|
133
142
|
|
|
@@ -21,16 +21,22 @@ 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
|
-
*
|
|
25
|
-
|
|
26
|
-
is
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
* the innermost point past the threshold is the bracket's outer end only if it
|
|
25
|
+
is firm: it converged from a warm start, or it has been restarted once. Any
|
|
26
|
+
other value is an upper bound with an unknown excess -- a warm restart from a
|
|
27
|
+
nearer neighbour only lowers it, and the crossing may then lie beyond it -- so
|
|
28
|
+
it is restarted at once (the nearest two such points), alongside the probes
|
|
29
|
+
that narrow the bracket, not after them. The optimizer is deterministic, so a
|
|
30
|
+
restart is planned only from a start the point did not have: a nearer inner
|
|
31
|
+
neighbour, or its own end state if it did not converge. The probe goes between
|
|
32
|
+
the innermost point at or below the threshold and the outer end (sqrt(dNLL)
|
|
33
|
+
interpolation), warm from the former;
|
|
32
34
|
* a bracketed side is narrowed by sqrt(dNLL) interpolation until the bracket is
|
|
33
|
-
tight, then filled in toward ``n_grid`` points so the curve has a shape
|
|
35
|
+
tight, then filled in toward ``n_grid`` points so the curve has a shape;
|
|
36
|
+
* workers left over once every side has its one probe are given extra probes
|
|
37
|
+
inside open brackets (see ``_SPEC_STEP``) and extra rungs beyond the next one
|
|
38
|
+
on a side still below the threshold (``_SPEC_MAX_EXT``), so a side advancing
|
|
39
|
+
one point per round does not hold the pool at a handful of cores.
|
|
34
40
|
|
|
35
41
|
Below-threshold points count whether or not they converged: an upper bound at or
|
|
36
42
|
under the threshold is still under it.
|
|
@@ -55,11 +61,74 @@ FIRST_PROBE_MODES = ("max", "slice", "slice_ladder")
|
|
|
55
61
|
# termination independent of them.
|
|
56
62
|
_SIDE_CEILING_EXTRA = 4
|
|
57
63
|
|
|
64
|
+
# Extra probes inside an open bracket, for workers that would otherwise idle. A
|
|
65
|
+
# bracket is narrowed by one probe per round, and a round is hours, so a side
|
|
66
|
+
# with one open bracket holds a single worker for as long as it takes; the rest
|
|
67
|
+
# of the pool drains as the other sides finish. The extras are spaced
|
|
68
|
+
# ``_SPEC_STEP`` of the bracket apart around the interpolated guess, nearest the
|
|
69
|
+
# guess first, and are only ever planned after every side has had its own
|
|
70
|
+
# primary probe (tier ``3 + j`` for the j-th extra), so they use cores that
|
|
71
|
+
# nothing else wanted.
|
|
72
|
+
_SPEC_STEP = 0.15
|
|
73
|
+
_SPEC_MAX = 4
|
|
74
|
+
# The same for a side still below the threshold: rungs beyond the next one.
|
|
75
|
+
_SPEC_MAX_EXT = 3
|
|
76
|
+
# Provisional points past the threshold restarted at once, beyond the nearest.
|
|
77
|
+
_SPEC_RERUN = 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _bracket_fracs(frac, n_extra=_SPEC_MAX, step=_SPEC_STEP):
|
|
81
|
+
"""Fractions of a bracket to probe: the guess, then alternately either side."""
|
|
82
|
+
out = [float(frac)]
|
|
83
|
+
for k in range(1, n_extra + 1):
|
|
84
|
+
off = step * ((k + 1) // 2) * (1 if k % 2 else -1)
|
|
85
|
+
f = float(min(max(frac + off, 0.05), 0.95))
|
|
86
|
+
if all(abs(f - g) > 1e-9 for g in out):
|
|
87
|
+
out.append(f)
|
|
88
|
+
return out
|
|
89
|
+
|
|
58
90
|
|
|
59
91
|
def _converged(rec):
|
|
60
92
|
return bool(rec.get("converged")) and not rec.get("interrupted")
|
|
61
93
|
|
|
62
94
|
|
|
95
|
+
def _rerun_count(rec):
|
|
96
|
+
"""How many warm restarts this point has had (the planner's, not the grid's)."""
|
|
97
|
+
n = rec.get("reruns_tried") or 0
|
|
98
|
+
if str(rec.get("pass_label", "")).endswith("rerun"):
|
|
99
|
+
n = max(int(n), 1)
|
|
100
|
+
return int(n)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_firm(rec):
|
|
104
|
+
"""Whether a point past the threshold can be trusted as a bracket end.
|
|
105
|
+
|
|
106
|
+
A value is firm once it has been through a warm start: it converged from a
|
|
107
|
+
neighbour's solution, or it has been restarted once and is accepted as it
|
|
108
|
+
stands. A cold or unconverged value is only an upper bound with an unknown
|
|
109
|
+
excess, so the crossing is not read from it until it has been restarted.
|
|
110
|
+
"""
|
|
111
|
+
return _rerun_count(rec) >= 1 or (_converged(rec) and bool(rec.get("warm_seeded")))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _rerun_seed(rec, inner):
|
|
115
|
+
"""Where to restart *rec* from, or None if that would repeat it exactly.
|
|
116
|
+
|
|
117
|
+
The optimizer is deterministic, so a restart from the start the point
|
|
118
|
+
already had reproduces it to the last digit. From a nearer inner neighbour
|
|
119
|
+
it is new information; failing that, an unconverged point is carried on from
|
|
120
|
+
where it stopped.
|
|
121
|
+
"""
|
|
122
|
+
if inner is not None:
|
|
123
|
+
xs, nx = rec.get("x_start"), inner.get("nuisance_x")
|
|
124
|
+
if (xs is None or nx is None or len(xs) != len(nx)
|
|
125
|
+
or not np.allclose(xs, nx, rtol=0.0, atol=1e-9)):
|
|
126
|
+
return inner
|
|
127
|
+
if not _converged(rec) and rec.get("nuisance_x"):
|
|
128
|
+
return rec
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
63
132
|
def _near(a, b, d_ref, rel=0.02):
|
|
64
133
|
"""Whether two opt-space values are the same point for planning purposes."""
|
|
65
134
|
return abs(a - b) <= rel * max(abs(d_ref), 1e-9)
|
|
@@ -148,7 +217,8 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
148
217
|
"tier"}``: the parameter index, the fixed value in optimizer space, the
|
|
149
218
|
record to warm-start from (or None for a cold start from the optimum), the
|
|
150
219
|
phase number the job should carry, why it was chosen, and its priority
|
|
151
|
-
(0 first probe, 1 stepping or re-running, 2 filling a bracket, 3 spare
|
|
220
|
+
(0 first probe, 1 stepping or re-running, 2 filling a bracket, 3 spare
|
|
221
|
+
ladder rung, 4 and up extra probes inside an open bracket).
|
|
152
222
|
|
|
153
223
|
*in_flight* is the list of job dicts currently running. Their positions
|
|
154
224
|
count as occupied: nothing is planned on top of one, and a side with a first
|
|
@@ -195,6 +265,26 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
195
265
|
def free(x, d_ref):
|
|
196
266
|
return not any(_near(x, o, d_ref) for o in occupied)
|
|
197
267
|
|
|
268
|
+
def probe_bracket(x_in, x_out, frac, seed, kind, d_ref):
|
|
269
|
+
"""The guess inside (x_in, x_out), then spare-core extras.
|
|
270
|
+
|
|
271
|
+
A guess that lands on a point already measured means the
|
|
272
|
+
crossing is located by that point, so nothing is added: the
|
|
273
|
+
extras are for a side still being narrowed, not a licence to
|
|
274
|
+
keep probing one that has an answer.
|
|
275
|
+
"""
|
|
276
|
+
if any(_near(x_in + frac * (x_out - x_in),
|
|
277
|
+
float(r["x_fixed"]), d_ref) for r in pts):
|
|
278
|
+
return
|
|
279
|
+
for j, fr in enumerate(_bracket_fracs(frac)):
|
|
280
|
+
x = x_in + fr * (x_out - x_in)
|
|
281
|
+
if free(x, d_ref) and \
|
|
282
|
+
ProfileCheckpointKey(x) not in completed.get(name, {}):
|
|
283
|
+
cands.append({"i": i, "name": name, "x": float(x),
|
|
284
|
+
"seed": seed, "phase": 1, "kind": kind,
|
|
285
|
+
"tier": 1 if j == 0 else 3 + j,
|
|
286
|
+
"sign": sign})
|
|
287
|
+
|
|
198
288
|
ceiling = n_grid + max_extend + _SIDE_CEILING_EXTRA
|
|
199
289
|
if len(pts) + len(fl) >= ceiling:
|
|
200
290
|
continue
|
|
@@ -249,46 +339,79 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
249
339
|
continue
|
|
250
340
|
|
|
251
341
|
below = [r for r in pts if dn(r) <= thr]
|
|
252
|
-
|
|
253
|
-
above_t = [r for r in pts if dn(r) > thr and not _converged(r)]
|
|
342
|
+
above = [r for r in pts if dn(r) > thr]
|
|
254
343
|
outer_pt = pts[-1]
|
|
255
344
|
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
345
|
+
if above:
|
|
346
|
+
# The innermost point past the threshold is the outer end of the
|
|
347
|
+
# bracket -- but only if it is a real measurement. A cold or
|
|
348
|
+
# unconverged value is an upper bound with an unknown excess
|
|
349
|
+
# (observed: 0.05, 0.3 and 3.8 nats), and a warm restart from a
|
|
350
|
+
# nearer neighbour only ever lowers it, so the crossing may lie
|
|
351
|
+
# beyond it and every probe planned inside the bracket would
|
|
352
|
+
# read "below". Such a point is provisional: it is restarted
|
|
353
|
+
# warm at once, alongside the probes, instead of last.
|
|
354
|
+
tgt = above[0]
|
|
355
|
+
x_t = tgt["x_fixed"]
|
|
356
|
+
d_t = _ext_distance(p_opt, x_t, is_log)
|
|
357
|
+
|
|
358
|
+
def inner_of(t):
|
|
359
|
+
d_k = _ext_distance(p_opt, t["x_fixed"], is_log)
|
|
360
|
+
c = [r for r in below
|
|
361
|
+
if _ext_distance(p_opt, r["x_fixed"], is_log) < d_k]
|
|
362
|
+
return c[-1] if c else None
|
|
363
|
+
|
|
364
|
+
inner = inner_of(tgt)
|
|
262
365
|
x_in = inner["x_fixed"] if inner else p_opt
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
366
|
+
|
|
367
|
+
# Only points before the first firm one can move the crossing;
|
|
368
|
+
# anything past a firm end is in the tail.
|
|
369
|
+
prov = []
|
|
370
|
+
for r in above:
|
|
371
|
+
if _is_firm(r):
|
|
372
|
+
break
|
|
373
|
+
prov.append(r)
|
|
374
|
+
prov = prov[:1 + _SPEC_RERUN]
|
|
375
|
+
for rank, t in enumerate(prov):
|
|
376
|
+
key = (name, ProfileCheckpointKey(t["x_fixed"]))
|
|
377
|
+
d_r = _ext_distance(p_opt, t["x_fixed"], is_log)
|
|
378
|
+
if key in attempted or _rerun_count(t) >= 1:
|
|
379
|
+
continue
|
|
380
|
+
if any(_near(f, t["x_fixed"], d_r) for f in fl):
|
|
381
|
+
continue
|
|
382
|
+
seed = _rerun_seed(t, inner_of(t))
|
|
383
|
+
if seed is None:
|
|
384
|
+
continue
|
|
385
|
+
cands.append({"i": i, "name": name, "x": float(t["x_fixed"]),
|
|
386
|
+
"seed": seed, "phase": 3, "kind": "rerun",
|
|
387
|
+
"tier": 1 if rank == 0 else 3 + rank,
|
|
388
|
+
"sign": sign, "attempt_key": key})
|
|
389
|
+
|
|
390
|
+
tight = _bracket_is_tight(x_in, x_t, p_opt, is_log, bracket_rtol)
|
|
266
391
|
if not tight:
|
|
267
|
-
|
|
392
|
+
d_in_nll = dn(inner) if inner else 0.0
|
|
393
|
+
r_in, r_out = np.sqrt(max(d_in_nll, 0.0)), np.sqrt(dn(tgt))
|
|
268
394
|
frac = ((np.sqrt(thr) - r_in) / (r_out - r_in)
|
|
269
395
|
if r_out > r_in + 1e-12 else 0.5)
|
|
270
396
|
frac = float(min(max(frac, 0.05), 0.95))
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
cands.append({"i": i, "name": name, "x": float(x),
|
|
275
|
-
"seed": inner, "phase": 1,
|
|
276
|
-
"kind": "bracket", "tier": 1,
|
|
277
|
-
"sign": sign})
|
|
397
|
+
probe_bracket(x_in, x_t, frac, inner,
|
|
398
|
+
"bracket" if _is_firm(tgt) else "narrow",
|
|
399
|
+
abs(x_t - p_opt))
|
|
278
400
|
continue
|
|
279
|
-
if len(pts) + len(fl) < n_grid:
|
|
401
|
+
if _is_firm(tgt) and len(pts) + len(fl) < n_grid:
|
|
280
402
|
# Fill the curve's shape: halve the widest relative gap
|
|
281
403
|
# between the optimum and the crossing.
|
|
282
404
|
ds = sorted({0.0} | {
|
|
283
405
|
_ext_distance(p_opt, r["x_fixed"], is_log)
|
|
284
406
|
for r in pts if _ext_distance(
|
|
285
|
-
p_opt, r["x_fixed"], is_log) <=
|
|
407
|
+
p_opt, r["x_fixed"], is_log) <= d_t})
|
|
286
408
|
gaps = []
|
|
287
|
-
for
|
|
288
|
-
gaps.append((
|
|
409
|
+
for g_a, g_b in zip(ds[:-1], ds[1:]):
|
|
410
|
+
gaps.append((g_b / g_a if g_a > 0 else float("inf"),
|
|
411
|
+
g_a, g_b))
|
|
289
412
|
if gaps:
|
|
290
|
-
_,
|
|
291
|
-
d_new =
|
|
413
|
+
_, g_a, g_b = max(gaps, key=lambda g: g[0])
|
|
414
|
+
d_new = g_b / 2.0 if g_a == 0 else float(np.sqrt(g_a * g_b))
|
|
292
415
|
x = (p_opt + sign * d_new if (is_log or p_opt <= 0)
|
|
293
416
|
else p_opt * 10.0 ** (sign * d_new))
|
|
294
417
|
ref = min((r for r in pts
|
|
@@ -297,71 +420,27 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
297
420
|
key=lambda r: -_ext_distance(
|
|
298
421
|
p_opt, r["x_fixed"], is_log),
|
|
299
422
|
default=None)
|
|
300
|
-
if free(x, abs(
|
|
423
|
+
if free(x, abs(x_t - p_opt)):
|
|
301
424
|
cands.append({"i": i, "name": name, "x": float(x),
|
|
302
425
|
"seed": ref, "phase": 1,
|
|
303
426
|
"kind": "fill", "tier": 2,
|
|
304
427
|
"sign": sign})
|
|
305
428
|
continue
|
|
306
429
|
|
|
307
|
-
if above_t:
|
|
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).
|
|
311
|
-
tgt = above_t[0]
|
|
312
|
-
x_t = tgt["x_fixed"]
|
|
313
|
-
d_t = _ext_distance(p_opt, x_t, is_log)
|
|
314
|
-
inner_c = [r for r in below
|
|
315
|
-
if _ext_distance(p_opt, r["x_fixed"], is_log) < d_t]
|
|
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})
|
|
359
|
-
continue
|
|
360
430
|
|
|
361
431
|
# ── below the threshold everywhere so far: step outward ────────
|
|
432
|
+
# One rung per round would hold a single worker for as long as the
|
|
433
|
+
# side stays open, and an open side says nothing about its tail
|
|
434
|
+
# (a nonlinear profile can flatten and then climb), so it is not
|
|
435
|
+
# assumed to be flat. Instead the next rungs go out together:
|
|
436
|
+
# up to ``_SPEC_MAX_EXT`` beyond the first, each stepping on from
|
|
437
|
+
# the farthest rung already running or planned, so a relaunch or a
|
|
438
|
+
# rung landing continues the ladder instead of restarting it.
|
|
362
439
|
far_fl = [f for f in fl
|
|
363
440
|
if abs(f - p_opt) > abs(outer_pt["x_fixed"] - p_opt)]
|
|
364
|
-
|
|
441
|
+
need = min(1 + _SPEC_MAX_EXT - len(far_fl),
|
|
442
|
+
ceiling - len(pts) - len(fl))
|
|
443
|
+
if need <= 0:
|
|
365
444
|
continue
|
|
366
445
|
d_out = _ext_distance(p_opt, outer_pt["x_fixed"], is_log)
|
|
367
446
|
growth = float(extend_growth)
|
|
@@ -370,15 +449,24 @@ def plan_next_points(n_free, *, param_names, completed, in_flight, res_x,
|
|
|
370
449
|
growth = max(growth, 1.05)
|
|
371
450
|
g = _extension_growth(pts, p_opt, is_log, thr, anchor, growth)
|
|
372
451
|
g = float(min(max(g, 1.05), growth))
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
452
|
+
x_ref = (max(far_fl) if sign > 0 else min(far_fl)) if far_fl \
|
|
453
|
+
else outer_pt["x_fixed"]
|
|
454
|
+
for j in range(need):
|
|
455
|
+
d_ref = _ext_distance(p_opt, x_ref, is_log)
|
|
456
|
+
g_j = g
|
|
457
|
+
if d_ref > 0 and max_step_decades:
|
|
458
|
+
g_j = min(g, max(1.0 + float(max_step_decades) / d_ref, 1.05))
|
|
459
|
+
x = _next_extension_value(p_opt, x_ref, lb, ub, sign, is_log, g_j)
|
|
460
|
+
if x is None:
|
|
461
|
+
break
|
|
462
|
+
x_ref = x
|
|
463
|
+
if free(x, abs(outer_pt["x_fixed"] - p_opt)) and \
|
|
464
|
+
ProfileCheckpointKey(x) not in completed.get(name, {}):
|
|
465
|
+
cands.append({"i": i, "name": name, "x": float(x),
|
|
466
|
+
"seed": outer_pt, "phase": 1, "kind": "extend",
|
|
467
|
+
"tier": 1 if (j == 0 and not far_fl)
|
|
468
|
+
else 3 + j + (1 if far_fl else 0),
|
|
469
|
+
"sign": sign})
|
|
382
470
|
|
|
383
471
|
cands.sort(key=lambda c: (c["tier"], c["i"], c["sign"]))
|
|
384
472
|
return cands[:n_free]
|