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,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
|
+
|