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
framework/AntimonyGen.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# from Model_Modules.Model_Events import generate_silk_events_from_data
|
|
5
|
+
|
|
6
|
+
from framework.antimony_utils import load_antimony_files
|
|
7
|
+
|
|
8
|
+
def AntimonyGen(MODEL_NAME, repo_root=None):
|
|
9
|
+
if repo_root is None:
|
|
10
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
# Fallback to current directory if not provided
|
|
12
|
+
repo_root = current_dir
|
|
13
|
+
data_path = os.path.join(repo_root, "data")
|
|
14
|
+
plot_path = os.path.normpath(os.path.join(repo_root, "results", MODEL_NAME))
|
|
15
|
+
if not os.path.exists(plot_path):
|
|
16
|
+
os.makedirs(plot_path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
event_block = ''
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
model_text = load_antimony_files(MODEL_NAME, repo_root)
|
|
24
|
+
|
|
25
|
+
if not model_text.strip():
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
"No model content loaded. Generate the model first: python Model_generate.py"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
events_path = os.path.join(repo_root, "generated", MODEL_NAME, MODEL_NAME + "_events.txt")
|
|
31
|
+
|
|
32
|
+
models_path = os.path.join(repo_root, "antimony_models", MODEL_NAME, f"{MODEL_NAME}_InitialConditions.csv")
|
|
33
|
+
|
|
34
|
+
project_root = os.path.join(repo_root, "Projects", MODEL_NAME)
|
|
35
|
+
|
|
36
|
+
paths = {
|
|
37
|
+
"MODEL_NAME": MODEL_NAME,
|
|
38
|
+
"data_path": data_path,
|
|
39
|
+
"plot_path": plot_path,
|
|
40
|
+
"repo_root": repo_root,
|
|
41
|
+
"events_path": events_path,
|
|
42
|
+
"models_path": models_path,
|
|
43
|
+
"project_root": project_root
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return model_text,paths
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convert reaction dictionaries to Antimony format.
|
|
3
|
+
Reactants/products may be list of strings or bracket string (e.g. '[A, B]' or '[A] + [B]').
|
|
4
|
+
Compartment can be explicit per reaction (compartment= / compartment_reverse=) or inferred from species suffix.
|
|
5
|
+
Rate units and volume scaling: see framework.rate_laws.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from framework.models import normalize_species_list, parse_species_list
|
|
13
|
+
|
|
14
|
+
def extract_species_and_parameters_from_reactions(reaction_string):
|
|
15
|
+
"""
|
|
16
|
+
Extract unique species and parameters from reaction string (excluding compartment declarations).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
reaction_string (str): The reaction string to analyze
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
tuple: (species_list, parameters_list, errors_list)
|
|
23
|
+
"""
|
|
24
|
+
# Sets to store unique species and parameters
|
|
25
|
+
species = set()
|
|
26
|
+
parameters = set()
|
|
27
|
+
errors = []
|
|
28
|
+
|
|
29
|
+
# Split into lines and process each line
|
|
30
|
+
lines = reaction_string.split('\n')
|
|
31
|
+
for line_num, line in enumerate(lines, 1):
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if not line: # Skip empty lines
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
# Skip compartment declarations
|
|
37
|
+
if line.startswith('compartment '):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
# Skip species declarations (they start with "substanceOnly species")
|
|
41
|
+
if line.startswith('substanceOnly species'):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Split at semicolon
|
|
45
|
+
parts = line.split(';')
|
|
46
|
+
if len(parts) != 2:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Extract species from left side
|
|
50
|
+
left_side = parts[0].strip()
|
|
51
|
+
# Split by + and -> to get individual species
|
|
52
|
+
species_parts = re.split(r'[+\->]+', left_side)
|
|
53
|
+
for part in species_parts:
|
|
54
|
+
part = part.strip()
|
|
55
|
+
# Remove leading digits and whitespace (e.g., '2 AB40_O12_ISF' -> 'AB40_O12_ISF')
|
|
56
|
+
part = re.sub(r'^\d+\s*', '', part)
|
|
57
|
+
# Skip empty strings and pure numbers
|
|
58
|
+
if part and not part.isdigit():
|
|
59
|
+
# Only add if it contains at least one letter (to avoid lone numbers)
|
|
60
|
+
if re.search(r'[A-Za-z]', part):
|
|
61
|
+
# If species contains a space, only take what is to the right of the space
|
|
62
|
+
if ' ' in part:
|
|
63
|
+
part = part.split(' ', 1)[1] # Split on first space and take right part
|
|
64
|
+
|
|
65
|
+
# Check for malformed species names
|
|
66
|
+
if part.startswith('[') or part.endswith(']') or "'" in part:
|
|
67
|
+
error_msg = f"ERROR: Malformed species name '{part}' in line {line_num}: {line}"
|
|
68
|
+
errors.append(error_msg)
|
|
69
|
+
else:
|
|
70
|
+
species.add(part)
|
|
71
|
+
|
|
72
|
+
# Extract parameters from right side
|
|
73
|
+
right_side = parts[1].strip()
|
|
74
|
+
# Find all words that look like parameters (containing letters, numbers, and underscores)
|
|
75
|
+
param_matches = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', right_side)
|
|
76
|
+
parameters.update(param_matches)
|
|
77
|
+
|
|
78
|
+
# Remove species from parameters
|
|
79
|
+
parameters = parameters - species
|
|
80
|
+
|
|
81
|
+
# # Clean up species list - remove any that look like parameters
|
|
82
|
+
# species = {s for s in species if not re.match(r'^[a-z]', s)}
|
|
83
|
+
|
|
84
|
+
return sorted(list(species)), sorted(list(parameters)), errors
|
|
85
|
+
|
|
86
|
+
def write_list_to_file(items, filename):
|
|
87
|
+
"""Write a list of items to a file, one per line."""
|
|
88
|
+
with open(filename, 'w') as f:
|
|
89
|
+
for item in items:
|
|
90
|
+
f.write(f"{item}\n")
|
|
91
|
+
|
|
92
|
+
def _infer_compartment_from_species(species_name: str) -> str:
|
|
93
|
+
"""Infer compartment from species name suffix (last segment after underscore)."""
|
|
94
|
+
if '_' in species_name:
|
|
95
|
+
return species_name.split('_')[-1]
|
|
96
|
+
return species_name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_species_declarations(species_list, species_compartment_map=None):
|
|
100
|
+
"""
|
|
101
|
+
Generate species declarations in the format 'substanceOnly species X in compartment'.
|
|
102
|
+
Uses explicit species_compartment_map when provided; otherwise infers compartment
|
|
103
|
+
from the last segment after underscore (so compartment names must not contain underscores).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
species_list (list): List of species names
|
|
107
|
+
species_compartment_map (dict, optional): Map species_name -> compartment name
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: Formatted species declarations
|
|
111
|
+
"""
|
|
112
|
+
declarations = []
|
|
113
|
+
for species in species_list:
|
|
114
|
+
compartment = (
|
|
115
|
+
species_compartment_map.get(species)
|
|
116
|
+
if species_compartment_map is not None
|
|
117
|
+
else None
|
|
118
|
+
) or _infer_compartment_from_species(species)
|
|
119
|
+
declarations.append(f"substanceOnly species {species} in {compartment}")
|
|
120
|
+
return '\n'.join(declarations)
|
|
121
|
+
|
|
122
|
+
def collect_unique_compartments_from_reactions(reactions):
|
|
123
|
+
"""
|
|
124
|
+
Collect all unique compartment names and build a species->compartment registry.
|
|
125
|
+
Uses explicit 'compartment' / 'compartment_reverse' on each reaction when present;
|
|
126
|
+
otherwise infers compartment from species name suffix (last segment after '_').
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
reactions (list): List of reaction dictionaries
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
tuple: (unique_compartments set, errors list, species_compartment_map dict)
|
|
133
|
+
"""
|
|
134
|
+
unique_compartments = set()
|
|
135
|
+
errors = []
|
|
136
|
+
species_compartment_map = {}
|
|
137
|
+
|
|
138
|
+
for i, reaction in enumerate(reactions):
|
|
139
|
+
reaction_name = reaction.get("Reaction_name", f"Reaction_{i}")
|
|
140
|
+
explicit_comp = reaction.get("compartment")
|
|
141
|
+
explicit_comp_reverse = reaction.get("compartment_reverse")
|
|
142
|
+
reactants = normalize_species_list(reaction.get("Reactants", ""))
|
|
143
|
+
products = normalize_species_list(reaction.get("Products", ""))
|
|
144
|
+
|
|
145
|
+
for reactant in reactants:
|
|
146
|
+
comp = explicit_comp if explicit_comp is not None else _infer_compartment_from_species(reactant)
|
|
147
|
+
if comp.startswith('[') or comp.endswith(']') or "'" in comp:
|
|
148
|
+
errors.append(
|
|
149
|
+
f"ERROR: Malformed compartment name '{comp}' in reactant '{reactant}' in reaction '{reaction_name}'"
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
unique_compartments.add(comp)
|
|
153
|
+
species_compartment_map[reactant] = comp
|
|
154
|
+
|
|
155
|
+
for product in products:
|
|
156
|
+
comp = explicit_comp_reverse if explicit_comp_reverse is not None else _infer_compartment_from_species(product)
|
|
157
|
+
if comp.startswith('[') or comp.endswith(']') or "'" in comp:
|
|
158
|
+
errors.append(
|
|
159
|
+
f"ERROR: Malformed compartment name '{comp}' in product '{product}' in reaction '{reaction_name}'"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
unique_compartments.add(comp)
|
|
163
|
+
species_compartment_map[product] = comp
|
|
164
|
+
|
|
165
|
+
return unique_compartments, errors, species_compartment_map
|
|
166
|
+
|
|
167
|
+
def _parse_rate_proto(rate_proto, rate_type):
|
|
168
|
+
"""Parse Rate_eqtn_prototype into a list of one or two rate expression strings."""
|
|
169
|
+
if rate_proto is None:
|
|
170
|
+
return []
|
|
171
|
+
if isinstance(rate_proto, list):
|
|
172
|
+
return [str(x).strip() for x in rate_proto if str(x).strip()]
|
|
173
|
+
s = str(rate_proto).strip()
|
|
174
|
+
if not s:
|
|
175
|
+
return []
|
|
176
|
+
if s.startswith('[') and s.endswith(']'):
|
|
177
|
+
return [x.strip() for x in s[1:-1].split(',') if x.strip()]
|
|
178
|
+
return [s]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def generate_single_reaction_from_dict(reaction_dict):
|
|
182
|
+
"""
|
|
183
|
+
Generate a single reaction string from a reaction dictionary.
|
|
184
|
+
Reactants/Products may be list of strings or bracket string (e.g. '[A, B]' or '[A] + [B]').
|
|
185
|
+
Uses explicit 'compartment' / 'compartment_reverse' when present.
|
|
186
|
+
"""
|
|
187
|
+
reactants = normalize_species_list(reaction_dict.get("Reactants", ""))
|
|
188
|
+
products = normalize_species_list(reaction_dict.get("Products", ""))
|
|
189
|
+
rate_proto = reaction_dict.get("Rate_eqtn_prototype")
|
|
190
|
+
rate_type = reaction_dict.get("Rate_type", "")
|
|
191
|
+
|
|
192
|
+
# Explicit compartment from reaction dict; else infer from first species
|
|
193
|
+
compartment = reaction_dict.get("compartment")
|
|
194
|
+
compartment_reverse = reaction_dict.get("compartment_reverse")
|
|
195
|
+
if compartment is None and reactants:
|
|
196
|
+
compartment = _infer_compartment_from_species(reactants[0])
|
|
197
|
+
if compartment and (compartment.startswith('[') or compartment.endswith(']') or "'" in compartment):
|
|
198
|
+
compartment = None
|
|
199
|
+
if compartment_reverse is None and products:
|
|
200
|
+
compartment_reverse = _infer_compartment_from_species(products[0])
|
|
201
|
+
if compartment_reverse and (compartment_reverse.startswith('[') or compartment_reverse.endswith(']') or "'" in compartment_reverse):
|
|
202
|
+
compartment_reverse = None
|
|
203
|
+
|
|
204
|
+
rate_constants = _parse_rate_proto(rate_proto, rate_type)
|
|
205
|
+
reaction_string = ""
|
|
206
|
+
|
|
207
|
+
if rate_type == "RMA":
|
|
208
|
+
# RMA (reversible mass action) needs two rate constants
|
|
209
|
+
if len(rate_constants) < 2:
|
|
210
|
+
raise ValueError(f"RMA reaction '{reaction_dict.get('Reaction_name', 'UNKNOWN')}' requires two rate constants in Rate_eqtn_prototype, got: '{reaction_dict['Rate_eqtn_prototype']}'")
|
|
211
|
+
# Forward
|
|
212
|
+
reactants_fwd_str = " + ".join(reactants)
|
|
213
|
+
products_fwd_str = " + ".join(products)
|
|
214
|
+
if reactants:
|
|
215
|
+
rate_fwd = f"{rate_constants[0]} * {' * '.join(reactants)}"
|
|
216
|
+
else:
|
|
217
|
+
rate_fwd = rate_constants[0] # Zero-order reaction
|
|
218
|
+
# Multiply by compartment volume for MA/RMA/custom_conc_per_time
|
|
219
|
+
if compartment:
|
|
220
|
+
rate_fwd = f"{rate_fwd} * V_{compartment}"
|
|
221
|
+
reaction_string += f"{reactants_fwd_str} -> {products_fwd_str}; {rate_fwd}\n"
|
|
222
|
+
# Reverse
|
|
223
|
+
reactants_rev_str = " + ".join(products)
|
|
224
|
+
products_rev_str = " + ".join(reactants)
|
|
225
|
+
if products:
|
|
226
|
+
rate_rev = f"{rate_constants[1]} * {' * '.join(products)}"
|
|
227
|
+
else:
|
|
228
|
+
rate_rev = rate_constants[1] # Zero-order reaction
|
|
229
|
+
# Multiply by compartment volume for MA/RMA/custom_conc_per_time
|
|
230
|
+
if compartment_reverse:
|
|
231
|
+
rate_rev = f"{rate_rev} * V_{compartment_reverse}"
|
|
232
|
+
reaction_string += f"{reactants_rev_str} -> {products_rev_str}; {rate_rev}\n"
|
|
233
|
+
elif rate_type == "BDF":
|
|
234
|
+
# BDF (bidirectional flow) uses the same rate constant for both directions
|
|
235
|
+
if len(rate_constants) < 1:
|
|
236
|
+
raise ValueError(f"BDF reaction '{reaction_dict.get('Reaction_name', 'UNKNOWN')}' requires at least one rate constant in Rate_eqtn_prototype, got: '{reaction_dict['Rate_eqtn_prototype']}'")
|
|
237
|
+
# Forward
|
|
238
|
+
reactants_fwd_str = " + ".join(reactants)
|
|
239
|
+
products_fwd_str = " + ".join(products)
|
|
240
|
+
if reactants:
|
|
241
|
+
rate_fwd = f"{rate_constants[0]} * {' * '.join(reactants)}"
|
|
242
|
+
else:
|
|
243
|
+
rate_fwd = rate_constants[0] # Zero-order reaction
|
|
244
|
+
reaction_string += f"{reactants_fwd_str} -> {products_fwd_str}; {rate_fwd}\n"
|
|
245
|
+
# Reverse (same rate constant)
|
|
246
|
+
reactants_rev_str = " + ".join(products)
|
|
247
|
+
products_rev_str = " + ".join(reactants)
|
|
248
|
+
if products:
|
|
249
|
+
rate_rev = f"{rate_constants[0]} * {' * '.join(products)}"
|
|
250
|
+
else:
|
|
251
|
+
rate_rev = rate_constants[0] # Zero-order reaction
|
|
252
|
+
reaction_string += f"{reactants_rev_str} -> {products_rev_str}; {rate_rev}\n"
|
|
253
|
+
elif rate_type == "MA" or rate_type == "UDF":
|
|
254
|
+
# UDF is treated the same as MA (unidirectional flow)
|
|
255
|
+
rate0 = (rate_constants[0] if rate_constants else "") or str(rate_proto or "").strip()
|
|
256
|
+
reactants_fwd_str = " + ".join(reactants)
|
|
257
|
+
products_fwd_str = " + ".join(products)
|
|
258
|
+
if reactants:
|
|
259
|
+
rate_fwd = f"{rate0} * {' * '.join(reactants)}"
|
|
260
|
+
else:
|
|
261
|
+
rate_fwd = rate0 # Zero-order reaction
|
|
262
|
+
# Multiply by compartment volume for MA/RMA/custom_conc_per_time
|
|
263
|
+
if rate_type == "MA" and compartment:
|
|
264
|
+
rate_fwd = f"{rate_fwd} * V_{compartment}"
|
|
265
|
+
if rate_type == "MA" and not reactants and compartment_reverse:
|
|
266
|
+
rate_fwd = f"{rate_fwd} * V_{compartment_reverse}"
|
|
267
|
+
reaction_string += f"{reactants_fwd_str} -> {products_fwd_str}; {rate_fwd}\n"
|
|
268
|
+
|
|
269
|
+
elif rate_type == "custom_conc_per_time":
|
|
270
|
+
reactants_side = " + ".join(reactants)
|
|
271
|
+
products_side = " + ".join(products) if products else ""
|
|
272
|
+
rate_eqtn = (rate_constants[0] if rate_constants else "") or str(rate_proto or "").strip()
|
|
273
|
+
if rate_eqtn.startswith('[') and rate_eqtn.endswith(']'):
|
|
274
|
+
rate_eqtn = rate_eqtn.strip('[]')
|
|
275
|
+
|
|
276
|
+
# Multiply by compartment volume for custom_conc_per_time
|
|
277
|
+
if compartment:
|
|
278
|
+
rate_eqtn = f"{rate_eqtn} * V_{compartment}"
|
|
279
|
+
|
|
280
|
+
reaction_string += f"{reactants_side} -> {products_side}; {rate_eqtn}\n"
|
|
281
|
+
|
|
282
|
+
elif rate_type == "custom_amt_per_time":
|
|
283
|
+
reactants_side = " + ".join(reactants)
|
|
284
|
+
products_side = " + ".join(products) if products else ""
|
|
285
|
+
rate_eqtn = (rate_constants[0] if rate_constants else "") or str(rate_proto or "").strip()
|
|
286
|
+
if rate_eqtn.startswith('[') and rate_eqtn.endswith(']'):
|
|
287
|
+
rate_eqtn = rate_eqtn.strip('[]')
|
|
288
|
+
|
|
289
|
+
# No multiplication for custom_amt_per_time
|
|
290
|
+
|
|
291
|
+
reaction_string += f"{reactants_side} -> {products_side}; {rate_eqtn}\n"
|
|
292
|
+
|
|
293
|
+
elif rate_type == "custom":
|
|
294
|
+
reactants_side = " + ".join(reactants)
|
|
295
|
+
products_side = " + ".join(products) if products else ""
|
|
296
|
+
rate_eqtn = (rate_constants[0] if rate_constants else "") or str(rate_proto or "").strip()
|
|
297
|
+
if rate_eqtn.startswith('[') and rate_eqtn.endswith(']'):
|
|
298
|
+
rate_eqtn = rate_eqtn.strip('[]')
|
|
299
|
+
|
|
300
|
+
# Use rate expression as-is, no multiplication by species or volume
|
|
301
|
+
|
|
302
|
+
reaction_string += f"{reactants_side} -> {products_side}; {rate_eqtn}\n"
|
|
303
|
+
|
|
304
|
+
return reaction_string
|
|
305
|
+
|
|
306
|
+
def convert_species_to_concentrations(reaction_string, species_list, species_compartment_map=None):
|
|
307
|
+
"""
|
|
308
|
+
Convert species in rate equations from amounts to concentrations by dividing by compartment volumes.
|
|
309
|
+
Uses explicit species_compartment_map when provided; otherwise infers from species name suffix.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
reaction_string (str): The reaction string with species as amounts
|
|
313
|
+
species_list (list): List of species names
|
|
314
|
+
species_compartment_map (dict, optional): Map species_name -> compartment name
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
str: Modified reaction string with species converted to concentrations in rate equations
|
|
318
|
+
"""
|
|
319
|
+
species_to_compartment = {}
|
|
320
|
+
for species in species_list:
|
|
321
|
+
if species_compartment_map is not None and species in species_compartment_map:
|
|
322
|
+
species_to_compartment[species] = species_compartment_map[species]
|
|
323
|
+
elif '_' in species:
|
|
324
|
+
species_to_compartment[species] = species.split('_')[-1]
|
|
325
|
+
else:
|
|
326
|
+
species_to_compartment[species] = species
|
|
327
|
+
|
|
328
|
+
# Process each line
|
|
329
|
+
lines = reaction_string.split('\n')
|
|
330
|
+
modified_lines = []
|
|
331
|
+
|
|
332
|
+
for line in lines:
|
|
333
|
+
line = line.strip()
|
|
334
|
+
if not line: # Skip empty lines
|
|
335
|
+
modified_lines.append(line)
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
# Skip compartment declarations
|
|
339
|
+
if line.startswith('compartment '):
|
|
340
|
+
modified_lines.append(line)
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Skip species declarations (they start with "substanceOnly species")
|
|
344
|
+
if line.startswith('substanceOnly species'):
|
|
345
|
+
modified_lines.append(line)
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
# Split at semicolon to separate reactants/products from rate
|
|
349
|
+
parts = line.split(';')
|
|
350
|
+
if len(parts) != 2:
|
|
351
|
+
modified_lines.append(line)
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
reactants_products = parts[0].strip()
|
|
355
|
+
rate_equation = parts[1].strip()
|
|
356
|
+
|
|
357
|
+
# Modify the rate equation to convert species to concentrations
|
|
358
|
+
modified_rate = rate_equation
|
|
359
|
+
|
|
360
|
+
# Find all species in the rate equation and replace them with species/V_compartment
|
|
361
|
+
for species in species_list:
|
|
362
|
+
if species in modified_rate:
|
|
363
|
+
compartment = species_to_compartment[species]
|
|
364
|
+
# Use word boundaries to avoid partial matches
|
|
365
|
+
pattern = r'\b' + re.escape(species) + r'\b'
|
|
366
|
+
replacement = f"({species}/V_{compartment})"
|
|
367
|
+
modified_rate = re.sub(pattern, replacement, modified_rate)
|
|
368
|
+
|
|
369
|
+
# Reconstruct the line
|
|
370
|
+
modified_line = f"{reactants_products}; {modified_rate}"
|
|
371
|
+
modified_lines.append(modified_line)
|
|
372
|
+
|
|
373
|
+
return '\n'.join(modified_lines)
|
|
374
|
+
|
|
375
|
+
def add_reaction_names_to_string(reaction_string, reactions):
|
|
376
|
+
"""
|
|
377
|
+
Add reaction names to each reaction line in the format "{reaction_name} : ".
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
reaction_string (str): The reaction string without names
|
|
381
|
+
reactions (list): List of reaction dictionaries with Reaction_name keys
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
str: Reaction string with names prepended
|
|
385
|
+
"""
|
|
386
|
+
lines = reaction_string.split('\n')
|
|
387
|
+
modified_lines = []
|
|
388
|
+
reaction_index = 0
|
|
389
|
+
reaction_line_count = 0 # Track how many lines the current reaction has generated
|
|
390
|
+
|
|
391
|
+
for line in lines:
|
|
392
|
+
line = line.strip()
|
|
393
|
+
if not line: # Keep empty lines as-is
|
|
394
|
+
modified_lines.append(line)
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
# Skip compartment and species declarations
|
|
398
|
+
if line.startswith('compartment ') or line.startswith('substanceOnly species'):
|
|
399
|
+
modified_lines.append(line)
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
# This is a reaction line - add reaction name
|
|
403
|
+
if reaction_index < len(reactions):
|
|
404
|
+
reaction_name = reactions[reaction_index].get("Reaction_name", "UNKNOWN")
|
|
405
|
+
rate_type = reactions[reaction_index].get("Rate_type", "")
|
|
406
|
+
|
|
407
|
+
# Add _fwd and _rev suffixes for BDF and RMA reactions
|
|
408
|
+
if rate_type in ["RMA", "BDF"]:
|
|
409
|
+
if reaction_line_count == 0:
|
|
410
|
+
# First line gets _fwd suffix
|
|
411
|
+
reaction_name_with_suffix = f"{reaction_name}_fwd"
|
|
412
|
+
else:
|
|
413
|
+
# Second line gets _rev suffix
|
|
414
|
+
reaction_name_with_suffix = f"{reaction_name}_rev"
|
|
415
|
+
else:
|
|
416
|
+
reaction_name_with_suffix = reaction_name
|
|
417
|
+
|
|
418
|
+
modified_lines.append(f"{reaction_name_with_suffix} : {line}")
|
|
419
|
+
reaction_line_count += 1
|
|
420
|
+
|
|
421
|
+
# Determine how many lines this reaction should generate
|
|
422
|
+
if rate_type in ["RMA", "BDF"]:
|
|
423
|
+
lines_per_reaction = 2
|
|
424
|
+
else:
|
|
425
|
+
lines_per_reaction = 1
|
|
426
|
+
|
|
427
|
+
# Move to next reaction if we've processed all lines for this one
|
|
428
|
+
if reaction_line_count >= lines_per_reaction:
|
|
429
|
+
reaction_index += 1
|
|
430
|
+
reaction_line_count = 0
|
|
431
|
+
else:
|
|
432
|
+
# Fallback if we run out of reactions
|
|
433
|
+
modified_lines.append(line)
|
|
434
|
+
|
|
435
|
+
return '\n'.join(modified_lines)
|
|
436
|
+
|
|
437
|
+
def read_reactions_from_txt(txt_file_path):
|
|
438
|
+
"""
|
|
439
|
+
Read reactions from a text file where each line is a Python dictionary string.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
txt_file_path (str): Path to the text file
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
list: List of reaction dictionaries
|
|
446
|
+
"""
|
|
447
|
+
reactions = []
|
|
448
|
+
|
|
449
|
+
with open(txt_file_path, 'r') as f:
|
|
450
|
+
for line_num, line in enumerate(f, 1):
|
|
451
|
+
line = line.strip()
|
|
452
|
+
if not line: # Skip empty lines
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
# Convert string representation of dict to actual dict
|
|
457
|
+
reaction_dict = eval(line)
|
|
458
|
+
reactions.append(reaction_dict)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
print(f"Error parsing line {line_num}: {e}")
|
|
461
|
+
print(f"Line content: {line}")
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
return reactions
|
|
465
|
+
|
|
466
|
+
def generate_antimony_from_txt(txt_file_path, name):
|
|
467
|
+
"""
|
|
468
|
+
Generate Antimony script from a text file containing individual reactions.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
txt_file_path (str): Path to the text file
|
|
472
|
+
name (str): Name to use in place of 'Geerts' in filenames
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
str: Generated Antimony script
|
|
476
|
+
"""
|
|
477
|
+
# Read reactions from text file
|
|
478
|
+
reactions = read_reactions_from_txt(txt_file_path)
|
|
479
|
+
|
|
480
|
+
# Collect unique compartments, errors, and explicit species->compartment registry
|
|
481
|
+
unique_compartments, compartment_errors, species_compartment_map = collect_unique_compartments_from_reactions(reactions)
|
|
482
|
+
|
|
483
|
+
# Generate reactions
|
|
484
|
+
reaction_string = ""
|
|
485
|
+
for reaction in reactions:
|
|
486
|
+
reaction_string += generate_single_reaction_from_dict(reaction)
|
|
487
|
+
|
|
488
|
+
# Extract species and parameters from reactions
|
|
489
|
+
species, parameters, species_errors = extract_species_and_parameters_from_reactions(reaction_string)
|
|
490
|
+
|
|
491
|
+
# Add compartment volume parameters to the parameters list
|
|
492
|
+
for compartment in unique_compartments:
|
|
493
|
+
parameters.append(f"V_{compartment}")
|
|
494
|
+
|
|
495
|
+
# Make parameters unique by converting to set and back to sorted list
|
|
496
|
+
parameters = sorted(list(set(parameters)))
|
|
497
|
+
|
|
498
|
+
# Convert species in rate equations to concentrations for antimony solver
|
|
499
|
+
reactions_antimony = convert_species_to_concentrations(reaction_string, species, species_compartment_map)
|
|
500
|
+
|
|
501
|
+
# Add reaction names to the reaction string (after all processing)
|
|
502
|
+
reactions_antimony = add_reaction_names_to_string(reactions_antimony, reactions)
|
|
503
|
+
|
|
504
|
+
# Generate the complete script with compartments, species, and reactions
|
|
505
|
+
complete_script = ""
|
|
506
|
+
|
|
507
|
+
# Add compartment declarations
|
|
508
|
+
for compartment in sorted(unique_compartments):
|
|
509
|
+
complete_script += f"compartment {compartment} = V_{compartment}\n"
|
|
510
|
+
complete_script += "\n" # Add blank line after compartments
|
|
511
|
+
|
|
512
|
+
# Add species declarations (use explicit compartment map when available)
|
|
513
|
+
species_declarations = generate_species_declarations(species, species_compartment_map)
|
|
514
|
+
complete_script += species_declarations
|
|
515
|
+
complete_script += "\n\n" # Add blank lines after species
|
|
516
|
+
|
|
517
|
+
# Add reactions (antimony version with species converted to concentrations in rate equations)
|
|
518
|
+
complete_script += reactions_antimony
|
|
519
|
+
|
|
520
|
+
# Combine all errors
|
|
521
|
+
all_errors = compartment_errors + species_errors
|
|
522
|
+
|
|
523
|
+
return complete_script, species, parameters, unique_compartments, all_errors
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
# Check if name argument is provided
|
|
527
|
+
if len(sys.argv) < 2:
|
|
528
|
+
print("Usage: python txt_to_antimony.py <name>")
|
|
529
|
+
print("Example: python txt_to_antimony.py Smith")
|
|
530
|
+
print("Exiting...")
|
|
531
|
+
exit()
|
|
532
|
+
else:
|
|
533
|
+
name = sys.argv[1]
|
|
534
|
+
rxn_filename = f"generated/{name}/{name}_reaction_dict.txt"
|
|
535
|
+
print(rxn_filename)
|
|
536
|
+
# Generate Antimony script from text file
|
|
537
|
+
complete_script, species, parameters, unique_compartments, errors = generate_antimony_from_txt(rxn_filename, name)
|
|
538
|
+
|
|
539
|
+
# Write complete script to file with name (under model subfolder)
|
|
540
|
+
antimony_models_name_dir = f"antimony_models/{name}"
|
|
541
|
+
antimony_filename = f"{antimony_models_name_dir}/{name}_reactions.txt"
|
|
542
|
+
os.makedirs(antimony_models_name_dir, exist_ok=True)
|
|
543
|
+
with open(antimony_filename, "w") as f:
|
|
544
|
+
f.write(complete_script)
|
|
545
|
+
|
|
546
|
+
# Write unique compartments/species/parameters under generated/name/
|
|
547
|
+
generated_name_dir = f"generated/{name}"
|
|
548
|
+
os.makedirs(generated_name_dir, exist_ok=True)
|
|
549
|
+
compartments_filename = f"{generated_name_dir}/{name}_unique_compartments.txt"
|
|
550
|
+
with open(compartments_filename, "w") as f:
|
|
551
|
+
for compartment in sorted(unique_compartments):
|
|
552
|
+
f.write(f"{compartment}\n")
|
|
553
|
+
|
|
554
|
+
species_filename = f'{generated_name_dir}/{name}_unique_species.txt'
|
|
555
|
+
parameters_filename = f'{generated_name_dir}/{name}_unique_parameters.txt'
|
|
556
|
+
write_list_to_file(species, species_filename)
|
|
557
|
+
write_list_to_file(parameters, parameters_filename)
|
|
558
|
+
|
|
559
|
+
errors_filename = f"{generated_name_dir}/conversion_errors_{name}.log"
|
|
560
|
+
with open(errors_filename, "w") as f:
|
|
561
|
+
for error in errors:
|
|
562
|
+
f.write(f"{error}\n")
|
|
563
|
+
|
|
564
|
+
# Print summary
|
|
565
|
+
print(f"Found {len(unique_compartments)} unique compartments:")
|
|
566
|
+
for compartment in sorted(unique_compartments):
|
|
567
|
+
print(f" - {compartment}")
|
|
568
|
+
print(f"Unique compartments written to '{compartments_filename}'")
|
|
569
|
+
print()
|
|
570
|
+
|
|
571
|
+
print(f"Found {len(species)} unique species and {len(parameters)} unique parameters")
|
|
572
|
+
print(f"Species written to '{species_filename}'")
|
|
573
|
+
print(f"Parameters written to '{parameters_filename}'")
|
|
574
|
+
print()
|
|
575
|
+
|
|
576
|
+
print(f"Antimony script written to '{antimony_filename}'")
|
|
577
|
+
print()
|
|
578
|
+
|
|
579
|
+
# Print errors
|
|
580
|
+
if errors:
|
|
581
|
+
print(f"Found {len(errors)} errors during conversion:")
|
|
582
|
+
print("=" * 50)
|
|
583
|
+
for error in errors:
|
|
584
|
+
print(error)
|
|
585
|
+
print("=" * 50)
|
|
586
|
+
print(f"Errors also written to '{errors_filename}'")
|
|
587
|
+
print()
|
|
588
|
+
else:
|
|
589
|
+
print("No errors found during conversion.")
|
|
590
|
+
print()
|
|
591
|
+
|
|
592
|
+
# print("Generated Antimony script:")
|
|
593
|
+
# print("=" * 50)
|
|
594
|
+
# print(complete_script)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import tellurium as te
|
|
2
|
+
from framework.antimony_utils import archive_antimony_snapshot
|
|
3
|
+
|
|
4
|
+
def TelluriumGen(model_text, paths, settings=None ):
|
|
5
|
+
print("Loading model into Tellurium...")
|
|
6
|
+
try:
|
|
7
|
+
r = te.loada(model_text)
|
|
8
|
+
except Exception as e:
|
|
9
|
+
raise RuntimeError(f"Error loading model: {e}") from e
|
|
10
|
+
|
|
11
|
+
if settings is not None:
|
|
12
|
+
if settings["save_SBML?"]:
|
|
13
|
+
sbml_content = r.getSBML()
|
|
14
|
+
archive_dir = archive_antimony_snapshot(paths["MODEL_NAME"], paths["repo_root"], sbml_content=sbml_content)
|
|
15
|
+
print(f"Archive and SBML written to: {archive_dir}")
|
|
16
|
+
return r
|
framework/__init__.py
ADDED
|
File without changes
|