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,320 @@
1
+ import os
2
+ import sys
3
+ from datetime import datetime
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+
7
+ try:
8
+ from SALib.sample import saltelli
9
+ from SALib.analyze import sobol
10
+ _SALIB_AVAILABLE = True
11
+ except ImportError:
12
+ _SALIB_AVAILABLE = False
13
+
14
+
15
+ def run_sobol_analysis(nll_func, param_names, bounds, res_x, N=128, range_factor=2.0,
16
+ mode='loss', nll_batch=None):
17
+ """
18
+ Run Sobol Global Sensitivity Analysis on the Negative Log-Likelihood (NLL).
19
+ This tests the identifiability of parameters against the experimental data.
20
+
21
+ Parameters
22
+ ----------
23
+ nll_func : callable
24
+ Function taking a parameter array and returning a scalar loss (NLL).
25
+ param_names : list
26
+ Names of the parameters being optimized.
27
+ bounds : list of tuples
28
+ Bounds for each parameter.
29
+ res_x : array
30
+ The nominal / optimized parameter values.
31
+ N : int
32
+ Number of Saltelli samples (generates N * (2D + 2) evaluations).
33
+ range_factor : float
34
+ If bounds are missing, use res_x / range_factor to res_x * range_factor.
35
+ mode : str
36
+ Currently supports 'loss' (identifiability). 'output' mode can be added for specific observables.
37
+ """
38
+ if not _SALIB_AVAILABLE:
39
+ print("Warning: SALib not installed. Skipping Sobol analysis.")
40
+ return None
41
+
42
+ if mode != 'loss':
43
+ raise NotImplementedError(f"Sobol mode '{mode}' is not yet implemented. Use 'loss'.")
44
+
45
+ problem = {
46
+ 'num_vars': len(param_names),
47
+ 'names': param_names,
48
+ 'bounds': []
49
+ }
50
+
51
+ for i, p in enumerate(param_names):
52
+ if bounds and bounds[i] is not None:
53
+ problem['bounds'].append(list(bounds[i]))
54
+ else:
55
+ opt_val = res_x[i]
56
+ lb = opt_val / range_factor
57
+ ub = opt_val * range_factor
58
+ if lb > ub: lb, ub = ub, lb
59
+ if lb == ub: ub = lb + 1e-6
60
+ problem['bounds'].append([lb, ub])
61
+
62
+ print(f"\n[Sobol {mode.capitalize()}] Generating Saltelli samples (N={N}) for {len(param_names)} parameters...")
63
+ param_values = saltelli.sample(problem, N, calc_second_order=True)
64
+ num_samples = param_values.shape[0]
65
+
66
+ print(f"[Sobol {mode.capitalize()}] Evaluating {num_samples} samples...")
67
+
68
+ import time
69
+ t0 = time.time()
70
+
71
+ if nll_batch is not None:
72
+ # Saltelli samples are independent by construction, so the whole design
73
+ # goes out as one batch -- the single largest parallel win available here.
74
+ Y = np.asarray(nll_batch(list(param_values), label="sobol"), dtype=float)
75
+ print(f"[Sobol {mode.capitalize()}] {num_samples} samples in "
76
+ f"{time.time() - t0:.1f}s")
77
+ else:
78
+ Y = np.zeros(num_samples)
79
+ for i, sample in enumerate(param_values):
80
+ try:
81
+ Y[i] = nll_func(sample)
82
+ except Exception:
83
+ Y[i] = np.nan
84
+
85
+ if (i+1) % 100 == 0 or (i+1) == num_samples:
86
+ elapsed = time.time() - t0
87
+ rate = (i+1) / elapsed if elapsed > 0 else 0
88
+ print(f" [{i+1}/{num_samples}] {elapsed:.1f}s ({rate:.1f} evals/s)", end='\r', flush=True)
89
+
90
+ print(f"\n[Sobol {mode.capitalize()}] Done. Running SALib analysis...")
91
+
92
+ valid_mask = np.isfinite(Y)
93
+ if not np.all(valid_mask):
94
+ n_invalid = np.sum(~valid_mask)
95
+ print(f" Warning: {n_invalid} invalid evaluations (NaN/Inf). Setting to max valid loss.")
96
+ max_valid = np.nanmax(Y[valid_mask]) if np.any(valid_mask) else 1e10
97
+ Y[~valid_mask] = max_valid
98
+
99
+ Si = sobol.analyze(problem, Y, calc_second_order=True, print_to_console=False)
100
+
101
+ results = {
102
+ "S1": Si["S1"],
103
+ "ST": Si["ST"],
104
+ "S1_conf": Si["S1_conf"],
105
+ "ST_conf": Si["ST_conf"],
106
+ "names": param_names,
107
+ "type": mode,
108
+ "N": N,
109
+ "num_samples": num_samples
110
+ }
111
+
112
+ return results
113
+
114
+
115
+ def save_sobol_plot(sobol_results, plot_path, model_name, tag="ALL"):
116
+ """
117
+ Generate horizontal bar charts for S1 and ST indices and save summary text.
118
+ """
119
+ if sobol_results is None:
120
+ return
121
+
122
+ os.makedirs(plot_path, exist_ok=True)
123
+
124
+ names = sobol_results["names"]
125
+ mode = sobol_results["type"]
126
+
127
+ S1 = sobol_results["S1"]
128
+ ST = sobol_results["ST"]
129
+ S1_conf = sobol_results.get("S1_conf", np.zeros_like(S1))
130
+ ST_conf = sobol_results.get("ST_conf", np.zeros_like(ST))
131
+
132
+ # Sort by Total Sensitivity (ST)
133
+ sort_idx = np.argsort(ST)
134
+ sorted_names = [names[i] for i in sort_idx]
135
+ sorted_S1 = S1[sort_idx]
136
+ sorted_ST = ST[sort_idx]
137
+ sorted_S1_conf = S1_conf[sort_idx]
138
+ sorted_ST_conf = ST_conf[sort_idx]
139
+
140
+ # Plotting
141
+ fig, ax = plt.subplots(figsize=(10, 5 + 0.3 * len(names)))
142
+ y_pos = np.arange(len(names))
143
+
144
+ ax.barh(y_pos, sorted_ST, xerr=sorted_ST_conf, color='skyblue', label='Total Order (ST)', alpha=0.8)
145
+ ax.barh(y_pos, sorted_S1, xerr=sorted_S1_conf, color='orange', label='First Order (S1)', alpha=0.8)
146
+
147
+ ax.set_yticks(y_pos)
148
+ ax.set_yticklabels(sorted_names)
149
+ ax.set_xlabel('Sensitivity Index')
150
+ title_mode = "Loss Identifiability" if mode == "loss" else "Model Output"
151
+ ax.set_title(f'Sobol Parameter Sensitivity ({title_mode})')
152
+ ax.legend()
153
+ ax.grid(axis='x', linestyle='--', alpha=0.5)
154
+
155
+ plt.tight_layout()
156
+ # Stamped like the profile and slice figures, so a re-run does not overwrite
157
+ # the analysis that prompted it. The figure and its summary share one stamp.
158
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
159
+ out_file = os.path.join(plot_path,
160
+ f"{model_name}_{tag}_sobol_{mode}_{ts}.png")
161
+ plt.savefig(out_file, dpi=300)
162
+ plt.close(fig)
163
+ print(f"Sobol sensitivity saved to: {out_file}")
164
+
165
+ # Text Summary
166
+ txt_file = os.path.join(
167
+ plot_path, f"{model_name}_{tag}_sobol_{mode}_summary_{ts}.txt")
168
+ with open(txt_file, "w") as f:
169
+ f.write(f"Sobol Global Sensitivity Analysis ({title_mode})\n")
170
+ f.write("========================================================\n")
171
+ f.write(f"N (Samples): {sobol_results['N']}\n")
172
+ f.write(f"Total Evaluations: {sobol_results['num_samples']}\n\n")
173
+ f.write(f"{'Parameter':<35} | {'ST':<15} | {'S1':<15}\n")
174
+ f.write("-" * 75 + "\n")
175
+
176
+ identifiable = []
177
+ unidentifiable = []
178
+
179
+ # Reverse sort for text file (highest sensitivity first)
180
+ for i in sort_idx[::-1]:
181
+ st_val = ST[i]
182
+ s1_val = S1[i]
183
+ f.write(f"{names[i]:<35} | {st_val:<15.5f} | {s1_val:<15.5f}\n")
184
+ if st_val > 0.01:
185
+ identifiable.append(names[i])
186
+ else:
187
+ unidentifiable.append(names[i])
188
+
189
+ f.write("\n\n=== IDENTIFIABILITY SUMMARY (ST > 0.01) ===\n")
190
+ f.write(f"Highly Influential / Identifiable ({len(identifiable)}):\n")
191
+ for p in identifiable: f.write(f" - {p}\n")
192
+
193
+ f.write(f"\nNon-Influential / Unidentifiable ({len(unidentifiable)}):\n")
194
+ for p in unidentifiable: f.write(f" - {p}\n")
195
+
196
+
197
+ def setup_sobol_analysis(settings, optimization_settings, experiment_dict):
198
+ """
199
+ Classical mode entry point for running Sobol Sensitivity on specific model outputs
200
+ (e.g., specific species concentrations over time).
201
+ Called standalone from *run.py similar to setup_optimization.
202
+ """
203
+ import AntiGen_paths
204
+ from framework.AntimonyGen import AntimonyGen
205
+ from framework.TelluriumGen import TelluriumGen
206
+ import pandas as pd
207
+
208
+ if not _SALIB_AVAILABLE:
209
+ print("SALib is required for Sobol Analysis. Please pip install SALib.")
210
+ return
211
+
212
+ MODEL_NAME = settings.get("MODEL_NAME", AntiGen_paths.MODEL_NAME)
213
+ model_text, paths = AntimonyGen(MODEL_NAME, repo_root=AntiGen_paths.REPO_ROOT)
214
+
215
+ param_names = optimization_settings["param_names"]
216
+ x0 = optimization_settings["x0"]
217
+ bounds = optimization_settings.get("bounds")
218
+ sobol_kwargs = optimization_settings.get("sobol_kwargs", {})
219
+ N = sobol_kwargs.get("N", 128)
220
+
221
+ observables = sobol_kwargs.get("observables", [])
222
+ times = sobol_kwargs.get("times", [10, 50, 100])
223
+
224
+ if not observables:
225
+ print("Error: No 'observables' specified in sobol_kwargs for classical output mode.")
226
+ return
227
+
228
+ problem = {
229
+ 'num_vars': len(param_names),
230
+ 'names': param_names,
231
+ 'bounds': []
232
+ }
233
+
234
+ for i, p in enumerate(param_names):
235
+ if bounds and bounds[i] is not None:
236
+ problem['bounds'].append(list(bounds[i]))
237
+ else:
238
+ opt_val = x0[i]
239
+ problem['bounds'].append([opt_val * 0.9, opt_val * 1.1])
240
+
241
+ print(f"\n[Sobol Output] Generating samples (N={N}) for {len(param_names)} parameters...")
242
+ param_values = saltelli.sample(problem, N, calc_second_order=True)
243
+ num_samples = param_values.shape[0]
244
+
245
+ experiment = experiment_dict.get("EXPERIMENT", experiment_dict.get("experiment"))
246
+ # Use the first replicate/experiment model to test outputs
247
+ if isinstance(experiment, dict) and "replicates" not in dir(experiment):
248
+ first_key = list(experiment.keys())[0]
249
+ rep = experiment[first_key]
250
+ else:
251
+ first_key = list(experiment.replicates.keys())[0]
252
+ rep = experiment.replicates[first_key]
253
+
254
+ df_dict = rep["Data"](rep, paths["data_path"])
255
+ events_str = rep["Events"](rep, df_dict)
256
+ r = TelluriumGen(model_text + "\n" + events_str, paths)
257
+ rep["Update_parameters"](r, rep)
258
+
259
+ from Engine.Event_times import attach_event_times
260
+ attach_event_times(rep, r)
261
+
262
+ # We will record Y of shape (num_samples, len(observables), len(times))
263
+ Y = np.zeros((num_samples, len(observables), len(times)))
264
+
265
+ import time
266
+ t0 = time.time()
267
+ for i, sample in enumerate(param_values):
268
+ r.resetAll()
269
+ for j, p_name in enumerate(param_names):
270
+ try:
271
+ r[p_name] = sample[j]
272
+ except: pass
273
+
274
+ try:
275
+ from Engine.Simulate import safe_simulate
276
+ block = {
277
+ "start": 0.0,
278
+ "end": max(times),
279
+ "n_points": 500,
280
+ "variable_step_size": True
281
+ }
282
+ res, _ = safe_simulate(r, block, ['time'] + observables)
283
+ for obs_idx, obs_name in enumerate(observables):
284
+ obs_data = res[obs_name]
285
+ time_data = res['time']
286
+ for t_idx, t_val in enumerate(times):
287
+ idx_t = np.argmin(np.abs(time_data - t_val))
288
+ Y[i, obs_idx, t_idx] = obs_data[idx_t]
289
+ except Exception:
290
+ Y[i, :, :] = np.nan
291
+
292
+ if (i+1) % 100 == 0 or (i+1) == num_samples:
293
+ elapsed = time.time() - t0
294
+ rate = (i+1) / elapsed if elapsed > 0 else 0
295
+ print(f" [{i+1}/{num_samples}] {elapsed:.1f}s ({rate:.1f} evals/s)", end='\r', flush=True)
296
+
297
+ print(f"\n[Sobol Output] Analysis complete. Generating reports...")
298
+ plot_path = paths["plot_path"]
299
+ os.makedirs(plot_path, exist_ok=True)
300
+
301
+ for obs_idx, obs_name in enumerate(observables):
302
+ for t_idx, t_val in enumerate(times):
303
+ y_slice = Y[:, obs_idx, t_idx]
304
+ valid_mask = np.isfinite(y_slice)
305
+ if not np.all(valid_mask):
306
+ y_slice[~valid_mask] = np.nanmedian(y_slice)
307
+
308
+ Si = sobol.analyze(problem, y_slice, calc_second_order=True, print_to_console=False)
309
+
310
+ res_dict = {
311
+ "S1": Si["S1"], "ST": Si["ST"], "names": param_names,
312
+ "type": "output", "N": N, "num_samples": num_samples
313
+ }
314
+ # Clean observable name for file saving
315
+ safe_obs = obs_name.replace('[', '').replace(']', '')
316
+ tag = f"{safe_obs}_t{t_val}"
317
+ save_sobol_plot(res_dict, plot_path, MODEL_NAME, tag=tag)
318
+
319
+ print(f"Classical Sobol Analysis outputs saved to {plot_path}")
320
+