PyAntiGen 1.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. framework/AntimonyGen.py +48 -0
  2. framework/RxnDict_to_antimony.py +594 -0
  3. framework/TelluriumGen.py +16 -0
  4. framework/__init__.py +0 -0
  5. framework/antimony_utils.py +294 -0
  6. framework/cli.py +229 -0
  7. framework/data_interpolation.py +340 -0
  8. framework/isotopomer_tools.py +41 -0
  9. framework/model_generation.py +46 -0
  10. framework/models.py +189 -0
  11. framework/module_base.py +42 -0
  12. framework/pyantigen.py +51 -0
  13. framework/rate_laws.py +101 -0
  14. framework/reaction_creation.py +43 -0
  15. framework/template/Example/AntiGen_paths.py +23 -0
  16. framework/template/Example/Engine/Anchor_cache.py +193 -0
  17. framework/template/Example/Engine/Deadline.py +535 -0
  18. framework/template/Example/Engine/Evaluator.py +1176 -0
  19. framework/template/Example/Engine/Event_times.py +491 -0
  20. framework/template/Example/Engine/Fast_profile.py +701 -0
  21. framework/template/Example/Engine/Fit_cache.py +329 -0
  22. framework/template/Example/Engine/Identifiability.py +698 -0
  23. framework/template/Example/Engine/Model_optimize.py +1483 -0
  24. framework/template/Example/Engine/Model_simulate.py +124 -0
  25. framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
  26. framework/template/Example/Engine/Optimize.py +6862 -0
  27. framework/template/Example/Engine/Petab_export.py +398 -0
  28. framework/template/Example/Engine/Preequil_cache.py +361 -0
  29. framework/template/Example/Engine/Profile_checkpoint.py +399 -0
  30. framework/template/Example/Engine/Results.py +395 -0
  31. framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
  32. framework/template/Example/Engine/Simulate.py +617 -0
  33. framework/template/Example/Flipflop_reference.py +401 -0
  34. framework/template/Example/Model_generate.py +37 -0
  35. framework/template/Example/Model_run.py +261 -0
  36. framework/template/Example/Modules/Data.py +63 -0
  37. framework/template/Example/Modules/Events.py +14 -0
  38. framework/template/Example/Modules/Experiment.py +194 -0
  39. framework/template/Example/Modules/Loss_config.py +61 -0
  40. framework/template/Example/Modules/Observed_species.py +3 -0
  41. framework/template/Example/Modules/Optimizer_settings.py +258 -0
  42. framework/template/Example/Modules/Plots.py +89 -0
  43. framework/template/Example/Modules/Solver_settings.py +16 -0
  44. framework/template/Example/Modules/Update_opt_parameters.py +24 -0
  45. framework/template/Example/Modules/Update_parameters.py +49 -0
  46. framework/template/data/ADneg.csv +27 -0
  47. framework/template/data/ADpos.csv +27 -0
  48. framework/template/data/Flipflop.csv +29 -0
  49. framework/template/data/make_flipflop_data.py +174 -0
  50. pyantigen-1.0.9.dist-info/METADATA +129 -0
  51. pyantigen-1.0.9.dist-info/RECORD +55 -0
  52. pyantigen-1.0.9.dist-info/WHEEL +5 -0
  53. pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
  54. pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
  55. pyantigen-1.0.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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