PyAntiGen 1.0.14__tar.gz → 1.0.15__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-1.0.14 → pyantigen-1.0.15}/PKG-INFO +1 -1
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/PKG-INFO +1 -1
- pyantigen-1.0.15/PyAntiGen.egg-info/scm_version.json +8 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/_version.py +3 -3
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Evaluator.py +21 -2
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Optimize.py +204 -28
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Profile_checkpoint.py +48 -9
- pyantigen-1.0.14/PyAntiGen.egg-info/scm_version.json +0 -8
- {pyantigen-1.0.14 → pyantigen-1.0.15}/.github/workflows/publish.yml +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/.gitignore +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/.vscode/launch.json +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/LICENSE +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/MANIFEST.in +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/SOURCES.txt +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/dependency_links.txt +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/entry_points.txt +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/requires.txt +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/scm_file_list.json +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/PyAntiGen.egg-info/top_level.txt +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/README.md +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/.agents/skills/module_generation/SKILL.md +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/.gitignore +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/AntimonyGen.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/RxnDict_to_antimony.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/TelluriumGen.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/__init__.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/antimony_utils.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/cli.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/data_interpolation.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/isotopomer_tools.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/model_generation.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/models.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/module_base.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/pyantigen.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/rate_laws.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/reaction_creation.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/AntiGen_paths.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Anchor_cache.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Deadline.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Differential_evolution.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Event_times.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Fast_profile.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Fit_cache.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Identifiability.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Model_optimize.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Model_simulate.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Nelder_mead.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Noise_floor.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Petab_export.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Preequil_cache.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/README.md +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Results.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Sensitivity_analysis.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Simulate.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Flipflop_reference.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Model_generate.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Model_run.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Data.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Events.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Experiment.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Loss_config.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Observed_species.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Optimizer_settings.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Plots.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Solver_settings.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Update_opt_parameters.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Update_parameters.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/data/ADneg.csv +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/data/ADpos.csv +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/data/Flipflop.csv +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/data/make_flipflop_data.py +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/pyproject.toml +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/setup.cfg +0 -0
- {pyantigen-1.0.14 → pyantigen-1.0.15}/setup.py +0 -0
|
@@ -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 = '1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.15'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 15)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd95e4a4f4'
|
|
@@ -197,6 +197,10 @@ def _init_worker(spec_blob):
|
|
|
197
197
|
replicate["Update_parameters"](
|
|
198
198
|
OptRoadRunnerProxy(r, spec.param_names), replicate
|
|
199
199
|
)
|
|
200
|
+
# Every run in this worker starts from here; see
|
|
201
|
+
# Optimize.restore_parameter_baseline.
|
|
202
|
+
from Engine.Optimize import remember_parameter_baseline
|
|
203
|
+
remember_parameter_baseline(r)
|
|
200
204
|
# The parent's attachment closes over the parent's RoadRunner and could
|
|
201
205
|
# not be shipped here, so it was stripped from the spec. A worker must
|
|
202
206
|
# attach against the model it will integrate anyway -- reusing the
|
|
@@ -402,7 +406,9 @@ def _profile_task(job):
|
|
|
402
406
|
res = None
|
|
403
407
|
outcome = "done"
|
|
404
408
|
if resumable:
|
|
405
|
-
identity = point_identity(
|
|
409
|
+
identity = point_identity(
|
|
410
|
+
param_idx, x_fixed, x_start.size, method,
|
|
411
|
+
param_name=spec.param_names[param_idx], bounds=bounds)
|
|
406
412
|
nm_state = state_from_json(
|
|
407
413
|
load_point_state(state_path, identity), x_start.size)
|
|
408
414
|
if nm_state is not None:
|
|
@@ -642,7 +648,9 @@ class ParallelEvaluator:
|
|
|
642
648
|
losses = ev.evaluate_batch(xs)
|
|
643
649
|
|
|
644
650
|
``evaluate_batch`` preserves input order. Failures come back as
|
|
645
|
-
``FAILURE_VALUE`` and are counted in ``ev.n_failures`` rather than raised
|
|
651
|
+
``FAILURE_VALUE`` and are counted in ``ev.n_failures`` rather than raised
|
|
652
|
+
-- except a broken pool, which raises ``BrokenProcessPool`` so the caller
|
|
653
|
+
can redo the batch rather than consume a half-sentinel result.
|
|
646
654
|
"""
|
|
647
655
|
|
|
648
656
|
def __init__(self, spec, n_workers=None, chunk_size=None, verbose=True,
|
|
@@ -814,6 +822,7 @@ class ParallelEvaluator:
|
|
|
814
822
|
self.start()
|
|
815
823
|
|
|
816
824
|
from concurrent.futures import wait, FIRST_COMPLETED
|
|
825
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
817
826
|
|
|
818
827
|
t0 = time.time()
|
|
819
828
|
tag = f" [{label}]" if label else ""
|
|
@@ -873,6 +882,16 @@ class ParallelEvaluator:
|
|
|
873
882
|
i = futures[fut]
|
|
874
883
|
try:
|
|
875
884
|
val, status, secs = fut.result()
|
|
885
|
+
except BrokenProcessPool as exc:
|
|
886
|
+
# A dead worker (segfault, OOM kill) fails every future
|
|
887
|
+
# still in flight, not just its own. Scoring those as
|
|
888
|
+
# sentinels would hand the caller a batch that is part
|
|
889
|
+
# real values and part 1e10 -- a Hessian stencil built
|
|
890
|
+
# from that is finite and silently meaningless. Raise so
|
|
891
|
+
# the caller redoes the whole batch serially instead.
|
|
892
|
+
raise BrokenProcessPool(
|
|
893
|
+
f"worker pool broke during batch{tag} after "
|
|
894
|
+
f"{done}/{n} evaluation(s): {exc}") from exc
|
|
876
895
|
except Exception as exc:
|
|
877
896
|
val = FAILURE_VALUE
|
|
878
897
|
status = f"error: {type(exc).__name__}: {exc}"
|
|
@@ -301,6 +301,59 @@ def _resolve_obs_df(df_dict, obs_cfg):
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Global parameters an event assigned survive r.reset()
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
#
|
|
308
|
+
# RoadRunner's reset() restores time, floating species and rate-rule values,
|
|
309
|
+
# but NOT a global parameter that an event assigned during the previous run.
|
|
310
|
+
# An arm whose events switch something on and leave it on -- a labelling
|
|
311
|
+
# fraction, a washout rate, a catheter drain -- therefore starts every
|
|
312
|
+
# simulation after the first from birth with that switch already thrown, and
|
|
313
|
+
# the objective stops being a function of the parameters alone: the first
|
|
314
|
+
# evaluation differs from every later one at the same point.
|
|
315
|
+
#
|
|
316
|
+
# Measured on the Cook arms, whose end-of-infusion event leaves k_leu_washout
|
|
317
|
+
# at 0.125: at cook_turnover's 2026-09-29 optimum the first evaluation drew a
|
|
318
|
+
# vehicle NewAB40_CM peak of 1523 pM at 18.5 h and every later one 690 pM at
|
|
319
|
+
# 13.3 h, the label washing out from t = 0. The fit minimized the second
|
|
320
|
+
# function while --simulate (a fresh model, one run) draws the first.
|
|
321
|
+
#
|
|
322
|
+
# Ported from PyAntiGen v2 (cdf1fd8). Each model's global parameter values are
|
|
323
|
+
# recorded once, when the Engine builds it, and run_all puts back any that
|
|
324
|
+
# differ after every reset(). Only parameters the run changed are touched, and
|
|
325
|
+
# assignment-rule parameters, which cannot be set, are skipped. A model built
|
|
326
|
+
# elsewhere is recorded the first time run_all sees it.
|
|
327
|
+
|
|
328
|
+
def remember_parameter_baseline(r):
|
|
329
|
+
"""Record r's settable global parameter values as its per-run start."""
|
|
330
|
+
ids = list(r.getGlobalParameterIds())
|
|
331
|
+
rules = set(r.getAssignmentRuleIds())
|
|
332
|
+
idx = np.array([i for i, p in enumerate(ids) if p not in rules], dtype=int)
|
|
333
|
+
vals = np.asarray(r.getGlobalParameterValues(), dtype=float)
|
|
334
|
+
r._pyantigen_param_baseline = (ids, idx, vals[idx].copy())
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def restore_parameter_baseline(r):
|
|
338
|
+
"""Put back every settable global parameter that differs from the baseline.
|
|
339
|
+
|
|
340
|
+
Returns the names restored (empty when nothing had changed).
|
|
341
|
+
"""
|
|
342
|
+
base = getattr(r, "_pyantigen_param_baseline", None)
|
|
343
|
+
if base is None:
|
|
344
|
+
remember_parameter_baseline(r)
|
|
345
|
+
return []
|
|
346
|
+
ids, idx, vals = base
|
|
347
|
+
cur = np.asarray(r.getGlobalParameterValues(), dtype=float)[idx]
|
|
348
|
+
changed = np.flatnonzero(~((cur == vals) | (np.isnan(cur) & np.isnan(vals))))
|
|
349
|
+
names = []
|
|
350
|
+
for j in changed:
|
|
351
|
+
name = ids[idx[j]]
|
|
352
|
+
r[name] = float(vals[j])
|
|
353
|
+
names.append(name)
|
|
354
|
+
return names
|
|
355
|
+
|
|
356
|
+
|
|
304
357
|
def run_all(r, exp_num, experiment, df_dict, set_parameters=None, parameters=None,
|
|
305
358
|
preequil_cache=None):
|
|
306
359
|
"""
|
|
@@ -318,6 +371,9 @@ def run_all(r, exp_num, experiment, df_dict, set_parameters=None, parameters=Non
|
|
|
318
371
|
Returns a results dict keyed by treatment label.
|
|
319
372
|
"""
|
|
320
373
|
r.reset()
|
|
374
|
+
# reset() leaves event-assigned global parameters at their end-of-run
|
|
375
|
+
# values; see restore_parameter_baseline.
|
|
376
|
+
restore_parameter_baseline(r)
|
|
321
377
|
|
|
322
378
|
# Re-apply treatment-specific parameters which were wiped out by r.reset()
|
|
323
379
|
update_params = experiment.get("Update_parameters")
|
|
@@ -1674,7 +1730,9 @@ def compute_hessian_numdifftools(func, params):
|
|
|
1674
1730
|
# _finite_difference_steps: 0.02 keeps the noise term 4*sigma/h^2 near 0.1 for
|
|
1675
1731
|
# objective noise anywhere from 1e-7 to 1e-4 nats, while the truncation term
|
|
1676
1732
|
# H*h^2/12 stays near 0.003 for curvatures of order 100. Two decades of margin
|
|
1677
|
-
# either side, which is what a numerically noisy objective needs.
|
|
1733
|
+
# either side, which is what a numerically noisy objective needs. It is the
|
|
1734
|
+
# starting and largest step: compute_hessian_batched shrinks it per parameter
|
|
1735
|
+
# where the curvature is too high for it (see _FD_TARGET_RISE).
|
|
1678
1736
|
_LOG_FD_STEP = 0.02
|
|
1679
1737
|
|
|
1680
1738
|
|
|
@@ -1706,6 +1764,10 @@ def _finite_difference_steps(params, epsilon=1e-4, abs_floor=None, scales=None,
|
|
|
1706
1764
|
1e-5 nats, not 1e-16. At h=3e-4 that is an error near 450 against curvatures
|
|
1707
1765
|
of order 100: the Hessian becomes noise, the matrix stops being positive
|
|
1708
1766
|
definite, and sqrt(diag(inv(H))) returns NaN.
|
|
1767
|
+
|
|
1768
|
+
For log10 coordinates this is only the starting step. A parameter whose
|
|
1769
|
+
SE is comparable to it is probed outside its quadratic region, and
|
|
1770
|
+
compute_hessian_batched shrinks the step for it (_calibrate_log_steps).
|
|
1709
1771
|
"""
|
|
1710
1772
|
params = np.atleast_1d(np.asarray(params, dtype=float))
|
|
1711
1773
|
if abs_floor is None:
|
|
@@ -1727,7 +1789,101 @@ def _finite_difference_steps(params, epsilon=1e-4, abs_floor=None, scales=None,
|
|
|
1727
1789
|
return steps
|
|
1728
1790
|
|
|
1729
1791
|
|
|
1730
|
-
|
|
1792
|
+
# A log10 step is sized so that one diagonal probe raises the NLL by about this
|
|
1793
|
+
# many nats. _LOG_FD_STEP alone is a single compromise for every parameter, and
|
|
1794
|
+
# it fails in the tight direction: on the Example spec two parameters with SEs
|
|
1795
|
+
# near 0.01 decades sat 2 SEs from the centre at a 0.02-decade step, outside
|
|
1796
|
+
# the quadratic region, and came back with SEs 12% and 18% off and a
|
|
1797
|
+
# correlation of -0.25 instead of -0.55. Sizing by the rise instead balances
|
|
1798
|
+
# both errors for every parameter at once: truncation is set by how far past
|
|
1799
|
+
# quadratic the probe reaches, which a rise of a tenth of a nat keeps small,
|
|
1800
|
+
# and the noise term 4*sigma/h^2 relative to the curvature is about
|
|
1801
|
+
# 2*sigma/rise, ~2e-4 at sigma = 1e-5 nats. At 0.1 the Example's log10 SEs
|
|
1802
|
+
# land within 0.3% of the exact values; at 0.2, within 0.5%.
|
|
1803
|
+
_FD_TARGET_RISE = 0.1
|
|
1804
|
+
# Smallest log10 step the calibration may choose, in decades. Below this the
|
|
1805
|
+
# noise term takes over again on an ODE objective.
|
|
1806
|
+
_LOG_FD_STEP_MIN = 1e-4
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _check_stencil_values(vals, failure_value, what):
|
|
1810
|
+
"""Raise if any stencil value is non-finite or the failure sentinel."""
|
|
1811
|
+
# The failure sentinel is finite, so a stencil point that failed to
|
|
1812
|
+
# integrate would otherwise pass straight through as a curvature of order
|
|
1813
|
+
# 1e10/h^2 and come out the other side as a plausible-looking SE.
|
|
1814
|
+
bad = ~np.isfinite(vals) | (vals >= failure_value)
|
|
1815
|
+
if np.any(bad):
|
|
1816
|
+
raise ValueError(
|
|
1817
|
+
f"{int(bad.sum())}/{vals.size} {what} point(s) failed to evaluate "
|
|
1818
|
+
f"(failure sentinel or non-finite NLL); the Hessian would be "
|
|
1819
|
+
f"meaningless.")
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _calibrate_log_steps(nll_batch, params, steps, scales, failure_value,
|
|
1823
|
+
target=_FD_TARGET_RISE, min_step=_LOG_FD_STEP_MIN,
|
|
1824
|
+
max_passes=4):
|
|
1825
|
+
"""Shrink each log10 step until one probe raises the NLL by ~*target* nats.
|
|
1826
|
+
|
|
1827
|
+
A pilot pass evaluates the centre and +/- the default step for every
|
|
1828
|
+
log10 parameter. Its mean rise r approximates H_ii h^2 / 2, so the step
|
|
1829
|
+
that gives a rise of *target* is h * sqrt(target / r). A step is accepted
|
|
1830
|
+
once its rise is at most twice *target*.
|
|
1831
|
+
|
|
1832
|
+
That estimate is taken from a probe that is, by construction, too far out,
|
|
1833
|
+
and on a likelihood shaped like (n/2) log(SSE) -- which flattens away from
|
|
1834
|
+
the optimum -- it underestimates the curvature, so a single rescaling can
|
|
1835
|
+
still land a probe at a rise of a nat or more. Parameters whose new step
|
|
1836
|
+
still rises too far are therefore probed again, up to *max_passes* passes
|
|
1837
|
+
in all; each extra pass costs 2 evaluations per such parameter only.
|
|
1838
|
+
|
|
1839
|
+
Steps only shrink: a parameter whose default probe rises less than the
|
|
1840
|
+
limit is inside its quadratic region, and there the larger step is the
|
|
1841
|
+
better defence against integration noise. A rise that is not positive -- a
|
|
1842
|
+
flat or noisy direction -- also keeps the default, as does a step that has
|
|
1843
|
+
reached *min_step*.
|
|
1844
|
+
|
|
1845
|
+
Returns ``(steps, known)``: the calibrated steps, and the pilot values
|
|
1846
|
+
``{point_key: value}`` evaluated at them, so the stencil does not evaluate
|
|
1847
|
+
the same point twice.
|
|
1848
|
+
"""
|
|
1849
|
+
pending = [i for i, s in enumerate(scales or [])
|
|
1850
|
+
if s == "log10" and i < params.size]
|
|
1851
|
+
if not pending:
|
|
1852
|
+
return steps, {}
|
|
1853
|
+
|
|
1854
|
+
steps = np.array(steps, dtype=float, copy=True)
|
|
1855
|
+
known = {}
|
|
1856
|
+
for _ in range(max_passes):
|
|
1857
|
+
points = [] if known else [params.copy()]
|
|
1858
|
+
for i in pending:
|
|
1859
|
+
p_plus = params.copy(); p_plus[i] += steps[i]
|
|
1860
|
+
p_minus = params.copy(); p_minus[i] -= steps[i]
|
|
1861
|
+
points += [p_plus, p_minus]
|
|
1862
|
+
vals = np.asarray(nll_batch(points, label="hessian-step"), dtype=float)
|
|
1863
|
+
_check_stencil_values(vals, failure_value, "Hessian step-calibration")
|
|
1864
|
+
if not known:
|
|
1865
|
+
known[("c",)] = float(vals[0])
|
|
1866
|
+
vals = vals[1:]
|
|
1867
|
+
f0 = known[("c",)]
|
|
1868
|
+
|
|
1869
|
+
still = []
|
|
1870
|
+
for k, i in enumerate(pending):
|
|
1871
|
+
f_plus, f_minus = float(vals[2 * k]), float(vals[2 * k + 1])
|
|
1872
|
+
rise = 0.5 * (f_plus + f_minus) - f0
|
|
1873
|
+
if rise > 2.0 * target and steps[i] > min_step:
|
|
1874
|
+
steps[i] = max(min_step, steps[i] * np.sqrt(target / rise))
|
|
1875
|
+
still.append(i)
|
|
1876
|
+
else:
|
|
1877
|
+
known[("d", i, +1)] = f_plus
|
|
1878
|
+
known[("d", i, -1)] = f_minus
|
|
1879
|
+
pending = still
|
|
1880
|
+
if not pending:
|
|
1881
|
+
break
|
|
1882
|
+
return steps, known
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
def compute_hessian_batched(nll_batch, params, epsilon=1e-4, scales=None,
|
|
1886
|
+
failure_value=1e10):
|
|
1731
1887
|
"""Central-difference Hessian evaluated as one batch.
|
|
1732
1888
|
|
|
1733
1889
|
The stencil is fixed in advance -- 1 centre, 2k diagonal points and 4 points
|
|
@@ -1735,41 +1891,49 @@ def compute_hessian_batched(nll_batch, params, epsilon=1e-4, scales=None):
|
|
|
1735
1891
|
instead of trickling through numdifftools one call at a time. That is
|
|
1736
1892
|
2k^2 + 1 evaluations with no dependencies, which is exactly what the pool
|
|
1737
1893
|
is for.
|
|
1894
|
+
|
|
1895
|
+
Log10 steps are calibrated first (see :func:`_calibrate_log_steps`). Its
|
|
1896
|
+
pilot values are reused for the final steps, so a parameter whose default
|
|
1897
|
+
step already fits costs nothing extra, and one that had to shrink costs 2
|
|
1898
|
+
evaluations per calibration pass.
|
|
1738
1899
|
"""
|
|
1739
1900
|
params = np.atleast_1d(np.asarray(params, dtype=float))
|
|
1740
1901
|
n = params.size
|
|
1741
1902
|
steps = _finite_difference_steps(params, epsilon, scales=scales)
|
|
1903
|
+
steps, known = _calibrate_log_steps(nll_batch, params, steps, scales,
|
|
1904
|
+
failure_value)
|
|
1742
1905
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1906
|
+
# Every stencil point by key; the ones the calibration already evaluated
|
|
1907
|
+
# are filled in from it, the rest go to the pool in one batch.
|
|
1908
|
+
keys, points = [("c",)], [params.copy()]
|
|
1746
1909
|
for i in range(n):
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
points += [p_plus, p_minus]
|
|
1751
|
-
|
|
1910
|
+
for sign in (+1, -1):
|
|
1911
|
+
p = params.copy(); p[i] += sign * steps[i]
|
|
1912
|
+
keys.append(("d", i, sign)); points.append(p)
|
|
1752
1913
|
for i in range(n):
|
|
1753
1914
|
for j in range(i + 1, n):
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1915
|
+
for si, sj in ((+1, +1), (+1, -1), (-1, +1), (-1, -1)):
|
|
1916
|
+
p = params.copy(); p[i] += si * steps[i]; p[j] += sj * steps[j]
|
|
1917
|
+
keys.append(("o", i, j, si, sj)); points.append(p)
|
|
1918
|
+
|
|
1919
|
+
todo = [k for k, key in enumerate(keys) if key not in known]
|
|
1920
|
+
fresh = np.asarray(nll_batch([points[k] for k in todo], label="hessian"),
|
|
1921
|
+
dtype=float)
|
|
1922
|
+
_check_stencil_values(fresh, failure_value, "Hessian stencil")
|
|
1923
|
+
val = dict(known)
|
|
1924
|
+
val.update({keys[k]: float(v) for k, v in zip(todo, fresh)})
|
|
1925
|
+
f0 = val[("c",)]
|
|
1763
1926
|
|
|
1764
1927
|
hessian = np.zeros((n, n))
|
|
1765
1928
|
for i in range(n):
|
|
1766
|
-
|
|
1767
|
-
|
|
1929
|
+
hessian[i, i] = (val[("d", i, +1)] - 2.0 * f0
|
|
1930
|
+
+ val[("d", i, -1)]) / (steps[i] ** 2)
|
|
1768
1931
|
for i in range(n):
|
|
1769
1932
|
for j in range(i + 1, n):
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1933
|
+
h_ij = (val[("o", i, j, +1, +1)] - val[("o", i, j, +1, -1)]
|
|
1934
|
+
- val[("o", i, j, -1, +1)] + val[("o", i, j, -1, -1)]
|
|
1935
|
+
) / (4.0 * steps[i] * steps[j])
|
|
1936
|
+
hessian[i, j] = hessian[j, i] = h_ij
|
|
1773
1937
|
return hessian
|
|
1774
1938
|
|
|
1775
1939
|
|
|
@@ -1875,11 +2039,20 @@ def compute_wald_uncertainty(nll_func, x, bounds=None, loss_scale=1.0, alpha=0.0
|
|
|
1875
2039
|
print(f"\n[Wald] Computing Hessian for {k} parameter(s) "
|
|
1876
2040
|
f"(~{n_evals} silent NLL evaluations)...")
|
|
1877
2041
|
|
|
2042
|
+
if nll_batch is None:
|
|
2043
|
+
# Serial runs (no worker pool) go through the same stencil as pooled
|
|
2044
|
+
# ones, evaluated one point at a time. The numdifftools route used to
|
|
2045
|
+
# be taken here, with a fixed absolute step of 1e-5 and no knowledge of
|
|
2046
|
+
# parameter scales: far below the ODE objective's numerical noise for a
|
|
2047
|
+
# log10 coordinate, and larger than the parameter itself for a small
|
|
2048
|
+
# linear rate constant -- the two failures _finite_difference_steps
|
|
2049
|
+
# exists to prevent. It also bypassed the failure-sentinel check, and
|
|
2050
|
+
# made a serial run's SEs incomparable with a pooled run's.
|
|
2051
|
+
def nll_batch(xs, label=None):
|
|
2052
|
+
return [nll_func(np.asarray(xi, dtype=float)) for xi in xs]
|
|
2053
|
+
|
|
1878
2054
|
try:
|
|
1879
|
-
|
|
1880
|
-
hessian_raw = compute_hessian_batched(nll_batch, x, scales=scales)
|
|
1881
|
-
else:
|
|
1882
|
-
hessian_raw = compute_hessian_numdifftools(nll_func, x)
|
|
2055
|
+
hessian_raw = compute_hessian_batched(nll_batch, x, scales=scales)
|
|
1883
2056
|
except Exception as exc:
|
|
1884
2057
|
print(f"[Wald] Hessian computation failed: {exc}")
|
|
1885
2058
|
return None, None, nan_ci
|
|
@@ -5575,6 +5748,7 @@ def run_optimization(
|
|
|
5575
5748
|
r = TelluriumGen(model_text + "\n" + events_str, paths)
|
|
5576
5749
|
r_proxy = OptRoadRunnerProxy(r, param_names)
|
|
5577
5750
|
experiment["Update_parameters"](r_proxy, experiment)
|
|
5751
|
+
remember_parameter_baseline(r)
|
|
5578
5752
|
attach_event_times(experiment, r, verbose=True)
|
|
5579
5753
|
|
|
5580
5754
|
models[exp_num] = {"r_ic": r_ic, "r": r, "df_dict": df_dict}
|
|
@@ -6015,6 +6189,8 @@ def run_optimization_from_groups(
|
|
|
6015
6189
|
r = TelluriumGen(model_text + "\n" + events_str, paths)
|
|
6016
6190
|
r_proxy = OptRoadRunnerProxy(r, param_names)
|
|
6017
6191
|
replicate["Update_parameters"](r_proxy, replicate)
|
|
6192
|
+
# Before anything integrates it: every later run starts from here.
|
|
6193
|
+
remember_parameter_baseline(r)
|
|
6018
6194
|
|
|
6019
6195
|
# Where this arm's discontinuities are, read off the compiled model.
|
|
6020
6196
|
# Attached rather than computed, because a trigger built on a fitted
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Profile_checkpoint.py
RENAMED
|
@@ -116,7 +116,7 @@ def point_state_path(state_dir, param_name, x_fixed):
|
|
|
116
116
|
return os.path.join(state_dir, _safe_name(f"{param_name}__{key!r}") + ".json")
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def point_identity(param_idx, x_fixed, n_nuisance, method):
|
|
119
|
+
def point_identity(param_idx, x_fixed, n_nuisance, method, param_name, bounds):
|
|
120
120
|
"""What a saved state must match to be resumed by a job.
|
|
121
121
|
|
|
122
122
|
Deliberately not the job's starting point or its simplex: those differ
|
|
@@ -124,10 +124,42 @@ def point_identity(param_idx, x_fixed, n_nuisance, method):
|
|
|
124
124
|
stopped on the clock is resumed from a record, not from the job that began
|
|
125
125
|
it), and a state is the more exact of the two resume sources whenever it
|
|
126
126
|
exists. What it must never be is another *problem*.
|
|
127
|
+
|
|
128
|
+
THAT GUARANTEE WAS MISSING until 2026-09-25. ``param_idx`` is a POSITION,
|
|
129
|
+
not an identity: a reparametrization that keeps the same total and
|
|
130
|
+
nuisance parameter counts but changes what occupies each slot leaves
|
|
131
|
+
``param_idx``, ``n`` and ``method`` all unchanged while the meaning of
|
|
132
|
+
every nuisance coordinate changes underneath them. This happened for
|
|
133
|
+
real the same day -- four independent k_gammasec_*_base constants
|
|
134
|
+
(silk_appfull, slots 5-8) were replaced by Vm_C99_Abeta_base and the
|
|
135
|
+
softmax logits y_gammaAB38/40/42, same 18 total, same 17 nuisance, same
|
|
136
|
+
slots -- and a state resumed from before the change into a run after it
|
|
137
|
+
would have been accepted, with the incumbent vertex it carried never
|
|
138
|
+
revalidated against the new bounds (``Engine.Nelder_mead.state_from_json``
|
|
139
|
+
checks shape, not bounds; only a *fresh* ``new_state`` clips). ``bounds``
|
|
140
|
+
closes the same gap for a bounds-only change: :func:`spec_fingerprint`
|
|
141
|
+
does not hash bounds at all, so tightening or correcting one does not
|
|
142
|
+
start a new checkpoint directory on its own, and nothing else stood
|
|
143
|
+
between an old, wider-bounded simplex and a run enforcing a new bound.
|
|
144
|
+
|
|
145
|
+
``param_name`` and ``bounds`` are therefore folded into the identity
|
|
146
|
+
below as a hash, not compared field by field: their exact representation
|
|
147
|
+
(a numpy dtype, a tuple vs a list) can differ harmlessly between the run
|
|
148
|
+
that wrote a state and the one resuming it, and a hash of their JSON form
|
|
149
|
+
is stable across that while still changing the moment either actually
|
|
150
|
+
means something different.
|
|
127
151
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
payload = {"param_idx": int(param_idx),
|
|
153
|
+
"x_fixed": round(float(x_fixed), 12),
|
|
154
|
+
"n": int(n_nuisance), "method": str(method).lower()}
|
|
155
|
+
blob = json.dumps({
|
|
156
|
+
"param_name": str(param_name),
|
|
157
|
+
"bounds": [None if b is None else
|
|
158
|
+
[None if v is None else round(float(v), 12) for v in b]
|
|
159
|
+
for b in bounds] if bounds is not None else None,
|
|
160
|
+
}, sort_keys=True)
|
|
161
|
+
payload["fingerprint"] = hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]
|
|
162
|
+
return payload
|
|
131
163
|
|
|
132
164
|
|
|
133
165
|
def save_point_state(path, identity, nm_state):
|
|
@@ -338,11 +370,18 @@ def spec_fingerprint(param_names, x_opt, groups, scales, model_text,
|
|
|
338
370
|
# sigma profiled out analytically. It is the same function the
|
|
339
371
|
# fit minimizes, so its dNLL is on a different scale again and
|
|
340
372
|
# v1/v2 points must not be resumed into a v3 run.
|
|
341
|
-
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
373
|
+
# v4 keeps v3 for the fit, Wald and slices, but evaluates every
|
|
374
|
+
# profile point with each sigma-floored block pinned at its
|
|
375
|
+
# own sigma_used from the fit (Optimize._freeze_floor), and
|
|
376
|
+
# measures dNLL from an anchor computed the same way. v3
|
|
377
|
+
# points re-concentrated those sigmas at every point, so they
|
|
378
|
+
# sit on a different curve and must not be resumed into v4.
|
|
379
|
+
"likelihood_convention": "v4-concentrated-gaussian-frozen-floor",
|
|
380
|
+
# The fit's per-block sigmas. Unfloored blocks are still profiled
|
|
381
|
+
# out per evaluation, but floored blocks are frozen at these values
|
|
382
|
+
# for every profile point, so they now enter dNLL directly -- a
|
|
383
|
+
# different sigma is a different profile. Also a compact
|
|
384
|
+
# fingerprint of the residuals at the optimum.
|
|
346
385
|
"sigmas": sorted(
|
|
347
386
|
(str(k), round(float(v), 12))
|
|
348
387
|
for k, v in (fixed_sigmas or {}).items()
|
|
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-1.0.14 → pyantigen-1.0.15}/framework/.agents/skills/ode_conversion_antimony/SKILL.md
RENAMED
|
File without changes
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/.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
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Differential_evolution.py
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
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Engine/Sensitivity_analysis.py
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
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Observed_species.py
RENAMED
|
File without changes
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Optimizer_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Update_opt_parameters.py
RENAMED
|
File without changes
|
{pyantigen-1.0.14 → pyantigen-1.0.15}/framework/template/Example/Modules/Update_parameters.py
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
|