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.
Files changed (55) hide show
  1. framework/AntimonyGen.py +48 -0
  2. framework/RxnDict_to_antimony.py +594 -0
  3. framework/TelluriumGen.py +16 -0
  4. framework/__init__.py +0 -0
  5. framework/antimony_utils.py +294 -0
  6. framework/cli.py +229 -0
  7. framework/data_interpolation.py +340 -0
  8. framework/isotopomer_tools.py +41 -0
  9. framework/model_generation.py +46 -0
  10. framework/models.py +189 -0
  11. framework/module_base.py +42 -0
  12. framework/pyantigen.py +51 -0
  13. framework/rate_laws.py +101 -0
  14. framework/reaction_creation.py +43 -0
  15. framework/template/Example/AntiGen_paths.py +23 -0
  16. framework/template/Example/Engine/Anchor_cache.py +193 -0
  17. framework/template/Example/Engine/Deadline.py +535 -0
  18. framework/template/Example/Engine/Evaluator.py +1176 -0
  19. framework/template/Example/Engine/Event_times.py +491 -0
  20. framework/template/Example/Engine/Fast_profile.py +701 -0
  21. framework/template/Example/Engine/Fit_cache.py +329 -0
  22. framework/template/Example/Engine/Identifiability.py +698 -0
  23. framework/template/Example/Engine/Model_optimize.py +1483 -0
  24. framework/template/Example/Engine/Model_simulate.py +124 -0
  25. framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
  26. framework/template/Example/Engine/Optimize.py +6862 -0
  27. framework/template/Example/Engine/Petab_export.py +398 -0
  28. framework/template/Example/Engine/Preequil_cache.py +361 -0
  29. framework/template/Example/Engine/Profile_checkpoint.py +399 -0
  30. framework/template/Example/Engine/Results.py +395 -0
  31. framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
  32. framework/template/Example/Engine/Simulate.py +617 -0
  33. framework/template/Example/Flipflop_reference.py +401 -0
  34. framework/template/Example/Model_generate.py +37 -0
  35. framework/template/Example/Model_run.py +261 -0
  36. framework/template/Example/Modules/Data.py +63 -0
  37. framework/template/Example/Modules/Events.py +14 -0
  38. framework/template/Example/Modules/Experiment.py +194 -0
  39. framework/template/Example/Modules/Loss_config.py +61 -0
  40. framework/template/Example/Modules/Observed_species.py +3 -0
  41. framework/template/Example/Modules/Optimizer_settings.py +258 -0
  42. framework/template/Example/Modules/Plots.py +89 -0
  43. framework/template/Example/Modules/Solver_settings.py +16 -0
  44. framework/template/Example/Modules/Update_opt_parameters.py +24 -0
  45. framework/template/Example/Modules/Update_parameters.py +49 -0
  46. framework/template/data/ADneg.csv +27 -0
  47. framework/template/data/ADpos.csv +27 -0
  48. framework/template/data/Flipflop.csv +29 -0
  49. framework/template/data/make_flipflop_data.py +174 -0
  50. pyantigen-1.0.9.dist-info/METADATA +129 -0
  51. pyantigen-1.0.9.dist-info/RECORD +55 -0
  52. pyantigen-1.0.9.dist-info/WHEEL +5 -0
  53. pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
  54. pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
  55. 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()