PyAntiGen 2.0.2__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PKG-INFO +1 -1
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/PKG-INFO +1 -1
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/scm_file_list.json +1 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/scm_version.json +2 -2
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/_version.py +3 -3
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Fit_cache.py +10 -2
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Model_optimize.py +15 -5
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Optimize.py +147 -9
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Simulate.py +58 -2
- {pyantigen-2.0.2 → pyantigen-2.0.3}/.github/workflows/publish.yml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/.github/workflows/tests.yml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/.gitignore +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/.vscode/launch.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/LICENSE +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/MANIFEST.in +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/SOURCES.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/README.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/docs/MULTISTART.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/docs/PROTOCOL_LAYER.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/docs/V2_DESIGN.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/.gitignore +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/cli.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Anchor_cache.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Deadline.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Differential_evolution.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Evaluator.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Event_times.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Fast_profile.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Identifiability.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Model_simulate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Nelder_mead.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Noise_floor.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Petab_export.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Preequil_cache.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Profile_checkpoint.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Profile_plan.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/README.md +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Results.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/Sensitivity_analysis.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/engine/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/AntimonyGen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/TelluriumGen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/antimony_utils.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/data_interpolation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/isotopomer_tools.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/model_generation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/models.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/module_base.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/pyantigen.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/rate_laws.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/generate/reaction_creation.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/__init__.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/assay.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/describe.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/design.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/lower_v1.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/optimization.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/params.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/quantity.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/reagent.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/refs.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/remarks.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/serialize.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/study/validate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Model_generate.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Model_run.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Data.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Events.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Plots.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/example.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/studies/example.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/studies/example.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/studies/flipflop.json +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/data/ADneg.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/data/ADpos.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/data/Flipflop.csv +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/data/make_flipflop_data.py +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/pyproject.toml +0 -0
- {pyantigen-2.0.2 → pyantigen-2.0.3}/setup.cfg +0 -0
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"tests/engine/test_fit_cache.py",
|
|
106
106
|
"tests/engine/test_hessian_steps.py",
|
|
107
107
|
"tests/engine/test_multistart_sampler.py",
|
|
108
|
+
"tests/engine/test_multistart_stage.py",
|
|
108
109
|
"tests/engine/test_nelder_mead.py",
|
|
109
110
|
"tests/engine/test_point_identity.py",
|
|
110
111
|
"tests/engine/test_profile_grid_open.py",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0,
|
|
21
|
+
__version__ = version = '2.0.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 3)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g0218ed4d7'
|
|
@@ -156,6 +156,13 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
156
156
|
for k in ("profile_method", "profile_optimizer_kwargs", "profile_grid",
|
|
157
157
|
"profile_without_opt"):
|
|
158
158
|
kwargs.pop(k, None)
|
|
159
|
+
# The multi-start stage decides where the fit starts, which matters only
|
|
160
|
+
# when there is more than one start. Dropped otherwise so that giving every
|
|
161
|
+
# spec a default stage does not invalidate the fits already cached.
|
|
162
|
+
if int(n_starts or 1) <= 1:
|
|
163
|
+
for k in ("multistart_method", "multistart_optimizer_kwargs",
|
|
164
|
+
"multistart_limits"):
|
|
165
|
+
kwargs.pop(k, None)
|
|
159
166
|
|
|
160
167
|
key = {
|
|
161
168
|
"format": _FORMAT,
|
|
@@ -175,8 +182,9 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
|
|
|
175
182
|
"data": data_hash,
|
|
176
183
|
}
|
|
177
184
|
# Left out at the default so fits cached before the sampler existed keep
|
|
178
|
-
# their hash
|
|
179
|
-
|
|
185
|
+
# their hash, and left out at one start, where nothing is sampled, so that
|
|
186
|
+
# a project-wide default of "sobol" does not invalidate single-start fits.
|
|
187
|
+
if int(n_starts or 1) > 1 and (start_sampler or "lhs") != "lhs":
|
|
180
188
|
key["start_sampler"] = start_sampler
|
|
181
189
|
blob = json.dumps(key, sort_keys=True, default=_json_default)
|
|
182
190
|
fit_hash = hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
|
|
@@ -973,12 +973,20 @@ def _slice_dnll_at_optimum(param_vals, dnll, opt_val, rtol=1e-6):
|
|
|
973
973
|
return float(y[i])
|
|
974
974
|
|
|
975
975
|
|
|
976
|
-
def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="ALL"
|
|
977
|
-
|
|
976
|
+
def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="ALL",
|
|
977
|
+
slice_settings=None):
|
|
978
|
+
"""Run likelihood slice once per parameter and save plot.
|
|
979
|
+
|
|
980
|
+
slice_settings is the spec's ``slice_settings`` ({"n_points", "range_factor"});
|
|
981
|
+
absent keys keep the values that used to be fixed here, 20 and 2.0.
|
|
982
|
+
"""
|
|
978
983
|
from datetime import datetime
|
|
979
984
|
import numpy as np
|
|
980
985
|
import matplotlib.pyplot as plt
|
|
981
986
|
|
|
987
|
+
n_points = int((slice_settings or {}).get("n_points", 20))
|
|
988
|
+
range_factor = float((slice_settings or {}).get("range_factor", 2.0))
|
|
989
|
+
|
|
982
990
|
slice_func = opt.get("stats", {}).get("likelihood_slice")
|
|
983
991
|
if not slice_func:
|
|
984
992
|
print("Warning: no likelihood_slice closure in opt['stats'] — skipping plot.")
|
|
@@ -1004,7 +1012,7 @@ def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="AL
|
|
|
1004
1012
|
results = None
|
|
1005
1013
|
if slice_all is not None:
|
|
1006
1014
|
try:
|
|
1007
|
-
results = slice_all(n_points=
|
|
1015
|
+
results = slice_all(n_points=n_points, range_factor=range_factor)
|
|
1008
1016
|
except Exception as exc:
|
|
1009
1017
|
print(f" batched slice failed ({exc}); falling back per parameter.")
|
|
1010
1018
|
|
|
@@ -1014,7 +1022,7 @@ def _save_likelihood_slice_plot(opt, param_names, plot_path, model_name, tag="AL
|
|
|
1014
1022
|
if results is not None and pname in results:
|
|
1015
1023
|
pv, nr = results[pname]
|
|
1016
1024
|
else:
|
|
1017
|
-
pv, nr = slice_func(i, n_points=
|
|
1025
|
+
pv, nr = slice_func(i, n_points=n_points, range_factor=range_factor)
|
|
1018
1026
|
cache[i] = (pv, nr)
|
|
1019
1027
|
slice_traces[pname] = {"x": np.asarray(pv).tolist(),
|
|
1020
1028
|
"y": np.asarray(nr).tolist()}
|
|
@@ -1233,7 +1241,9 @@ def setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_d
|
|
|
1233
1241
|
|
|
1234
1242
|
if settings.get("slice_analysis") and opt.get("stats", {}).get("likelihood_slice"):
|
|
1235
1243
|
_save_likelihood_slice_plot(opt, optimization_settings.param_names, paths["plot_path"],
|
|
1236
|
-
MODEL_NAME, tag=groups_tag
|
|
1244
|
+
MODEL_NAME, tag=groups_tag,
|
|
1245
|
+
slice_settings=getattr(optimization_settings,
|
|
1246
|
+
"slice_settings", None))
|
|
1237
1247
|
if (settings.get("profile_likelihood_analysis") or settings.get("fast_profile_likelihood_analysis")) and opt.get("stats", {}).get("profile_likelihood"):
|
|
1238
1248
|
_save_profile_likelihood_plot(
|
|
1239
1249
|
opt, optimization_settings.param_names, paths["plot_path"],
|
|
@@ -2259,6 +2259,10 @@ _ENGINE_ONLY_OPTIMIZER_KEYS = (
|
|
|
2259
2259
|
"profile_method",
|
|
2260
2260
|
"profile_optimizer_kwargs",
|
|
2261
2261
|
"profile_grid", # read by pyantigen.engine.Model_optimize._profile_kwargs
|
|
2262
|
+
# Multi-start as its own stage: see _resolve_multistart_stage.
|
|
2263
|
+
"multistart_method",
|
|
2264
|
+
"multistart_optimizer_kwargs",
|
|
2265
|
+
"multistart_limits",
|
|
2262
2266
|
)
|
|
2263
2267
|
|
|
2264
2268
|
|
|
@@ -2314,6 +2318,58 @@ def _resolve_profile_optimizer(method, optimizer_kwargs):
|
|
|
2314
2318
|
return profile_method, profile_kwargs
|
|
2315
2319
|
|
|
2316
2320
|
|
|
2321
|
+
def _resolve_multistart_stage(method, optimizer_kwargs):
|
|
2322
|
+
"""Method and kwargs for a separate multi-start stage, or (None, {}).
|
|
2323
|
+
|
|
2324
|
+
Absent ``multistart_method`` in optimizer_kwargs, multi-start is what it
|
|
2325
|
+
always was: every start is fitted with the spec's own method and options and
|
|
2326
|
+
the best of them IS the fit.
|
|
2327
|
+
|
|
2328
|
+
Present, multi-start becomes a stage that runs BEFORE the fit. Each start is
|
|
2329
|
+
run with this method and ``multistart_optimizer_kwargs`` -- typically a
|
|
2330
|
+
bounds-respecting method with a small iteration cap, since the point is to
|
|
2331
|
+
find the basin, not to converge -- and the best point it reaches is where
|
|
2332
|
+
the standard fit then starts. A global method is not accepted here: it would
|
|
2333
|
+
search the box the starts are meant to sample.
|
|
2334
|
+
"""
|
|
2335
|
+
kw = optimizer_kwargs or {}
|
|
2336
|
+
ms_method = kw.get("multistart_method")
|
|
2337
|
+
if not ms_method:
|
|
2338
|
+
return None, {}
|
|
2339
|
+
if str(ms_method).lower() in _GLOBAL_METHODS:
|
|
2340
|
+
raise ValueError(
|
|
2341
|
+
f"multistart_method {ms_method!r} is a global method; the multi-start "
|
|
2342
|
+
f"stage runs a local method from each sampled start.")
|
|
2343
|
+
return ms_method, dict(kw.get("multistart_optimizer_kwargs") or {})
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
# What a multi-start evaluation may spend, and what a failure costs.
|
|
2347
|
+
# max_attempts safe_simulate attempts per simulation, the first included.
|
|
2348
|
+
# max_depth levels of time-span subdivision after those fail; 0 = none.
|
|
2349
|
+
# failure_dnll a failed evaluation is scored at the worst finite NLL among
|
|
2350
|
+
# the screened starts PLUS this, instead of 1e10.
|
|
2351
|
+
# 1e10 is a cliff: a gradient method that steps into it reads a slope of
|
|
2352
|
+
# ~1e10/step, shrinks to nothing and stalls. A finite, merely large penalty is
|
|
2353
|
+
# something the line search can step back from. It is fixed once, from the
|
|
2354
|
+
# screen, so the surface does not move under the optimizer while it runs.
|
|
2355
|
+
_MULTISTART_LIMIT_DEFAULTS = {"max_attempts": 4, "max_depth": 0, "failure_dnll": 1000.0}
|
|
2356
|
+
|
|
2357
|
+
|
|
2358
|
+
def _resolve_multistart_limits(optimizer_kwargs):
|
|
2359
|
+
"""The multi-start stage's evaluation limits: defaults overlaid by the spec's
|
|
2360
|
+
``multistart_limits``. An unknown key is an error, not a silent no-op."""
|
|
2361
|
+
given = dict((optimizer_kwargs or {}).get("multistart_limits") or {})
|
|
2362
|
+
unknown = sorted(set(given) - set(_MULTISTART_LIMIT_DEFAULTS))
|
|
2363
|
+
if unknown:
|
|
2364
|
+
raise ValueError(f"unknown multistart_limits key(s) {unknown}; expected a "
|
|
2365
|
+
f"subset of {sorted(_MULTISTART_LIMIT_DEFAULTS)}")
|
|
2366
|
+
out = {**_MULTISTART_LIMIT_DEFAULTS, **given}
|
|
2367
|
+
if out["failure_dnll"] is not None and not float(out["failure_dnll"]) > 0:
|
|
2368
|
+
raise ValueError("multistart_limits['failure_dnll'] must be positive, or "
|
|
2369
|
+
"None to keep the 1e10 failure value")
|
|
2370
|
+
return out
|
|
2371
|
+
|
|
2372
|
+
|
|
2317
2373
|
def _resolve_named(value, param_names, label):
|
|
2318
2374
|
"""Normalize a per-parameter spec value (``x0`` or ``bounds``) to a list
|
|
2319
2375
|
aligned with ``param_names``.
|
|
@@ -2558,7 +2614,8 @@ def _multistart_points(x0, bounds, scales, n_starts, search_decades=None,
|
|
|
2558
2614
|
|
|
2559
2615
|
|
|
2560
2616
|
def _run_multistart(objective, starts, method, bounds, opt_kw, scales,
|
|
2561
|
-
screen=True, failure_value=1e10, verbose=True
|
|
2617
|
+
screen=True, failure_value=1e10, verbose=True,
|
|
2618
|
+
failure_dnll=None):
|
|
2562
2619
|
"""Fit from every start, keep the best, and report the spread.
|
|
2563
2620
|
|
|
2564
2621
|
The spread is as much the point as the best value. If every start lands on
|
|
@@ -2567,6 +2624,13 @@ def _run_multistart(objective, starts, method, bounds, opt_kw, scales,
|
|
|
2567
2624
|
confidence interval anchored on any single one of them is measuring the
|
|
2568
2625
|
wrong basin -- which the engine can otherwise only discover much later, via
|
|
2569
2626
|
``profile_anchor_gap`` finding a point better than the reported optimum.
|
|
2627
|
+
|
|
2628
|
+
``failure_dnll``: when given, an evaluation that fails (the objective's
|
|
2629
|
+
``failure_value``, or a non-finite NLL) is scored at the worst finite NLL
|
|
2630
|
+
seen in the screen plus this, for the fits. The fits then see a large but
|
|
2631
|
+
finite wall rather than a 1e10 cliff, which a gradient method such as
|
|
2632
|
+
L-BFGS-B can step back from. The reference is fixed once, so the surface
|
|
2633
|
+
does not move while the optimizer runs.
|
|
2570
2634
|
"""
|
|
2571
2635
|
from scipy.optimize import minimize
|
|
2572
2636
|
|
|
@@ -2582,17 +2646,24 @@ def _run_multistart(objective, starts, method, bounds, opt_kw, scales,
|
|
|
2582
2646
|
#
|
|
2583
2647
|
# Start 1 is never screened out. It is the spec's own x0, and if that
|
|
2584
2648
|
# cannot be integrated the caller needs to see the failure, not a silent skip.
|
|
2649
|
+
def _eval(s):
|
|
2650
|
+
try:
|
|
2651
|
+
return float(objective(s))
|
|
2652
|
+
except Exception:
|
|
2653
|
+
return float("inf")
|
|
2654
|
+
|
|
2655
|
+
seen = [] # finite NLLs from the screen: the failure penalty's reference
|
|
2585
2656
|
if screen and len(starts) > 1:
|
|
2586
2657
|
kept, dropped = [], 0
|
|
2587
2658
|
for i, s in enumerate(starts):
|
|
2588
|
-
if i == 0:
|
|
2659
|
+
if i == 0 and failure_dnll is None:
|
|
2589
2660
|
kept.append(s)
|
|
2590
2661
|
continue
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
v
|
|
2595
|
-
if
|
|
2662
|
+
v = _eval(s)
|
|
2663
|
+
ok = np.isfinite(v) and v < failure_value
|
|
2664
|
+
if ok:
|
|
2665
|
+
seen.append(v)
|
|
2666
|
+
if i == 0 or ok:
|
|
2596
2667
|
kept.append(s)
|
|
2597
2668
|
else:
|
|
2598
2669
|
dropped += 1
|
|
@@ -2601,14 +2672,42 @@ def _run_multistart(objective, starts, method, bounds, opt_kw, scales,
|
|
|
2601
2672
|
f"the model does not integrate there.")
|
|
2602
2673
|
starts = kept
|
|
2603
2674
|
|
|
2675
|
+
fit_objective, failed, penalty = objective, [0], None
|
|
2676
|
+
if failure_dnll is not None:
|
|
2677
|
+
if not seen and starts:
|
|
2678
|
+
v = _eval(starts[0])
|
|
2679
|
+
if np.isfinite(v) and v < failure_value:
|
|
2680
|
+
seen.append(v)
|
|
2681
|
+
if seen:
|
|
2682
|
+
penalty = max(seen) + float(failure_dnll)
|
|
2683
|
+
|
|
2684
|
+
def fit_objective(x):
|
|
2685
|
+
v = _eval(x)
|
|
2686
|
+
if np.isfinite(v) and v < failure_value:
|
|
2687
|
+
return v
|
|
2688
|
+
failed[0] += 1
|
|
2689
|
+
return penalty
|
|
2690
|
+
|
|
2691
|
+
if verbose:
|
|
2692
|
+
print(f" [opt] a failed evaluation scores {penalty:.6g} "
|
|
2693
|
+
f"(worst screened nll {max(seen):.6g} + {float(failure_dnll):g}) "
|
|
2694
|
+
f"during the multi-start fits.")
|
|
2695
|
+
elif verbose:
|
|
2696
|
+
print(" [opt] no screened start evaluated, so failed evaluations keep "
|
|
2697
|
+
"their 1e10 value in the multi-start fits.")
|
|
2698
|
+
|
|
2604
2699
|
records, best, best_i = [], None, -1
|
|
2605
2700
|
for i, s in enumerate(starts):
|
|
2606
2701
|
s = np.asarray(s, dtype=float)
|
|
2607
2702
|
try:
|
|
2608
|
-
r = minimize(
|
|
2703
|
+
r = minimize(fit_objective, s, method=method,
|
|
2609
2704
|
bounds=bounds or None, **opt_kw)
|
|
2610
2705
|
fun = float(r.fun)
|
|
2611
2706
|
ok, msg = bool(r.success), str(r.message)
|
|
2707
|
+
if penalty is not None and fun >= penalty:
|
|
2708
|
+
# Ended on the failure plateau: not an optimum of anything.
|
|
2709
|
+
fun, ok = float("inf"), False
|
|
2710
|
+
msg = "ended where the model does not integrate (failure penalty)"
|
|
2612
2711
|
except Exception as exc:
|
|
2613
2712
|
r, fun, ok = None, float("inf"), False
|
|
2614
2713
|
msg = f"{type(exc).__name__}: {exc}"
|
|
@@ -2626,6 +2725,10 @@ def _run_multistart(objective, starts, method, bounds, opt_kw, scales,
|
|
|
2626
2725
|
print(f" [opt] start {i + 1}/{len(starts)}: nll={fun:.6g}{mark}",
|
|
2627
2726
|
flush=True)
|
|
2628
2727
|
|
|
2728
|
+
if verbose and failed[0]:
|
|
2729
|
+
print(f" [opt] {failed[0]} evaluation(s) failed during the multi-start fits "
|
|
2730
|
+
f"and were scored at the failure penalty.")
|
|
2731
|
+
|
|
2629
2732
|
if verbose:
|
|
2630
2733
|
vals = [rec["fun"] for rec in records if rec["fun"] is not None]
|
|
2631
2734
|
if len(vals) > 1:
|
|
@@ -6713,8 +6816,43 @@ def run_optimization_from_groups(
|
|
|
6713
6816
|
seed=getattr(optimization_spec, "start_seed", None),
|
|
6714
6817
|
sampler=getattr(optimization_spec, "start_sampler", "lhs"),
|
|
6715
6818
|
)
|
|
6819
|
+
# A separate multi-start stage, when the spec asks for one: the
|
|
6820
|
+
# starts are run with their own (cheaper, bounds-respecting) method
|
|
6821
|
+
# and the best point reached is where the standard fit below starts.
|
|
6822
|
+
# Without it, the branch further down fits every start with the
|
|
6823
|
+
# spec's own method and the best of them is the answer.
|
|
6824
|
+
staged_start = None
|
|
6825
|
+
ms_method, ms_kw_raw = _resolve_multistart_stage(method, optimizer_kwargs)
|
|
6826
|
+
if len(starts) > 1 and ms_method:
|
|
6827
|
+
if fit_cache is not None and fit_cache.load_partial() is not None:
|
|
6828
|
+
# The standard fit already started in an earlier launch, so
|
|
6829
|
+
# the stage finished then; its answer is in the partial.
|
|
6830
|
+
print("[opt] multi-start stage skipped: an earlier launch "
|
|
6831
|
+
"already began the standard fit from its result.")
|
|
6832
|
+
else:
|
|
6833
|
+
ms_kw = _prepare_optimizer_kwargs(ms_method, ms_kw_raw, fast, None, None)
|
|
6834
|
+
limits = _resolve_multistart_limits(optimizer_kwargs)
|
|
6835
|
+
print(f"[opt] multi-start stage: {len(starts)} starts with "
|
|
6836
|
+
f"{ms_method}, then the standard fit ({method}) from the best. "
|
|
6837
|
+
f"Per evaluation: {limits['max_attempts']} solver attempt(s), "
|
|
6838
|
+
f"subdivision depth {limits['max_depth']}.")
|
|
6839
|
+
from pyantigen.engine.Simulate import retry_limits
|
|
6840
|
+
# The full ladder is restored on leaving the block, so the
|
|
6841
|
+
# standard fit and every diagnostic after it run as before.
|
|
6842
|
+
with retry_limits(limits["max_attempts"], limits["max_depth"]):
|
|
6843
|
+
ms_res, start_records, _best_start = _run_multistart(
|
|
6844
|
+
objective, starts, ms_method, bounds, ms_kw, scales,
|
|
6845
|
+
failure_dnll=limits["failure_dnll"])
|
|
6846
|
+
if ms_res is None:
|
|
6847
|
+
print("[opt] every multi-start fit failed; the standard "
|
|
6848
|
+
"fit starts from the spec's x0.")
|
|
6849
|
+
else:
|
|
6850
|
+
staged_start = np.asarray(ms_res.x, dtype=float)
|
|
6851
|
+
print(f"[opt] multi-start stage best nll {float(ms_res.fun):.6g} "
|
|
6852
|
+
f"(start {_best_start + 1}); the standard fit starts there.")
|
|
6853
|
+
starts = starts[:1]
|
|
6716
6854
|
if len(starts) == 1:
|
|
6717
|
-
x_start = x0
|
|
6855
|
+
x_start = x0 if staged_start is None else staged_start
|
|
6718
6856
|
partial = fit_cache.load_partial() if fit_cache is not None else None
|
|
6719
6857
|
if partial is not None:
|
|
6720
6858
|
# A previous launch of this same fit was killed before it
|
|
@@ -359,6 +359,62 @@ class search_mode:
|
|
|
359
359
|
return False
|
|
360
360
|
|
|
361
361
|
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# Retry limits: a shorter ladder, but still a ladder
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
#
|
|
366
|
+
# Search mode (above) removes the ladder altogether. The multi-start stage wants
|
|
367
|
+
# something in between: most vectors it samples integrate on the first attempt,
|
|
368
|
+
# and a handful need a looser tolerance, but none is worth the full ladder of ten
|
|
369
|
+
# attempts and four levels of time-span subdivision that a vector somebody cares
|
|
370
|
+
# about gets. Limits cap both. With max_attempts=4 and max_depth=0 a vector gets
|
|
371
|
+
# the fast path plus three loosened retries, is never subdivided, and a failure
|
|
372
|
+
# raises -- which the objective turns into its failure value.
|
|
373
|
+
#
|
|
374
|
+
# Unlike search mode this does not cap the step allowance, so a vector that
|
|
375
|
+
# integrates slowly still integrates. Process-global, like search mode; the
|
|
376
|
+
# default (None) is the original behaviour, 10 attempts and depth 4.
|
|
377
|
+
_DEFAULT_MAX_ATTEMPTS = 10
|
|
378
|
+
_DEFAULT_MAX_DEPTH = 4
|
|
379
|
+
_RETRY_LIMITS = None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def set_retry_limits(max_attempts=None, max_depth=None):
|
|
383
|
+
"""Cap safe_simulate's ladder. ``None`` for both restores the defaults.
|
|
384
|
+
|
|
385
|
+
max_attempts counts the first (fast-path) attempt, as the log's
|
|
386
|
+
"Attempt n/N" does. Returns the previous setting so a caller can restore it.
|
|
387
|
+
"""
|
|
388
|
+
global _RETRY_LIMITS
|
|
389
|
+
previous = _RETRY_LIMITS
|
|
390
|
+
if max_attempts is None and max_depth is None:
|
|
391
|
+
_RETRY_LIMITS = None
|
|
392
|
+
return previous
|
|
393
|
+
attempts = _DEFAULT_MAX_ATTEMPTS if max_attempts is None else int(max_attempts)
|
|
394
|
+
depth = _DEFAULT_MAX_DEPTH if max_depth is None else int(max_depth)
|
|
395
|
+
if attempts < 1 or depth < 0:
|
|
396
|
+
raise ValueError(f"retry limits need max_attempts >= 1 and max_depth >= 0, "
|
|
397
|
+
f"got {attempts} and {depth}")
|
|
398
|
+
_RETRY_LIMITS = (attempts, depth)
|
|
399
|
+
return previous
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class retry_limits:
|
|
403
|
+
"""``with retry_limits(4, 0): ...`` -- the limits for the block."""
|
|
404
|
+
|
|
405
|
+
def __init__(self, max_attempts=None, max_depth=None):
|
|
406
|
+
self.max_attempts, self.max_depth = max_attempts, max_depth
|
|
407
|
+
|
|
408
|
+
def __enter__(self):
|
|
409
|
+
self._previous = set_retry_limits(self.max_attempts, self.max_depth)
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
def __exit__(self, *exc):
|
|
413
|
+
global _RETRY_LIMITS
|
|
414
|
+
_RETRY_LIMITS = self._previous
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
|
|
362
418
|
def _simulate_search(r, start, end, points, observed_species):
|
|
363
419
|
"""One capped attempt. Raises on failure; leaves the step allowance as found."""
|
|
364
420
|
original = r.integrator.maximum_num_steps
|
|
@@ -454,7 +510,7 @@ def safe_simulate(r, solver_settings, observed_species, depth=0, label=None):
|
|
|
454
510
|
label_prefix = f" [{label}]" if label else ""
|
|
455
511
|
print(f" [safe_simulate]{label_prefix} (Depth {depth}) Initial attempt failed: {first_err_str}")
|
|
456
512
|
|
|
457
|
-
max_attempts =
|
|
513
|
+
max_attempts, max_depth = _RETRY_LIMITS or (_DEFAULT_MAX_ATTEMPTS, _DEFAULT_MAX_DEPTH)
|
|
458
514
|
attempt = 1 # one attempt already burned on the fast path
|
|
459
515
|
|
|
460
516
|
while attempt < max_attempts:
|
|
@@ -513,7 +569,7 @@ def safe_simulate(r, solver_settings, observed_species, depth=0, label=None):
|
|
|
513
569
|
)
|
|
514
570
|
|
|
515
571
|
# Subdivide as last resort
|
|
516
|
-
if depth <
|
|
572
|
+
if depth < max_depth and points >= 2:
|
|
517
573
|
mid = (start + end) / 2.0
|
|
518
574
|
p1 = max(2, points // 2)
|
|
519
575
|
p2 = max(2, points - p1)
|
|
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.0.2 → pyantigen-2.0.3}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md
RENAMED
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/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
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Optimizer_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/Modules/Update_opt_parameters.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example1_ADneg.json
RENAMED
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example1_ADpos.json
RENAMED
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/pyantigen/template/Example/optimizations/Example3_joint.json
RENAMED
|
File without changes
|
{pyantigen-2.0.2 → pyantigen-2.0.3}/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
|