PyAntiGen 1.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- framework/AntimonyGen.py +48 -0
- framework/RxnDict_to_antimony.py +594 -0
- framework/TelluriumGen.py +16 -0
- framework/__init__.py +0 -0
- framework/antimony_utils.py +294 -0
- framework/cli.py +229 -0
- framework/data_interpolation.py +340 -0
- framework/isotopomer_tools.py +41 -0
- framework/model_generation.py +46 -0
- framework/models.py +189 -0
- framework/module_base.py +42 -0
- framework/pyantigen.py +51 -0
- framework/rate_laws.py +101 -0
- framework/reaction_creation.py +43 -0
- framework/template/Example/AntiGen_paths.py +23 -0
- framework/template/Example/Engine/Anchor_cache.py +193 -0
- framework/template/Example/Engine/Deadline.py +535 -0
- framework/template/Example/Engine/Evaluator.py +1176 -0
- framework/template/Example/Engine/Event_times.py +491 -0
- framework/template/Example/Engine/Fast_profile.py +701 -0
- framework/template/Example/Engine/Fit_cache.py +329 -0
- framework/template/Example/Engine/Identifiability.py +698 -0
- framework/template/Example/Engine/Model_optimize.py +1483 -0
- framework/template/Example/Engine/Model_simulate.py +124 -0
- framework/template/Example/Engine/Nuisance_sensitivity.py +298 -0
- framework/template/Example/Engine/Optimize.py +6862 -0
- framework/template/Example/Engine/Petab_export.py +398 -0
- framework/template/Example/Engine/Preequil_cache.py +361 -0
- framework/template/Example/Engine/Profile_checkpoint.py +399 -0
- framework/template/Example/Engine/Results.py +395 -0
- framework/template/Example/Engine/Sensitivity_analysis.py +320 -0
- framework/template/Example/Engine/Simulate.py +617 -0
- framework/template/Example/Flipflop_reference.py +401 -0
- framework/template/Example/Model_generate.py +37 -0
- framework/template/Example/Model_run.py +261 -0
- framework/template/Example/Modules/Data.py +63 -0
- framework/template/Example/Modules/Events.py +14 -0
- framework/template/Example/Modules/Experiment.py +194 -0
- framework/template/Example/Modules/Loss_config.py +61 -0
- framework/template/Example/Modules/Observed_species.py +3 -0
- framework/template/Example/Modules/Optimizer_settings.py +258 -0
- framework/template/Example/Modules/Plots.py +89 -0
- framework/template/Example/Modules/Solver_settings.py +16 -0
- framework/template/Example/Modules/Update_opt_parameters.py +24 -0
- framework/template/Example/Modules/Update_parameters.py +49 -0
- framework/template/data/ADneg.csv +27 -0
- framework/template/data/ADpos.csv +27 -0
- framework/template/data/Flipflop.csv +29 -0
- framework/template/data/make_flipflop_data.py +174 -0
- pyantigen-1.0.9.dist-info/METADATA +129 -0
- pyantigen-1.0.9.dist-info/RECORD +55 -0
- pyantigen-1.0.9.dist-info/WHEEL +5 -0
- pyantigen-1.0.9.dist-info/entry_points.txt +2 -0
- pyantigen-1.0.9.dist-info/licenses/LICENSE +21 -0
- pyantigen-1.0.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)})")
|