PyAntiGen 2.0.0__tar.gz → 2.0.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.0.0 → pyantigen-2.0.2}/PKG-INFO +2 -1
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/PKG-INFO +2 -1
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/SOURCES.txt +2 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/scm_file_list.json +12 -0
- pyantigen-2.0.2/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/README.md +1 -0
- pyantigen-2.0.2/docs/MULTISTART.md +67 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/_version.py +3 -3
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Evaluator.py +34 -3
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Fast_profile.py +5 -4
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Fit_cache.py +9 -6
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Identifiability.py +181 -15
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Model_optimize.py +12 -2
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Optimize.py +267 -17
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Profile_checkpoint.py +11 -1
- pyantigen-2.0.2/pyantigen/engine/Profile_plan.py +343 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/rate_laws.py +2 -2
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/lower_v1.py +3 -1
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/optimization.py +4 -1
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Model_run.py +17 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Optimizer_settings.py +28 -0
- pyantigen-2.0.0/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-2.0.0 → pyantigen-2.0.2}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/.gitignore +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/.vscode/launch.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/LICENSE +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/MANIFEST.in +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/.gitignore +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/__init__.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/cli.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Preequil_cache.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Results.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/Simulate.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/design.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/params.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/pyproject.toml +0 -0
- {pyantigen-2.0.0 → pyantigen-2.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAntiGen
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Declarative generation, simulation and identifiability analysis of compartmental Antimony/SBML models
|
|
5
5
|
Author-email: Don Elbert <elbert5770@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -191,6 +191,7 @@ python Model_run.py --optimize Example2
|
|
|
191
191
|
python Model_run.py --optimize Example3
|
|
192
192
|
python Model_run.py --optimize Example4
|
|
193
193
|
python Model_run.py --optimize Example5
|
|
194
|
+
python Model_run.py --optimize Example6 # Sobol multi-start, see docs/MULTISTART.md
|
|
194
195
|
```
|
|
195
196
|
|
|
196
197
|
Each example prints a banner explaining what it demonstrates before the run and a "what to look for" summary after it. All output lands in `results/Example/`, prefixed with the example and group names so runs never overwrite each other — e.g. `Example_Example2_ADpos_profile_likelihood.png`, `Example_Example4_Flipflop_optimization_results.csv`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAntiGen
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Declarative generation, simulation and identifiability analysis of compartmental Antimony/SBML models
|
|
5
5
|
Author-email: Don Elbert <elbert5770@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -191,6 +191,7 @@ python Model_run.py --optimize Example2
|
|
|
191
191
|
python Model_run.py --optimize Example3
|
|
192
192
|
python Model_run.py --optimize Example4
|
|
193
193
|
python Model_run.py --optimize Example5
|
|
194
|
+
python Model_run.py --optimize Example6 # Sobol multi-start, see docs/MULTISTART.md
|
|
194
195
|
```
|
|
195
196
|
|
|
196
197
|
Each example prints a banner explaining what it demonstrates before the run and a "what to look for" summary after it. All output lands in `results/Example/`, prefixed with the example and group names so runs never overwrite each other — e.g. `Example_Example2_ADpos_profile_likelihood.png`, `Example_Example4_Flipflop_optimization_results.csv`.
|
|
@@ -14,6 +14,7 @@ PyAntiGen.egg-info/requires.txt
|
|
|
14
14
|
PyAntiGen.egg-info/scm_file_list.json
|
|
15
15
|
PyAntiGen.egg-info/scm_version.json
|
|
16
16
|
PyAntiGen.egg-info/top_level.txt
|
|
17
|
+
docs/MULTISTART.md
|
|
17
18
|
docs/PROTOCOL_LAYER.md
|
|
18
19
|
docs/V2_DESIGN.md
|
|
19
20
|
pyantigen/.gitignore
|
|
@@ -39,6 +40,7 @@ pyantigen/engine/Optimize.py
|
|
|
39
40
|
pyantigen/engine/Petab_export.py
|
|
40
41
|
pyantigen/engine/Preequil_cache.py
|
|
41
42
|
pyantigen/engine/Profile_checkpoint.py
|
|
43
|
+
pyantigen/engine/Profile_plan.py
|
|
42
44
|
pyantigen/engine/README.md
|
|
43
45
|
pyantigen/engine/Results.py
|
|
44
46
|
pyantigen/engine/Sensitivity_analysis.py
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"LICENSE",
|
|
8
8
|
"MANIFEST.in",
|
|
9
9
|
"README.md",
|
|
10
|
+
"docs/MULTISTART.md",
|
|
10
11
|
"docs/PROTOCOL_LAYER.md",
|
|
11
12
|
"docs/V2_DESIGN.md",
|
|
12
13
|
"pyantigen/.agents/skills/module_generation/SKILL.md",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"pyantigen/engine/Petab_export.py",
|
|
32
33
|
"pyantigen/engine/Preequil_cache.py",
|
|
33
34
|
"pyantigen/engine/Profile_checkpoint.py",
|
|
35
|
+
"pyantigen/engine/Profile_plan.py",
|
|
34
36
|
"pyantigen/engine/README.md",
|
|
35
37
|
"pyantigen/engine/Results.py",
|
|
36
38
|
"pyantigen/engine/Sensitivity_analysis.py",
|
|
@@ -102,15 +104,25 @@
|
|
|
102
104
|
"tests/engine/test_fast_profile.py",
|
|
103
105
|
"tests/engine/test_fit_cache.py",
|
|
104
106
|
"tests/engine/test_hessian_steps.py",
|
|
107
|
+
"tests/engine/test_multistart_sampler.py",
|
|
105
108
|
"tests/engine/test_nelder_mead.py",
|
|
106
109
|
"tests/engine/test_point_identity.py",
|
|
110
|
+
"tests/engine/test_profile_grid_open.py",
|
|
107
111
|
"tests/engine/test_profile_quadratic.py",
|
|
112
|
+
"tests/engine/test_profile_records.py",
|
|
113
|
+
"tests/engine/test_profile_stream.py",
|
|
108
114
|
"tests/engine/test_search_mode.py",
|
|
109
115
|
"tests/engine/test_sigma_floor.py",
|
|
110
116
|
"tests/engine/test_slice_screen.py",
|
|
117
|
+
"tests/engine/test_slice_window.py",
|
|
111
118
|
"tests/engine/test_solver_fingerprint.py",
|
|
112
119
|
"tests/engine/test_tolerance_floor.py",
|
|
113
120
|
"tests/engine/test_wald_units.py",
|
|
121
|
+
"tests/generate/test_gen_antimony_utils.py",
|
|
122
|
+
"tests/generate/test_gen_data_interpolation.py",
|
|
123
|
+
"tests/generate/test_gen_models.py",
|
|
124
|
+
"tests/generate/test_gen_pyantigen_api.py",
|
|
125
|
+
"tests/generate/test_gen_rxndict_to_antimony.py",
|
|
114
126
|
"tests/silk_fixtures/antimony_models/Antimony_Bloomingdale_2021_1a_all_reactions.txt",
|
|
115
127
|
"tests/silk_fixtures/antimony_models/Antimony_Bloomingdale_2021_1a_rules.txt",
|
|
116
128
|
"tests/silk_fixtures/antimony_models/Antimony_Elbert_2022_1a_all_reactions.txt",
|
|
@@ -159,6 +159,7 @@ python Model_run.py --optimize Example2
|
|
|
159
159
|
python Model_run.py --optimize Example3
|
|
160
160
|
python Model_run.py --optimize Example4
|
|
161
161
|
python Model_run.py --optimize Example5
|
|
162
|
+
python Model_run.py --optimize Example6 # Sobol multi-start, see docs/MULTISTART.md
|
|
162
163
|
```
|
|
163
164
|
|
|
164
165
|
Each example prints a banner explaining what it demonstrates before the run and a "what to look for" summary after it. All output lands in `results/Example/`, prefixed with the example and group names so runs never overwrite each other — e.g. `Example_Example2_ADpos_profile_likelihood.png`, `Example_Example4_Flipflop_optimization_results.csv`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Multi-start and the start sampler
|
|
2
|
+
|
|
3
|
+
Nelder-Mead is a local optimizer, so a fit lands in whichever basin it starts
|
|
4
|
+
in. Multi-start runs it from several points and keeps the best. Four fields on
|
|
5
|
+
the optimization spec control it:
|
|
6
|
+
|
|
7
|
+
| field | default | meaning |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `n_starts` | `1` | number of starts. `1` is a single fit and never touches the random generator |
|
|
10
|
+
| `search_decades` | `None` | radius around `x0` that starts 2..n are drawn from, in log10 units for a log-scaled parameter. Unset, the radius is half the declared bound range |
|
|
11
|
+
| `start_seed` | `None` | seed for the draw. Set it in the same edit that raises `n_starts`, or runs are not reproducible and cached fits cannot resume |
|
|
12
|
+
| `start_sampler` | `"lhs"` | how starts 2..n are drawn: `"lhs"` (Latin hypercube) or `"sobol"` (scrambled Sobol sequence) |
|
|
13
|
+
|
|
14
|
+
Start 1 is always the spec's own `x0`, so a multi-start fit cannot come back
|
|
15
|
+
worse than the single fit it replaces. Cost is linear in `n_starts`.
|
|
16
|
+
|
|
17
|
+
## Choosing a sampler
|
|
18
|
+
|
|
19
|
+
Both are low-discrepancy samplers from `scipy.stats.qmc`, so for a handful of
|
|
20
|
+
starts in a few parameters they behave alike. Sobol fills the box more evenly
|
|
21
|
+
as the number of parameters grows.
|
|
22
|
+
|
|
23
|
+
Sobol's balance holds only when the number of points drawn is a power of two.
|
|
24
|
+
Starts 2..n are `n_starts - 1` points, so use `n_starts` of 3, 5, 9, 17, ...
|
|
25
|
+
Other counts still run, with a warning from SciPy. Latin hypercube has no such
|
|
26
|
+
restriction.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
Optimization(
|
|
30
|
+
...,
|
|
31
|
+
n_starts=9, # 1 + 8 Sobol points
|
|
32
|
+
start_sampler="sobol",
|
|
33
|
+
search_decades=0.5,
|
|
34
|
+
start_seed=20260909,
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Keep the radius small
|
|
39
|
+
|
|
40
|
+
Sample around `x0`, not across the whole bound box. Drawing log-uniformly over
|
|
41
|
+
bounds such as (1e-9, 1) puts starts many decades from the answer, and they
|
|
42
|
+
converge to far worse basins. A `search_decades` of 0.5 to 1.0 is a reasonable
|
|
43
|
+
first try. `Example6` in the template uses 1.0, enough to reach the true mode of
|
|
44
|
+
the flip-flop problem from the wrong basin.
|
|
45
|
+
|
|
46
|
+
## Reading the result
|
|
47
|
+
|
|
48
|
+
The spread of the final NLLs across starts is the report. If they agree, the
|
|
49
|
+
objective is unimodal in that region and one start will do. If they scatter,
|
|
50
|
+
the fit depends on where it began, and a profile or confidence interval
|
|
51
|
+
anchored on any one of them may be measuring the wrong basin.
|
|
52
|
+
|
|
53
|
+
## Caching
|
|
54
|
+
|
|
55
|
+
`start_sampler` is part of the fit-cache key only when it is not `"lhs"`, so
|
|
56
|
+
fits cached before the field existed keep their hash. Switching to `"sobol"`
|
|
57
|
+
runs a fresh fit.
|
|
58
|
+
|
|
59
|
+
## What it does not cover
|
|
60
|
+
|
|
61
|
+
Multi-start applies to local methods. `differential_evolution`,
|
|
62
|
+
`basin_hopping`, `dual_annealing` and `shgo` search the whole domain
|
|
63
|
+
themselves and ignore `n_starts` and `start_sampler`.
|
|
64
|
+
|
|
65
|
+
This is distinct from `sobol_analysis`, the variance-based sensitivity
|
|
66
|
+
diagnostic (`_SOBOL_ONLY`), which samples the bounds to apportion variance in
|
|
67
|
+
the loss and does not search for a minimum.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0,
|
|
21
|
+
__version__ = version = '2.0.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6b1549beb'
|
|
@@ -924,7 +924,7 @@ class ParallelEvaluator:
|
|
|
924
924
|
|
|
925
925
|
def profile_batch(self, jobs, on_result=None, label=None,
|
|
926
926
|
heartbeat_s=_HEARTBEAT_SECONDS, budget=None,
|
|
927
|
-
frozen_sigmas=None, state_dir=None):
|
|
927
|
+
frozen_sigmas=None, state_dir=None, refill=None):
|
|
928
928
|
"""Run profile-likelihood points in parallel, within a wall budget.
|
|
929
929
|
|
|
930
930
|
``state_dir`` is where each running point keeps its own resumable
|
|
@@ -973,8 +973,20 @@ class ParallelEvaluator:
|
|
|
973
973
|
|
|
974
974
|
*on_result* is called with each result dict the moment it arrives.
|
|
975
975
|
Returns results in completion order; callers key off the job fields.
|
|
976
|
+
|
|
977
|
+
*refill*, when given, turns the batch into a stream. It is called as
|
|
978
|
+
``refill(n_free, in_flight)`` at the start and again each time results
|
|
979
|
+
land, with the number of worker slots not occupied by a running or
|
|
980
|
+
waiting point and the job dicts currently running, and returns up to
|
|
981
|
+
*n_free* new jobs. Those are queued behind anything already waiting and
|
|
982
|
+
admitted like the rest, so the pool holds ``n_workers`` points whenever
|
|
983
|
+
there is work to give it and never more, and each new point is planned
|
|
984
|
+
from every result so far rather than from the state when the batch was
|
|
985
|
+
assembled. The batch ends when nothing is running and *refill* returns
|
|
986
|
+
nothing. It is not consulted once the wall budget has stopped
|
|
987
|
+
admissions, so a stream cannot plan work the clock will not allow.
|
|
976
988
|
"""
|
|
977
|
-
if not jobs:
|
|
989
|
+
if not jobs and refill is None:
|
|
978
990
|
return []
|
|
979
991
|
if self._pool is None:
|
|
980
992
|
self.start()
|
|
@@ -1002,6 +1014,23 @@ class ParallelEvaluator:
|
|
|
1002
1014
|
# them.
|
|
1003
1015
|
sweep_stale_temp_files(state_dir)
|
|
1004
1016
|
|
|
1017
|
+
def top_up():
|
|
1018
|
+
"""Ask the planner for work to fill whatever slots are empty."""
|
|
1019
|
+
nonlocal n_jobs, halted
|
|
1020
|
+
if refill is None or halted:
|
|
1021
|
+
return
|
|
1022
|
+
if budget is not None and not budget.admits():
|
|
1023
|
+
# Nothing planned now could start, so do not plan it.
|
|
1024
|
+
halted = True
|
|
1025
|
+
return
|
|
1026
|
+
n_free = self.n_workers - len(pending) - len(backlog)
|
|
1027
|
+
if n_free <= 0:
|
|
1028
|
+
return
|
|
1029
|
+
new = refill(n_free, [futures[f] for f in pending]) or []
|
|
1030
|
+
for j in list(new)[:n_free]:
|
|
1031
|
+
backlog.append(dict(j, frozen_sigmas=frozen_sigmas))
|
|
1032
|
+
n_jobs += 1
|
|
1033
|
+
|
|
1005
1034
|
def admit():
|
|
1006
1035
|
"""Start points until the pool is full or the clock says stop."""
|
|
1007
1036
|
nonlocal halted
|
|
@@ -1035,6 +1064,7 @@ class ParallelEvaluator:
|
|
|
1035
1064
|
print(f"[pool]{tag} {budget.describe()}",
|
|
1036
1065
|
flush=True)
|
|
1037
1066
|
|
|
1067
|
+
top_up()
|
|
1038
1068
|
admit()
|
|
1039
1069
|
|
|
1040
1070
|
while pending:
|
|
@@ -1097,6 +1127,7 @@ class ParallelEvaluator:
|
|
|
1097
1127
|
f"({res.get('n_evals')} evals, {res.get('wall_s', 0):.0f}s)"
|
|
1098
1128
|
f" [{_fmt_dur(elapsed)} elapsed]", flush=True)
|
|
1099
1129
|
|
|
1130
|
+
top_up()
|
|
1100
1131
|
admit()
|
|
1101
1132
|
|
|
1102
1133
|
if self.verbose:
|
|
@@ -1105,7 +1136,7 @@ class ParallelEvaluator:
|
|
|
1105
1136
|
print(f"[pool]{tag} {done} profile points in {elapsed:.0f}s wall "
|
|
1106
1137
|
f"({work:.0f}s of work, {work / elapsed:.1f}x)", flush=True)
|
|
1107
1138
|
|
|
1108
|
-
if backlog:
|
|
1139
|
+
if backlog or (halted and refill is not None):
|
|
1109
1140
|
if budget is not None:
|
|
1110
1141
|
budget.stopped_early = True
|
|
1111
1142
|
raise DeadlineReached(len(backlog), label)
|
|
@@ -282,7 +282,7 @@ def run_fast_profile(batch, nll_batch, res_x, nll_at_optimum, param_names,
|
|
|
282
282
|
near_zero_frac=DEFAULT_NEAR_ZERO_FRAC,
|
|
283
283
|
span_decades=SPAN_DECADES,
|
|
284
284
|
min_reach_decades=MIN_REACH_DECADES, verbose=True,
|
|
285
|
-
nll_at_optimum_profile=None):
|
|
285
|
+
nll_at_optimum_profile=None, window_hi=None):
|
|
286
286
|
"""Screen, then one capped profile point per crossed side, in rounds.
|
|
287
287
|
|
|
288
288
|
*batch* has the profile_batch signature: ``batch(jobs, on_result, label)``.
|
|
@@ -314,13 +314,13 @@ def run_fast_profile(batch, nll_batch, res_x, nll_at_optimum, param_names,
|
|
|
314
314
|
|
|
315
315
|
# ── The screen, reused when this fit already has one ──────────────────
|
|
316
316
|
screen = load_screen(ckpt_dir, param_names, res_x, threshold,
|
|
317
|
-
span_decades, min_reach_decades)
|
|
317
|
+
span_decades, min_reach_decades, window_hi)
|
|
318
318
|
if screen is None:
|
|
319
319
|
screen = run_slice_screen(
|
|
320
320
|
nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
321
321
|
scales=scales, wald_se=wald_se, threshold=threshold,
|
|
322
322
|
span_decades=span_decades, min_reach_decades=min_reach_decades,
|
|
323
|
-
verbose=verbose)
|
|
323
|
+
verbose=verbose, window_hi=window_hi)
|
|
324
324
|
save_screen(screen, ckpt_dir)
|
|
325
325
|
elif verbose:
|
|
326
326
|
print(f"\n[fast profile] reusing the slice screen already run for this "
|
|
@@ -400,7 +400,8 @@ def run_fast_profile(batch, nll_batch, res_x, nll_at_optimum, param_names,
|
|
|
400
400
|
# A cold grid point as far as the full profile is concerned:
|
|
401
401
|
# its warm pass owes this point a sweep like any other.
|
|
402
402
|
"phase": 1,
|
|
403
|
-
"direction":
|
|
403
|
+
"direction": -1 if x_slice < p_opt else 1,
|
|
404
|
+
"pass_label": "fast",
|
|
404
405
|
"x_step": abs(x_slice - p_opt),
|
|
405
406
|
"fast_profile": True,
|
|
406
407
|
"_side": side_name,
|
|
@@ -126,7 +126,8 @@ def data_fingerprint(models):
|
|
|
126
126
|
|
|
127
127
|
def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
128
128
|
method, optimizer_kwargs, n_starts=1, start_seed=None,
|
|
129
|
-
search_decades=None, solver_hash=None, data_hash=None
|
|
129
|
+
search_decades=None, solver_hash=None, data_hash=None,
|
|
130
|
+
start_sampler="lhs"):
|
|
130
131
|
"""Hashes identifying a fit *problem*, before it has been solved.
|
|
131
132
|
|
|
132
133
|
Deliberately distinct from :func:`pyantigen.engine.Profile_checkpoint.spec_fingerprint`,
|
|
@@ -156,8 +157,7 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
156
157
|
"profile_without_opt"):
|
|
157
158
|
kwargs.pop(k, None)
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
{
|
|
160
|
+
key = {
|
|
161
161
|
"format": _FORMAT,
|
|
162
162
|
"param_names": list(param_names),
|
|
163
163
|
"x0": [_r(v) for v in np.atleast_1d(x0_lin)],
|
|
@@ -173,9 +173,12 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
173
173
|
"search_decades": search_decades,
|
|
174
174
|
"solver": solver_hash,
|
|
175
175
|
"data": data_hash,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
}
|
|
177
|
+
# Left out at the default so fits cached before the sampler existed keep
|
|
178
|
+
# their hash; it matters only when n_starts > 1.
|
|
179
|
+
if (start_sampler or "lhs") != "lhs":
|
|
180
|
+
key["start_sampler"] = start_sampler
|
|
181
|
+
blob = json.dumps(key, sort_keys=True, default=_json_default)
|
|
179
182
|
fit_hash = hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
|
|
180
183
|
return model_hash, fit_hash
|
|
181
184
|
|
|
@@ -98,6 +98,17 @@ SPAN_DECADES = 3.0
|
|
|
98
98
|
# stops evaluating close in and the walk cannot get out to the span.
|
|
99
99
|
MIN_REACH_DECADES = 1.0
|
|
100
100
|
|
|
101
|
+
# The slice's own crossing, located. The ladder stops the round a side first
|
|
102
|
+
# rises above THRESHOLD, and on a model whose other parameters compensate that
|
|
103
|
+
# first point is wildly above it (a slice dNLL of 1e4 against a profile of 5).
|
|
104
|
+
# With ``window_hi`` set, a side whose first crossing lands above it is walked
|
|
105
|
+
# back inward until a point lands in (THRESHOLD, window_hi]: a slice crossing
|
|
106
|
+
# that is actually near the threshold, and so a lower bound on the profile
|
|
107
|
+
# crossing worth having. None keeps the old behaviour exactly.
|
|
108
|
+
MAX_REFINE_ROUNDS = 6
|
|
109
|
+
_REFINE_MIN_RATIO = 1.0 / 16.0
|
|
110
|
+
_REFINE_MAX_RATIO = 0.95
|
|
111
|
+
|
|
101
112
|
# One side's verdict. Only "open" halts the run.
|
|
102
113
|
# crossed the slice rose above the threshold: profiling may proceed
|
|
103
114
|
# open the slice is still below the threshold decades out: PROVEN open
|
|
@@ -392,11 +403,93 @@ def _verdict(points, bound, threshold, p_opt, sign, is_log,
|
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
|
|
406
|
+
def _side_distance(p_opt, x, is_log, ratio_mode):
|
|
407
|
+
"""Distance from the optimum in the walk's own measure.
|
|
408
|
+
|
|
409
|
+
Opt-space offset for a log10 parameter, log10 ratio for a positive linear
|
|
410
|
+
one while every point so far is positive, plain offset otherwise.
|
|
411
|
+
"""
|
|
412
|
+
if is_log:
|
|
413
|
+
return abs(float(x) - float(p_opt))
|
|
414
|
+
if ratio_mode:
|
|
415
|
+
return abs(float(np.log10(x)) - float(np.log10(p_opt)))
|
|
416
|
+
return abs(float(x) - float(p_opt))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _side_value(p_opt, d, sign, is_log, ratio_mode):
|
|
420
|
+
"""Inverse of :func:`_side_distance`."""
|
|
421
|
+
if is_log:
|
|
422
|
+
return float(p_opt) + sign * float(d)
|
|
423
|
+
if ratio_mode:
|
|
424
|
+
return float(p_opt) * float(10.0 ** (sign * d))
|
|
425
|
+
return float(p_opt) + sign * float(d)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _refine_value(entry, threshold, window_hi):
|
|
429
|
+
"""The next value on a side that is being walked back in, or None to stop.
|
|
430
|
+
|
|
431
|
+
Aims at the geometric middle of the window. With a point already at or
|
|
432
|
+
below the threshold inside the one that was too high, the crossing is
|
|
433
|
+
bracketed and the step is a sqrt(dNLL) interpolation, which is linear in
|
|
434
|
+
distance for a locally quadratic curve. With only the optimum inside, the
|
|
435
|
+
step is a power-law extrapolation from the too-high points, dNLL = c d^p
|
|
436
|
+
with p fitted from two of them when it can be and 2 otherwise. A value the
|
|
437
|
+
model could not evaluate counts as too high and halves the distance.
|
|
438
|
+
"""
|
|
439
|
+
p_opt, sign, is_log = entry["p_opt"], entry["sign"], entry["is_log"]
|
|
440
|
+
pts = entry["points"]
|
|
441
|
+
ratio_mode = (not is_log) and p_opt > 0 and all(
|
|
442
|
+
float(p["x"]) > 0 for p in pts)
|
|
443
|
+
target = float(np.sqrt(threshold * window_hi))
|
|
444
|
+
|
|
445
|
+
recs = []
|
|
446
|
+
for p in pts:
|
|
447
|
+
d = _side_distance(p_opt, p["x"], is_log, ratio_mode)
|
|
448
|
+
usable = _is_usable(p)
|
|
449
|
+
recs.append((d, float(p["dnll"]) if usable else float("inf"), usable))
|
|
450
|
+
|
|
451
|
+
high = sorted((r for r in recs if r[1] > window_hi or not r[2]),
|
|
452
|
+
key=lambda r: r[0])
|
|
453
|
+
if not high:
|
|
454
|
+
return None
|
|
455
|
+
d_hi, dnll_hi, hi_ok = high[0]
|
|
456
|
+
low = [r for r in recs if r[2] and r[1] <= threshold and r[0] < d_hi]
|
|
457
|
+
d_lo, dnll_lo = (max(low, key=lambda r: r[0])[:2]) if low else (0.0, 0.0)
|
|
458
|
+
|
|
459
|
+
if hi_ok and np.isfinite(dnll_hi) and dnll_hi > dnll_lo and d_hi > d_lo:
|
|
460
|
+
if d_lo > 0.0 and dnll_lo > 0.0:
|
|
461
|
+
r_lo, r_hi = np.sqrt(dnll_lo), np.sqrt(dnll_hi)
|
|
462
|
+
frac = (np.sqrt(target) - r_lo) / (r_hi - r_lo)
|
|
463
|
+
frac = float(min(max(frac, 0.1), 0.9))
|
|
464
|
+
d_new = d_lo + frac * (d_hi - d_lo)
|
|
465
|
+
else:
|
|
466
|
+
exponent = 2.0
|
|
467
|
+
ups = [r for r in high if r[2] and np.isfinite(r[1]) and r[1] > 0]
|
|
468
|
+
if len(ups) >= 2 and ups[1][0] > ups[0][0] * (1.0 + 1e-9) \
|
|
469
|
+
and ups[1][1] > ups[0][1] * (1.0 + 1e-9):
|
|
470
|
+
cand = (np.log(ups[1][1] / ups[0][1])
|
|
471
|
+
/ np.log(ups[1][0] / ups[0][0]))
|
|
472
|
+
if np.isfinite(cand) and cand > 0:
|
|
473
|
+
exponent = float(np.clip(cand, 0.5, 6.0))
|
|
474
|
+
ratio = (target / dnll_hi) ** (1.0 / exponent)
|
|
475
|
+
ratio = float(np.clip(ratio, _REFINE_MIN_RATIO, _REFINE_MAX_RATIO))
|
|
476
|
+
d_new = d_hi * ratio
|
|
477
|
+
else:
|
|
478
|
+
d_new = d_lo + 0.5 * (d_hi - d_lo)
|
|
479
|
+
|
|
480
|
+
if not np.isfinite(d_new) or d_new <= 1e-9:
|
|
481
|
+
return None
|
|
482
|
+
if any(abs(d_new - r[0]) <= 1e-12 * max(d_new, 1.0) for r in recs):
|
|
483
|
+
return None
|
|
484
|
+
return _side_value(p_opt, d_new, sign, is_log, ratio_mode)
|
|
485
|
+
|
|
486
|
+
|
|
395
487
|
def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
396
488
|
scales=None, wald_se=None, threshold=THRESHOLD,
|
|
397
489
|
max_points=6, growth=2.0, range_factor=2.0,
|
|
398
490
|
span_decades=SPAN_DECADES,
|
|
399
|
-
min_reach_decades=MIN_REACH_DECADES, verbose=True
|
|
491
|
+
min_reach_decades=MIN_REACH_DECADES, verbose=True,
|
|
492
|
+
window_hi=None, max_refine_rounds=MAX_REFINE_ROUNDS):
|
|
400
493
|
"""Evaluate every parameter's slice out across decades, stopping each side
|
|
401
494
|
the moment it crosses.
|
|
402
495
|
|
|
@@ -430,6 +523,15 @@ def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
430
523
|
been read as "open" by a full evaluation, and reads "crossed" here
|
|
431
524
|
instead. That trades a slower profile on a rare, specific slice shape for
|
|
432
525
|
never paying an unbounded evaluation on the common one.
|
|
526
|
+
|
|
527
|
+
``window_hi`` adds one thing after that first crossing. A side whose first
|
|
528
|
+
point above the threshold is also above ``window_hi`` is walked back inward
|
|
529
|
+
(see :func:`_refine_value`) until a point lands in (threshold, window_hi],
|
|
530
|
+
for at most ``max_refine_rounds`` further rounds. The points that come back
|
|
531
|
+
inside the first crossing are real evaluations and are kept: a slice point
|
|
532
|
+
at or below the threshold there is certified inside the interval, which is
|
|
533
|
+
what ``inner_bracket`` records. The "open" verdict is not touched, since a
|
|
534
|
+
side only enters this walk after it has crossed.
|
|
433
535
|
"""
|
|
434
536
|
from pyantigen.engine.Optimize import _param_bounds
|
|
435
537
|
|
|
@@ -448,7 +550,8 @@ def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
448
550
|
span_decades=span_decades)
|
|
449
551
|
plan.append({"index": i, "name": name, "side": side, "sign": sign,
|
|
450
552
|
"is_log": is_log, "values": vals, "points": [],
|
|
451
|
-
"settled": False,
|
|
553
|
+
"settled": False, "ladder_i": 0, "refine": False,
|
|
554
|
+
"n_refine": 0,
|
|
452
555
|
"p_opt": float(res_x[i]),
|
|
453
556
|
"bound": (lb if sign < 0 else ub)})
|
|
454
557
|
|
|
@@ -464,31 +567,56 @@ def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
464
567
|
f"the profile, which is all the screen needs.", flush=True)
|
|
465
568
|
|
|
466
569
|
n_evaluations = 0
|
|
467
|
-
|
|
570
|
+
r = 0
|
|
571
|
+
max_total = max_rounds + (int(max_refine_rounds) if window_hi else 0)
|
|
572
|
+
while r < max_total:
|
|
468
573
|
round_xs, round_owner = [], []
|
|
469
574
|
for entry in plan:
|
|
470
|
-
if entry["settled"]
|
|
575
|
+
if entry["settled"]:
|
|
471
576
|
continue
|
|
472
|
-
|
|
577
|
+
if entry["refine"]:
|
|
578
|
+
v = _refine_value(entry, threshold, window_hi)
|
|
579
|
+
if v is None:
|
|
580
|
+
entry["settled"] = True
|
|
581
|
+
continue
|
|
582
|
+
else:
|
|
583
|
+
if entry["ladder_i"] >= len(entry["values"]):
|
|
584
|
+
continue
|
|
585
|
+
v = entry["values"][entry["ladder_i"]]
|
|
586
|
+
entry["ladder_i"] += 1
|
|
473
587
|
x = res_x.copy()
|
|
474
588
|
x[entry["index"]] = v
|
|
475
589
|
round_xs.append(x)
|
|
476
590
|
round_owner.append((entry, v))
|
|
477
591
|
if not round_xs:
|
|
478
592
|
break
|
|
593
|
+
r += 1
|
|
479
594
|
if verbose:
|
|
480
|
-
|
|
481
|
-
|
|
595
|
+
n_ref = sum(1 for e, _ in round_owner if e["refine"])
|
|
596
|
+
extra = (f", {n_ref} walking back in toward the window"
|
|
597
|
+
if n_ref else "")
|
|
598
|
+
print(f"[screen] round {r}: {len(round_xs)} side(s) still "
|
|
599
|
+
f"undecided{extra}.", flush=True)
|
|
482
600
|
n_evaluations += len(round_xs)
|
|
483
|
-
nlls = nll_batch(round_xs, label=f"slice-screen-r{r
|
|
601
|
+
nlls = nll_batch(round_xs, label=f"slice-screen-r{r}")
|
|
484
602
|
for (entry, v), nll in zip(round_owner, nlls):
|
|
485
603
|
nll = float(nll)
|
|
486
604
|
point = {"x": float(v),
|
|
487
605
|
"x_linear": float(10.0 ** v if entry["is_log"] else v),
|
|
488
606
|
"nll": nll, "dnll": float(nll - nll_at_optimum)}
|
|
489
607
|
entry["points"].append(point)
|
|
490
|
-
|
|
491
|
-
|
|
608
|
+
usable = _is_usable(point)
|
|
609
|
+
if entry["refine"]:
|
|
610
|
+
entry["n_refine"] += 1
|
|
611
|
+
if usable and threshold < point["dnll"] <= window_hi:
|
|
612
|
+
entry["settled"] = True
|
|
613
|
+
elif entry["n_refine"] >= int(max_refine_rounds):
|
|
614
|
+
entry["settled"] = True
|
|
615
|
+
elif usable and point["dnll"] > threshold:
|
|
616
|
+
if window_hi is None or point["dnll"] <= window_hi:
|
|
617
|
+
entry["settled"] = True
|
|
618
|
+
else:
|
|
619
|
+
entry["refine"] = True
|
|
492
620
|
|
|
493
621
|
report = {"threshold": float(threshold),
|
|
494
622
|
"anchor": float(nll_at_optimum),
|
|
@@ -496,17 +624,25 @@ def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
496
624
|
"param_names": list(param_names),
|
|
497
625
|
"span_decades": float(span_decades),
|
|
498
626
|
"min_reach_decades": float(min_reach_decades),
|
|
627
|
+
"window_hi": (float(window_hi) if window_hi else None),
|
|
499
628
|
"n_evaluations": n_evaluations,
|
|
500
629
|
"n_candidates": n_candidates,
|
|
501
630
|
"parameters": {}}
|
|
502
631
|
|
|
503
632
|
for entry in plan:
|
|
633
|
+
# Evaluation order is not distance order once a side has been walked
|
|
634
|
+
# back in, and everything downstream (the verdict, the inner bracket,
|
|
635
|
+
# the fast profile's crossing) reads points outward from the optimum.
|
|
636
|
+
entry["points"].sort(key=lambda q: abs(q["x"] - entry["p_opt"]))
|
|
504
637
|
side = _verdict(entry["points"], entry["bound"], threshold,
|
|
505
638
|
entry["p_opt"], entry["sign"], entry["is_log"],
|
|
506
639
|
min_reach_decades=min_reach_decades)
|
|
507
640
|
side["is_log"] = entry["is_log"]
|
|
508
641
|
side["stopped_early"] = bool(entry["settled"]
|
|
509
|
-
and
|
|
642
|
+
and entry["ladder_i"] < len(entry["values"]))
|
|
643
|
+
side["crossing"] = _crossing_record(entry["points"], threshold,
|
|
644
|
+
window_hi, entry["p_opt"],
|
|
645
|
+
entry["is_log"])
|
|
510
646
|
report["parameters"].setdefault(entry["name"], {})[entry["side"]] = side
|
|
511
647
|
|
|
512
648
|
states = [s["state"] for sides in report["parameters"].values()
|
|
@@ -520,6 +656,27 @@ def run_slice_screen(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
520
656
|
return report
|
|
521
657
|
|
|
522
658
|
|
|
659
|
+
def _crossing_record(points, threshold, window_hi, p_opt, is_log):
|
|
660
|
+
"""The slice point nearest the threshold from above, or None.
|
|
661
|
+
|
|
662
|
+
This is the slice's crossing as far as the screen located it: the usable
|
|
663
|
+
point with the smallest dNLL among those above the threshold. ``in_window``
|
|
664
|
+
says whether it landed inside (threshold, window_hi]; when it did not, the
|
|
665
|
+
refinement ran out of rounds or was never asked for, and the crossing is
|
|
666
|
+
only an upper limit on where the slice crosses.
|
|
667
|
+
"""
|
|
668
|
+
above = [q for q in points if _is_usable(q) and q["dnll"] > threshold]
|
|
669
|
+
if not above:
|
|
670
|
+
return None
|
|
671
|
+
q = min(above, key=lambda t: t["dnll"])
|
|
672
|
+
ratio_mode = (not is_log) and p_opt > 0 and q["x"] > 0
|
|
673
|
+
return {"x": float(q["x"]), "x_linear": float(q["x_linear"]),
|
|
674
|
+
"dnll": float(q["dnll"]),
|
|
675
|
+
"decades": float(_side_distance(p_opt, q["x"], is_log, ratio_mode)),
|
|
676
|
+
"in_window": bool(window_hi is not None
|
|
677
|
+
and q["dnll"] <= window_hi)}
|
|
678
|
+
|
|
679
|
+
|
|
523
680
|
def failing_sides(report):
|
|
524
681
|
"""Every side the screen proved unbounded, flattened for reporting."""
|
|
525
682
|
out = []
|
|
@@ -550,6 +707,7 @@ def screen_summary(report):
|
|
|
550
707
|
"parameters": {
|
|
551
708
|
name: {side: {"state": rec["state"],
|
|
552
709
|
"inner_bracket": rec["inner_bracket"],
|
|
710
|
+
"crossing": rec.get("crossing"),
|
|
553
711
|
"reach": rec["reach"],
|
|
554
712
|
"reach_decades": rec["reach_decades"],
|
|
555
713
|
"dnll_at_reach": rec["dnll_at_reach"],
|
|
@@ -658,7 +816,7 @@ def save_screen(report, ckpt_dir):
|
|
|
658
816
|
|
|
659
817
|
def load_screen(ckpt_dir, param_names, res_x, threshold=THRESHOLD,
|
|
660
818
|
span_decades=SPAN_DECADES,
|
|
661
|
-
min_reach_decades=MIN_REACH_DECADES):
|
|
819
|
+
min_reach_decades=MIN_REACH_DECADES, window_hi=None):
|
|
662
820
|
"""A screen already run for this exact fit, or None.
|
|
663
821
|
|
|
664
822
|
The checkpoint directory is keyed by model hash, spec hash and the optimum,
|
|
@@ -689,6 +847,13 @@ def load_screen(ckpt_dir, param_names, res_x, threshold=THRESHOLD,
|
|
|
689
847
|
return None
|
|
690
848
|
except (TypeError, ValueError):
|
|
691
849
|
return None
|
|
850
|
+
# A screen run without the window must not answer for one asked to refine,
|
|
851
|
+
# and the reverse: the points it holds are different points.
|
|
852
|
+
stored_hi = report.get("window_hi")
|
|
853
|
+
if (stored_hi is None) != (window_hi is None):
|
|
854
|
+
return None
|
|
855
|
+
if window_hi is not None and abs(float(stored_hi) - float(window_hi)) > 1e-12:
|
|
856
|
+
return None
|
|
692
857
|
stored = np.asarray(report.get("res_x") or [], dtype=float)
|
|
693
858
|
current = np.asarray(res_x, dtype=float)
|
|
694
859
|
if stored.shape != current.shape or not np.allclose(stored, current,
|
|
@@ -701,7 +866,8 @@ def screen_or_raise(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
701
866
|
scales=None, wald_se=None, ckpt_dir=None,
|
|
702
867
|
threshold=THRESHOLD, max_points=6, growth=2.0,
|
|
703
868
|
range_factor=2.0, span_decades=SPAN_DECADES,
|
|
704
|
-
min_reach_decades=MIN_REACH_DECADES, verbose=True
|
|
869
|
+
min_reach_decades=MIN_REACH_DECADES, verbose=True,
|
|
870
|
+
window_hi=None):
|
|
705
871
|
"""Run the screen (or reuse one), report it, and stop the run if it failed.
|
|
706
872
|
|
|
707
873
|
Raises :class:`UnidentifiableParameters` when any side is proven unbounded.
|
|
@@ -709,7 +875,7 @@ def screen_or_raise(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
709
875
|
to lie inside the confidence interval.
|
|
710
876
|
"""
|
|
711
877
|
report = load_screen(ckpt_dir, param_names, res_x, threshold,
|
|
712
|
-
span_decades, min_reach_decades)
|
|
878
|
+
span_decades, min_reach_decades, window_hi)
|
|
713
879
|
if report is not None:
|
|
714
880
|
if verbose:
|
|
715
881
|
print(f"\n[screen] reusing the slice screen already run for this "
|
|
@@ -722,7 +888,7 @@ def screen_or_raise(nll_batch, res_x, nll_at_optimum, param_names, bounds,
|
|
|
722
888
|
scales=scales, wald_se=wald_se, threshold=threshold,
|
|
723
889
|
max_points=max_points, growth=growth, range_factor=range_factor,
|
|
724
890
|
span_decades=span_decades, min_reach_decades=min_reach_decades,
|
|
725
|
-
verbose=verbose,
|
|
891
|
+
verbose=verbose, window_hi=window_hi,
|
|
726
892
|
)
|
|
727
893
|
path = save_screen(report, ckpt_dir)
|
|
728
894
|
|