PyAntiGen 1.0.9__py3-none-any.whl
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.
- framework/AntimonyGen.py +48 -0
- framework/RxnDict_to_antimony.py +594 -0
- framework/TelluriumGen.py +16 -0
- framework/__init__.py +0 -0
- framework/antimony_utils.py +294 -0
- framework/cli.py +229 -0
- framework/data_interpolation.py +340 -0
- framework/isotopomer_tools.py +41 -0
- framework/model_generation.py +46 -0
- framework/models.py +189 -0
- framework/module_base.py +42 -0
- framework/pyantigen.py +51 -0
- framework/rate_laws.py +101 -0
- framework/reaction_creation.py +43 -0
- framework/template/Example/AntiGen_paths.py +23 -0
- framework/template/Example/Engine/Anchor_cache.py +193 -0
- framework/template/Example/Engine/Deadline.py +535 -0
- framework/template/Example/Engine/Evaluator.py +1176 -0
- framework/template/Example/Engine/Event_times.py +491 -0
- framework/template/Example/Engine/Fast_profile.py +701 -0
- framework/template/Example/Engine/Fit_cache.py +329 -0
- framework/template/Example/Engine/Identifiability.py +698 -0
- framework/template/Example/Engine/Model_optimize.py +1483 -0
- framework/template/Example/Engine/Model_simulate.py +124 -0
- framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
- framework/template/Example/Engine/Optimize.py +6862 -0
- framework/template/Example/Engine/Petab_export.py +398 -0
- framework/template/Example/Engine/Preequil_cache.py +361 -0
- framework/template/Example/Engine/Profile_checkpoint.py +399 -0
- framework/template/Example/Engine/Results.py +395 -0
- framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
- framework/template/Example/Engine/Simulate.py +617 -0
- framework/template/Example/Flipflop_reference.py +401 -0
- framework/template/Example/Model_generate.py +37 -0
- framework/template/Example/Model_run.py +261 -0
- framework/template/Example/Modules/Data.py +63 -0
- framework/template/Example/Modules/Events.py +14 -0
- framework/template/Example/Modules/Experiment.py +194 -0
- framework/template/Example/Modules/Loss_config.py +61 -0
- framework/template/Example/Modules/Observed_species.py +3 -0
- framework/template/Example/Modules/Optimizer_settings.py +258 -0
- framework/template/Example/Modules/Plots.py +89 -0
- framework/template/Example/Modules/Solver_settings.py +16 -0
- framework/template/Example/Modules/Update_opt_parameters.py +24 -0
- framework/template/Example/Modules/Update_parameters.py +49 -0
- framework/template/data/ADneg.csv +27 -0
- framework/template/data/ADpos.csv +27 -0
- framework/template/data/Flipflop.csv +29 -0
- framework/template/data/make_flipflop_data.py +174 -0
- pyantigen-1.0.9.dist-info/METADATA +129 -0
- pyantigen-1.0.9.dist-info/RECORD +55 -0
- pyantigen-1.0.9.dist-info/WHEEL +5 -0
- pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
- pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
- pyantigen-1.0.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Append optimization results as a single row to a CSV file, and write a
|
|
3
|
+
per-run JSON snapshot alongside it.
|
|
4
|
+
|
|
5
|
+
CSV columns written:
|
|
6
|
+
timestamp, model_name, experiment_id, method, success, optimizer_message,
|
|
7
|
+
total_loss, aic, bic,
|
|
8
|
+
{param} – optimized value for each parameter
|
|
9
|
+
wald_SE_{param} – Wald standard error (NaN when Hessian is not PD)
|
|
10
|
+
wald_CI95_lower_{param} – lower bound of Wald 95% CI
|
|
11
|
+
wald_CI95_upper_{param} – upper bound of Wald 95% CI
|
|
12
|
+
wald_corr_{a}_{b} – Wald off-diagonal correlation for every pair a < b
|
|
13
|
+
profile_CI95_lower_{param} – lower bound of profile likelihood 95% CI
|
|
14
|
+
profile_CI95_upper_{param} – upper bound of profile likelihood 95% CI
|
|
15
|
+
profile_CI95_status_{param} – ok / flat / open / open_lower / open_upper
|
|
16
|
+
profile_reach_{side}_{param} – why that side stopped: "crossed" (a bound was
|
|
17
|
+
found), "bound" (walked to the parameter's own
|
|
18
|
+
limit without dNLL reaching 1.9207, so it is
|
|
19
|
+
unidentifiable everywhere it is allowed to go) or
|
|
20
|
+
"budget" (the outward extension ran out of steps,
|
|
21
|
+
so nothing was established either way)
|
|
22
|
+
profile_maxdNLL_{side}_{param} – highest dNLL reached on that side
|
|
23
|
+
profile_capped_{param} – profile points whose nuisance optimization hit
|
|
24
|
+
the iteration cap; non-zero means that CI is
|
|
25
|
+
too narrow
|
|
26
|
+
profile_points_not_converged – the same count summed over all parameters
|
|
27
|
+
profile_warm_improved – points the warm-started continuation pass
|
|
28
|
+
lowered; non-zero means the cold grid alone
|
|
29
|
+
would have given narrower intervals
|
|
30
|
+
profile_warm_nats_recovered – total NLL recovered by that pass
|
|
31
|
+
|
|
32
|
+
JSON file (one per run, timestamped to avoid overwrite):
|
|
33
|
+
{base}_{YYYYMMDD_HHMMSS}.json alongside the CSV, with
|
|
34
|
+
metadata – timestamp, model_name, experiment_id, method, success,
|
|
35
|
+
message, total_loss, aic, bic, n_iter, n_fev
|
|
36
|
+
parameters – {name: value} pairs, registry-shaped for direct copy
|
|
37
|
+
into Modules/utils/*_registry.py
|
|
38
|
+
wald_se – {name: SE}
|
|
39
|
+
wald_ci95 – {name: [lo, hi]}
|
|
40
|
+
profile_ci95 – {name: [lo, hi]}
|
|
41
|
+
wald_correlation – {"a|b": corr} for every off-diagonal pair (a < b)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
from datetime import datetime
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
import pandas as pd
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _finite_or_none(x):
|
|
53
|
+
"""Convert NaN/inf to None so the value is valid strict JSON."""
|
|
54
|
+
try:
|
|
55
|
+
v = float(x)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
return None
|
|
58
|
+
return v if np.isfinite(v) else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _write_results_row(df_row, csv_path):
|
|
62
|
+
"""Append one results row, reconciling the header instead of assuming it.
|
|
63
|
+
|
|
64
|
+
A plain ``mode="a"`` append writes the row's values in the row's own column
|
|
65
|
+
order under whatever header the file already carries, so the moment the two
|
|
66
|
+
disagree every value lands in whichever column happens to occupy its
|
|
67
|
+
position. That is not hypothetical: a Lecanemab CSV whose header still held
|
|
68
|
+
an older parameter set stored CLrecycle_Tissue's confidence interval under
|
|
69
|
+
``profile_CI95_lower_CLup_Tissue``, and nothing in the file said so. Any run
|
|
70
|
+
that changes the parameter list -- or adds a diagnostic column, as the
|
|
71
|
+
profile reach columns do -- hits this.
|
|
72
|
+
|
|
73
|
+
When the columns match exactly the append is unchanged. When they do not,
|
|
74
|
+
the file is rewritten over the union: older rows keep their own columns,
|
|
75
|
+
this row keeps its, and the gaps are empty. Empty is honest; a positional
|
|
76
|
+
append made them wrong instead.
|
|
77
|
+
"""
|
|
78
|
+
if not os.path.exists(csv_path):
|
|
79
|
+
df_row.to_csv(csv_path, index=False)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
df_old = pd.read_csv(csv_path)
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
# Unreadable is not a reason to overwrite: whatever is in there is the
|
|
86
|
+
# only copy of the earlier runs, and a file this far gone cannot be
|
|
87
|
+
# repaired without guessing which column each orphaned value belonged
|
|
88
|
+
# to. Append so nothing is lost, and say plainly what is wrong -- rows
|
|
89
|
+
# with differing field counts are what earlier appends under a stale
|
|
90
|
+
# header produced, and only a fresh file gets out of that state.
|
|
91
|
+
print(f" [results] WARNING: {os.path.basename(csv_path)} cannot be "
|
|
92
|
+
f"parsed ({exc}).")
|
|
93
|
+
print(f" Its rows do not all have the same number of columns, which "
|
|
94
|
+
f"is what appending under a stale header produces. The values "
|
|
95
|
+
f"already in it may be filed under the wrong column names.")
|
|
96
|
+
print(f" This row is being appended so nothing is lost, but rename "
|
|
97
|
+
f"or archive that file to start a clean one.")
|
|
98
|
+
df_row.to_csv(csv_path, mode="a", header=False, index=False)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if list(df_old.columns) == list(df_row.columns):
|
|
102
|
+
df_row.to_csv(csv_path, mode="a", header=False, index=False)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
added = [c for c in df_row.columns if c not in df_old.columns]
|
|
106
|
+
dropped = [c for c in df_old.columns if c not in df_row.columns]
|
|
107
|
+
print(f" [results] column set changed ({len(added)} added, "
|
|
108
|
+
f"{len(dropped)} no longer written); rewriting "
|
|
109
|
+
f"{os.path.basename(csv_path)} so earlier rows keep their columns.")
|
|
110
|
+
pd.concat([df_old, df_row], ignore_index=True).to_csv(csv_path, index=False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def log_optimization_results(
|
|
114
|
+
opt,
|
|
115
|
+
param_names,
|
|
116
|
+
csv_path,
|
|
117
|
+
model_name="",
|
|
118
|
+
experiment_id="",
|
|
119
|
+
method="",
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Append one row of optimization results to *csv_path*.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
opt : dict
|
|
127
|
+
Return value of ``run_optimization()``. Keys used:
|
|
128
|
+
``x``, ``fun``, ``success``, ``message``, ``stats``.
|
|
129
|
+
param_names : list[str]
|
|
130
|
+
Parameter names in the same order as ``opt["x"]``.
|
|
131
|
+
csv_path : str
|
|
132
|
+
Absolute path to the target CSV file. Created with a header on the
|
|
133
|
+
first call; subsequent calls append without writing the header again.
|
|
134
|
+
model_name : str, optional
|
|
135
|
+
experiment_id : str, optional
|
|
136
|
+
method : str, optional
|
|
137
|
+
"""
|
|
138
|
+
stats = opt.get("stats", {})
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
# Build the row as an ordered dict so column order is deterministic. #
|
|
142
|
+
# ------------------------------------------------------------------ #
|
|
143
|
+
row = {}
|
|
144
|
+
|
|
145
|
+
# --- bookkeeping --------------------------------------------------- #
|
|
146
|
+
row["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
147
|
+
row["model_name"] = model_name
|
|
148
|
+
row["experiment_id"] = experiment_id
|
|
149
|
+
row["method"] = method
|
|
150
|
+
|
|
151
|
+
# --- core optimization results ------------------------------------- #
|
|
152
|
+
row["success"] = opt.get("success", False)
|
|
153
|
+
row["optimizer_message"] = str(opt.get("message", ""))
|
|
154
|
+
row["total_loss"] = float(opt.get("fun", float("nan")))
|
|
155
|
+
row["nll_proper"] = float(stats.get("nll_proper", float("nan")))
|
|
156
|
+
|
|
157
|
+
# --- information criteria ------------------------------------------ #
|
|
158
|
+
row["aic"] = float(stats.get("aic", float("nan")))
|
|
159
|
+
row["bic"] = float(stats.get("bic", float("nan")))
|
|
160
|
+
|
|
161
|
+
# --- parameter values ---------------------------------------------- #
|
|
162
|
+
x_opt = np.atleast_1d(opt.get("x", []))
|
|
163
|
+
for name, val in zip(param_names, x_opt):
|
|
164
|
+
row[name] = float(val)
|
|
165
|
+
|
|
166
|
+
# --- Wald standard errors ------------------------------------------ #
|
|
167
|
+
se = stats.get("wald_se")
|
|
168
|
+
se_arr = np.atleast_1d(se) if se is not None else [float("nan")] * len(param_names)
|
|
169
|
+
for name, s in zip(param_names, se_arr):
|
|
170
|
+
row[f"wald_SE_{name}"] = float(s) if s is not None and not np.isnan(float(s)) else float("nan")
|
|
171
|
+
|
|
172
|
+
# --- Wald 95% confidence intervals --------------------------------- #
|
|
173
|
+
ci = stats.get("wald_ci")
|
|
174
|
+
if ci is None:
|
|
175
|
+
ci = [(float("nan"), float("nan"))] * len(param_names)
|
|
176
|
+
for name, (lo, hi) in zip(param_names, ci):
|
|
177
|
+
row[f"wald_CI95_lower_{name}"] = float(lo)
|
|
178
|
+
row[f"wald_CI95_upper_{name}"] = float(hi)
|
|
179
|
+
|
|
180
|
+
# --- Wald off-diagonal correlations -------------------------------- #
|
|
181
|
+
corr = stats.get("wald_correlation")
|
|
182
|
+
if corr is not None:
|
|
183
|
+
corr = np.atleast_2d(corr)
|
|
184
|
+
for i, a in enumerate(param_names):
|
|
185
|
+
for j, b in enumerate(param_names):
|
|
186
|
+
if j > i:
|
|
187
|
+
val = corr[i, j]
|
|
188
|
+
row[f"wald_corr_{a}_{b}"] = float(val) if np.isfinite(val) else float("nan")
|
|
189
|
+
else:
|
|
190
|
+
for i, a in enumerate(param_names):
|
|
191
|
+
for j, b in enumerate(param_names):
|
|
192
|
+
if j > i:
|
|
193
|
+
row[f"wald_corr_{a}_{b}"] = float("nan")
|
|
194
|
+
|
|
195
|
+
# --- Profile likelihood 95% confidence intervals ------------------- #
|
|
196
|
+
profile_ci = stats.get("profile_ci")
|
|
197
|
+
if profile_ci is None:
|
|
198
|
+
profile_ci = [(float("nan"), float("nan"))] * len(param_names)
|
|
199
|
+
ci_status = stats.get("profile_ci_status") or ["missing"] * len(param_names)
|
|
200
|
+
for name, (lo, hi) in zip(param_names, profile_ci):
|
|
201
|
+
row[f"profile_CI95_lower_{name}"] = float(lo)
|
|
202
|
+
row[f"profile_CI95_upper_{name}"] = float(hi)
|
|
203
|
+
# A bare nan cannot distinguish "flat / non-identifiable" from "grid too
|
|
204
|
+
# narrow", and those call for opposite fixes.
|
|
205
|
+
for name, st in zip(param_names, ci_status):
|
|
206
|
+
row[f"profile_CI95_status_{name}"] = st
|
|
207
|
+
|
|
208
|
+
# Why each side stopped: "crossed" (a bound was found), "bound" (the profile
|
|
209
|
+
# walked to the parameter's own limit without dNLL reaching 1.9207, so it is
|
|
210
|
+
# unidentifiable everywhere it is allowed to go), or "budget" (the outward
|
|
211
|
+
# extension ran out of steps, so nothing has been established either way).
|
|
212
|
+
# The middle case is a result and the last is an unfinished run; a status of
|
|
213
|
+
# "open" alone cannot tell them apart, and only one of them is worth
|
|
214
|
+
# re-running with a wider grid.
|
|
215
|
+
_reach = (stats.get("profile_convergence") or {}).get("reach") or {}
|
|
216
|
+
for name in param_names:
|
|
217
|
+
sides = _reach.get(name) or {}
|
|
218
|
+
for key in ("lower", "upper"):
|
|
219
|
+
d = sides.get(key) or {}
|
|
220
|
+
row[f"profile_reach_{key}_{name}"] = d.get("state", "missing")
|
|
221
|
+
row[f"profile_maxdNLL_{key}_{name}"] = float(
|
|
222
|
+
d.get("max_dnll") if d.get("max_dnll") is not None else float("nan"))
|
|
223
|
+
|
|
224
|
+
# Nuisance optimizations that stopped on the iteration cap rather than
|
|
225
|
+
# converging. Those points overstate the profile, so the CI above them is
|
|
226
|
+
# too narrow -- the caveat has to travel with the interval into the CSV, not
|
|
227
|
+
# live only in the console log of the run that produced it.
|
|
228
|
+
_conv = stats.get("profile_convergence") or {}
|
|
229
|
+
_per_param = _conv.get("per_param", {})
|
|
230
|
+
row["profile_points_not_converged"] = float(_conv.get("n_not_converged", 0))
|
|
231
|
+
|
|
232
|
+
# What the warm-started continuation pass recovered. Non-zero means the
|
|
233
|
+
# cold-started grid alone would have reported narrower intervals than these,
|
|
234
|
+
# which is the bias that pass measures and removes.
|
|
235
|
+
_warm = _conv.get("warm") or {}
|
|
236
|
+
row["profile_warm_improved"] = float(_warm.get("n_improved", 0))
|
|
237
|
+
row["profile_warm_nats_recovered"] = float(_warm.get("nats_recovered", 0.0))
|
|
238
|
+
for name in param_names:
|
|
239
|
+
row[f"profile_capped_{name}"] = float(
|
|
240
|
+
(_per_param.get(name) or {}).get("n_not_converged", 0)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# How far below the reported optimum the profile got. With one shared
|
|
244
|
+
# objective anything materially negative is a fit convergence failure.
|
|
245
|
+
row["profile_anchor_gap"] = float(stats.get("profile_anchor_gap", 0.0))
|
|
246
|
+
row["k_effective"] = float(stats.get("k_effective", float("nan")))
|
|
247
|
+
row["n_data_points"] = float(stats.get("n_data_points", float("nan")))
|
|
248
|
+
|
|
249
|
+
# ------------------------------------------------------------------ #
|
|
250
|
+
# Console summary #
|
|
251
|
+
# ------------------------------------------------------------------ #
|
|
252
|
+
nit = opt.get("nit")
|
|
253
|
+
nfev = opt.get("nfev")
|
|
254
|
+
iter_str = (f" Iterations: {nit} | Func evals: {nfev}"
|
|
255
|
+
if nit is not None else "")
|
|
256
|
+
print(f'\n{"=" * 80}')
|
|
257
|
+
print(f'OPTIMIZATION COMPLETE [{experiment_id}]')
|
|
258
|
+
print(f'{"=" * 80}')
|
|
259
|
+
print(f' Model: {model_name}')
|
|
260
|
+
print(f' Method: {method}')
|
|
261
|
+
print(f' Success: {opt.get("success", False)}')
|
|
262
|
+
print(f' Message: {opt.get("message", "")}')
|
|
263
|
+
print(f' Final loss: {float(opt.get("fun", float("nan"))):.6e}')
|
|
264
|
+
if iter_str:
|
|
265
|
+
print(iter_str)
|
|
266
|
+
if param_names and len(x_opt) == len(param_names):
|
|
267
|
+
print(f'\n {"Parameter":<45} {"Value":>18}')
|
|
268
|
+
print(f' {"-" * 63}')
|
|
269
|
+
for name, val in zip(param_names, x_opt):
|
|
270
|
+
print(f' {name:<45} {float(val):>18.8e}')
|
|
271
|
+
print(f'{"=" * 80}\n')
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------ #
|
|
274
|
+
# Append to CSV. #
|
|
275
|
+
# ------------------------------------------------------------------ #
|
|
276
|
+
df_row = pd.DataFrame([row])
|
|
277
|
+
_write_results_row(df_row, csv_path)
|
|
278
|
+
print(f"Optimization results appended to: {csv_path}")
|
|
279
|
+
|
|
280
|
+
# ------------------------------------------------------------------ #
|
|
281
|
+
# Per-run JSON snapshot. The "parameters" block is shaped like the #
|
|
282
|
+
# INDEPENDENT_*_REGISTRY dicts in Modules/utils/ so it can be copied #
|
|
283
|
+
# directly into the registry source. #
|
|
284
|
+
# ------------------------------------------------------------------ #
|
|
285
|
+
ts_compact = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
286
|
+
base, _ = os.path.splitext(csv_path)
|
|
287
|
+
json_path = f"{base}_{ts_compact}.json"
|
|
288
|
+
|
|
289
|
+
parameters_dict = {
|
|
290
|
+
name: _finite_or_none(val) for name, val in zip(param_names, x_opt)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
wald_se_dict = {
|
|
294
|
+
name: _finite_or_none(s) for name, s in zip(param_names, se_arr)
|
|
295
|
+
}
|
|
296
|
+
wald_ci_dict = {
|
|
297
|
+
name: [_finite_or_none(lo), _finite_or_none(hi)]
|
|
298
|
+
for name, (lo, hi) in zip(param_names, ci)
|
|
299
|
+
}
|
|
300
|
+
profile_ci_dict = {
|
|
301
|
+
name: [_finite_or_none(lo), _finite_or_none(hi)]
|
|
302
|
+
for name, (lo, hi) in zip(param_names, profile_ci)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
wald_corr_dict = {}
|
|
306
|
+
if corr is not None:
|
|
307
|
+
for i, a in enumerate(param_names):
|
|
308
|
+
for j, b in enumerate(param_names):
|
|
309
|
+
if j > i:
|
|
310
|
+
wald_corr_dict[f"{a}|{b}"] = _finite_or_none(corr[i, j])
|
|
311
|
+
|
|
312
|
+
snapshot = {
|
|
313
|
+
"metadata": {
|
|
314
|
+
"timestamp": row["timestamp"],
|
|
315
|
+
"model_name": model_name,
|
|
316
|
+
"experiment_id": experiment_id,
|
|
317
|
+
"method": method,
|
|
318
|
+
"success": bool(opt.get("success", False)),
|
|
319
|
+
"message": str(opt.get("message", "")),
|
|
320
|
+
# objective_kind names the function 'total_loss' and every dNLL
|
|
321
|
+
# refer to. Archived runs where it is absent used a weighted
|
|
322
|
+
# chi-square for the fit and a different frozen-sigma NLL for the
|
|
323
|
+
# diagnostics, so their numbers are not comparable with these.
|
|
324
|
+
"objective_kind": "concentrated_gaussian_nll",
|
|
325
|
+
"total_loss": _finite_or_none(opt.get("fun")),
|
|
326
|
+
# Same objective as total_loss, plus the (n/2)(1+log 2pi) constant
|
|
327
|
+
# the fit drops. Absolute, so AIC/BIC need it; it cancels from dNLL.
|
|
328
|
+
"nll_proper": _finite_or_none(stats.get("nll_proper")),
|
|
329
|
+
"aic": _finite_or_none(stats.get("aic")),
|
|
330
|
+
"bic": _finite_or_none(stats.get("bic")),
|
|
331
|
+
# Parameters + one profiled-out sigma per block.
|
|
332
|
+
"k_effective": stats.get("k_effective"),
|
|
333
|
+
"n_data_points": _finite_or_none(stats.get("n_data_points")),
|
|
334
|
+
"profile_anchor_gap": _finite_or_none(stats.get("profile_anchor_gap")),
|
|
335
|
+
"n_iterations": opt.get("nit"),
|
|
336
|
+
"n_fevals": opt.get("nfev"),
|
|
337
|
+
# Recorded so a run is reproducible: fit_mode says whether the
|
|
338
|
+
# optimizer ran at all, and x0 pins down a randomized multi-start.
|
|
339
|
+
"fit_mode": opt.get("fit_mode"),
|
|
340
|
+
# Path of the cached fit the optimum was reused from (a relaunch
|
|
341
|
+
# of the same problem), or null when the optimizer ran in this
|
|
342
|
+
# process.
|
|
343
|
+
"fit_source": opt.get("fit_source"),
|
|
344
|
+
},
|
|
345
|
+
"parameters": parameters_dict,
|
|
346
|
+
"wald_se": wald_se_dict,
|
|
347
|
+
"wald_ci95": wald_ci_dict,
|
|
348
|
+
"profile_ci95": profile_ci_dict,
|
|
349
|
+
"wald_correlation": wald_corr_dict,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if opt.get("x0") is not None:
|
|
353
|
+
snapshot["x0"] = {
|
|
354
|
+
name: _finite_or_none(val)
|
|
355
|
+
for name, val in zip(param_names, np.atleast_1d(opt["x0"]))
|
|
356
|
+
}
|
|
357
|
+
if opt.get("parameter_scale") is not None:
|
|
358
|
+
snapshot["parameter_scale"] = dict(zip(param_names, opt["parameter_scale"]))
|
|
359
|
+
if stats.get("curvature_se"):
|
|
360
|
+
snapshot["curvature_se"] = {
|
|
361
|
+
name: _finite_or_none(val)
|
|
362
|
+
for name, val in stats["curvature_se"].items()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if stats.get("profile_ci_status"):
|
|
366
|
+
snapshot["profile_ci95_status"] = dict(
|
|
367
|
+
zip(param_names, stats["profile_ci_status"])
|
|
368
|
+
)
|
|
369
|
+
if stats.get("block_sigmas"):
|
|
370
|
+
# Fitted noise level per block, with its point count. Under the
|
|
371
|
+
# concentrated likelihood a block's entire contribution is
|
|
372
|
+
# (n/2)log(sigma^2), so these two numbers say which data the fit is
|
|
373
|
+
# actually being driven by — and an outlying sigma is how a block the
|
|
374
|
+
# model cannot fit announces itself.
|
|
375
|
+
block_n = stats.get("block_n") or {}
|
|
376
|
+
snapshot["block_sigmas"] = {
|
|
377
|
+
k: {"sigma": _finite_or_none(v), "n": block_n.get(k)}
|
|
378
|
+
for k, v in stats["block_sigmas"].items()
|
|
379
|
+
}
|
|
380
|
+
if stats.get("profile_convergence"):
|
|
381
|
+
snapshot["profile_convergence"] = stats["profile_convergence"]
|
|
382
|
+
if stats.get("profile_better_point"):
|
|
383
|
+
snapshot["profile_better_point"] = stats["profile_better_point"]
|
|
384
|
+
if stats.get("fast_profile"):
|
|
385
|
+
# Per-side verdicts on "the interval is closed on this side", from
|
|
386
|
+
# one capped profile point at the slice crossing. See Engine.Fast_profile.
|
|
387
|
+
snapshot["fast_profile"] = stats["fast_profile"]
|
|
388
|
+
if "profile_traces" in stats:
|
|
389
|
+
snapshot["profile_traces"] = stats["profile_traces"]
|
|
390
|
+
if "slice_traces" in stats:
|
|
391
|
+
snapshot["slice_traces"] = stats["slice_traces"]
|
|
392
|
+
|
|
393
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
394
|
+
json.dump(snapshot, f, indent=2, allow_nan=False)
|
|
395
|
+
print(f"Optimization snapshot written to: {json_path}")
|