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,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ground-truth reference profiles for the flip-flop example (Example4/Example5).
|
|
3
|
+
|
|
4
|
+
This script recomputes the true profile likelihood of k_A_to_B, k_B_to_C and
|
|
5
|
+
SF against data/Flipflop.csv using the CLOSED-FORM solution of the A -> B -> C
|
|
6
|
+
chain and scipy only — no RoadRunner, no framework loss code. It follows the
|
|
7
|
+
same conventions the framework's diagnostics use:
|
|
8
|
+
|
|
9
|
+
* observables are log10-transformed with a 1e-12 floor;
|
|
10
|
+
* per-observable sigmas are estimated by MLE from the residuals at the
|
|
11
|
+
anchor optimum and then frozen (the framework's fixed_sigmas), with the
|
|
12
|
+
same dof correction sqrt(SSR / max(1, n - k/n_observables));
|
|
13
|
+
* dNLL is the plain unweighted sum over observables, relative to the anchor
|
|
14
|
+
optimum, compared against chi2(1, 0.95)/2 = 1.9207.
|
|
15
|
+
|
|
16
|
+
Because the model here is exact, any disagreement between these curves and the
|
|
17
|
+
framework's profile output (results/Example/*_profile_likelihood.png and the
|
|
18
|
+
profile_traces in the optimization results CSV) is an error in the profile
|
|
19
|
+
machinery — the walker, the nuisance re-optimization, the sigma freezing, or
|
|
20
|
+
the dNLL bookkeeping — not in the model.
|
|
21
|
+
|
|
22
|
+
Run from Projects/Example/:
|
|
23
|
+
|
|
24
|
+
python Flipflop_reference.py # anchored at the global mode (Example4)
|
|
25
|
+
python Flipflop_reference.py --anchor swapped # anchored at the wrong mode (Example5)
|
|
26
|
+
|
|
27
|
+
Prints the swap-mode dNLL gap, the inter-mode barrier height, and the correct
|
|
28
|
+
95% confidence SET for each parameter (which may be a union of disjoint
|
|
29
|
+
intervals — something a first-crossing CI extractor cannot represent).
|
|
30
|
+
"""
|
|
31
|
+
import argparse
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
import pandas as pd
|
|
36
|
+
|
|
37
|
+
THRESHOLD = 1.9207 # chi2(df=1, p=0.95) / 2
|
|
38
|
+
FLOOR = 1e-12
|
|
39
|
+
K_PARAMS = 3
|
|
40
|
+
PARAM_NAMES = ["k_A_to_B", "k_B_to_C", "SF"]
|
|
41
|
+
BOUNDS = [(0.005, 5.0), (0.005, 5.0), (0.05, 50.0)]
|
|
42
|
+
|
|
43
|
+
TREATMENTS = {
|
|
44
|
+
"Early": {"dose": 10.0, "delay": 5.0},
|
|
45
|
+
"Late": {"dose": 5.0, "delay": 10.0},
|
|
46
|
+
}
|
|
47
|
+
# observables per loss config: Early has logB+logA, Late has logB only
|
|
48
|
+
N_OBS_CFG = {"Flipflop_Early": 2, "Flipflop_Early_A": 2, "Flipflop_Late": 1}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _find_data_file():
|
|
52
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
53
|
+
candidates = []
|
|
54
|
+
try:
|
|
55
|
+
import AntiGen_paths
|
|
56
|
+
candidates.append(os.path.join(AntiGen_paths.REPO_ROOT, "data", "Flipflop.csv"))
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
candidates.append(os.path.normpath(os.path.join(here, "..", "..", "data", "Flipflop.csv")))
|
|
60
|
+
candidates.append(os.path.normpath(os.path.join(here, "..", "data", "Flipflop.csv")))
|
|
61
|
+
for c in candidates:
|
|
62
|
+
if os.path.isfile(c):
|
|
63
|
+
return c
|
|
64
|
+
raise FileNotFoundError("Flipflop.csv not found; run data/make_flipflop_data.py first.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def chain_A(t, dose, delay, k1):
|
|
68
|
+
tau = np.maximum(np.asarray(t, dtype=float) - delay, 0.0)
|
|
69
|
+
return np.where(tau > 0, dose * np.exp(-k1 * tau), 0.0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def chain_B(t, dose, delay, k1, k2):
|
|
73
|
+
tau = np.maximum(np.asarray(t, dtype=float) - delay, 0.0)
|
|
74
|
+
if abs(k1 - k2) < 1e-10:
|
|
75
|
+
core = dose * k1 * tau * np.exp(-k1 * tau)
|
|
76
|
+
else:
|
|
77
|
+
core = dose * k1 * (np.exp(-k1 * tau) - np.exp(-k2 * tau)) / (k2 - k1)
|
|
78
|
+
return np.where(tau > 0, core, 0.0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def log10f(x):
|
|
82
|
+
return np.log10(np.maximum(x, FLOOR))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_data():
|
|
86
|
+
df = pd.read_csv(_find_data_file())
|
|
87
|
+
data = {}
|
|
88
|
+
for name in TREATMENTS:
|
|
89
|
+
sub = df[df["Treatment"] == name]
|
|
90
|
+
t = np.concatenate([sub["time"].to_numpy()] * 3)
|
|
91
|
+
y = np.concatenate([sub[f"logB{i}"].to_numpy() for i in (1, 2, 3)])
|
|
92
|
+
data[f"Flipflop_{name}"] = (t, y)
|
|
93
|
+
early_A = df[(df["Treatment"] == "Early") & df["logA"].notna()]
|
|
94
|
+
data["Flipflop_Early_A"] = (early_A["time"].to_numpy(), early_A["logA"].to_numpy())
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ssr_terms(p, data):
|
|
99
|
+
"""{observable_key: (SSR, n)} at p = (k_A_to_B, k_B_to_C, SF)."""
|
|
100
|
+
k1, k2, sf = p
|
|
101
|
+
out = {}
|
|
102
|
+
for name, tr in TREATMENTS.items():
|
|
103
|
+
t, y = data[f"Flipflop_{name}"]
|
|
104
|
+
pred = log10f(sf * chain_B(t, tr["dose"], tr["delay"], k1, k2))
|
|
105
|
+
out[f"Flipflop_{name}"] = (float(np.sum((y - pred) ** 2)), len(y))
|
|
106
|
+
t, y = data["Flipflop_Early_A"]
|
|
107
|
+
pred = log10f(chain_A(t, TREATMENTS["Early"]["dose"], TREATMENTS["Early"]["delay"], k1))
|
|
108
|
+
out["Flipflop_Early_A"] = (float(np.sum((y - pred) ** 2)), len(y))
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def frozen_sigmas_at(p, data):
|
|
113
|
+
return {key: max(np.sqrt(ssr / max(1, n - K_PARAMS / N_OBS_CFG[key])), 1e-6)
|
|
114
|
+
for key, (ssr, n) in ssr_terms(p, data).items()}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def nll(p, data, sigmas):
|
|
118
|
+
"""Frozen-sigma NLL including the Gaussian constant terms (they cancel in
|
|
119
|
+
dNLL, but keeping them matches the framework's -sum(norm.logpdf(...))."""
|
|
120
|
+
total = 0.0
|
|
121
|
+
for key, (ssr, n) in ssr_terms(p, data).items():
|
|
122
|
+
s = sigmas[key]
|
|
123
|
+
total += ssr / (2.0 * s ** 2) + n * np.log(s * np.sqrt(2.0 * np.pi))
|
|
124
|
+
return total
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# sigma values declared in Flipflop_loss_config (shape the fit, not inference)
|
|
128
|
+
FIT_SIGMAS = {"Flipflop_Early": 0.05, "Flipflop_Late": 0.05, "Flipflop_Early_A": 0.75}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def fit_objective(p, data):
|
|
132
|
+
"""The framework's FITTING objective, which is not the joint NLL: each
|
|
133
|
+
observable's chi-square term is divided by its own point count (the
|
|
134
|
+
per-observable mean the loss functions compute), using the fixed sigmas
|
|
135
|
+
declared in the loss config. With 45 logB points against 4 logA points
|
|
136
|
+
this upweights the noisy logA data ~11x relative to the likelihood, so
|
|
137
|
+
the fit optimum lands measurably away from the NLL optimum — which is
|
|
138
|
+
why the framework's (correct) profiles dip below zero even in Example4.
|
|
139
|
+
Reproducing that anchor offset here is what lets the reference curves be
|
|
140
|
+
compared with the framework's point-for-point."""
|
|
141
|
+
return sum(ssr / (2.0 * FIT_SIGMAS[key] ** 2 * n)
|
|
142
|
+
for key, (ssr, n) in ssr_terms(p, data).items())
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _minimize(obj, x0_log, bounds_log):
|
|
146
|
+
from scipy.optimize import minimize
|
|
147
|
+
res = minimize(obj, x0_log, method="Nelder-Mead",
|
|
148
|
+
options={"maxiter": 4000, "xatol": 1e-11, "fatol": 1e-13})
|
|
149
|
+
x = np.clip(res.x, [b[0] for b in bounds_log], [b[1] for b in bounds_log])
|
|
150
|
+
return x, obj(x)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def fit_full(x0, data, sigmas):
|
|
154
|
+
bounds_log = [np.log10(b) for b in BOUNDS]
|
|
155
|
+
obj = lambda q: nll(10.0 ** np.asarray(q), data, sigmas)
|
|
156
|
+
q, f = _minimize(obj, np.log10(x0), bounds_log)
|
|
157
|
+
return 10.0 ** q, f
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def profile_one(idx, grid, data, sigmas, mode_a, mode_b):
|
|
161
|
+
"""True profile of parameter idx over *grid* (linear values).
|
|
162
|
+
|
|
163
|
+
Nuisance parameters re-optimized by Nelder-Mead in log10 space with three
|
|
164
|
+
starts per grid point — continuation from the previous point, plus the
|
|
165
|
+
nuisance components of each known mode — so the profile finds the better
|
|
166
|
+
basin on both sides of the barrier. This multi-start is what makes these
|
|
167
|
+
curves 'reference': a single-start continuation walker can lag behind the
|
|
168
|
+
basin switch and overestimate the barrier.
|
|
169
|
+
"""
|
|
170
|
+
nuis_idx = [j for j in range(3) if j != idx]
|
|
171
|
+
bounds_log = [np.log10(BOUNDS[j]) for j in nuis_idx]
|
|
172
|
+
|
|
173
|
+
def obj_at(v_fixed):
|
|
174
|
+
def obj(q_nuis):
|
|
175
|
+
p = np.empty(3)
|
|
176
|
+
p[idx] = v_fixed
|
|
177
|
+
for j, q in zip(nuis_idx, q_nuis):
|
|
178
|
+
p[j] = 10.0 ** q
|
|
179
|
+
return nll(p, data, sigmas)
|
|
180
|
+
return obj
|
|
181
|
+
|
|
182
|
+
prof = np.empty(len(grid))
|
|
183
|
+
prev = None
|
|
184
|
+
for i, v in enumerate(grid):
|
|
185
|
+
starts = []
|
|
186
|
+
if prev is not None:
|
|
187
|
+
starts.append(prev)
|
|
188
|
+
for mode in (mode_a, mode_b):
|
|
189
|
+
starts.append(np.log10([mode[j] for j in nuis_idx]))
|
|
190
|
+
best_f, best_q = np.inf, None
|
|
191
|
+
for s in starts:
|
|
192
|
+
q, f = _minimize(obj_at(v), np.asarray(s, dtype=float), bounds_log)
|
|
193
|
+
if f < best_f:
|
|
194
|
+
best_f, best_q = f, q
|
|
195
|
+
prof[i] = best_f
|
|
196
|
+
prev = best_q
|
|
197
|
+
return prof
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def confidence_set(grid, dnll, threshold=THRESHOLD):
|
|
201
|
+
"""All intervals where the profile is below threshold (linear interp at edges)."""
|
|
202
|
+
below = dnll < threshold
|
|
203
|
+
intervals = []
|
|
204
|
+
i = 0
|
|
205
|
+
n = len(grid)
|
|
206
|
+
while i < n:
|
|
207
|
+
if below[i]:
|
|
208
|
+
j = i
|
|
209
|
+
while j + 1 < n and below[j + 1]:
|
|
210
|
+
j += 1
|
|
211
|
+
lo = grid[i]
|
|
212
|
+
if i > 0:
|
|
213
|
+
y0, y1 = dnll[i - 1], dnll[i]
|
|
214
|
+
lo = grid[i - 1] + (threshold - y0) * (grid[i] - grid[i - 1]) / (y1 - y0)
|
|
215
|
+
hi = grid[j]
|
|
216
|
+
if j + 1 < n:
|
|
217
|
+
y0, y1 = dnll[j], dnll[j + 1]
|
|
218
|
+
hi = grid[j] + (threshold - y0) * (grid[j + 1] - grid[j]) / (y1 - y0)
|
|
219
|
+
intervals.append((lo, hi))
|
|
220
|
+
i = j + 1
|
|
221
|
+
else:
|
|
222
|
+
i += 1
|
|
223
|
+
return intervals
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def compare_with_framework(json_path, rows, p_anchor, p_other, gap):
|
|
227
|
+
"""Score the framework's profile_traces (from the per-run results JSON)
|
|
228
|
+
against the reference curves. Prints one verdict per parameter:
|
|
229
|
+
|
|
230
|
+
* max |dNLL_framework - dNLL_reference| over the framework's trace, with
|
|
231
|
+
the reference interpolated in log-parameter space (only where the
|
|
232
|
+
reference is below 6 — beyond that both curves are off any CI scale);
|
|
233
|
+
* whether the trace ever reaches the other mode's neighbourhood, and if
|
|
234
|
+
so the dNLL it reports there versus the known gap. A trace that never
|
|
235
|
+
gets there means the walker stopped at the first threshold crossing
|
|
236
|
+
and the reported CI silently assumes unimodality.
|
|
237
|
+
"""
|
|
238
|
+
import json as _json
|
|
239
|
+
with open(json_path) as f:
|
|
240
|
+
snap = _json.load(f)
|
|
241
|
+
traces = snap.get("profile_traces")
|
|
242
|
+
if not traces:
|
|
243
|
+
print(f"\n[compare] no profile_traces found in {json_path}")
|
|
244
|
+
return
|
|
245
|
+
ci = snap.get("profile_ci95", {})
|
|
246
|
+
print(f"\n[compare] framework traces from {os.path.basename(json_path)}")
|
|
247
|
+
for idx, name in enumerate(PARAM_NAMES):
|
|
248
|
+
tr = traces.get(name)
|
|
249
|
+
if not tr:
|
|
250
|
+
print(f" {name}: no trace")
|
|
251
|
+
continue
|
|
252
|
+
x = np.asarray(tr["x"], dtype=float)
|
|
253
|
+
y = np.asarray(tr["y"], dtype=float)
|
|
254
|
+
grid, dnll = rows[name]
|
|
255
|
+
ref = np.interp(np.log10(x), np.log10(grid), dnll)
|
|
256
|
+
band = ref < 6.0
|
|
257
|
+
max_err = float(np.max(np.abs(y[band] - ref[band]))) if band.any() else np.nan
|
|
258
|
+
other = p_other[idx]
|
|
259
|
+
near_other = np.abs(np.log10(x) - np.log10(other)) < 0.15
|
|
260
|
+
print(f" {name}: {len(x)} pts, span [{x.min():.4g}, {x.max():.4g}], "
|
|
261
|
+
f"max |dNLL err| (ref<6) = {max_err:.3g}")
|
|
262
|
+
if near_other.any():
|
|
263
|
+
print(f" reaches other mode ({other:.4g}): reported dNLL "
|
|
264
|
+
f"{float(np.min(y[near_other])):+.3g} vs expected {gap:+.3g}")
|
|
265
|
+
else:
|
|
266
|
+
print(f" *** never reaches the other mode at {other:.4g} - "
|
|
267
|
+
f"CI assumes unimodality ***")
|
|
268
|
+
if name in ci:
|
|
269
|
+
print(f" framework CI95: {ci[name]}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def main():
|
|
273
|
+
ap = argparse.ArgumentParser()
|
|
274
|
+
ap.add_argument("--anchor", choices=["true", "swapped"], default="true",
|
|
275
|
+
help="which mode to freeze sigmas at and anchor dNLL=0 to "
|
|
276
|
+
"(matches Example4 / Example5 respectively)")
|
|
277
|
+
ap.add_argument("--n-grid", type=int, default=81)
|
|
278
|
+
ap.add_argument("--out-dir", type=str, default=None)
|
|
279
|
+
ap.add_argument("--compare", type=str, default=None, metavar="RESULTS_JSON",
|
|
280
|
+
help="per-run optimization results JSON (contains "
|
|
281
|
+
"profile_traces) to score against the reference")
|
|
282
|
+
args = ap.parse_args()
|
|
283
|
+
|
|
284
|
+
data = load_data()
|
|
285
|
+
|
|
286
|
+
# Replicate the framework pipeline end-to-end:
|
|
287
|
+
# 1. minimize the FITTING objective (per-observable means, config sigmas)
|
|
288
|
+
# from Example4's / Example5's x0 — this is where the framework's fit
|
|
289
|
+
# lands, and it is NOT the NLL optimum;
|
|
290
|
+
bounds_log = [np.log10(b) for b in BOUNDS]
|
|
291
|
+
def fit_obj_full(x0):
|
|
292
|
+
obj = lambda q: fit_objective(10.0 ** np.asarray(q), data)
|
|
293
|
+
q, f = _minimize(obj, np.log10(x0), bounds_log)
|
|
294
|
+
return 10.0 ** q, f
|
|
295
|
+
|
|
296
|
+
p_fit_true, _ = fit_obj_full([0.3, 0.08, 1.5]) # Example4 x0
|
|
297
|
+
p_fit_swap, _ = fit_obj_full([0.06, 0.4, 7.0]) # Example5 x0
|
|
298
|
+
p_fit, p_fit_other = ((p_fit_true, p_fit_swap) if args.anchor == "true"
|
|
299
|
+
else (p_fit_swap, p_fit_true))
|
|
300
|
+
|
|
301
|
+
# 2. freeze per-observable MLE sigmas at the fit point (fixed_sigmas);
|
|
302
|
+
sigmas = frozen_sigmas_at(p_fit, data)
|
|
303
|
+
# 3. anchor dNLL = 0 at the fit point's inference NLL — exactly what the
|
|
304
|
+
# framework's diagnostics do. The NLL minima of the two basins under
|
|
305
|
+
# these frozen sigmas are then located for reference markers.
|
|
306
|
+
nll_anchor = nll(p_fit, data, sigmas)
|
|
307
|
+
p_anchor, nll_min_anchor = fit_full(p_fit, data, sigmas)
|
|
308
|
+
p_other, nll_other = fit_full(p_fit_other, data, sigmas)
|
|
309
|
+
|
|
310
|
+
print(f"fit point (anchor={args.anchor}, fitting objective): "
|
|
311
|
+
+ " ".join(f"{n}={v:.4f}" for n, v in zip(PARAM_NAMES, p_fit)))
|
|
312
|
+
print(f"NLL minimum, same basin: "
|
|
313
|
+
+ " ".join(f"{n}={v:.4f}" for n, v in zip(PARAM_NAMES, p_anchor)))
|
|
314
|
+
print(f"NLL minimum, other basin: "
|
|
315
|
+
+ " ".join(f"{n}={v:.4f}" for n, v in zip(PARAM_NAMES, p_other)))
|
|
316
|
+
print("frozen sigmas: " + ", ".join(f"{k}={v:.4f}" for k, v in sigmas.items()))
|
|
317
|
+
print(f"anchor offset (fit objective vs NLL optimum): "
|
|
318
|
+
f"{nll_min_anchor - nll_anchor:+.4f} <- profiles must dip to this")
|
|
319
|
+
print(f"dNLL at other-basin minimum = {nll_other - nll_anchor:+.4f} "
|
|
320
|
+
f"(95% threshold {THRESHOLD})")
|
|
321
|
+
|
|
322
|
+
rows = {}
|
|
323
|
+
fig = None
|
|
324
|
+
try:
|
|
325
|
+
import matplotlib
|
|
326
|
+
matplotlib.use("Agg")
|
|
327
|
+
import matplotlib.pyplot as plt
|
|
328
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))
|
|
329
|
+
except Exception:
|
|
330
|
+
axes = [None] * 3
|
|
331
|
+
|
|
332
|
+
for idx, (name, bounds) in enumerate(zip(PARAM_NAMES, BOUNDS)):
|
|
333
|
+
grid = np.logspace(np.log10(bounds[0]), np.log10(bounds[1]), args.n_grid)
|
|
334
|
+
# make sure both mode values are exactly on the grid
|
|
335
|
+
grid = np.unique(np.concatenate([grid, [p_anchor[idx], p_other[idx]]]))
|
|
336
|
+
prof = profile_one(idx, grid, data, sigmas, p_anchor, p_other)
|
|
337
|
+
dnll = prof - nll_anchor
|
|
338
|
+
rows[name] = (grid, dnll)
|
|
339
|
+
|
|
340
|
+
cs = confidence_set(grid, dnll)
|
|
341
|
+
in_band = dnll < THRESHOLD
|
|
342
|
+
barrier = np.nan
|
|
343
|
+
i_a = int(np.argmin(np.abs(grid - p_anchor[idx])))
|
|
344
|
+
i_o = int(np.argmin(np.abs(grid - p_other[idx])))
|
|
345
|
+
lo_i, hi_i = sorted((i_a, i_o))
|
|
346
|
+
if hi_i > lo_i:
|
|
347
|
+
barrier = float(np.max(dnll[lo_i:hi_i + 1]))
|
|
348
|
+
cs_str = " U ".join(f"[{a:.4g}, {b:.4g}]" for a, b in cs) or "(empty)"
|
|
349
|
+
print(f"\n{name}:")
|
|
350
|
+
print(f" dNLL at anchor {p_anchor[idx]:.4g}: {dnll[i_a]:+.4g} "
|
|
351
|
+
f"at other mode {p_other[idx]:.4g}: {dnll[i_o]:+.4g}")
|
|
352
|
+
print(f" inter-mode barrier height: {barrier:.4g}")
|
|
353
|
+
print(f" correct 95% confidence set: {cs_str}"
|
|
354
|
+
+ (" <-- DISJOINT" if len(cs) > 1 else ""))
|
|
355
|
+
|
|
356
|
+
if axes[idx] is not None:
|
|
357
|
+
ax = axes[idx]
|
|
358
|
+
ax.semilogx(grid, dnll, "-", lw=1.5)
|
|
359
|
+
ax.axhline(THRESHOLD, color="gray", ls=":", label="95% threshold")
|
|
360
|
+
ax.axhline(0, color="gray", lw=0.5)
|
|
361
|
+
ax.axvline(p_anchor[idx], color="red", ls="--", alpha=0.6, label="anchor mode")
|
|
362
|
+
ax.axvline(p_other[idx], color="green", ls="--", alpha=0.6, label="other mode")
|
|
363
|
+
ax.set_ylim(min(-3.5, np.nanmin(dnll) - 0.5), 8)
|
|
364
|
+
ax.set_xlabel(name)
|
|
365
|
+
ax.set_ylabel("dNLL")
|
|
366
|
+
ax.legend(fontsize=7)
|
|
367
|
+
ax.grid(True, alpha=0.3)
|
|
368
|
+
|
|
369
|
+
out_dir = args.out_dir
|
|
370
|
+
if out_dir is None:
|
|
371
|
+
try:
|
|
372
|
+
import AntiGen_paths
|
|
373
|
+
out_dir = os.path.join(AntiGen_paths.REPO_ROOT, "results", "Example")
|
|
374
|
+
except Exception:
|
|
375
|
+
out_dir = os.path.dirname(os.path.abspath(__file__))
|
|
376
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
377
|
+
|
|
378
|
+
max_len = max(len(g) for g, _ in rows.values())
|
|
379
|
+
table = {}
|
|
380
|
+
for name, (grid, dnll) in rows.items():
|
|
381
|
+
pad = max_len - len(grid)
|
|
382
|
+
table[f"{name}_value"] = np.concatenate([grid, [np.nan] * pad])
|
|
383
|
+
table[f"{name}_dnll"] = np.concatenate([dnll, [np.nan] * pad])
|
|
384
|
+
csv_path = os.path.join(out_dir, f"Flipflop_reference_profiles_{args.anchor}.csv")
|
|
385
|
+
pd.DataFrame(table).to_csv(csv_path, index=False, float_format="%.6g")
|
|
386
|
+
print(f"\nReference profiles written to {csv_path}")
|
|
387
|
+
|
|
388
|
+
if args.compare:
|
|
389
|
+
compare_with_framework(args.compare, rows, p_anchor, p_other,
|
|
390
|
+
nll_other - nll_anchor)
|
|
391
|
+
|
|
392
|
+
if fig is not None:
|
|
393
|
+
fig.suptitle(f"Flip-flop reference profiles (closed form, anchor={args.anchor})")
|
|
394
|
+
fig.tight_layout()
|
|
395
|
+
png_path = os.path.join(out_dir, f"Flipflop_reference_profiles_{args.anchor}.png")
|
|
396
|
+
fig.savefig(png_path, dpi=130, bbox_inches="tight")
|
|
397
|
+
print(f"Reference plot written to {png_path}")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model builder. Outputs go to antimony_models/{MODEL_NAME}/ and generated/{MODEL_NAME}/.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import AntiGen_paths
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from framework.pyantigen import PyAntiGen
|
|
11
|
+
from antimony_modules.Basic.ma_reaction import BasicMAReaction, BasicChainReaction
|
|
12
|
+
|
|
13
|
+
def generate_antimony_model(Isotopes=['']):
|
|
14
|
+
MODEL_NAME = AntiGen_paths.MODEL_NAME
|
|
15
|
+
model = PyAntiGen(name=MODEL_NAME, isotopes=Isotopes)
|
|
16
|
+
BasicMAReaction(model)
|
|
17
|
+
# Second chain step B -> C. Inert for Example1-3 (k_B_to_C defaults to 0);
|
|
18
|
+
# fit by Example4/Example5 to create flip-flop bimodality.
|
|
19
|
+
BasicChainReaction(model)
|
|
20
|
+
|
|
21
|
+
print(f"Reactions generated: {model.counter}")
|
|
22
|
+
print(f"Rules generated: {len(model.rules)}")
|
|
23
|
+
|
|
24
|
+
model.generate(__file__, model_name=MODEL_NAME)
|
|
25
|
+
|
|
26
|
+
print("\nModel generated successfully.")
|
|
27
|
+
print("Next steps:")
|
|
28
|
+
print(f" 1. Optionally edit parameters in antimony_models/{MODEL_NAME}/{MODEL_NAME}_parameters.csv")
|
|
29
|
+
print(f" 2. From Projects/{MODEL_NAME}/, run: python Model_run.py")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def update_antimony_model():
|
|
33
|
+
Isotopes = ['']
|
|
34
|
+
generate_antimony_model(Isotopes)
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
update_antimony_model()
|