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,261 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from Engine.Model_simulate import setup_simulation
|
|
5
|
+
from Engine.Model_optimize import (
|
|
6
|
+
setup_optimization_from_groups,
|
|
7
|
+
_FULL_DIAGNOSTICS,
|
|
8
|
+
_SLICE_ONLY,
|
|
9
|
+
_PROFILE_ONLY,
|
|
10
|
+
_SOBOL_ONLY,
|
|
11
|
+
_FAST_PROFILE_ONLY,
|
|
12
|
+
_NO_DIAGNOSTICS,
|
|
13
|
+
DIAGNOSTICS_PRESETS,
|
|
14
|
+
)
|
|
15
|
+
from Modules.Plots import *
|
|
16
|
+
from Modules.Experiment import get_EXPERIMENT
|
|
17
|
+
from Modules.Optimizer_settings import get_OPTIMIZATION
|
|
18
|
+
from AntiGen_paths import MODEL_NAME, REPO_ROOT
|
|
19
|
+
from Model_generate import update_antimony_model
|
|
20
|
+
|
|
21
|
+
EXPERIMENT_dict = {
|
|
22
|
+
"Example": {'EXPERIMENT': get_EXPERIMENT('EXPERIMENT_Example'), 'plot': plot_results, 'opt_settings_key': 'Example'},
|
|
23
|
+
"Flipflop": {'EXPERIMENT': get_EXPERIMENT('EXPERIMENT_Flipflop'), 'plot': plot_flipflop, 'opt_settings_key': 'Flipflop'},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
diagnostics = _NO_DIAGNOSTICS
|
|
27
|
+
|
|
28
|
+
OPTIMIZATION_REGISTRY = {
|
|
29
|
+
# One CLI selection can run multiple independent Optimization specs in
|
|
30
|
+
# sequence (e.g. parameters split across specs for identifiability
|
|
31
|
+
# reasons); opt_key may be a single name or a list of names.
|
|
32
|
+
# Optional "diagnostics" dict is merged into run_settings for every spec
|
|
33
|
+
# run under that selection (wald/slice/profile-likelihood/Sobol flags).
|
|
34
|
+
# 'title' / 'description' are printed before the run, 'interpretation'
|
|
35
|
+
# after it, so the console output explains what the example demonstrates
|
|
36
|
+
# and what to look for in the numbers and figures. Every figure and CSV
|
|
37
|
+
# written by a run is prefixed with the example name (e.g.
|
|
38
|
+
# Example_Example4_...) so the five examples never overwrite each other.
|
|
39
|
+
"Example1": {
|
|
40
|
+
'opt_key': ['OPTIMIZATION_Example1_ADpos', 'OPTIMIZATION_Example1_ADneg'],
|
|
41
|
+
'experiment': 'EXPERIMENT_Example',
|
|
42
|
+
'title': "Baseline well-posed fits, split by identifiability",
|
|
43
|
+
'description': """\
|
|
44
|
+
Two independent optimizations run in sequence, no diagnostics:
|
|
45
|
+
1. k_A_to_B and SF fit jointly against the ADpos replicates (they are
|
|
46
|
+
jointly identifiable there).
|
|
47
|
+
2. V_Comp1 fit alone against the ADneg replicates. It is kept OUT of the
|
|
48
|
+
first fit because SF and V_Comp1 trade off against the same [B]
|
|
49
|
+
trajectories -- fitting all three together lands on a ridge, not a
|
|
50
|
+
minimum (Example3 demonstrates exactly that failure).""",
|
|
51
|
+
'interpretation': """\
|
|
52
|
+
Both optimizations should report success=True with a small final loss.
|
|
53
|
+
Figures Example_Example1_ADpos.png and Example_Example1_ADneg.png show the
|
|
54
|
+
fitted [A]/[B] curves passing through the measured [B] points. Run Example2
|
|
55
|
+
for the same fits with full identifiability diagnostics.""",
|
|
56
|
+
},
|
|
57
|
+
"Example2": {
|
|
58
|
+
'opt_key': ['OPTIMIZATION_Example1_ADpos', 'OPTIMIZATION_Example1_ADneg'],
|
|
59
|
+
'experiment': 'EXPERIMENT_Example',
|
|
60
|
+
'diagnostics': _FULL_DIAGNOSTICS,
|
|
61
|
+
'title': "Example1 fits with full identifiability diagnostics",
|
|
62
|
+
'description': """\
|
|
63
|
+
The same two fits as Example1, with every diagnostic enabled: Wald
|
|
64
|
+
statistics (Hessian-based SEs/CIs), likelihood slices (vary one parameter,
|
|
65
|
+
hold the rest), true profile likelihood (vary one parameter, RE-OPTIMIZE
|
|
66
|
+
the rest), and Sobol sensitivity indices. This is the positive control:
|
|
67
|
+
both fits are well-posed, so every diagnostic should agree.""",
|
|
68
|
+
'interpretation': """\
|
|
69
|
+
What a well-identified fit looks like:
|
|
70
|
+
* Wald SEs are finite; Wald and profile 95% CIs roughly agree.
|
|
71
|
+
* Every profile-likelihood curve is a clean parabola-like bowl crossing
|
|
72
|
+
the 95% threshold (dNLL = 1.92) on BOTH sides -> finite CIs, no [nan].
|
|
73
|
+
* Slices and profiles nearly coincide (little parameter compensation).
|
|
74
|
+
* No '*** FLAT ***' warnings.
|
|
75
|
+
Figures (prefix Example_Example2_<group>_): *_profile_likelihood.png,
|
|
76
|
+
*_likelihood_slice.png (+ _zoom variants), *_sobol.png; fit curves in
|
|
77
|
+
Example_Example2_ADpos.png / _ADneg.png. Contrast with Example3, where the
|
|
78
|
+
joint fit breaks these diagnostics in a recognizable way.""",
|
|
79
|
+
},
|
|
80
|
+
"Example4": {
|
|
81
|
+
'opt_key': ['OPTIMIZATION_Example4_flipflop'],
|
|
82
|
+
'experiment': 'EXPERIMENT_Flipflop',
|
|
83
|
+
'diagnostics': _FULL_DIAGNOSTICS,
|
|
84
|
+
'title': "Multimodal flip-flop kinetics, started in the TRUE basin",
|
|
85
|
+
'description': """\
|
|
86
|
+
A -> B -> C with rates k_A_to_B, k_B_to_C and a fitted scale factor SF,
|
|
87
|
+
observed on a log10 scale. Swapping the two rate constants rescales B(t)
|
|
88
|
+
by a constant that SF absorbs exactly ('flip-flop'), so the likelihood has
|
|
89
|
+
a second local minimum at the swapped parameters. Four noisy predicted_A
|
|
90
|
+
points break the symmetry by a small, known amount. The fit starts inside
|
|
91
|
+
the true-mode basin (k_A_to_B > k_B_to_C). This example tests the ACCURACY
|
|
92
|
+
of profile-likelihood dNLL values, not just flat-direction detection.""",
|
|
93
|
+
'interpretation': """\
|
|
94
|
+
What accurate diagnostics must show (reference values from
|
|
95
|
+
Flipflop_reference.py, which recomputes everything in closed form):
|
|
96
|
+
* Likelihood slices: ONE sharp minimum. Slices cannot see the second
|
|
97
|
+
mode -- reaching it requires the other two parameters to move.
|
|
98
|
+
* Every true profile DIPS to dNLL ~ -2.12 next to the fit point. Not a
|
|
99
|
+
bug: the fitting objective weights observables differently from the
|
|
100
|
+
inference NLL the profiles are anchored to, so the fit optimum sits
|
|
101
|
+
slightly off the NLL optimum. A profile that does not dip is wrong.
|
|
102
|
+
* Each profile shows a SECOND minimum (the swapped mode) at dNLL ~ +0.8,
|
|
103
|
+
BELOW the 1.92 threshold: the correct 95% confidence set is a union of
|
|
104
|
+
two disjoint intervals even though the fit found the right mode. A
|
|
105
|
+
profile walker that stops at the first threshold crossing misses it.
|
|
106
|
+
Compare figures Example_Example4_Flipflop_profile_likelihood*.png against
|
|
107
|
+
'python Flipflop_reference.py'; any disagreement is a bug in the profile
|
|
108
|
+
machinery, not the model. Then run Example5 for the wrong-basin version.""",
|
|
109
|
+
},
|
|
110
|
+
"Example5": {
|
|
111
|
+
'opt_key': ['OPTIMIZATION_Example5_flipflop_swapped'],
|
|
112
|
+
'experiment': 'EXPERIMENT_Flipflop',
|
|
113
|
+
'diagnostics': _FULL_DIAGNOSTICS,
|
|
114
|
+
'title': "Flip-flop kinetics, started in the WRONG (swapped) basin",
|
|
115
|
+
'description': """\
|
|
116
|
+
Same problem as Example4 but started with k_A_to_B < k_B_to_C, so
|
|
117
|
+
Nelder-Mead converges to the swapped LOCAL minimum -- the situation
|
|
118
|
+
multimodal problems create in practice, where nobody tells you the
|
|
119
|
+
optimizer found the wrong mode. All sigmas are frozen by MLE at that local
|
|
120
|
+
optimum: the logA sigma absorbs the swapped mode's misfit (~1.73 dex
|
|
121
|
+
instead of ~0.92), which flattens every dNLL built on it.""",
|
|
122
|
+
'interpretation': """\
|
|
123
|
+
The unambiguous signature that the fit missed the global optimum:
|
|
124
|
+
* An accurate profile goes NEGATIVE, dipping to dNLL ~ -0.98 at the true
|
|
125
|
+
mode. The analysis must report that dip, not clip or re-anchor it.
|
|
126
|
+
* With the true mode at -0.98, BOTH modes lie below the 1.92 threshold:
|
|
127
|
+
the correct 95% confidence set for every parameter is two disjoint
|
|
128
|
+
intervals (e.g. k_A_to_B in [0.072, 0.076] U [0.284, 0.363]). A
|
|
129
|
+
first-crossing CI extractor reports only the narrow interval around
|
|
130
|
+
the WRONG mode and silently discards the one containing the truth.
|
|
131
|
+
Compare Example_Example5_Flipflop_profile_likelihood*.png against
|
|
132
|
+
'python Flipflop_reference.py --anchor swapped' (same wrong-mode anchor).""",
|
|
133
|
+
},
|
|
134
|
+
"Example3": {
|
|
135
|
+
'opt_key': ['OPTIMIZATION_Example3_joint'],
|
|
136
|
+
'experiment': 'EXPERIMENT_Example',
|
|
137
|
+
'diagnostics': _FULL_DIAGNOSTICS,
|
|
138
|
+
'title': "Negative example: structurally unidentifiable joint fit",
|
|
139
|
+
'description': """\
|
|
140
|
+
k_A_to_B, SF, and V_Comp1 fit JOINTLY against the ADpos data -- the fit
|
|
141
|
+
Example1 deliberately avoids. The confound is exact: V_Comp1 cancels out
|
|
142
|
+
of the ODE entirely, and the output map predicted_B = SF*B_Comp1/V_Comp1
|
|
143
|
+
depends only on the ratio SF/V_Comp1, so any (SF, V_Comp1) pair with the
|
|
144
|
+
same ratio fits identically. Full diagnostics are on to show what
|
|
145
|
+
structural unidentifiability looks like.""",
|
|
146
|
+
'interpretation': """\
|
|
147
|
+
The signature of a structurally unidentifiable pair:
|
|
148
|
+
* Profile likelihood: k_A_to_B gets a tight, finite 95% CI, but SF and
|
|
149
|
+
V_Comp1 come back [nan, nan] -- their profiles stay flat below the
|
|
150
|
+
threshold because the optimizer trades one against the other to hold
|
|
151
|
+
SF/V_Comp1 constant at identical loss.
|
|
152
|
+
* Wald: SF/V_Comp1 correlation ~ +1 (or the Hessian is not positive
|
|
153
|
+
definite), so their SEs are meaningless or absent.
|
|
154
|
+
* CAVEAT -- Sobol says the OPPOSITE (large ST for SF and V_Comp1, ~0 for
|
|
155
|
+
k_A_to_B), and that is expected, not a bug: Sobol perturbs parameters
|
|
156
|
+
independently with no re-optimization, so it measures uncorrelated
|
|
157
|
+
loss sensitivity, not identifiability. For a ridge these two questions
|
|
158
|
+
have opposite answers. Treat Sobol as a sensitivity screen and profile
|
|
159
|
+
likelihood as the identifiability check.
|
|
160
|
+
Figures carry the prefix Example_Example3_ADpos_.""",
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _print_block(header, body):
|
|
166
|
+
"""Print a framed explanation block so it stands out in the run log."""
|
|
167
|
+
bar = "=" * 78
|
|
168
|
+
print(f"\n{bar}\n{header}\n{bar}")
|
|
169
|
+
print(body)
|
|
170
|
+
print(bar + "\n")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
import argparse
|
|
175
|
+
parser = argparse.ArgumentParser(description="Tellurium/RoadRunner Simulation and Optimization Runner")
|
|
176
|
+
parser.add_argument("--optimize", type=str, choices=list(OPTIMIZATION_REGISTRY.keys()),
|
|
177
|
+
help="Optimization mode")
|
|
178
|
+
parser.add_argument("--simulate", type=str, choices=list(EXPERIMENT_dict.keys()),
|
|
179
|
+
help="Simulation mode")
|
|
180
|
+
parser.add_argument("--diagnostics", type=str, default="_NO_DIAGNOSTICS",
|
|
181
|
+
choices=list(DIAGNOSTICS_PRESETS.keys()),
|
|
182
|
+
help="Diagnostics settings preset to run (default: _NO_DIAGNOSTICS)")
|
|
183
|
+
parser.add_argument("--no-fit", action="store_true", dest="no_fit",
|
|
184
|
+
help="Skip the optimizer and evaluate the spec's x0 instead, "
|
|
185
|
+
"then run the requested diagnostics against it.")
|
|
186
|
+
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
if len(sys.argv) == 1:
|
|
190
|
+
parser.error("No arguments provided.")
|
|
191
|
+
|
|
192
|
+
update_antimony_model()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --- Optimization ------------------------------------
|
|
196
|
+
if args.optimize:
|
|
197
|
+
opt_info = OPTIMIZATION_REGISTRY[args.optimize]
|
|
198
|
+
model_name_to_use = MODEL_NAME
|
|
199
|
+
|
|
200
|
+
run_settings = {
|
|
201
|
+
"run_steady_state_first": False,
|
|
202
|
+
"Verbose": True,
|
|
203
|
+
"save_SBML?": False,
|
|
204
|
+
"MODEL_NAME": model_name_to_use,
|
|
205
|
+
"slice_analysis": False,
|
|
206
|
+
"fit_mode": "evaluate_x0" if args.no_fit else "optimize",
|
|
207
|
+
# Prefixes every figure/CSV name with the example name so runs
|
|
208
|
+
# of different examples never overwrite each other's output.
|
|
209
|
+
"run_label": args.optimize,
|
|
210
|
+
}
|
|
211
|
+
selected_diagnostics = DIAGNOSTICS_PRESETS.get(args.diagnostics, _NO_DIAGNOSTICS)
|
|
212
|
+
run_settings.update(selected_diagnostics)
|
|
213
|
+
if "diagnostics" in opt_info:
|
|
214
|
+
run_settings.update(opt_info["diagnostics"])
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Retrieve experiment object & optimization spec(s)
|
|
218
|
+
exp_obj = get_EXPERIMENT(opt_info["experiment"])
|
|
219
|
+
opt_keys = opt_info["opt_key"]
|
|
220
|
+
if isinstance(opt_keys, str):
|
|
221
|
+
opt_keys = [opt_keys]
|
|
222
|
+
|
|
223
|
+
# Plot the fitted curves after each optimization with the experiment's
|
|
224
|
+
# plot function (figures are tagged with the example/group names, e.g.
|
|
225
|
+
# Example_Example1_ADpos.png).
|
|
226
|
+
exp_short = opt_info["experiment"].replace("EXPERIMENT_", "")
|
|
227
|
+
experiment_arg = {
|
|
228
|
+
"EXPERIMENT": exp_obj,
|
|
229
|
+
"plot": EXPERIMENT_dict.get(exp_short, {}).get("plot"),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if opt_info.get("title"):
|
|
233
|
+
_print_block(f"{args.optimize}: {opt_info['title']}",
|
|
234
|
+
opt_info.get("description", ""))
|
|
235
|
+
|
|
236
|
+
for opt_key in opt_keys:
|
|
237
|
+
opt_spec = get_OPTIMIZATION(opt_key)
|
|
238
|
+
print(f"Starting domain optimization for: {args.optimize} [{opt_key}]")
|
|
239
|
+
setup_optimization_from_groups(run_settings, opt_spec, experiment_arg)
|
|
240
|
+
|
|
241
|
+
if opt_info.get("interpretation"):
|
|
242
|
+
_print_block(f"{args.optimize}: what to look for in the results",
|
|
243
|
+
opt_info["interpretation"])
|
|
244
|
+
|
|
245
|
+
# --- Simulation ---------------------------------------------
|
|
246
|
+
elif args.simulate:
|
|
247
|
+
run_name = args.simulate
|
|
248
|
+
fig_config = EXPERIMENT_dict[run_name]
|
|
249
|
+
model_name_to_use = MODEL_NAME
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
run_settings = {
|
|
253
|
+
"run_steady_state_first": False,
|
|
254
|
+
"Verbose": True,
|
|
255
|
+
"save_SBML?": False,
|
|
256
|
+
"MODEL_NAME": model_name_to_use
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setup_simulation(run_settings, fig_config)
|
|
260
|
+
|
|
261
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def load_no_data(replicate, data_path):
|
|
6
|
+
data_dict = {}
|
|
7
|
+
return data_dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_ad_data(replicate, data_path):
|
|
11
|
+
def reshape_df(df):
|
|
12
|
+
return pd.concat([
|
|
13
|
+
df[['time', 'B1']].rename(columns={'B1': 'B'}),
|
|
14
|
+
df[['time', 'B2']].rename(columns={'B2': 'B'}),
|
|
15
|
+
df[['time', 'B3']].rename(columns={'B3': 'B'})
|
|
16
|
+
], ignore_index=True)
|
|
17
|
+
|
|
18
|
+
treatment_path = os.path.join(data_path, 'ADneg.csv')
|
|
19
|
+
df_neg = pd.read_csv(treatment_path)
|
|
20
|
+
df_neg_early = reshape_df(df_neg[df_neg['Treatment'] == 'Early'])
|
|
21
|
+
df_neg_late = reshape_df(df_neg[df_neg['Treatment'] == 'Late'])
|
|
22
|
+
treatment_path = os.path.join(data_path, 'ADpos.csv')
|
|
23
|
+
df_pos = pd.read_csv(treatment_path)
|
|
24
|
+
df_pos_early = reshape_df(df_pos[df_pos['Treatment'] == 'Early'])
|
|
25
|
+
df_pos_late = reshape_df(df_pos[df_pos['Treatment'] == 'Late'])
|
|
26
|
+
|
|
27
|
+
data_dict = {
|
|
28
|
+
"ADneg_Early": df_neg_early,
|
|
29
|
+
"ADneg_Late": df_neg_late,
|
|
30
|
+
"ADpos_Early": df_pos_early,
|
|
31
|
+
"ADpos_Late": df_pos_late,
|
|
32
|
+
}
|
|
33
|
+
return data_dict
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_flipflop_data(replicate, data_path):
|
|
37
|
+
"""Load the synthetic flip-flop dataset (see data/make_flipflop_data.py).
|
|
38
|
+
|
|
39
|
+
Returns log10-scale data: for each treatment a stacked frame of the three
|
|
40
|
+
logB replicate columns, plus a separate sparse frame of the noisy logA
|
|
41
|
+
observations (Early treatment only), keyed "<label>_A". The logA points are
|
|
42
|
+
what break the flip-flop swap symmetry and set the height of the second
|
|
43
|
+
likelihood mode.
|
|
44
|
+
"""
|
|
45
|
+
def stack_logB(df):
|
|
46
|
+
return pd.concat([
|
|
47
|
+
df[['time', 'logB1']].rename(columns={'logB1': 'logB'}),
|
|
48
|
+
df[['time', 'logB2']].rename(columns={'logB2': 'logB'}),
|
|
49
|
+
df[['time', 'logB3']].rename(columns={'logB3': 'logB'})
|
|
50
|
+
], ignore_index=True)
|
|
51
|
+
|
|
52
|
+
df = pd.read_csv(os.path.join(data_path, 'Flipflop.csv'))
|
|
53
|
+
data_dict = {}
|
|
54
|
+
for treatment in ('Early', 'Late'):
|
|
55
|
+
sub = df[df['Treatment'] == treatment]
|
|
56
|
+
data_dict[f"Flipflop_{treatment}"] = stack_logB(sub)
|
|
57
|
+
logA = sub[sub['logA'].notna()][['time', 'logA']].reset_index(drop=True)
|
|
58
|
+
if len(logA):
|
|
59
|
+
data_dict[f"Flipflop_{treatment}_A"] = logA
|
|
60
|
+
return data_dict
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from framework.data_interpolation import generate_antimony_piecewise
|
|
2
|
+
|
|
3
|
+
def generate_no_events(replicate, df_dict):
|
|
4
|
+
events = ''
|
|
5
|
+
return events
|
|
6
|
+
|
|
7
|
+
def Example_event(replicate, df_dict):
|
|
8
|
+
dose = replicate["dose"]
|
|
9
|
+
delay = replicate["delay"]
|
|
10
|
+
events = f'at (time >= {delay}): A_Comp1 = {dose}'
|
|
11
|
+
return events
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Experiment registry: one 'replicate' entry per simulation,
|
|
2
|
+
simulations are optimized together if in the same opt_group.
|
|
3
|
+
"""
|
|
4
|
+
from .Data import *
|
|
5
|
+
from .Events import *
|
|
6
|
+
from .Loss_config import *
|
|
7
|
+
from .Observed_species import *
|
|
8
|
+
from .Solver_settings import *
|
|
9
|
+
from .Update_parameters import *
|
|
10
|
+
from .Update_opt_parameters import *
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Experiment:
|
|
16
|
+
replicates: dict = field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def opt_groups(self):
|
|
20
|
+
"""Return {opt_group: [replicate_key, ...]} by scanning replicates."""
|
|
21
|
+
groups = {}
|
|
22
|
+
for key, rep in self.replicates.items():
|
|
23
|
+
og = rep.get("Opt_group")
|
|
24
|
+
if og is not None:
|
|
25
|
+
groups.setdefault(og, []).append(key)
|
|
26
|
+
return groups
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_replicate(config, **meta):
|
|
30
|
+
"""
|
|
31
|
+
Build a single replicate entry dict from an explicit config dict.
|
|
32
|
+
|
|
33
|
+
config keys (all required unless noted):
|
|
34
|
+
"Label" : str
|
|
35
|
+
"Events" : callable(replicate, df_dict) -> str
|
|
36
|
+
"Data" : callable(replicate, data_path) -> Dict of DataFrame or None
|
|
37
|
+
"Observed_species" : callable(RoadRunner_instance or None) -> list[str]
|
|
38
|
+
"Solver_settings" : callable(replicate) -> dict
|
|
39
|
+
"Update_parameters": callable(RoadRunner_instance, replicate, mode) -> dict
|
|
40
|
+
"Loss_config" : callable(replicate) -> dict
|
|
41
|
+
"Opt_group" : str
|
|
42
|
+
|
|
43
|
+
'callable' functions are contained in separate files with the same name as the key.
|
|
44
|
+
For example, "Events" is contained in the file "Events.py", "Data" is contained in
|
|
45
|
+
the file "Data.py", etc.
|
|
46
|
+
|
|
47
|
+
'replicate' is the output of make_replicate. It contains the dictionary above plus
|
|
48
|
+
any optional keyword arguments.
|
|
49
|
+
|
|
50
|
+
Any arguments required by the 'callable' functions should be passed as keyword arguments.
|
|
51
|
+
|
|
52
|
+
Keyword arguments are stored in 'replicate' as additional keys. Example arguments:
|
|
53
|
+
Age = 70, Status = True, Population = "Amyloid_negative", Drug = "Lecanemab",
|
|
54
|
+
dose_nmol = 10, Schedule = "default", Type = "default", …
|
|
55
|
+
|
|
56
|
+
Example::
|
|
57
|
+
|
|
58
|
+
make_replicate(
|
|
59
|
+
{
|
|
60
|
+
"Label": "Example_1",
|
|
61
|
+
"Events": generate_no_events,
|
|
62
|
+
"Data": load_data_Example,
|
|
63
|
+
"Observed_species": observed_Example,
|
|
64
|
+
"Solver_settings": solver_settings_Example,
|
|
65
|
+
"Update_parameters": update_no_parameters,
|
|
66
|
+
"Loss_config": Example_loss_config,
|
|
67
|
+
"Opt_group": opt_group
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
entry = dict(config)
|
|
72
|
+
return {**entry, **meta}
|
|
73
|
+
|
|
74
|
+
# ***************************************************************************
|
|
75
|
+
# USER DEFINED OPTIMIZATION
|
|
76
|
+
# ***************************************************************************
|
|
77
|
+
|
|
78
|
+
# Build each replicate so that each element described in the
|
|
79
|
+
# make_replicate function has a value.
|
|
80
|
+
# It can be helpful (but not required) to define experimental groups
|
|
81
|
+
# and treatments (conditions) as separate dictionaries, and then
|
|
82
|
+
# build each replicate by combining elements from the experimental groups
|
|
83
|
+
# and treatments. "params" are used to pass arguments to the 'callable' functions.
|
|
84
|
+
# The "opt_group" is a string that defines which replicates are optimized
|
|
85
|
+
# together, each contributing to the total loss function value when their
|
|
86
|
+
# "Opt_group" valuse are the same. For example, if "Opt_group" is "ADneg" for
|
|
87
|
+
# "Early" and "Late", then the loss function will be calculated for both
|
|
88
|
+
# "Early" and "Late" replicates.
|
|
89
|
+
# The "Label" should be unique for each replicate, although not required.
|
|
90
|
+
# The "replicate" designation implies an experimental replicate, but it is rare
|
|
91
|
+
# to optimize at the replicate level. Typically, replicates are handled at
|
|
92
|
+
# the "Data" level, with all replicates' data fit simultaneously.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_experiment():
|
|
97
|
+
# Initialize Experiment class object
|
|
98
|
+
exp = Experiment()
|
|
99
|
+
|
|
100
|
+
# Treatments are typically linked to events, but other settings can also be linked.
|
|
101
|
+
# In the example below, "loss_config" is also linked to the treatment because
|
|
102
|
+
# the data loaded depends on the treatment.
|
|
103
|
+
# Treatment types may also comprise the 'opt_group' in some cases.
|
|
104
|
+
treatments = {
|
|
105
|
+
"Early": {"params": {"dose": 10, "delay": 5}},
|
|
106
|
+
"Late": {"params": {"dose": 5, "delay": 10}},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Experimental groups are not required but are natural for some
|
|
110
|
+
# experimental designs. For example, when studying the effect of a treatment
|
|
111
|
+
# in different populations.
|
|
112
|
+
exp_groups = {
|
|
113
|
+
"ADneg": {"opt_group": "ADneg", "params": {"amyloid_positive": False}},
|
|
114
|
+
"ADpos": {"opt_group": "ADpos", "params": {"amyloid_positive": True}}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for exp_group_name, exp_group_data in exp_groups.items():
|
|
118
|
+
opt_group = exp_group_data.get("opt_group", exp_group_name)
|
|
119
|
+
exp_params = exp_group_data.get("params", {})
|
|
120
|
+
for treatment_name, treatment_data in treatments.items():
|
|
121
|
+
treatment_params = treatment_data.get("params", {})
|
|
122
|
+
|
|
123
|
+
key = f"{exp_group_name}_{treatment_name}"
|
|
124
|
+
|
|
125
|
+
replicate = make_replicate(
|
|
126
|
+
{
|
|
127
|
+
"Label": key,
|
|
128
|
+
"Events": Example_event,
|
|
129
|
+
"Data": load_ad_data,
|
|
130
|
+
"Observed_species": all_species,
|
|
131
|
+
"Solver_settings": solver_settings_Example,
|
|
132
|
+
"Update_opt_parameters": update_opt_no_parameters,
|
|
133
|
+
"Update_parameters": update_Example,
|
|
134
|
+
},
|
|
135
|
+
**exp_params,
|
|
136
|
+
**treatment_params
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Store replicate in the flat registry and nested structure
|
|
140
|
+
exp.replicates[key] = replicate
|
|
141
|
+
|
|
142
|
+
return exp
|
|
143
|
+
|
|
144
|
+
EXPERIMENT_Example = _build_experiment()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _build_flipflop_experiment():
|
|
148
|
+
"""Flip-flop identifiability experiment (Example4/Example5).
|
|
149
|
+
|
|
150
|
+
Two treatments of the A -> B -> C chain, dosed by the same event style as
|
|
151
|
+
the base example, observed through predicted_B on a log10 scale. The Early
|
|
152
|
+
treatment additionally carries four very noisy predicted_A observations
|
|
153
|
+
("has_A_data") — without them the likelihood would have two *exactly*
|
|
154
|
+
equal modes; with them the swapped mode sits at a known dNLL of ~2.4
|
|
155
|
+
(printed by data/make_flipflop_data.py when regenerating the data).
|
|
156
|
+
"""
|
|
157
|
+
exp = Experiment()
|
|
158
|
+
treatments = {
|
|
159
|
+
"Early": {"params": {"dose": 10, "delay": 5, "has_A_data": True}},
|
|
160
|
+
"Late": {"params": {"dose": 5, "delay": 10, "has_A_data": False}},
|
|
161
|
+
}
|
|
162
|
+
for treatment_name, treatment_data in treatments.items():
|
|
163
|
+
key = f"Flipflop_{treatment_name}"
|
|
164
|
+
exp.replicates[key] = make_replicate(
|
|
165
|
+
{
|
|
166
|
+
"Label": key,
|
|
167
|
+
"Events": Example_event,
|
|
168
|
+
"Data": load_flipflop_data,
|
|
169
|
+
"Observed_species": all_species,
|
|
170
|
+
"Solver_settings": solver_settings_Example,
|
|
171
|
+
"Update_opt_parameters": update_opt_no_parameters,
|
|
172
|
+
"Update_parameters": update_flipflop,
|
|
173
|
+
},
|
|
174
|
+
**treatment_data.get("params", {}),
|
|
175
|
+
)
|
|
176
|
+
return exp
|
|
177
|
+
|
|
178
|
+
EXPERIMENT_Flipflop = _build_flipflop_experiment()
|
|
179
|
+
|
|
180
|
+
# ***************************************************************************
|
|
181
|
+
# END USER DEFINED EXPERIMENTS
|
|
182
|
+
# ***************************************************************************
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Registry accessors
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def get_EXPERIMENTS():
|
|
189
|
+
"""Returns a dict mapping experiment name -> Experiment."""
|
|
190
|
+
return {k: v for k, v in globals().items() if k.startswith('EXPERIMENT_')}
|
|
191
|
+
|
|
192
|
+
def get_EXPERIMENT(name):
|
|
193
|
+
"""Returns a single Experiment by name."""
|
|
194
|
+
return globals().get(name)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
def no_optimization(replicate):
|
|
2
|
+
return {}
|
|
3
|
+
|
|
4
|
+
def Example1_loss_config(replicate):
|
|
5
|
+
label = replicate["Label"]
|
|
6
|
+
return {"observables": [{
|
|
7
|
+
"observed_variable": "predicted_B",
|
|
8
|
+
"data_column": "B",
|
|
9
|
+
"time_column": "time",
|
|
10
|
+
"data_dict_key": label,
|
|
11
|
+
}]}
|
|
12
|
+
|
|
13
|
+
def Flipflop_loss_config(replicate):
|
|
14
|
+
"""Log10-objective loss for the flip-flop example (Example4/Example5).
|
|
15
|
+
|
|
16
|
+
Both observables are fit in log10 space: the data columns already hold
|
|
17
|
+
log10 values, and the observed_variable expressions log-transform the model
|
|
18
|
+
output (with a floor, since predicted_B is exactly 0 before the dose event
|
|
19
|
+
and log10(0) would otherwise poison the interpolation).
|
|
20
|
+
|
|
21
|
+
The sigma_method/sigma_value entries only shape the *fitting* objective
|
|
22
|
+
(they set the relative weight of the dense, precise logB data against the
|
|
23
|
+
sparse, very noisy logA data). The dNLL diagnostics deliberately ignore
|
|
24
|
+
them: profile/slice/Wald re-estimate each observable's sigma by MLE from
|
|
25
|
+
the residuals at the optimum and freeze it (fixed_sigmas). Comparing the
|
|
26
|
+
resulting profiles against Flipflop_reference.py checks that this
|
|
27
|
+
re-scaling is done correctly — log10 objectives are where an incorrect
|
|
28
|
+
sigma convention is most visible, because the heuristic sigma estimates
|
|
29
|
+
(mean/std of the log data) are 10-20x larger than the actual residual
|
|
30
|
+
scale in dex, which suppresses every dNLL by 2-3 orders of magnitude.
|
|
31
|
+
"""
|
|
32
|
+
label = replicate["Label"]
|
|
33
|
+
observables = [{
|
|
34
|
+
"observed_variable": "np.log10(np.maximum(predicted_B, 1e-12))",
|
|
35
|
+
"data_column": "logB",
|
|
36
|
+
"time_column": "time",
|
|
37
|
+
"data_dict_key": label,
|
|
38
|
+
"sigma_method": "fixed",
|
|
39
|
+
"sigma_value": 0.05,
|
|
40
|
+
}]
|
|
41
|
+
if replicate.get("has_A_data"):
|
|
42
|
+
observables.append({
|
|
43
|
+
"observed_variable": "np.log10(np.maximum(predicted_A, 1e-12))",
|
|
44
|
+
"data_column": "logA",
|
|
45
|
+
"time_column": "time",
|
|
46
|
+
"data_dict_key": f"{label}_A",
|
|
47
|
+
"sigma_method": "fixed",
|
|
48
|
+
"sigma_value": 0.75,
|
|
49
|
+
})
|
|
50
|
+
return {"observables": observables}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# def Example2_loss_config(replicate):
|
|
54
|
+
# label = replicate["Label"]
|
|
55
|
+
# return {"observables": [{
|
|
56
|
+
# "observed_variable": "predicted_A",
|
|
57
|
+
# "data_column": "A",
|
|
58
|
+
# "time_column": "time",
|
|
59
|
+
# "data_dict_key": label,
|
|
60
|
+
# }]}
|
|
61
|
+
|