PyAntiGen 1.0__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 +14 -0
- framework/__init__.py +0 -0
- framework/antimony_utils.py +294 -0
- framework/cli.py +208 -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/Example_generate.py +30 -0
- framework/template/Example/Example_run.py +37 -0
- framework/template/Example/Modules/Data.py +36 -0
- framework/template/Example/Modules/Events.py +14 -0
- framework/template/Example/Modules/Experiment.py +160 -0
- framework/template/Example/Modules/Loss_config.py +12 -0
- framework/template/Example/Modules/Observed_species.py +3 -0
- framework/template/Example/Modules/Optimizer_settings.py +25 -0
- framework/template/Example/Modules/Plots.py +51 -0
- framework/template/Example/Modules/Solver_settings.py +15 -0
- framework/template/Example/Modules/Update_parameters.py +26 -0
- framework/template/data/ADneg.csv +27 -0
- framework/template/data/ADpos.csv +27 -0
- pyantigen-1.0.dist-info/METADATA +106 -0
- pyantigen-1.0.dist-info/RECORD +33 -0
- pyantigen-1.0.dist-info/WHEEL +5 -0
- pyantigen-1.0.dist-info/licenses/LICENSE +21 -0
- pyantigen-1.0.dist-info/top_level.txt +1 -0
framework/pyantigen.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from framework.isotopomer_tools import ensure_isotopes_format
|
|
3
|
+
from framework.model_generation import generate_model
|
|
4
|
+
|
|
5
|
+
class PyAntiGen:
|
|
6
|
+
def __init__(self, name, isotopes=None):
|
|
7
|
+
self.name = name
|
|
8
|
+
self.isotopes = ensure_isotopes_format(isotopes or [''])
|
|
9
|
+
self.reactions = []
|
|
10
|
+
self.rules = []
|
|
11
|
+
self.counter = 0
|
|
12
|
+
|
|
13
|
+
def add_reaction(
|
|
14
|
+
self,
|
|
15
|
+
name,
|
|
16
|
+
reactants,
|
|
17
|
+
products,
|
|
18
|
+
rate_type,
|
|
19
|
+
rate_eqtn,
|
|
20
|
+
compartment=None,
|
|
21
|
+
compartment_reverse=None,
|
|
22
|
+
):
|
|
23
|
+
from framework.reaction_creation import reaction_creation
|
|
24
|
+
self.counter, self.reactions = reaction_creation(
|
|
25
|
+
self.reactions,
|
|
26
|
+
self.counter,
|
|
27
|
+
name,
|
|
28
|
+
reactants,
|
|
29
|
+
products,
|
|
30
|
+
rate_type,
|
|
31
|
+
rate_eqtn,
|
|
32
|
+
compartment=compartment,
|
|
33
|
+
compartment_reverse=compartment_reverse,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def add_rule(self, rule):
|
|
37
|
+
self.rules.append(rule)
|
|
38
|
+
|
|
39
|
+
def generate(self, calling_file_path, model_name=None):
|
|
40
|
+
"""
|
|
41
|
+
Generates the Antimony model. This wraps the existing model_generation logic.
|
|
42
|
+
"""
|
|
43
|
+
if model_name is None:
|
|
44
|
+
model_name = self.name
|
|
45
|
+
|
|
46
|
+
def build_reactions_wrapper(Isotopes):
|
|
47
|
+
# The isotopes passed in here by generate_model are essentially self.isotopes
|
|
48
|
+
# because we formatted them earlier. So we can just return our state.
|
|
49
|
+
return self.reactions, self.rules
|
|
50
|
+
|
|
51
|
+
generate_model(build_reactions_wrapper, self.isotopes, calling_file_path, model_name)
|
framework/rate_laws.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate law semantics and volumetric scaling for PyAntiGen.
|
|
3
|
+
|
|
4
|
+
Each rate type has defined units and explicit volume scaling behavior so that
|
|
5
|
+
generated Antimony has correct [amount/time] ODE terms.
|
|
6
|
+
|
|
7
|
+
Units summary:
|
|
8
|
+
- MA / RMA: User supplies [concentration]^n / time; framework multiplies by V_compartment
|
|
9
|
+
to yield [amount/time]. Single-compartment reactions.
|
|
10
|
+
- UDF / BDF: User supplies [volume/time] (flow rate); framework uses concentration
|
|
11
|
+
of species (no global V multiplier). Trans-compartment transport.
|
|
12
|
+
- custom_conc_per_time: User equation in [concentration]^n/time; framework multiplies
|
|
13
|
+
by V_compartment. Set species_names_are_conc_per_time=True (default).
|
|
14
|
+
- custom_amt_per_time: User equation already in [amount/time]; no volume scaling.
|
|
15
|
+
- custom: Raw expression as-is; user must include volume scaling if needed.
|
|
16
|
+
"""
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RateLaw(ABC):
|
|
22
|
+
"""Base for rate law semantics (units and volume scaling)."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def multiplies_by_volume(self) -> bool:
|
|
27
|
+
"""Whether the framework multiplies the rate expression by compartment volume."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def user_units_description(self) -> str:
|
|
33
|
+
"""Expected units of the user-supplied rate expression."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MassActionLaw(RateLaw):
|
|
38
|
+
"""MA / RMA: [concentration]^n / time; framework multiplies by V_compartment."""
|
|
39
|
+
rate_type = "MA"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def multiplies_by_volume(self) -> bool:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def user_units_description(self) -> str:
|
|
47
|
+
return "[concentration]^n / time → framework multiplies by V_compartment to yield amount/time"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VolumeTransportLaw(RateLaw):
|
|
51
|
+
"""UDF / BDF: [volume/time]; species appear as concentration; no V multiplier on expression."""
|
|
52
|
+
rate_type = "UDF"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def multiplies_by_volume(self) -> bool:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def user_units_description(self) -> str:
|
|
60
|
+
return "[volume/time]; species in equation are concentrations; no V_compartment multiplier"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CustomLaw(RateLaw):
|
|
64
|
+
"""
|
|
65
|
+
custom_conc_per_time / custom_amt_per_time / custom.
|
|
66
|
+
custom_conc_per_time: equation in conc/time → we multiply by V.
|
|
67
|
+
custom_amt_per_time: equation already in amount/time → no scaling.
|
|
68
|
+
custom: as-is; user must include volume scaling if needed.
|
|
69
|
+
"""
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
rate_type: str = "custom",
|
|
73
|
+
species_names_are_conc_per_time: Optional[bool] = None,
|
|
74
|
+
):
|
|
75
|
+
self.rate_type = rate_type
|
|
76
|
+
self._mult_vol = (
|
|
77
|
+
species_names_are_conc_per_time
|
|
78
|
+
if species_names_are_conc_per_time is not None
|
|
79
|
+
else (rate_type == "custom_conc_per_time")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def multiplies_by_volume(self) -> bool:
|
|
84
|
+
return self._mult_vol
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def user_units_description(self) -> str:
|
|
88
|
+
if self.multiplies_by_volume:
|
|
89
|
+
return "[concentration]^n/time → framework multiplies by V_compartment"
|
|
90
|
+
return "Equation already in [amount/time]; no volume scaling applied"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_rate_law_info(rate_type: str) -> str:
|
|
94
|
+
"""Return a short docstring for the given rate type (units and scaling)."""
|
|
95
|
+
if rate_type in ("MA", "RMA"):
|
|
96
|
+
return MassActionLaw(rate_type).user_units_description
|
|
97
|
+
if rate_type in ("UDF", "BDF"):
|
|
98
|
+
return VolumeTransportLaw(rate_type).user_units_description
|
|
99
|
+
if rate_type in ("custom_conc_per_time", "custom_amt_per_time", "custom"):
|
|
100
|
+
return CustomLaw(rate_type).user_units_description
|
|
101
|
+
return "Unknown rate type"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from framework.models import (
|
|
2
|
+
reaction_from_args,
|
|
3
|
+
VALID_RATE_TYPES,
|
|
4
|
+
RATE_TYPES_TWO_CONSTANTS,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def reaction_creation(
|
|
9
|
+
all_reactions,
|
|
10
|
+
counter,
|
|
11
|
+
Reaction_name,
|
|
12
|
+
Reactants,
|
|
13
|
+
Products,
|
|
14
|
+
Rate_type,
|
|
15
|
+
Rate_eqtn_prototype,
|
|
16
|
+
compartment=None,
|
|
17
|
+
compartment_reverse=None,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Create and append a validated reaction to all_reactions.
|
|
21
|
+
Accepts reactants/products as either list of strings or bracket string (e.g. "[A, B]").
|
|
22
|
+
Validates required fields and rate type at call time; raises on invalid input.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
reaction = reaction_from_args(
|
|
26
|
+
name=Reaction_name,
|
|
27
|
+
reactants=Reactants,
|
|
28
|
+
products=Products,
|
|
29
|
+
rate_type=Rate_type,
|
|
30
|
+
rate_eqtn=Rate_eqtn_prototype,
|
|
31
|
+
compartment=compartment,
|
|
32
|
+
compartment_reverse=compartment_reverse,
|
|
33
|
+
)
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
raise ValueError(f"add_reaction validation failed: {e}") from e
|
|
36
|
+
|
|
37
|
+
if reaction.Rate_type in RATE_TYPES_TWO_CONSTANTS:
|
|
38
|
+
counter += 2
|
|
39
|
+
else:
|
|
40
|
+
counter += 1
|
|
41
|
+
|
|
42
|
+
all_reactions.append(reaction.to_dict())
|
|
43
|
+
return counter, all_reactions
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Use location: import Modules from the same folder as this script (model folder)
|
|
5
|
+
_project_dir = Path(__file__).resolve().parent
|
|
6
|
+
if str(_project_dir) not in sys.path:
|
|
7
|
+
sys.path.insert(0, str(_project_dir))
|
|
8
|
+
|
|
9
|
+
MODEL_NAME = _project_dir.name
|
|
10
|
+
|
|
11
|
+
# Add PyAntiGen root to sys.path if running from within the framework template
|
|
12
|
+
if _project_dir.parent.name == "template":
|
|
13
|
+
_pyantigen_root = _project_dir.parents[2]
|
|
14
|
+
if str(_pyantigen_root) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_pyantigen_root))
|
|
16
|
+
|
|
17
|
+
if _project_dir.name == "scripts":
|
|
18
|
+
REPO_ROOT = str(_project_dir.parent)
|
|
19
|
+
else:
|
|
20
|
+
REPO_ROOT = str(_project_dir.parents[1])
|
|
21
|
+
|
|
22
|
+
if REPO_ROOT not in sys.path:
|
|
23
|
+
sys.path.insert(0, REPO_ROOT)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example model builder. Outputs go to antimony_models/Example/ and generated/Example/.
|
|
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
|
|
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
|
+
|
|
18
|
+
print(f"Reactions generated: {model.counter}")
|
|
19
|
+
print(f"Rules generated: {len(model.rules)}")
|
|
20
|
+
|
|
21
|
+
model.generate(__file__, model_name=MODEL_NAME)
|
|
22
|
+
|
|
23
|
+
print("\nModel generated successfully.")
|
|
24
|
+
print("Next steps:")
|
|
25
|
+
print(f" 1. Optionally edit parameters in antimony_models/{MODEL_NAME}/{MODEL_NAME}_parameters.csv")
|
|
26
|
+
print(f" 2. From scripts/{MODEL_NAME}/, run: python {MODEL_NAME}_run.py")
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
Isotopes = ['']
|
|
30
|
+
generate_antimony_model(Isotopes)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from Engine.Model_simulate import setup_simulation
|
|
5
|
+
from Engine.Model_optimize import setup_optimization_from_groups
|
|
6
|
+
from Modules.Plots import *
|
|
7
|
+
from Modules.Experiment import get_experiment
|
|
8
|
+
from Modules.Optimizer_settings import OPTIMIZATION_SETTINGS
|
|
9
|
+
from AntiGen_paths import MODEL_NAME
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
EXPERIMENT_dict = {
|
|
13
|
+
"Example": {'EXPERIMENT': get_experiment('EXPERIMENT_Example'), 'plot': plot_results, 'opt_settings_key': 'Example'},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run_settings = {
|
|
17
|
+
"run_steady_state_first" : True,
|
|
18
|
+
"Verbose" : True,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
print("Run_settings: ", run_settings)
|
|
24
|
+
|
|
25
|
+
run_optimization = True
|
|
26
|
+
run_name = "Example"
|
|
27
|
+
|
|
28
|
+
if run_optimization:
|
|
29
|
+
opt_key = EXPERIMENT_dict[run_name].get('opt_settings_key', run_name)
|
|
30
|
+
experiment_arg = {
|
|
31
|
+
"EXPERIMENT": EXPERIMENT_dict[run_name]['EXPERIMENT'],
|
|
32
|
+
"plot": EXPERIMENT_dict[run_name]['plot'],
|
|
33
|
+
}
|
|
34
|
+
setup_optimization_from_groups(run_settings, OPTIMIZATION_SETTINGS[opt_key], experiment_arg)
|
|
35
|
+
else:
|
|
36
|
+
setup_simulation(run_settings, EXPERIMENT_dict[run_name])
|
|
37
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
|
|
@@ -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,160 @@
|
|
|
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
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Experiment:
|
|
15
|
+
replicates: dict = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def opt_groups(self):
|
|
19
|
+
"""Return {opt_group: [replicate_key, ...]} by scanning replicates."""
|
|
20
|
+
groups = {}
|
|
21
|
+
for key, rep in self.replicates.items():
|
|
22
|
+
og = rep.get("Opt_group")
|
|
23
|
+
if og is not None:
|
|
24
|
+
groups.setdefault(og, []).append(key)
|
|
25
|
+
return groups
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_replicate(config, **meta):
|
|
29
|
+
"""
|
|
30
|
+
Build a single replicate entry dict from an explicit config dict.
|
|
31
|
+
|
|
32
|
+
config keys (all required unless noted):
|
|
33
|
+
"Label" : str
|
|
34
|
+
"Events" : callable(replicate, df_dict) -> str
|
|
35
|
+
"Data" : callable(replicate, data_path) -> Dict of DataFrame or None
|
|
36
|
+
"Observed_species" : callable(RoadRunner_instance or None) -> list[str]
|
|
37
|
+
"Solver_settings" : callable(replicate) -> dict
|
|
38
|
+
"Update_parameters": callable(RoadRunner_instance, replicate, mode) -> dict
|
|
39
|
+
"Loss_config" : callable(replicate) -> dict
|
|
40
|
+
"Opt_group" : str
|
|
41
|
+
|
|
42
|
+
'callable' functions are contained in separate files with the same name as the key.
|
|
43
|
+
For example, "Events" is contained in the file "Events.py", "Data" is contained in
|
|
44
|
+
the file "Data.py", etc.
|
|
45
|
+
|
|
46
|
+
'replicate' is the output of make_replicate. It contains the dictionary above plus
|
|
47
|
+
any optional keyword arguments.
|
|
48
|
+
|
|
49
|
+
Any arguments required by the 'callable' functions should be passed as keyword arguments.
|
|
50
|
+
|
|
51
|
+
Keyword arguments are stored in 'replicate' as additional keys. Example arguments:
|
|
52
|
+
Age = 70, Status = True, Population = "Amyloid_negative", Drug = "Lecanemab",
|
|
53
|
+
dose_nmol = 10, Schedule = "default", Type = "default", …
|
|
54
|
+
|
|
55
|
+
Example::
|
|
56
|
+
|
|
57
|
+
make_replicate(
|
|
58
|
+
{
|
|
59
|
+
"Label": "Example_1",
|
|
60
|
+
"Events": generate_no_events,
|
|
61
|
+
"Data": load_data_Example,
|
|
62
|
+
"Observed_species": observed_Example,
|
|
63
|
+
"Solver_settings": solver_settings_Example,
|
|
64
|
+
"Update_parameters": update_no_parameters,
|
|
65
|
+
"Loss_config": Example_loss_config,
|
|
66
|
+
"Opt_group": opt_group
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
"""
|
|
70
|
+
entry = dict(config)
|
|
71
|
+
return {**entry, **meta}
|
|
72
|
+
|
|
73
|
+
# ***************************************************************************
|
|
74
|
+
# USER DEFINED OPTIMIZATION
|
|
75
|
+
# ***************************************************************************
|
|
76
|
+
|
|
77
|
+
# Build each replicate so that each element described in the
|
|
78
|
+
# make_replicate function has a value.
|
|
79
|
+
# It can be helpful (but not required) to define experimental groups
|
|
80
|
+
# and treatments (conditions) as separate dictionaries, and then
|
|
81
|
+
# build each replicate by combining elements from the experimental groups
|
|
82
|
+
# and treatments. "params" are used to pass arguments to the 'callable' functions.
|
|
83
|
+
# The "opt_group" is a string that defines which replicates are optimized
|
|
84
|
+
# together, each contributing to the total loss function value when their
|
|
85
|
+
# "Opt_group" valuse are the same. For example, if "Opt_group" is "ADneg" for
|
|
86
|
+
# "Early" and "Late", then the loss function will be calculated for both
|
|
87
|
+
# "Early" and "Late" replicates.
|
|
88
|
+
# The "Label" should be unique for each replicate, although not required.
|
|
89
|
+
# The "replicate" designation implies an experimental replicate, but it is rare
|
|
90
|
+
# to optimize at the replicate level. Typically, replicates are handled at
|
|
91
|
+
# the "Data" level, with all replicates' data fit simultaneously.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_experiment():
|
|
96
|
+
# Initialize Experiment class object
|
|
97
|
+
exp = Experiment()
|
|
98
|
+
|
|
99
|
+
# Treatments are typically linked to events, but other settings can also be linked.
|
|
100
|
+
# In the example below, "loss_config" is also linked to the treatment because
|
|
101
|
+
# the data loaded depends on the treatment.
|
|
102
|
+
# Treatment types may also comprise the 'opt_group' in some cases.
|
|
103
|
+
treatments = {
|
|
104
|
+
"Early": {"params": {"dose": 10, "delay": 5}},
|
|
105
|
+
"Late": {"params": {"dose": 5, "delay": 10}},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Experimental groups are not always necessary but are natural for some
|
|
109
|
+
# experimental designs. For example, when studying the effect of a treatment
|
|
110
|
+
# in different populations.
|
|
111
|
+
exp_groups = {
|
|
112
|
+
"ADneg": {"opt_group": "ADneg", "params": {"amyloid_positive": False}},
|
|
113
|
+
"ADpos": {"opt_group": "ADpos", "params": {"amyloid_positive": True}}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for exp_group_name, exp_group_data in exp_groups.items():
|
|
117
|
+
opt_group = exp_group_data.get("opt_group", exp_group_name)
|
|
118
|
+
exp_params = exp_group_data.get("params", {})
|
|
119
|
+
for treatment_name, treatment_data in treatments.items():
|
|
120
|
+
treatment_params = treatment_data.get("params", {})
|
|
121
|
+
|
|
122
|
+
key = f"{exp_group_name}__{treatment_name}"
|
|
123
|
+
|
|
124
|
+
replicate = make_replicate(
|
|
125
|
+
{
|
|
126
|
+
"Label": key,
|
|
127
|
+
"Events": Example_event,
|
|
128
|
+
"Data": load_ad_data,
|
|
129
|
+
"Observed_species": all_species,
|
|
130
|
+
"Solver_settings": solver_settings_Example,
|
|
131
|
+
"Update_parameters": update_Example,
|
|
132
|
+
"Loss_config": Example_loss_config_AD,
|
|
133
|
+
"Opt_group": opt_group,
|
|
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
|
+
# END USER DEFINED EXPERIMENTS
|
|
148
|
+
# ***************************************************************************
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Registry accessors
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def get_experiments():
|
|
155
|
+
"""Returns a dict mapping experiment name -> experiment dict."""
|
|
156
|
+
return {k: v for k, v in globals().items() if k.startswith('EXPERIMENT_')}
|
|
157
|
+
|
|
158
|
+
def get_experiment(name):
|
|
159
|
+
"""Returns an experiment dict by name."""
|
|
160
|
+
return globals().get(name)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def no_optimization(replicate):
|
|
2
|
+
return {}
|
|
3
|
+
|
|
4
|
+
def Example_loss_config_AD(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
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
OPTIMIZATION_SETTINGS = {
|
|
2
|
+
|
|
3
|
+
"Example": {
|
|
4
|
+
"ADneg": {
|
|
5
|
+
"param_names": ["k_A_to_B", "SF"],
|
|
6
|
+
"x0": [0.5, 2.0],
|
|
7
|
+
"bounds": [(0.01, 10.0), (0.01, 10.0)],
|
|
8
|
+
"method": "Nelder-Mead",
|
|
9
|
+
"optimizer_kwargs": {"options": {"maxiter": 500}},
|
|
10
|
+
"wald_analysis": False,
|
|
11
|
+
"slice_analysis": True,
|
|
12
|
+
"profile_likelihood_analysis": False,
|
|
13
|
+
},
|
|
14
|
+
"ADpos": {
|
|
15
|
+
"param_names": ["k_A_to_B", "SF"],
|
|
16
|
+
"x0": [0.5, 2.0],
|
|
17
|
+
"bounds": [(0.01, 10.0), (0.01, 10.0)],
|
|
18
|
+
"method": "Nelder-Mead",
|
|
19
|
+
"optimizer_kwargs": {"options": {"maxiter": 500}},
|
|
20
|
+
"wald_analysis": False,
|
|
21
|
+
"slice_analysis": False,
|
|
22
|
+
"profile_likelihood_analysis": False,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import matplotlib.gridspec as gridspec
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def plot_results(paths, results_dict):
|
|
8
|
+
"""
|
|
9
|
+
Plot simulation results for N experiments.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
plot_path: Directory to save the plot.
|
|
13
|
+
MODEL_NAME: Model name for title/filename.
|
|
14
|
+
results: List of dicts from Experiment.run_all: each has "result", "data", "label".
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
plot_path = paths["plot_path"]
|
|
18
|
+
MODEL_NAME = paths["MODEL_NAME"]
|
|
19
|
+
repo_root = paths["repo_root"]
|
|
20
|
+
n = max(len(results_dict), 1)
|
|
21
|
+
color_A = ["blue","green","cyan","black"]
|
|
22
|
+
color_B = ["red","orange","purple","brown"]
|
|
23
|
+
fig = plt.figure(figsize=(10, 10))
|
|
24
|
+
gs = gridspec.GridSpec(1, 1, figure=fig, hspace=0.5, wspace=0.35)
|
|
25
|
+
ax = fig.add_subplot(gs[0, 0])
|
|
26
|
+
for i, (label, item) in enumerate(results_dict.items()):
|
|
27
|
+
|
|
28
|
+
results = item["results"]
|
|
29
|
+
data_dict = item["data"]
|
|
30
|
+
|
|
31
|
+
data = data_dict[f"{label}"]
|
|
32
|
+
|
|
33
|
+
time_points = results["time"]
|
|
34
|
+
|
|
35
|
+
ax.plot(time_points, results["predicted_A"], label=f"[A] {label}", color=color_A[i])
|
|
36
|
+
ax.plot(time_points, results["predicted_B"], label=f"[B] {label}", color=color_B[i])
|
|
37
|
+
if "time" in data.columns and "B" in data.columns:
|
|
38
|
+
ax.scatter(data["time"], data["B"], color=color_B[i], s=30, zorder=5, label=f"Measured [B] {label}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ax.set_xlabel("Time")
|
|
43
|
+
ax.set_ylabel("Concentration")
|
|
44
|
+
ax.set_title("Simulation Results for " + MODEL_NAME)
|
|
45
|
+
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
|
|
46
|
+
plt.subplots_adjust(right=0.65)
|
|
47
|
+
plot_name = os.path.join(plot_path, MODEL_NAME + ".png")
|
|
48
|
+
plt.savefig(plot_name, bbox_inches="tight")
|
|
49
|
+
print(f"Plot saved to: {plot_name}")
|
|
50
|
+
plt.show()
|
|
51
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
def make_solver_settings(blocks, abs_tol=1e-10, rel_tol=1e-10):
|
|
2
|
+
"""Factory for solver settings dicts. Reduces boilerplate across all figure settings."""
|
|
3
|
+
return {
|
|
4
|
+
'integrator': 'cvode',
|
|
5
|
+
'absolute_tolerance': abs_tol,
|
|
6
|
+
'relative_tolerance': rel_tol,
|
|
7
|
+
'stiff': True,
|
|
8
|
+
'variable_step_size': True,
|
|
9
|
+
'simulation_blocks': blocks,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def solver_settings_Example(replicate):
|
|
13
|
+
return make_solver_settings(
|
|
14
|
+
{'block1': {'start': 0, 'end': 48, 'n_points': 1000, 'abs_tol':1e-12, 'rel_tol':1e-12}}
|
|
15
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Update_parameters.py is used in these cases:
|
|
2
|
+
# 1. The base parameters change from one treatment to another
|
|
3
|
+
# 2. The base parameters change from one replicate to another
|
|
4
|
+
# 3. The base parameters are modified by events during the treatment
|
|
5
|
+
# Update_parameters is called after the parameters are set, but before the simulation is run.
|
|
6
|
+
# The function does not need to return anything, but modifies the parameters in the
|
|
7
|
+
# RoadRunner instance (r).
|
|
8
|
+
# Keyword arguments are passed to the function as meta data.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
#
|
|
12
|
+
# def update_parameters(r, replicate):
|
|
13
|
+
# dose = replicate.get("dose", 10)
|
|
14
|
+
# r.A_Comp1 = dose
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def update_no_parameters(r, replicate, mode):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
def update_Example(r, replicate, mode):
|
|
23
|
+
if mode == "Simulator":
|
|
24
|
+
if replicate["amyloid_positive"]:
|
|
25
|
+
r['k_A_to_B'] = 0.2
|
|
26
|
+
return {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Treatment,time,B1,B2,B3
|
|
2
|
+
Early,0,0.38342206790138894,0.091549388,0.5900498343353935
|
|
3
|
+
Early,4,0.2703454588887822,0.17383910077895837,0.6020884589334081
|
|
4
|
+
Early,8,2.296824911197708,2.487658225541256,2.293891968985253
|
|
5
|
+
Early,12,5.277374209126811,5.601574235385922,5.1235773990747475
|
|
6
|
+
Early,16,6.294924781277804,6.741032603463997,6.207362167461668
|
|
7
|
+
Early,20,7.627622224028859,7.827731764768038,7.637070537276994
|
|
8
|
+
Early,24,8.095154334660107,8.483255473019799,8.632232828229437
|
|
9
|
+
Early,28,8.824407618146214,9.305510261116316,8.776431868004604
|
|
10
|
+
Early,32,9.577892749513929,9.121325348548881,9.481106334
|
|
11
|
+
Early,36,9.803956887789985,8.999973449725445,9.596915791033005
|
|
12
|
+
Early,40,9.972862529,9.945594885,9.674311540321838
|
|
13
|
+
Early,44,9.367666080270844,10.008398237577786,9.880527147349609
|
|
14
|
+
Early,48,9.655247074718085,10.145675796028533,9.929383743361324
|
|
15
|
+
Late,0,0.1791263071636229,0.10003528830134165,0.12159208964547695
|
|
16
|
+
Late,4,0.19491688193363405,0.12378603847264884,0.2462734469696091
|
|
17
|
+
Late,8,0.092482469,0.29851688388349296,0.19202822519921844
|
|
18
|
+
Late,12,0.7697289334950888,0.6124998769902201,1.318366837872463
|
|
19
|
+
Late,16,2.418552127081854,2.4872151677545156,2.5537485789706498
|
|
20
|
+
Late,20,2.5945616441206996,2.6845832675225005,3.1989915955519637
|
|
21
|
+
Late,24,3.5197094831506863,4.049684883564003,3.848379545818031
|
|
22
|
+
Late,28,4.095113399003736,4.132972884332342,3.9449664938773834
|
|
23
|
+
Late,32,4.557037667813649,4.4178217770342565,4.252760815246755
|
|
24
|
+
Late,36,4.498622402962995,4.424129799453062,4.968822520193124
|
|
25
|
+
Late,40,4.796580380353387,4.575674896370977,4.614222306643472
|
|
26
|
+
Late,44,4.9971092743924075,4.492494963763387,5.129134868989082
|
|
27
|
+
Late,48,5.067487553321051,5.042477656094307,4.6794104140856545
|