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,398 @@
1
+ """Generic PEtab v2 archive exporter.
2
+
3
+ Writes 6 files to ``out_dir``:
4
+ - parameters.tsv parameterId, lowerBound, upperBound, nominalValue, estimate, parameterScale
5
+ - conditions.tsv conditionId, targetId, targetValue
6
+ - experiments.tsv experimentId, time, conditionId
7
+ - observables.tsv observableId, observableFormula, noiseFormula, observableTransformation, noiseDistribution
8
+ - measurements.tsv observableId, experimentId, time, measurement
9
+ - problem.yaml top-level PEtab problem manifest
10
+
11
+ Mapping rules
12
+ -------------
13
+ - Each Experiment.replicates entry maps to one experimentId.
14
+ - Each Antimony event line ``[name:] at (trigger): t = v, t2 = v2`` maps to:
15
+ * one conditionId per unique set of (target, value) assignments
16
+ * one row in experiments.tsv at the evaluated trigger time
17
+ - Each loss_config observable maps to one observableId; the data
18
+ referenced via data_dict_key/data_column/time_column is emitted into
19
+ measurements.tsv.
20
+ - Each parameter in optimization_settings (across all sub-groups) becomes
21
+ a row in parameters.tsv with bounds + optimized value (or x0 fallback).
22
+ """
23
+ import math
24
+ import os
25
+ import re
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+
31
+ PETAB_FORMAT_VERSION = "2.0.0"
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Event-line parser
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _EVENT_RE = re.compile(
39
+ r'^\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*:\s*)?' # optional "name:"
40
+ r'at\s*\(\s*(?P<trigger>.+?)\s*\)\s*:\s*' # at (trigger):
41
+ r'(?P<rhs>.+?)\s*;?\s*$' # assignments, optional trailing ;
42
+ )
43
+
44
+
45
+ def _split_top_level(s, sep):
46
+ """Split *s* on *sep* characters that are not inside parentheses/brackets."""
47
+ out, depth, buf = [], 0, []
48
+ for ch in s:
49
+ if ch in '([{':
50
+ depth += 1
51
+ elif ch in ')]}':
52
+ depth = max(0, depth - 1)
53
+ if ch == sep and depth == 0:
54
+ out.append(''.join(buf))
55
+ buf = []
56
+ else:
57
+ buf.append(ch)
58
+ if buf:
59
+ out.append(''.join(buf))
60
+ return out
61
+
62
+
63
+ def _parse_event_line(line):
64
+ """Parse one Antimony event line; return (time_expr, [(target, value), ...]) or None."""
65
+ m = _EVENT_RE.match(line.strip())
66
+ if not m:
67
+ return None
68
+ trigger = m.group('trigger').strip()
69
+ rhs = m.group('rhs').strip().rstrip(';').strip()
70
+ t_match = re.match(r'time\s*>=\s*(.+)$', trigger)
71
+ time_expr = t_match.group(1).strip() if t_match else trigger
72
+ assigns = []
73
+ for chunk in _split_top_level(rhs, ','):
74
+ chunk = chunk.strip()
75
+ if not chunk or '=' not in chunk:
76
+ continue
77
+ target, value = chunk.split('=', 1)
78
+ assigns.append((target.strip(), value.strip()))
79
+ return time_expr, assigns
80
+
81
+
82
+ def _eval_time_expr(expr, param_values):
83
+ """Evaluate a time-trigger expression numerically. Return float or None."""
84
+ try:
85
+ ns = {"__builtins__": {}, "math": math, "np": np, "pi": math.pi}
86
+ ns.update(param_values)
87
+ return float(eval(expr, ns))
88
+ except Exception:
89
+ return None
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Identifier helpers
94
+ # ---------------------------------------------------------------------------
95
+
96
+ def _sanitize_id(s):
97
+ out = re.sub(r'[^A-Za-z0-9_]', '_', str(s))
98
+ if not out:
99
+ return "_id"
100
+ if not (out[0].isalpha() or out[0] == '_'):
101
+ out = '_' + out
102
+ return out
103
+
104
+
105
+ def _observable_id(obs_cfg):
106
+ obs = obs_cfg["observed_variable"]
107
+ if callable(obs):
108
+ return _sanitize_id(getattr(obs, "__name__", "obs_callable"))
109
+ return _sanitize_id(obs)
110
+
111
+
112
+ def _observable_formula(obs_cfg):
113
+ obs = obs_cfg["observed_variable"]
114
+ if callable(obs):
115
+ return f"callable:{getattr(obs, '__name__', repr(obs))}"
116
+ return str(obs)
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # df_dict resolution (mirrors Engine.Optimize._resolve_obs_df)
121
+ # ---------------------------------------------------------------------------
122
+
123
+ def _resolve_obs_df(df_dict, obs_cfg):
124
+ if not isinstance(df_dict, dict):
125
+ return df_dict if hasattr(df_dict, 'columns') else None
126
+ key = obs_cfg.get("data_dict_key")
127
+ if key is not None:
128
+ return df_dict.get(key)
129
+ d_col = obs_cfg["data_column"]
130
+ t_col = obs_cfg["time_column"]
131
+ return next(
132
+ (v for v in df_dict.values()
133
+ if hasattr(v, 'columns') and d_col in v.columns and t_col in v.columns),
134
+ None,
135
+ )
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Parameter-table assembly
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def _as_list(value):
143
+ """Coerce *value* to a plain Python list. None becomes []."""
144
+ if value is None:
145
+ return []
146
+ return list(value)
147
+
148
+
149
+ def _build_parameter_rows(optimization_settings, group_optimizations):
150
+ """Flatten per-group settings into one parameter table.
151
+
152
+ Per-group sub-dicts and flat optimization_settings are both supported.
153
+ Optimized x from group_optimizations replaces x0 when available.
154
+ A parameter that appears in multiple groups is written once (first seen).
155
+ """
156
+ rows = []
157
+ seen = set()
158
+
159
+ def _add(names, x0s, bounds, optimized_x):
160
+ for i, pn in enumerate(names):
161
+ if pn in seen:
162
+ continue
163
+ seen.add(pn)
164
+ lo, hi = (bounds[i] if i < len(bounds) else (None, None))
165
+ if optimized_x is not None and i < len(optimized_x):
166
+ nominal = float(optimized_x[i])
167
+ elif i < len(x0s):
168
+ nominal = float(x0s[i])
169
+ else:
170
+ nominal = None
171
+ rows.append({
172
+ "parameterId": pn,
173
+ "lowerBound": lo if lo is not None else "",
174
+ "upperBound": hi if hi is not None else "",
175
+ "nominalValue": nominal if nominal is not None else "",
176
+ "estimate": 1,
177
+ "parameterScale": "lin",
178
+ })
179
+
180
+ # Flat mode
181
+ if "param_names" in optimization_settings:
182
+ opt = group_optimizations.get("__flat__") if group_optimizations else None
183
+ opt_x = _as_list(opt.get("x")) if opt is not None else None
184
+ _add(
185
+ _as_list(optimization_settings.get("param_names")),
186
+ _as_list(optimization_settings.get("x0")),
187
+ _as_list(optimization_settings.get("bounds")),
188
+ opt_x,
189
+ )
190
+ return rows
191
+
192
+ # Per-group mode — only emit params from groups that actually ran.
193
+ for group_name, settings in optimization_settings.items():
194
+ if not isinstance(settings, dict) or "param_names" not in settings:
195
+ continue
196
+ opt = group_optimizations.get(group_name) if group_optimizations else None
197
+ if opt is None:
198
+ continue
199
+ _add(
200
+ _as_list(settings.get("param_names")),
201
+ _as_list(settings.get("x0")),
202
+ _as_list(settings.get("bounds")),
203
+ _as_list(opt.get("x")),
204
+ )
205
+ return rows
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Main entry
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def export_petab(
213
+ out_dir,
214
+ model_name,
215
+ experiment,
216
+ optimization_settings,
217
+ group_optimizations,
218
+ data_path,
219
+ model_file_rel="model.antimony",
220
+ ):
221
+ """Write a PEtab v2 archive to *out_dir*.
222
+
223
+ Parameters
224
+ ----------
225
+ out_dir : str
226
+ Destination directory (created if missing).
227
+ model_name : str
228
+ Model identifier used inside problem.yaml.
229
+ experiment : Experiment
230
+ Object with .replicates dict.
231
+ optimization_settings : dict
232
+ Either flat {param_names, x0, bounds, ...} or per-group sub-dicts.
233
+ group_optimizations : dict
234
+ {group_name: opt_result_dict}. For flat settings use the key
235
+ "__flat__". opt_result_dict must have an "x" key with the
236
+ optimized parameter values. Empty dict is allowed (uses x0).
237
+ data_path : str
238
+ Root data directory passed to replicate Data loaders.
239
+ model_file_rel : str
240
+ Relative path to the model file as referenced from problem.yaml.
241
+ """
242
+ os.makedirs(out_dir, exist_ok=True)
243
+
244
+ # Flat name->value lookup for evaluating event-trigger expressions
245
+ param_values = {}
246
+ param_rows = _build_parameter_rows(optimization_settings, group_optimizations)
247
+ for r in param_rows:
248
+ v = r.get("nominalValue")
249
+ if isinstance(v, (int, float)) and not (isinstance(v, float) and math.isnan(v)):
250
+ param_values[r["parameterId"]] = float(v)
251
+
252
+ cond_rows = []
253
+ expt_rows = []
254
+ obs_defs = {}
255
+ meas_rows = []
256
+
257
+ cond_cache = {}
258
+ cond_counter = [0]
259
+
260
+ def _condition_for(assigns):
261
+ key = tuple(sorted(assigns))
262
+ if key in cond_cache:
263
+ return cond_cache[key]
264
+ cond_counter[0] += 1
265
+ cid = f"cond_{cond_counter[0]:03d}"
266
+ cond_cache[key] = cid
267
+ for tgt, val in assigns:
268
+ cond_rows.append({
269
+ "conditionId": cid,
270
+ "targetId": tgt,
271
+ "targetValue": val,
272
+ })
273
+ return cid
274
+
275
+ for rep_key, rep in experiment.replicates.items():
276
+ experiment_id = _sanitize_id(rep_key)
277
+
278
+ try:
279
+ df_dict = rep["Data"](rep, data_path)
280
+ except Exception as exc:
281
+ print(f"[petab] {rep_key}: Data loader failed — {exc}")
282
+ df_dict = {}
283
+
284
+ events_fn = rep.get("Events")
285
+ events_str = ""
286
+ if events_fn is not None:
287
+ try:
288
+ events_str = events_fn(rep, df_dict, r_ic=None)
289
+ except TypeError:
290
+ try:
291
+ events_str = events_fn(rep, df_dict)
292
+ except Exception as exc:
293
+ print(f"[petab] {rep_key}: Events call failed — {exc}")
294
+ except Exception as exc:
295
+ print(f"[petab] {rep_key}: Events call failed — {exc}")
296
+
297
+ for line in (events_str or "").splitlines():
298
+ parsed = _parse_event_line(line)
299
+ if parsed is None:
300
+ continue
301
+ time_expr, assigns = parsed
302
+ if not assigns:
303
+ continue
304
+ t_num = _eval_time_expr(time_expr, param_values)
305
+ cid = _condition_for(assigns)
306
+ expt_rows.append({
307
+ "experimentId": experiment_id,
308
+ "time": t_num if t_num is not None else time_expr,
309
+ "conditionId": cid,
310
+ })
311
+
312
+ lc_fn = rep.get("Loss_config")
313
+ if lc_fn is None:
314
+ continue
315
+ try:
316
+ lc = lc_fn(rep)
317
+ except Exception:
318
+ continue
319
+ observables_cfg = (lc or {}).get("observables", [])
320
+ if not observables_cfg:
321
+ continue
322
+
323
+ for obs_cfg in observables_cfg:
324
+ obs_id = _observable_id(obs_cfg)
325
+ if obs_id not in obs_defs:
326
+ obs_defs[obs_id] = {
327
+ "observableId": obs_id,
328
+ "observableFormula": _observable_formula(obs_cfg),
329
+ "noiseFormula": f"noiseParameter1_{obs_id}",
330
+ "observableTransformation": "lin",
331
+ "noiseDistribution": "normal",
332
+ }
333
+ obs_df = _resolve_obs_df(df_dict, obs_cfg)
334
+ if obs_df is None:
335
+ continue
336
+ d_col = obs_cfg["data_column"]
337
+ t_col = obs_cfg["time_column"]
338
+ if d_col not in obs_df.columns or t_col not in obs_df.columns:
339
+ continue
340
+ t_vals = np.asarray(obs_df[t_col].values)
341
+ m_vals = np.asarray(obs_df[d_col].values)
342
+ for t_val, m_val in zip(t_vals, m_vals):
343
+ try:
344
+ fm = float(m_val)
345
+ ft = float(t_val)
346
+ except (TypeError, ValueError):
347
+ continue
348
+ if not (np.isfinite(fm) and np.isfinite(ft)):
349
+ continue
350
+ meas_rows.append({
351
+ "observableId": obs_id,
352
+ "experimentId": experiment_id,
353
+ "time": ft,
354
+ "measurement": fm,
355
+ })
356
+
357
+ # ---- write tables ----
358
+ pd.DataFrame(param_rows, columns=[
359
+ "parameterId", "lowerBound", "upperBound",
360
+ "nominalValue", "estimate", "parameterScale",
361
+ ]).to_csv(os.path.join(out_dir, "parameters.tsv"), sep='\t', index=False)
362
+
363
+ pd.DataFrame(cond_rows, columns=["conditionId", "targetId", "targetValue"])\
364
+ .to_csv(os.path.join(out_dir, "conditions.tsv"), sep='\t', index=False)
365
+
366
+ pd.DataFrame(expt_rows, columns=["experimentId", "time", "conditionId"])\
367
+ .to_csv(os.path.join(out_dir, "experiments.tsv"), sep='\t', index=False)
368
+
369
+ pd.DataFrame(list(obs_defs.values()), columns=[
370
+ "observableId", "observableFormula", "noiseFormula",
371
+ "observableTransformation", "noiseDistribution",
372
+ ]).to_csv(os.path.join(out_dir, "observables.tsv"), sep='\t', index=False)
373
+
374
+ pd.DataFrame(meas_rows, columns=[
375
+ "observableId", "experimentId", "time", "measurement",
376
+ ]).to_csv(os.path.join(out_dir, "measurements.tsv"), sep='\t', index=False)
377
+
378
+ yaml_lines = [
379
+ f"format_version: {PETAB_FORMAT_VERSION}",
380
+ f"parameter_file: parameters.tsv",
381
+ f"problems:",
382
+ f" - model_files:",
383
+ f" {model_name}:",
384
+ f" location: {model_file_rel}",
385
+ f" language: antimony",
386
+ f" condition_files: [conditions.tsv]",
387
+ f" experiment_files: [experiments.tsv]",
388
+ f" observable_files: [observables.tsv]",
389
+ f" measurement_files: [measurements.tsv]",
390
+ "",
391
+ ]
392
+ with open(os.path.join(out_dir, "problem.yaml"), "w") as f:
393
+ f.write("\n".join(yaml_lines))
394
+
395
+ print(f"[petab] Archive written to {out_dir} "
396
+ f"(conditions={len(cond_rows)}, experiments={len(expt_rows)}, "
397
+ f"observables={len(obs_defs)}, measurements={len(meas_rows)}, "
398
+ f"parameters={len(param_rows)})")