PyBNF PEtab v2 interoperability (pybnf.petab)¶
The pybnf.petab package is PyBNF’s adapter to and from the PEtab v2 standard: a PEtab problem (problem.yaml
plus its parameter, observable, measurement, and condition tables) and a native
PyBNF .conf produce the same internal FreeParameter / Prior /
NoiseModel / MeasurementModel objects, so a problem imports, fits, and
exports back without loss (ADR-0004). The tables map one-to-one onto the
submodules below; import and export are the two whole-problem entry points.
This is the module reference; for the task-oriented guide – importing a
problem, exporting a job, linting, and what round-trips – see PEtab interoperability. The
package requires the optional pybnf[petab] extra.
Parameters¶
PEtab v2 parameters table -> FreeParameter (issue #407, Step 1).
This is the first, self-contained step of the PEtab v2 problem importer – the
two-adapter proof the M2 refactor anticipated (ADR-0004): a native .conf
and a PEtab problem should land on the same internal FreeParameter /
Prior objects. If they do, the abstractions are right; where PEtab forces a
special case, we learn where they are wrong.
Two deliberately separated layers (the “neutral seam”):
The TSV reader (
read_parameter_table) – the disposable half: a dependency-freecsvparse ofparameters.tsvintoPetabParameterRowrecords. When the later importer chunks pull in thepetablibrary for the SBML model and theobservableFormulasympy layer, this is swapped forpetab’sparameter_dfreader with no change below.The mapping (
free_parameter_from_row) – the asset: aPetabParameterRow->FreeParameter, driven by the prior-family registry (ADR-0010). It synthesizes the equivalent legacy*_varkeyword and builds theFreeParameterthrough its ordinary constructor, so the importer lands on a bit-identical object to the native config path – the strongest form of the two-adapter proof – rather than a parallel mapping table.
PEtab v2 specifics this encodes (current spec, not the v1 shape):
There is no
parameterScalecolumn (removed in v2); everything is in linear space, and the parameter’s PyBNFScaleis derived from the prior family instead (alog-*prior ->Log10).Priors are
priorDistribution/priorParameters(renamed from v1’sobjectivePrior*); a single prior, used for the objective only.log-normal/log-laplaceparameters are the location/scale of the natural log of the parameter; PyBNF’s log families parameterize in log10, so we convert(mu, sigma) -> (mu/ln10, sigma/ln10). The resulting distribution over theta is identical – PyBNF’s scale lives in the sampling parameterization, so there is no change-of-variables term to add (ADR-0003).The full v2 prior catalog maps (#417): besides uniform / normal / laplace, the five families that were a catalog gap –
cauchy(loc, scale),gamma(shape, scale),exponential(scale),chisquare(dof),rayleigh(scale) – now each have a registered PyBNF prior family (ADR-0010). The parameterizations are verified against petab’s ownv1.distributionsclasses (gamma is shape+scale, not shape+rate; exponential’s parameter is the scale, not the rate). PEtab defines no log- form for these five.Bounds truncate the prior. A Uniform prior truncates exactly (we intersect the box). For an unbounded-support family, two-sided finite bounds map to a truncated prior on a finite reflecting box (ADR-0020,
TruncatedPrior); a one-sided truncation – one finite wall and the other side covering the family’s natural domain (an infinite bound, or a log form’stheta <= 0) – maps to a half-bounded box, a single reflecting wall (ADR-0047, #432). Both directions are theub->inflimit of the two-sided fold; the open side is the family’s natural support endpoint.
Gaps are surfaced as NotImplementedError with clear messages so the boundary
is documented in code, not silent: estimate = false fixed parameters (those
become model constants, handled by a later importer chunk, not here).
- class pybnf.petab.parameters.PetabParameterRow(parameter_id: str, estimate: bool, lower_bound: float | None = None, upper_bound: float | None = None, nominal_value: float | None = None, prior_distribution: str | None = None, prior_parameters: tuple[float, ...] = ())[source]¶
One row of a PEtab v2 parameters table, in PyBNF’s neutral vocabulary.
The dependency-free seam between the (disposable) TSV reader and the (asset) registry-driven mapping: the mapping never depends on how the row was read, so a future
petab-library adoption feeds it by constructing these fromProblem.parameter_dfrecords.lower_bound/upper_bound/nominal_valueareNonewhen the column is absent or blank;prior_distributionisNonefor an estimated parameter with no explicit prior (PEtab v2 defaults that to a uniform over the bounds).prior_parametersis the parsed semicolon-delimited tuple.
- pybnf.petab.parameters.free_parameter_from_row(row)[source]¶
Map one estimated PEtab v2 parameters row to a
FreeParameter.Finite bounds truncating an unbounded-support family map to a bounded
FreeParameter– two finite walls to a two-sided box (ADR-0020), one finite wall plus an open side to a half-bounded box (ADR-0047, #432). The full v2 prior catalog maps (uniform / normal / laplace / cauchy / gamma / exponential / chisquare / rayleigh, #417). RaisesNotImplementedErrorat the remaining PEtab/PyBNF boundary (estimate=falsefixed parameters) andPybnfErrorfor malformed rows (unknown prior type, wrong parameter count, reversed bounds).
- pybnf.petab.parameters.free_parameters_from_file(path)[source]¶
Read
parameters.tsvatpathand map it toFreeParameterobjects.
- pybnf.petab.parameters.free_parameters_from_table(rows)[source]¶
Map the estimated rows of a parameters table to
FreeParameterobjects.estimate=falserows are skipped (they are fixed model constants, not free parameters), so this returns oneFreeParameterper estimated row.
- pybnf.petab.parameters.petab_parameter_row(free_parameter, parameter_id=None)[source]¶
Map a PyBNF
FreeParameterback to aPetabParameterRow.The exact reverse of
free_parameter_from_row(): a native.conffree parameter and a PEtab row land on the same object, so this read backwards is the two-adapter proof in the export direction.parameter_iddefaults to the free parameter’s name – new-era binds a free parameter to its model parameter by id (ADR-0034), so the name is theparameterId; a caller that has resolved the model parameter name authoritatively (the exporter, which renames a fit-and-mutated parameter to its<p>__REFsurrogate) passes it explicitly.Exports the whole proper-prior catalog – the reverse of ADR-0019’s import map:
uniform_var– the bounds are[p1, p2]and nopriorDistributionis written (PEtab v2 defaults an estimated, prior-less parameter to uniform-over-bounds, so the row round-trips).loguniform_var– the same linear bounds, butpriorDistributionis stated (log-uniform; PEtab’s default uniform is linear) withpriorParameters = (p1, p2).normal_var/laplace_var/cauchy_var/gamma_varand the log forms of normal/laplace – a two-parameter prior:priorParametersare(p1, p2)((loc, scale)/(shape, scale)), in natural log for the log families (PyBNF parameterizes in log10, so they are scaled back byln 10).exponential_var/chisquare_var/rayleigh_var– a one-parameter prior:priorParametersis a single(p1,)(the scale, or chisquare’s dof).
A truncated parameter writes its reflecting box as the truncating bounds: two finite walls (two-sided, ADR-0020) or one finite wall with the open side as an explicit infinity (half-bounded, ADR-0047). An unbounded one writes blank bounds. The no-prior
var/logvarpoint-start keywords and the log forms of the five catalog families (no PEtablog-spelling) raiseNotImplementedError– surfaced in code, not mis-exported.
- pybnf.petab.parameters.read_parameter_table(path)[source]¶
Read a PEtab v2
parameters.tsvintoPetabParameterRowrecords.Dependency-free (stdlib
csv). Unknown extra columns (e.g.parameterName) are tolerated and ignored.
- pybnf.petab.parameters.write_parameter_table(rows, path)[source]¶
Write parameter
rowstopathas a PEtab v2parameters.tsv.Always writes
parameterId/estimate/lowerBound/upperBound. The prior columns (priorDistribution/priorParameters) are appended only when some row carries an explicit prior, so a plainuniform_varjob keeps the four-column chunk-1 shape (PEtab v2 defaults a prior-less estimated parameter to uniform-over-bounds). An unbounded location-scale family writes blank bounds;nominalValueis optional in PEtab v2 and omitted while unused.
Observables and noise¶
PEtab v2 observables table (noise half) -> (NoiseModel, SigmaSource)
(issue #407; depends on #410, ADR-0021; scales per ADR-0022).
The observables chunk of the PEtab v2 importer. It mirrors
pybnf.petab.parameters exactly – the same two-adapter proof (ADR-0019): a
native noise_model .conf line and a PEtab observables row land on the
same internal (NoiseModel, SigmaSource) pair where the native surface can
express it. PyBNF’s noise engine (ADR-0021) is richer than PEtab v2’s noise
vocabulary, so PEtab maps onto a subset of it.
Two deliberately separated layers (the “neutral seam”, as in parameters.py):
The TSV reader (
read_observable_table) – the disposable half: a dependency-freecsvparse ofobservables.tsvintoPetabObservableRowrecords. When the laterobservableFormulachunk pulls in thepetablibrary, this is swapped forpetab’sobservable_dfreader with no change below.The mapping (
noise_model_from_row) – the asset: aPetabObservableRow->(NoiseModel, SigmaSource), built through the ordinaryGaussian/Laplaceconstructors.
Scope: the noise half only. The PEtab v2 spec (verified against the current spec, not the v1 shape – see the note below) gives the noise model through two columns:
noiseDistribution– a single column carrying both the distribution family and the scale its noise is additive on. PEtab v2 allows exactlynormal/log-normal/laplace/log-laplace(defaultnormal). PEtab’s log is the natural log, so the log forms map toLN(ADR-0022), notLOG10.noiseFormula– the sigma-source (the noise distribution’s scale parameter): a number ->ConstantSigma, a bare noise-parameter id ->FreeParameterSigma.
The mapping (all with the prediction taken as the distribution’s median – PEtab v2 specifies this for every noise distribution; the location axis, ADR-0011):
noiseDistribution |
PyBNF (NoiseModel) |
|---|---|
|
|
|
|
|
|
|
|
Spec note (PEtab v2 vs v1). PEtab v2 removed the separate
observableTransformation column (lin / log / log10) and folded it
into noiseDistribution as the log- prefixes above; and v2’s log is natural
(base e), with no log10 form. (An earlier draft of this adapter encoded the v1
shape; this is the corrected v2 mapping.)
The two-adapter equivalence is exact for the linear families and ``laplace``,
structural for the natural-log families. laplace matches the native
laplace token exactly (Laplace(LINEAR, MEDIAN)), and normal matches the
native normal token exactly too (Gaussian(LINEAR, MEDIAN)): native normal
now also defaults to MEDIAN (ADR-0031). The natural-log families have
no native ``.conf`` token – the native lognormal token is log10, and the
native surface has no natural-log family – so log-normal / log-laplace are
validated structurally and against the kernels’ analytic NLL rather than a native
config line. That PyBNF can represent natural-log noise the native surface does not
expose is fine: the engine is the superset, the native grammar a convenience.
The noise mapping is complete for PEtab v2: every one of the four
noiseDistribution values maps with no gaps (the Laplace kernel landed in #410,
the LN scale in ADR-0022). The only deferred capability is a non-trivial
noiseFormula expression – the sympy layer where the petab library earns
its keep – surfaced as an explicit NotImplementedError. Malformed rows (an
unknown noiseDistribution spelling – e.g. a future PEtab value – a missing
noiseFormula, a blank observableId) raise PybnfError.
The observableFormula (the model-output expression) and the
observablePlaceholders / noisePlaceholders columns are the deferred
sibling half – a separate, later chunk that adopts the petab sympy layer.
observable_formula is recorded on PetabObservableRow so that chunk
reuses this reader, but the noise asset neither reads nor validates it (real
observableFormula values are non-trivial expressions, so coupling that boundary
here would make the noise asset raise on nearly every real PEtab problem).
- class pybnf.petab.observables.PetabObservableRow(observable_id: str, observable_formula: str | None = None, noise_formula: str | None = None, noise_distribution: str | None = None, observable_placeholders: str | None = None, noise_placeholders: str | None = None)[source]¶
One row of a PEtab v2 observables table, in PyBNF’s neutral vocabulary.
The dependency-free seam between the (disposable) TSV reader and the (asset) mapping: the mapping never depends on how the row was read, so a future
petab-library adoption feeds it by constructing these fromProblem.observable_dfrecords.noise_distributionisNonewhen the column is absent or blank; the mapping applies the PEtab v2 default (normal).observable_formula– the model-output expression – is recorded for the deferredobservableFormulachunk but is not consumed by the noise mapping (this is the noise half only).
- pybnf.petab.observables.noise_model_from_row(row)[source]¶
Map one PEtab v2 observables row’s noise half to an
(NoiseModel, SigmaSource)pair (ADR-0021, ADR-0023).noiseDistributionselects the family and the additive scale together (normal->Gaussian(LINEAR),log-normal->Gaussian(LN),laplace->Laplace(LINEAR),log-laplace->Laplace(LN)); the prediction is the median (PEtab default).noiseFormulabecomes the sigma-source: a number ->ConstantSigma, a bare noise-parameter id ->FreeParameterSigma.Raises
NotImplementedErrorfor a non-trivialnoiseFormulaexpression (the deferred sympy layer) andPybnfErrorfor a malformed row (unknownnoiseDistributionspelling, missingnoiseFormula).
- pybnf.petab.observables.noise_models_from_file(path)[source]¶
Read
observables.tsvatpathand map it to the noise override map.
- pybnf.petab.observables.noise_models_from_table(rows)[source]¶
Map an observables table to a
{observable_id: (NoiseModel, SigmaSource)}override map – exactly theLikelihoodObjective(overrides=...)map the nativenoise_modellines produce (ADR-0021), the two-adapter proof at table level.
- pybnf.petab.observables.petab_observable_row(model_name, kind, noise_distribution, noise_source, observable_formula=None)[source]¶
Map one fitted BNGL column to a
PetabObservableRow(ADR-0025).model_nameis the BNGL observable/function name (an.expcolumn header);kindis'observable'or'function'. TheobservableIdis the prefixed name (obs_<name>/func_<name>).kind == 'measurement'is a conf-declared measurement model (ADR-0036): theobservableIdismodel_nameverbatim (no prefix – it is already a PEtab id) andobservable_formula(the conf formula) is required.observableFormulais the bare model name by default: a BNGL function (including a function of a function) is carried verbatim in the model file and evaluated there, so the table only references it by name and the export is lossless andpetab-free. The exporter’s opt-in inlining mode (ADR-0035) overrides it for a function column by passingobservable_formula(the function body translated to PEtab math), so the emitted problem carries its own measurement model and round-trips against the importer’s synthesis. An observable column is never inlined (an observable is a model species/group, not an algebraic expression), so callers passobservable_formulaonly forkind == 'function'.noise_distributionis the PEtab family the job’s objective maps to (ADR-0023 reversed:gaussian->normal,laplace->laplace).noise_sourceis the PEtab representation of the objective’s sigma source (ADR-0021 reversed), one of:('placeholder', None)– a per-point_SDdata column: a declared noise placeholder (noiseFormula=noisePlaceholders=noiseParameter1_<id>) whose per-point value the measurements’noiseParameterscolumn supplies (chi_sq).('constant', value)– a fixed sigma written inline as a numericnoiseFormulawith no placeholder: afix_atconstant (sos-> 1,sod-> 1) or an observable’s column mean (ave_norm_sos).('formula', expr)– an expression sigma (FormulaSigma, ADR-0044/0045): the PEtab-mathnoiseFormulaover free-parameter ids + constants, emitted verbatim with no placeholder. The inverse of the importer’s expression-noiseFormulaclassification, so a whole-fitFormulaSigmaround-trips.('free_param', id)– a free-parameter (estimated) sigma (FreeParameterSigma, #439): the bare noise-parameteridas thenoiseFormula, no placeholder, declared estimated in the parameter table and an observation-layer nuisance (not a model entity). The importer reads a bare-idnoiseFormulaback to afitsource (ADR-0044), so a per-observable estimated sigma round-trips.('per_measurement', expr)– a row-varying placeholder sigma (PerMeasurementFormulaSigma, ADR-0045): thenoiseFormulaexpression carries a per-measurement placeholder (noiseParameter1_<id>) whose value differs row to row, supplied by the measurements’noiseParameterscolumn. ThenoiseFormulais emitted verbatim and the placeholder(s) it references declared innoisePlaceholders– the inverse of the importer routing a row-varyingnoiseParametersid to the binding table.
- pybnf.petab.observables.read_observable_table(path)[source]¶
Read a PEtab v2
observables.tsvintoPetabObservableRowrecords.Dependency-free (stdlib
csv).noisePlaceholdersis recorded (it marks a named noiseFormula placeholder whose value the measurements’noiseParameterscolumn supplies – a per-observable estimated sigma, ADR-0037). Other unknown extra columns (e.g.observableName,observablePlaceholders) are tolerated and ignored.
- pybnf.petab.observables.write_observable_table(rows, path)[source]¶
Write observable
rowstopathas a PEtab v2observables.tsv.The optional
observablePlaceholderscolumn (a row-varying observable scale’s declared placeholder, ADR-0045) is appended only when some row carries one, so a job with none stays byte-identical to the pre-row-varying output (the byte-equal export round-trip oracle).
Measurements¶
PEtab v2 measurements table, both directions (issue #407; ADR-0025/the importer
read path).
PyBNF stores a dataset as a wide pybnf.data.Data (column 0 the
independent variable, the other columns named after model observables/functions,
optional _SD noise columns); PEtab stores it long (one row per measured
point). This module pivots between the two, on the neutral PetabMeasurementRow
seam shared with the other PEtab tables:
export –
measurement_rows_from_data(wideData-> neutral rows) +write_measurement_table(rows -> TSV, the disposable half).import –
read_measurement_table(TSV -> rows, the disposable half) +data_from_measurement_rows(long rows -> the wideDatareplicates per(experiment, model), the exact inverse ofmeasurement_rows_from_data: it groups rows by(experimentId, modelId)(the model->data link, ADR-0041), deals repeated(observable, time)rows into replicate grids (ADR-0039), pivots long->wide, and rebuilds each observable’s_SDcompanion column from the per-pointnoiseParameters).
Dose-response (ADR-0046). A parameter_scan .exp – whose independent axis (column
0) is a swept parameter, not time – maps the other way: its swept axis lives in the PEtab
conditions/experiments tables (each dose a Condition setting the swept parameter + an
Experiment, measured at a constant time – inf for steady state). measurement_rows_from_data
still refuses such an .exp (export routes it through dose_response_measurement_rows
instead), and reconstruct_dose_responses is the import inverse: it detects those experiment
groups and rebuilds each as a single swept-axis Data.
- class pybnf.petab.measurements.PetabMeasurementRow(observable_id: str, time: float, measurement: float, experiment_id: str = '', model_id: str = '', noise_parameters: float | None = None, noise_parameter_id: str | None = None, observable_parameters: tuple = ())[source]¶
One row of a PEtab v2 measurements table, in PyBNF’s neutral vocabulary.
experiment_idis''for a base time-course with no condition changes (PEtab’s “model as is”);model_idis the optional model->data link (ADR-0041) – themodelIdof the model that produced the row (''for a single-model job, where the column is omitted).noise_parametersis the per-point numeric noise value (the_SDcell) feeding the observable’s single declared noise placeholder, orNonewhen the column carries no number.noise_parameter_idis the alternative: a parameter id in thenoiseParameterscolumn (Boehm’ssd_pSTAT5A_rel) – a PEtab placeholder override that, when constant across an observable’s rows, is a per-observable estimated sigma (ADR-0021/0037); exactly one of the two is set (or neither, for a blank cell).observable_parametersis the semicolon-split tokens of theobservableParameterscolumn (the n-th token bindsobservableParameter${n}_${observableId}in theobservableFormula/noiseFormula). Each token is a number or a parameter id; when the tuple is constant across an observable’s rows it reduces to a per-observable scale/offset (substituted into the formula, ADR-0044), else it is the deferred row-varying frontier.()for a blank/absent cell.
- pybnf.petab.measurements.data_from_measurement_rows(rows, observable_id_to_column, sd_suffix='_SD', indvar='time')[source]¶
Pivot long measurement
rowsback to the wideDatareplicates per(experiment, model)– the inverse ofmeasurement_rows_from_data().observable_id_to_columnmaps a PEtabobservableId(e.g.obs_x) to the model column header it measures (e.g.x); its iteration order fixes the wide column order, so a re-export classifies columns in the same order the original export did (the byte-equal round trip). Rows are grouped by(experimentId, modelId); within a group the sorted-uniquetimevalues become column 0 and each measured observable becomes a value column,NaN-filled where a(time, observable)cell is absent (the forward pivot skipsNaN, so this restores the ragged grid). When any row in the group carries anoiseParametersvalue, each value column gets a<col><sd_suffix>companion rebuilt from those per-point values (the_SDsource achi_sqre-export reads back); a group with nonoiseParameters(a fixed / column-mean sigma objective) gets no_SDcolumns.Why group on ``(experimentId, modelId)`` (ADR-0041): two wildtype experiments on different models both carry
experimentId = ''(PEtab’s “model as is”); the modelId is what distinguishes them. Grouping on the pair keeps them separate without needing synthesized experimentIds; a single-model job carriesmodelId = ''on every row, so its grouping is identical to keying onexperimentIdalone.Replicates. PEtab models replicates as repeated
(experiment, model, observable, time)rows with no replicate index. They are dealt across grids in first-seen order: the k-th occurrence of a cell goes to the k-thData(the first grid is the full one; later grids hold only the cells that repeat). This is the exact inverse of the forward export’sfor data in exp['datas']stacking (ADR-0039), so a homogeneous-grid replicate set re-exports to byte-identical long rows; PyBNF’s summing objective scores the dealt grids exactly as it scored the source.expfiles (the partition PEtab never recorded does not affect the fit). Replicate dealing runs within one(experimentId, modelId)group, so the two groupings compose.Returns
{(experiment_id, model_id): [Data, ...]}– a list of replicate grids per(experiment, model)(length 1 for the common no-replicate case),experiment_idbeing''for the “model as is” base time course andmodel_id''for a single-model job. RaisesPybnfErrorif a row names anobservableIdabsent from the map.
- pybnf.petab.measurements.dose_response_measurement_rows(data, column_to_observable_id, experiment_ids, scan_time, sd_suffix='_SD', model_id='')[source]¶
Pivot a dose-response (swept-axis) wide
Datato long rows.The dual of
measurement_rows_from_data()for a Parameter Scan.expwhose independent axis (column 0) is the swept parameter, not time. Each data row is one measured dose mapped to its own experiment (experiment_ids[i], aligned with thedatarow order), and the measurementtimeis the scan’s fixedscan_time– a scalar,inffor a steady-state scan (PEtab time=inf) or a finitet_end:for a fixed-endpoint scan (ADR-0046), not a data column.column_to_observable_idand the<col><sd_suffix>noise companion behave as in the time-course pivot (sd_suffix=Nonedisables per-point noise); the swept-parameter column 0 is not in the map, so it is never emitted as a measurement.model_idis the optional model->data link (ADR-0041), stamped on every row (''for a single-model job).
- pybnf.petab.measurements.measurement_param_bindings(rows, observable_id_to_column, row_varying_noise=(), row_varying_obs=())[source]¶
Per-experiment per-measurement binding tables for the row-varying observables (ADR-0045) – the source the importer writes to the sidecar TSV.
Returns
{(experiment_id, model_id): {column: {placeholder: {time: token}}}}, collecting both row-varying frontiers into the one sidecar shape:noise (
row_varying_noise) – each measurement row’snoiseParametersid bindsnoiseParameter1_<observable_id>at that row’stime(thePerMeasurementFormulaSigmasource);observable (
row_varying_obs) – the n-thobservableParameterstoken bindsobservableParameter${n}_<observable_id>at that row’stime(thePerMeasurementModelscale/offset).
The table is keyed by the experimental-data column (
observable_id_to_column[oid]– the model entity / materialized measurement-model column the objective compares, what the per-point evaluator looks up at eval), not the PEtab observableId, and grouped by(experiment_id, model_id)to matchdata_from_measurement_rows()so each experiment gets its own sidecar. Two replicate rows at the same(observable, time)share the token (last-wins; a per-replicate-varying token is out of scope). The two placeholder families (noise vs observable) coexist under one column with distinct placeholder keys.
- pybnf.petab.measurements.measurement_rows_from_data(data, column_to_observable_id, experiment_id='', sd_suffix='_SD', model_id='', measurement_params=None)[source]¶
Pivot one experiment’s wide
Datato long measurement rows.column_to_observable_idmaps aDatacolumn header (a model observable/function name, e.g.x) to its PEtabobservableId(e.g.obs_x); only those columns become measurements. For each such column its<col><sd_suffix>companion (if present) supplies the per-pointnoiseParametersvalue.sd_suffixis either a single suffix applied to every column, or a{column: suffix_or_None}map (the per-column form the per-observable noise export needs – ADR-0021/0045): each column reads its own_SDcompanion (or none).None(a column’s suffix, or the whole argument) disables per-point numeric noise (noiseParametersleft blank) – used when the objective’s sigma source is not a data column (a fixed / column-mean / formula sigma carried inline innoiseFormula), so a stray_SDcolumn does not produce anoiseParametersoverride with no placeholder to bind to.model_idis the optional model->data link (ADR-0041): themodelIdof the model the experiment simulates, stamped on every row (''for a single-model job, where the column is omitted on write).NaNcells are skipped (a ragged long table round trips through PyBNF’sNaN-skipping objective). Rows are grouped by observable, then ordered by the independent variable as it appears indata.measurement_params(ADR-0045) is this experiment’s per-measurement binding table{column: {placeholder: {time: token}}}(themeasurement_params:sidecar), the source of a row-varying placeholder’s per-row token. For a column whose sidecar carriesnoiseParameter1_<id>the row’s token becomes itsnoiseParametersid (aPerMeasurementFormulaSigmanoise source); forobservableParameter{n}_<id>the n-th token becomes the row’sobservableParameterstuple (aPerMeasurementModelscale/offset). The default (None/ absent) leaves both blank, so a non-row-varying export is byte-identical.Raises
NotImplementedErrorif the independent variable is nottime(a dose-response /parameter_scan.exp– a later export chunk).
- pybnf.petab.measurements.noise_parameter_ids_by_observable(rows)[source]¶
{observable_id: parameter_id}for observables whosenoiseParametersis a single parameter id constant across all of that observable’s rows (ADR-0037).A constant-per-observable parameter-id placeholder is exactly PyBNF’s native per-observable estimated sigma (
noise_model <obs> = <family>, sigma = fit <id>, ADR-0021): the importer emits one such line per entry. Observables whosenoiseParametersis numeric (per-point_SD) or blank are simply absent from the map.A row-varying id (differing across the observable’s rows) is no longer an error: it routes to the per-measurement binding table (
row_varying_noise_ids()/measurement_param_bindings(), ADR-0045) and is excluded here. An id/numeric mix is still deferred (raises in_classify_noise_ids()).
- pybnf.petab.measurements.observable_parameters_by_observable(rows)[source]¶
{observable_id: (token, ...)}for observables whoseobservableParameterstuple is constant across all of that observable’s rows (ADR-0044) – the sibling ofnoise_parameter_ids_by_observable().A constant-per-observable
observableParametersis a per-observable scale/offset: the n-th token bindsobservableParameter${n}_${observableId}and is substituted into theobservableFormula/noiseFormula(an id stays a free symbol that resolves from the PSet, a number inlines – ADR-0044). Observables with a blankobservableParametersare absent from the map.A row-varying
observableParameters(differing across the observable’s rows) is no longer an error: it routes to the per-measurement binding table (row_varying_observable_ids()/measurement_param_bindings(), ADR-0045) and is excluded here – the observable side of the Phase-2 frontier that Phase 1 deferred.
- pybnf.petab.measurements.read_measurement_table(path)[source]¶
Read a PEtab v2
measurements.tsvintoPetabMeasurementRowrecords.Dependency-free (stdlib
csv), mirroringparameters.read_parameter_table.experimentIdis optional (blank ->'', the base time course);modelIdis the optional model->data link (blank ->'', a single-model job; ADR-0041);timeandmeasurementare required;noiseParametersis the optional per-point_SDvalue (blank ->None). Unknown extra columns are tolerated and ignored.
- pybnf.petab.measurements.reconstruct_dose_responses(measurement_rows, condition_rows, experiment_rows, observable_id_to_column, sd_suffix='_SD')[source]¶
Detect + reconstruct dose-response (parameter_scan) groups – the inverse of the exporter’s
build_dose_response_conditions+dose_response_measurement_rows(ADR-0046).A dose-response is represented in PEtab v2 as N Conditions (each setting one swept parameter to a distinct dose) + N Experiments measured at a constant time –
inffor the steady-state default (the common case), or a finitet_end. This recovers each such group back to a single swept-axisData(column 0 = the swept parameter, its values the doses; the observable columns the per-dose measurements) plus the scan endpoint, the form a new-eraexperiment:(its.expdriving the parameter_scan type inference) re-exports.Detection heuristic. A PEtab experiment is a dose-response point when its condition sets exactly one target to a numeric value (a dose – a surrogate
<p>__REFexpression is not numeric, so a conditioned time course is excluded) and all its measurements share one time. Points group by their experimentId stem (<stem>_<i>->stem), the swept parameter, the scan time, and the modelId. A steady-state (time=inf) point is always a dose-response – steady state has no other meaning in PyBNF. A finite-time point is treated as one only when its experimentId follows the exporter’s<stem>_<i>convention, so a single-timepoint time course (one measured time, a one-target condition, but an ordinary name) is not misread. Within a stem group the swept parameter and scan time must agree – a mixed group raises (ambiguous).Returns
(dose_responses, remaining_rows, consumed_condition_ids, consumed_experiment_ids):dose_responses– a list of{name, model_id, swept_param, scan_time, data}(one per reconstructed scan;scan_timeisinffor steady state);remaining_rows– the measurement rows NOT in any dose-response (the time-course pivot handles them);consumed_condition_ids/consumed_experiment_ids– the condition/experiment ids absorbed into the scans, so the caller drops them from the time-course reconstruction.
- pybnf.petab.measurements.row_varying_noise_ids(rows)[source]¶
The set of
observable_ids whosenoiseParametersparameter id differs across the observable’s rows – the row-varying per-measurement noise frontier bound per data point from the binding table (measurement_param_bindings(), ADR-0045).A constant-per-observable id (
noise_parameter_ids_by_observable()) and a numeric / blank cell are absent. The complement of the constant map over the id-valued observables.
- pybnf.petab.measurements.row_varying_observable_ids(rows)[source]¶
The set of
observable_ids whoseobservableParameterstuple differs across the observable’s rows – the row-varying observable scale/offset frontier bound per data point from the binding table (measurement_param_bindings(), ADR-0045).The observable-side sibling of
row_varying_noise_ids(). A constant-per-observable tuple (observable_parameters_by_observable()) and a blank cell are absent.
- pybnf.petab.measurements.write_measurement_table(rows, path)[source]¶
Write measurement
rowstopathas a PEtab v2measurements.tsv.Two columns are emitted only when some row needs them, so a table that needs neither stays byte-identical to the pre-multi-model / pre-row-varying output (the byte-equal round-trip oracle):
modelId(the model->data link, ADR-0041) – emitted when some row carries a non-emptymodel_id(a multi-model job); a single-model job stamps''.observableParameters(a row-varying observable scale/offset, ADR-0045) – emitted when some row carries anobservable_parameterstuple, its tokens semicolon-joined.
The
noiseParameterscell is the per-row parameter-id token of a row-varying noise placeholder (the sidecar source, ADR-0045) when set, else the numeric_SDvalue (chi_sq), else blank.
Conditions and experiments¶
PEtab v2 conditions/experiments tables, both directions (#422/#423/#407;
ADR-0027/0028/the importer read path).
The two tables that make a PyBNF job’s simulation vary per dataset. A new-era
condition: (a named MutationSet of var op val perturbations) maps onto a PEtab
Condition (targetId/targetValue overrides) referenced by an Experiment (a
period sequence). This module is the neutral seam, mirroring parameters.py /
observables.py: the asset is the neutral rows + the pure op->``targetValue``
mapping + the builders; the disposable half is the TSV readers/writers.
The importer reverse (conditions_from_rows + read_condition_table /
read_experiment_table) inverts build_experiment_conditions(), undoing the
surrogate-base machinery: a <p>__REF base pin (a row whose targetValue is the
surrogate name) is dropped, a relative op in the surrogate (v1__REF * 2) recovers the
fit-parameter perturbation (v1 * 2), a bare-number target recovers an absolute set
(a fixed parameter’s relative op was lossily precomputed on export, so it round-trips as
var = <num> – the same PEtab value either way), and the synthesized cond_wildtype
maps back to a wildtype experiment (no condition:), not a condition: line.
The surrogate-base parameter (the crux, ADR-0027). PEtab forbids one id from
appearing in both the parameter table and a condition target. A PyBNF condition
routinely perturbs a fit parameter, and a relative op on one (v1*2) can’t be
precomputed (the base is the estimated value). So a fit-and-perturbed parameter v1 is
split: the estimated quantity is renamed to a surrogate v1__REF (which lives only
in the parameter table), while the model name v1 becomes a pure condition target. The
__REF marker is a double-underscore suffix, mirroring PyBNF’s own __FREE is-fit
marker, so it can never clash with a user-defined model name.
The exporter reads the new-era surface (ADR-0028): build_experiment_conditions()
transcribes named condition:/experiment: lines, and build_dose_response_conditions()
maps a dose-response (parameter_scan) experiment to one Condition per dose + an Experiment
measured at the scan time (inf => steady state, ADR-0046) – both live export paths.
- pybnf.petab.conditions.CONDITION_ID_PREFIX = 'cond_'¶
The
conditionIdprefix the exporter wraps every condition name in (cond_<name>), and the synthesized base condition for wildtype experiments.
- class pybnf.petab.conditions.PetabConditionRow(condition_id: str, target_id: str, target_value: str)[source]¶
One row of a PEtab v2 conditions table: a single entity override.
target_valueis a ready-to-write string – a bare number (an absolute set or a precomputed relative op on a fixed target) or a sympy-parseable expression in a surrogate parameter (a relative op on a fit target, e.g.v1__REF * 2).
- class pybnf.petab.conditions.PetabExperimentRow(experiment_id: str, time: float, condition_id: str)[source]¶
One period of a PEtab v2 experiments table.
In chunk 2 every experiment is a single period applied at
time=0(a Mutant or a dose sets initial conditions; measurements then occur at their own times). The surrogate split makes this the periodCheckInitialChangeSymbolsinspects.
- pybnf.petab.conditions.REF_MARKER = '__REF'¶
The surrogate-base marker (a double-underscore suffix, like PyBNF’s
__FREE).
- pybnf.petab.conditions.build_dose_response_conditions(stem, swept_param, dose_values, scan_time)[source]¶
Build the conditions/experiments for a dose-response Parameter Scan.
Each measured dose (a
.expcolumn-0 cell) becomes its own Condition setting the swept parameter and a single-period Experiment attime=0(the dose is an initial condition; the measurement occurs later, atscan_time). Returns(condition_rows, experiment_rows, experiment_ids)whereexperiment_ids[i]is the experimentId for dose rowi(for tagging that row’s measurements).
- pybnf.petab.conditions.build_experiment_conditions(experiments, conditions, fit_params, nominal_of, extra_surrogate=frozenset({}))[source]¶
Build conditions/experiments for a new-era job (ADR-0028 Chunk 5b).
Generalizes
build_mutant_conditions()from “base + mutants each carrying their own data” to “named conditions + named experiments that reference them” – the new era decouples a Condition from the Experiment that applies it (acondition:is named once; Nexperiment:records may reference it, so a shared condition emits its rows once).experimentsis a list of(experiment_name, condition_name_or_None)in declaration order.conditionsmaps a condition name to its perturbations[(var, op, val), ...](vala float).fit_paramsis the set of model-parameter names that are fit;nominal_of(var)returns a fixed parameter’s numeric nominal (orNonefor an expression/unknown).Returns
(condition_rows, experiment_rows, surrogate_params, experiment_to_id):surrogate_params(the setM) – fit parameters perturbed by some referenced condition (an unused condition contributes nothing), unioned withextra_surrogate(fit params perturbed by a pre-equilibration condition in the same job –build_preequilibration_conditions()’s contribution, threaded in by the orchestrator soMstays problem-global across both experiment shapes). They are renamed to<p>__REFin the parameter table and pinned in every experiment’s Condition:Mis problem-global, because the model name<p>becomes a pure condition target, so every simulation must re-supply it (the surrogate-base machinery, ADR-0027). A param that only a pre-equilibration condition perturbs is thus still re-pinned (base value) in every time-course/wildtype Condition here.condition_rows– each referenced condition’s targets emitted once (conditionIdcond_<name>): a fit target’s relative op is symbolic in its surrogate (v1__REF * 2), a fixed target’s relative op is precomputed; plus a base pinp = p__REFfor eachp in Mthe condition does not itself set. Plus a shared synthesized base conditioncond_wildtype(pinning all ofM) whenMis non-empty and some experiment is wildtype.experiment_to_id–{experiment_name: experimentId}: the name for a conditioned experiment, or for a wildtype one whenMis non-empty;''(“model as is”) for a wildtype experiment whenMis empty (the chunk-1 base behaviour preserved, so a condition-free job needs no experiments table).
- pybnf.petab.conditions.build_preequilibration_conditions(experiments, conditions, nominal_of, surrogate=frozenset({}), existing_condition_ids=frozenset({}))[source]¶
Build the conditions/experiments for new-era pre-equilibration experiments (ADR-0052, #441 Phase 2 + #443 Phase 2.x) – the multi-period structural sibling of
build_experiment_conditions().A pre-equilibration experiment maps to a PEtab v2 two-period Experiment (ADR-0052’s bidirectional rule): a leading
time = -infperiod under the pre-equilibration condition (equilibrate to steady state, unmeasured) followed by atime = 0period under the measurement condition (the data grid is measured there).experimentsis a list of(name, preequil_cond, measurement_cond_or_None);conditionsmaps a condition name to its perturbations[(var, op, val), ...];nominal_of(var)returns a fixed parameter’s numeric nominal (the fit-vs-fixed split a target needs is carried bysurrogate, below – a target inMis a surrogate-handled fit param, the rest are fixed).surrogateis the problem-globalM– the full fit-and-perturbed set, already split to<p>__REF, including any param a pre-equilibration condition itself perturbs (the orchestrator threads the pre-equilibration contribution intoMviabuild_experiment_conditions()’sextra_surrogate, so both builders share oneM). The shared_condition_rows_for()re-pins all ofMon every period’s condition, so a fit-parameter perturbation in a pre-equilibration period composes correctly (#443): the perturbing period emits the surrogate op (k = k__REF * 2/ an absolutek = 0.5) and every other period re-pins the base value (k = k__REF).existing_condition_idsis the set ofconditionIdvaluesbuild_experiment_conditions()already emitted (its time-course conditions plus the synthesizedcond_wildtypebase when present); a condition shared between a time course and a pre-equilibration experiment is emitted once, and the wash-out base condition is reused rather than re-emitted.Returns
(condition_rows, experiment_rows, experiment_to_id).experiment_to_id[name] = name(a pre-equilibration experiment always has an experiments table – two periods – so it is never the empty-id “model as is” case).A wash-out (no measurement condition) measures at the model default: an empty
conditionIdon thetime = 0period whenMis empty, else the synthesized base conditionWILDTYPE_CONDITION_ID(re-pinning every removed fit param at its base value – the same basebuild_experiment_conditions()pins for a wildtype time course, emitted once and shared). The importer maps that base back to “nocondition:” (a wash-out), so the round trip is preserved.
- pybnf.petab.conditions.condition_name_from_id(condition_id)[source]¶
The new-era
condition:name for a PEtabconditionId, orNone.Nonefor the synthesizedWILDTYPE_CONDITION_ID(which maps back to a wildtype experiment with nocondition:) and for an absent/blank id. Otherwise thecond_prefix is stripped (an externally-authored id without the prefix passes through unchanged, defensively).
- pybnf.petab.conditions.conditions_from_rows(condition_rows, surrogate_params)[source]¶
Invert
build_experiment_conditions()’ condition rows to new-era perturbations{condition_name: [(var, op, val), ...]}.surrogate_paramsis the set of model-parameter names that are fit-and-perturbed (the<p>__REFsurrogates, recovered from the parameter table by the orchestrator). Rows of the synthesized wildtype base are skipped; base pins (a row whosetargetValueis exactly a surrogate name) are dropped as machinery; the rest map to(var, op, val)perturbations (see_perturbation_from_row()). Declaration order within a condition is preserved (the wide<->long byte-equal round trip).
- pybnf.petab.conditions.mutation_target_value(op, val, *, nominal=None, surrogate=None)[source]¶
Map one PyBNF mutation
<op> <val>to a PEtabtargetValuestring.An absolute set (
=) is the bare number, regardless of target kind. A relative op (* / + -) needs the base value:Fit target – pass
surrogate(the<p>__REFsymbol): the result is a symbolic expression in it (v1__REF * 2), whose free symbol is the parameter-table surrogate (CheckInitialChangeSymbols-clean).Fixed target – pass
nominal(the model’s numeric value): the result is the relative op precomputed to a bare number. A relative op withnominal is None(an expression-RHS / unknown nominal) raisesNotImplementedError– evaluating a BNGL expression tree is simulation-grade work, out of scope (ADR-0026 precedent).
- pybnf.petab.conditions.read_condition_table(path)[source]¶
Read a PEtab v2
conditions.tsvintoPetabConditionRowrecords (stdlibcsv;targetValuekept as the raw string).
- pybnf.petab.conditions.read_experiment_table(path)[source]¶
Read a PEtab v2
experiments.tsvintoPetabExperimentRowrecords (stdlibcsv;timecoerced to float).
- pybnf.petab.conditions.surrogate_name(model_param)[source]¶
The surrogate-base parameter id for a fit-and-mutated model parameter.
Measurement formulas¶
PEtab-math translation for the observableFormula layer (issue #407, ADR-0035/0036).
Two production directions, both over petab’s sympy-backed math grammar
(petab.v2.math) – PEtab math is a specified grammar, so we translate via its parsed
tree rather than a hand-rolled string tokenizer (ADR-0033 warned precisely against the
string approach – operator precedence, the ^ power operator, and the
ln/log10/sqrt spellings are where it would silently go wrong):
bngl_body_to_petab_math()– a BNGL function body -> a PEtab math expression (the exporter’s opt-in inlining mode, which generates the round-trip oracle). The hard semantic part (precedence,^,sqrt) is written once and guarded by a numeric self-check (_assert_round_trips()).compile_petab_formula()– a PEtab math expression -> a vectorizednumpycallable (the measurement-model observation layer, ADR-0036): the formula is evaluated post-simulation over the output trajectory + the PSet, never by editing a model file. This is the direction SBML import and the retrofitted BNGL-expression path turn on; it superseded ADR-0035’s synthesis into the model file (thePEtab-math -> BNGL function bodyprinter the importer once used to inject abegin functionsentry).
petab/sympy is the optional runtime extra pybnf[petab] – imported lazily,
only on these expression paths. The bare-name observableFormula common case never
reaches this module and stays dependency-free + simulator-free (ADR-0019); an expression
import with petab absent raises a clear “install pybnf[petab]” error, not an
ImportError.
MVP scope (ADR-0035/0036). Arithmetic over existing model entities (BNGL parameters /
observables / functions; SBML species / parameters) – the surface a measurement model needs
(Boehm’s quotient of sums is the worked fixture). A free symbol that is not a model entity is
an error; a PEtab observableParameter*/noiseParameter* per-measurement placeholder is
the deferred frontier and raises pointing here.
- pybnf.petab.formula.bngl_body_to_petab_math(body, entities)[source]¶
Translate a BNGL function
bodyto a PEtab math expression string.The exporter’s inlining mode (ADR-0035): a fitted function column emits its body as
observableFormulainstead of the bare name. Every free symbol is validated against the model namespace (parameters u observables u functions), then the parsed tree is serialized by our own precedence-safe PEtab printer (_petab_printer_cls()) so the emitted formula is math the PEtab oracle accepts and re-parses to itself. A final round-trip self-check (_assert_round_trips()) refuses to emit any string that does not parse back to the same expression – a wrong observableFormula is worse than a refused one (ADR-0035). A BNGLfunc()reference to another global function is rewritten to a bare symbol first (PEtab math has no user zero-arg functions); the function set is closed and known, so this is a bounded rename, not a tokenizer.Raises
PybnfErroron a missingpetabextra, an unknown free symbol, an unparseable body, or a body that does not survive the serialize/re-parse round trip;NotImplementedErroron a per-measurement placeholder symbol.
- pybnf.petab.formula.compile_objective_expression(formula, free_params, *, data_columns=(), backend='numpy')[source]¶
Compile a bring-your-own objective
expressionto(callable, ordered_names)(ADR-0050; the JAX backend is ADR-0059 item 2; data columns are the data-binding follow-up).The fourth direction of the translator: the user writes a closed-form negative log-likelihood (or cost) as PEtab math over the declared free parameters –
objective = expression+expression = 0.5*((1 - x1)^2 + 100*(x2 - x1^2)^2)– with no model file (the inline analytical objective the #425 epic asks for). The sympy backend is identical tocompile_petab_formula(); the only differences are the validation namespace and the wording: every free symbol must be a declared free parameter (so the expression binds by name to the PSet, ADR-0050 §4 – the bind-by-name footgun fix the named-target slice deferred), not a model entity, and an unknown symbol is reported in those terms (naming it). PEtab math uses^for exponentiation (not**); a**surfaces as a clear parse error here.free_paramsis the set/iterable of declared free-parameter names.data_columnsis the set/iterable of experimental-data column names also allowed as symbols when the objective binds measurements (data = curve.exp; the data-binding follow-up): a data-bound expression is a per-observation NLL contribution over the parameters and the row’s data columns ((y - vmax*x/(km+x))^2references the free parametersvmax/kmand the data columnsx/y), which the model evaluates per row and sums (ExpressionModel.execute). With no bound datadata_columnsis empty and the contract is unchanged.Returns the callable and the sorted free-symbol names it expects positionally (
callable(*[binding[name] for name in ordered_names]), where a parameter name binds to a scalar and a data-column name to that column’s array); a declared parameter the expression does not reference is simply absent from the list (a likelihood flat in that direction).backendselects the lambdify target:'numpy'(the default) feeds the gradient-freescore-column path (de/am/dream);'jax'produces a JAX-traceable callable sojob_type = hmccanjax.gradthe user’s expression (ADR-0059 item 2). The parse, symbol validation, andordered_namesare backend-independent (one sympy expression, one sorted free-symbol set), so the numpy and JAX callables bind their arguments identically – the bind-by-name contract holds across both, exactly as the prior families’ numpy/JAX logpdfs agree by construction.Raises
PybnfErroron a missingpetabextra, an unparseable expression, or a free symbol that is neither a declared free parameter nor a bound data column.
- pybnf.petab.formula.compile_petab_formula(formula, allowed_symbols, *, detail=None)[source]¶
Compile a PEtab math
observableFormulato(numpy_callable, ordered_names).The third direction of the translator (ADR-0036, the measurement-model observation layer): instead of serializing to a model-language body, the parsed PEtab expression is turned into a vectorized
numpycallable so the layer can evaluate the measurement model post-simulation over the output trajectory’s columns + the PSet – no model-file edit, identical for BNGL and SBML and every backend.allowed_symbolsis the model’s expression namespace (BNGLParamList, or SBML species u parameters; ADR-0026/0036);detailoverrides the unknown-symbol error’s namespace listing.Returns the callable and the sorted free-symbol name list it expects positionally (
callable(*[binding[name] for name in ordered_names])); the caller binds each name to a trajectory column or a scalar. Reusessympify_petab+ the shared free-symbol validation, thenlambdifys with thenumpybackend.Raises
PybnfErroron a missingpetabextra, an unparseable formula, or an unknown free symbol;NotImplementedErroron a per-measurement placeholder symbol.
- pybnf.petab.formula.compile_petab_formula_derivatives(formula, allowed_symbols, *, detail=None)[source]¶
Compile the partial derivatives of a PEtab
observableFormula->({name: d_callable}, ordered_names)(#453/#385, the gradient sibling ofcompile_petab_formula()).A per-measurement measurement model (ADR-0045) is a general PEtab formula over sim-output columns + per-row scale/offset placeholders, applied in
_prediction; its contribution to∂pred/∂θis the symbolic gradient of that formula chained through each referenced column’s sensitivity and any estimated placeholder/parameter it names.sympy.diffgives the partials exactly (the formula is already a parsedsympytree), so the chain rule is the exact derivative of the same callablecompile_petab_formula()builds – not a finite difference.Returns
derivs– a{free_symbol_name: numpy_callable}map, one entry per free symbol of the formula – and the sortedordered_namesthe value callable expects. Every derivative callable takes the same positional arguments in the same ``ordered_names`` order ascompile_petab_formula()’s value callable, so the caller binds one argument list once and evaluatesfand every∂f/∂symbolfrom it (a derivative that does not depend on a given symbol simply ignores that slot). Same parse + symbol validation as the value compiler (so an unknown symbol is the same error and a placeholder is admitted identically).Raises
PybnfErroron a missingpetabextra, an unparseable formula, or an unknown free symbol;NotImplementedErroron a per-measurement placeholder symbol the value compiler would also reject (it does not, for a registeredPerMeasurementModel).
- pybnf.petab.formula.formula_free_symbols(formula)[source]¶
The sorted free-symbol names of a PEtab math
formula– the parameters aFormulaSigma(or a measurement model) reads from the PSet (ADR-0044).A parse only (no namespace validation, no
lambdify): used to derive the free parameters an expressionnoiseFormularequires the fit to declare, before the callable is compiled. RaisesPybnfErroron a missingpetabextra or an unparseable formula.
- pybnf.petab.formula.inline_assignment_rules(formula, rules, *, observable_id=None)[source]¶
Inline the SBML assignment-rule variables a measurement
observableFormulareferences down to the species/parameters their rules are defined over (#465, ADR-0036).An SBML
assignmentRule variable="X"makesXan algebraic function of other model entities, recomputed every step – never a simulation-output column and value-less, so the measurement layer cannot resolveXas a symbol (#464). But its RHS is resolvable, and SBML authors define exactly these convenience observables (the D2DEpo_cells := Epo_EpoRi + dEpoi), soobservable: Epo_cells, formula: Epo_cellsshould just work. This substitutes each referenced rule variable with its rule’s RHS – recursively, so a rule defined over another rule resolves through (SBML allows forward references; resolution is by dependency, not document order) – leaving a formula over only species/parameters that the existingcompile_petab_formula()+ the chain-rule gradient (MeasurementModel.prediction_sensitivity()) handle unchanged: the rule’s species sensitivities flow in automatically.rulesmaps every assignment-rule target id to its RHS as a PEtab-math infix string (the stdlib_sbmlscanner’s serialization), or toNonewhen the rule’s MathML used a construct the scanner could not translate. A formula that references no rule variable is returned verbatim (the common case never reaches sympy, so it stays byte-stable). The substitution + serialization go throughsympy(never a string tokenizer – ADR-0033), guarded by the same numeric round-trip self-check as the exporter.Raises
PybnfErroron a missingpetabextra, an unparseable formula/RHS, a reference to a rule whose MathML was not translatable (NoneRHS), a cyclic rule dependency, or a substitution that fails its round-trip self-check.
- pybnf.petab.formula.inline_constants(formula, constants)[source]¶
Substitute fixed-parameter constants into a PEtab math
observableFormula.A PEtab
observableFormulamay reference a fixed parameter that lives only in the PEtab parameters table, not in the model file (Boehm’sspecC17 = 0.107– a species-activity constant absent from the SBML; ADR-0037). Such a symbol resolves against neither the model namespace nor the simulation trajectory, so the measurement layer cannot evaluate it. Because it is fixed, substituting its numeric value is exact: the measurement model then references only model entities + estimated parameters and the model file stays unedited (ADR-0036).constantsis the{name: value}map of fixed PEtab parameters not present as model entities (a fixed parameter that is a model entity stays a symbol – it resolves as a model constant). Substitution + serialization go throughsympy(never a string tokenizer – ADR-0033), guarded by the same numeric round-trip self-check as the exporter. If no constant is a free symbol offormulathe original string is returned verbatim (the bare-name / model-only common case never reachessympy, so the demo round trip is byte-stable).Raises
PybnfErroron a missingpetabextra, an unparseable formula, or a substitution that fails its round-trip self-check.
- pybnf.petab.formula.substitute_placeholders(formula, substitutions)[source]¶
Substitute per-measurement placeholder symbols into a PEtab math
formula(ADR-0044).The sibling of
inline_constants(), for the constant-per-observable placeholder reduction: a PEtabobservableParameter${n}_${id}/noiseParameter${n}_${id}placeholder whose measurements-table value is constant across an observable’s rows is not per-measurement at all – it is a single scalar for that observable. Substituting it reduces the placeholder to the existing per-observable machinery (the ADR-0036 measurement layer for anobservableFormula; a sigma source for anoiseFormula).substitutionsmaps a placeholder name to its token: a number is substituted as a constant (sp.Float); a parameter id is substituted as a free symbol (sp.Symbol), which resolves from the PSet at eval time (a nuisance free parameter, ADR-0034). Substitution + serialization go throughsympy(never a string tokenizer – ADR-0033), guarded by the same numeric round-trip self-check as the exporter. A formula with no substituted placeholder (the bare-name / no-placeholder common case) is returned verbatim (it never reachessympy). A placeholder not insubstitutionsis left in place (the caller validates it downstream).Raises
PybnfErroron a missingpetabextra, an unparseable formula, or a substitution that fails its round-trip self-check.
BNGL model loader¶
A PEtab Model adapter for BNGL, registered into petab at runtime (ADR-0026).
petab 0.8.x ships only sbml/pysb model loaders, so petablint cannot
load a language: bngl problem – it dies at model-load before any table check.
BnglModel is the missing loader: a petab.v1.models.model.Model
backed by PyBNF’s own stdlib BNGL reader (pybnf.petab._bngl), so the PEtab v2
exporter’s emitted problems validate at model level (the ~5 model-cross checks
ADR-0025 had to exclude), with no BNG2.pl needed for the enumeration.
This is Step A of issue #420 – a local reference implementation. register_bngl()
teaches a running petab about BNGL by rebinding petab.v2.core.model_factory
(petab’s factory is a hardcoded if/elif with no plugin hook). Step B upstreams
the same class to PEtab-dev/libpetab-python as a peer of PySBModel; once that
lands, register_bngl() collapses to a no-op.
Validation needs only parsing, so every ABC method is backed by the parsed entity
sets – except BnglModel.is_valid(), which shells out to BNG2.pl --check (the
real validator) when a BNG2.pl is locatable, and degrades gracefully to True when
it is not. This module imports petab and is therefore not imported by
pybnf.petab.__init__; it is reached only from the test tier / an explicit
register_bngl() call, keeping core dependency-free (ADR-0025).
- class pybnf.petab.bngl_model.BnglModel(entities, model_id, path=None)[source]¶
PEtab
Modelwrapper for a BNGL model (validation-grade; no simulation).- static from_file(filepath_or_buffer, model_id=None, base_path=None)[source]¶
Load the model from the given path/URL
- Parameters:
filepath_or_buffer – Absolute or relative path/URL to the model file. If relative, it is interpreted relative to base_path, if given.
base_path – Base path for relative paths in the model file.
model_id – Model ID
- Returns:
A
Modelinstance holding the given model
- get_free_parameter_ids_with_values()[source]¶
Get free model parameters along with their values
- Returns:
Iterator over tuples of (parameter_id, parameter_value)
- get_parameter_ids()[source]¶
Get all parameter IDs from this model
- Returns:
Iterator over model parameter IDs
- get_parameter_value(id_)[source]¶
Get a parameter value
- Parameters:
id – ID of the parameter whose value is to be returned
- Raises:
ValueError – If no parameter with the given ID exists
- Returns:
The value of the given parameter as specified in the model
- get_valid_ids_for_condition_table()[source]¶
Get IDs of all model entities that are allowed to occur as columns in the PEtab conditions table.
- Returns:
Iterator over model entity IDs
- get_valid_parameters_for_parameter_table()[source]¶
Get IDs of all parameters that are allowed to occur in the PEtab parameters table
- Returns:
Iterator over parameter IDs
- has_entity_with_id(entity_id)[source]¶
Any declared identifier: the full BNGL model-entity namespace.
- is_state_variable(id_)[source]¶
A species. At validation grade, only the concrete
seed speciesare known (the full set is a network-generation product, out of scope; ADR-0026).
- is_valid()[source]¶
BNG2.pl --check(real validation, no network gen) when locatable, elseTrue– never a false failure where no BNG backend is available.
- pybnf.petab.bngl_model.MODEL_TYPE_BNGL = 'bngl'¶
BNGL model type, as used in a PEtab v2 yaml file as
language.
- pybnf.petab.bngl_model.register_bngl()[source]¶
Teach a running
petabto loadlanguage: bnglmodels viaBnglModel.Idempotent and additive: rebinds
petab.v2.core.model_factoryto routebngltoBnglModeland delegate every other language to the captured original (sosbml/pysbare untouched), and registers'bngl'as a known model type. A guarded permanent rebind (no teardown) – the temporary local stand-in for Step B’s upstream loader (ADR-0026).
Importing a problem¶
PEtab v2 problem importer: a BNGL-native PEtab v2 problem -> a new-era PyBNF job (issue #407; the importer read path, ADR-0025 reversed / ADR-0032).
The inverse of pybnf.petab.export.export_job(). Given a problem.yaml + its TSV
tables + a BNGL model, import_job() writes a runnable new-era (edition 2) .conf
plus the .exp data files and a verbatim copy of the model (new-era binds free
parameters by id, ADR-0034, so the model needs no re-instrumentation) – the form the
exporter reads. It closes the “two-adapter proof” at the read level for BNGL-native
problems: the reverse asset mappers (parameters/observables/measurements/conditions) run
backwards onto the shared neutral rows, and this module is the disposable orchestrator
that ties them together (problem.yaml reader + .conf/.exp writers).
PEtab is a problem spec; PyBNF is a job spec (the run-recipe gap). A PEtab problem
fixes the objective landscape (model, data, conditions, parameters + priors, the noise
model) but deliberately says nothing about how to search it – no optimizer/sampler, no
algorithm settings, no simulation method, no seed – because PEtab is a cross-tool
exchange format and the method belongs to the tool. So import = PEtab problem +
a supplied run-recipe. The problem half is recovered exactly (and round-trips
byte-for-byte through a re-export); the recipe half – job_type + that fit’s
algorithm settings (SEARCH), the per-experiment method: (SIMULATION), and
output_dir / verbosity / required keys (PLUMBING) – is supplied, not
recovered, and is excluded from the round-trip identity. The recipe is not a new
language: each group is an existing PyBNF/ADR-0028 surface, and its defaults come from the
registry/schema, never a parallel table here.
Concretely, the recipe is supplied through import_job()’s parameters:
job_type– the SEARCH method token ('de'by default).'all'enumerates the fit-type registry (everyoptimizer+sampler; thecheckchecker excluded) and writes one runnableimported_<jt>.confper method, the existing benchmark-harness pattern (ADR-0012): the importer covers the whole toolbox and stays correct as it grows. Sampler-vs-optimizer is a genuine scientific choice (a sampler treats the priors as Bayesian priors), so the importer must not pick for the user.method/method_overrides– the SIMULATION method, emitted on everyexperiment:line ('ode'by default;method_overrides={exp: method}sets per-experiment values). It is per-experiment, never a single global knob: a job can have multiple models/experiments each simulated differently, and the method is not derivable from data (deterministic and stochastic models yield identically-shaped traces). Round-trip is lossy here – export dropsmethod(no PEtab home), import defaults it toode– so a stochastic model does not survive a PEtab hop.settings– overrides for the required algorithm/run settings (population_size/max_iterations/verbosity); the per-method schema defaults the rest.
Dependency-free + simulator-free on the bare-name path. Like the other read-path
chunks, the import path uses only stdlib + pybnf.data.Data + the asset mappers, so the
bare-name common case runs in the bngsim-less CI tier. problem.yaml is hand-parsed (the
exporter emits a fixed, simple shape). The petab library is the test-only oracle for the
bare-name path, and the optional pybnf[petab] extra for an expression observableFormula.
Scope (read path: BNGL and SBML, one or many models). Both model languages import
(ADR-0036): the model file is carried verbatim for each, and an expression
observableFormula becomes a first-class measurement model – a PEtab math expression
evaluated as a post-simulation transform over the output trajectory (the observation layer),
emitted as an observable: <id>, formula: <expr> conf line – never by editing the model
file (the begin functions synthesis of ADR-0035 is superseded). The bare-name common case
still needs no translator and stays dependency-free. SBML observables are 100% expressions, so
SBML import pulls in the pybnf[petab] extra. A multi-model problem imports too (ADR-0041):
each model_files entry is carried verbatim and declared with its own model: line, an
expression observableFormula validates against the union of every model’s namespace, and each
experiment’s model is recovered from the modelId on its measurement rows (emitted as a
per-experiment model: field; a BNGL + SBML mix is fine). A constant-per-observable
observableParameters scale/offset and an expression noiseFormula import too (ADR-0044):
the placeholder is substituted into the observable/noise formula (an id resolves from the PSet,
a number inlines), an expression noiseFormula becoming a FormulaSigma (noise_model
<obs> = <family>, <param> = formula <expr>). A dose-response (parameter_scan) problem
imports too (ADR-0046): N Conditions each setting one swept parameter at a constant measurement
time (inf => steady state, or a finite t_end) are reconstructed into a single swept-axis
.exp (column 0 the swept parameter) + a parameter_scan experiment: (the inverse of
the exporter’s dose-response emission – reconstruct_dose_responses). Out of scope, each
mirroring an export-side boundary: a condition-table sympy layer; the five PEtab prior families
PyBNF lacks; a dose-response that also carries a named condition or row-varying per-measurement
placeholders. (One-sided truncation now maps to a half-bounded box – ADR-0047, #432.)
- class pybnf.petab.import_.ImportedExperiment(name, condition, preequilibrate, data_files, model_location, measparams_file, t_end)¶
- condition¶
Alias for field number 1
- data_files¶
Alias for field number 3
- measparams_file¶
Alias for field number 5
- model_location¶
Alias for field number 4
- name¶
Alias for field number 0
- preequilibrate¶
Alias for field number 2
- t_end¶
Alias for field number 6
- pybnf.petab.import_.import_job(problem_yaml_path, out_dir, job_type='de', method='ode', method_overrides=None, settings=None)[source]¶
Import the BNGL-native PEtab v2 problem at
problem_yaml_pathintoout_dir.Reads the problem’s tables + model, reconstructs the experiments’ data, and writes a new-era PyBNF job: the
.expdata files, a verbatim copy of the BNGL model (new-era binds free parameters by id, so the model needs no re-instrumentation – ADR-0034), and one or more.conffiles. The problem (parameters/priors, observables/noise, measurements, conditions/experiments) is recovered exactly; the run-recipe (job_type,method,settings) is supplied by the caller (see the module docstring). Returns theout_dirpath.job_typeis the SEARCH method token, or'all'to emit oneimported_<jt>.confper registered optimizer + sampler.method(default'ode') is the per-experiment SIMULATION method;method_overrides(a{experiment_name: method}map) sets per-experiment values.settingsoverrides the required algorithm/run settings.Both BNGL and SBML models import (ADR-0036): the model file is carried verbatim, and an expression
observableFormula(e.g. a quotient of sums) becomes a conf measurement model (observable: <id>, formula: <expr>) evaluated post-simulation – the optionalpybnf[petab]extra. A constant-per-observableobservableParametersscale/offset and an expressionnoiseFormulaare substituted/reduced and import too (ADR-0044). A dose-response (parameter_scan) problem – N conditions each setting one swept parameter at a constant measurement time (inf=> steady state, ADR-0046) – is reconstructed into a single swept-axis.exp+ aparameter_scanexperiment. RaisesNotImplementedErrorat the remaining PEtab/PyBNF boundaries (a model language other thanbngl/sbml; the five unsupported prior families; a log-normal/log-laplace noise distribution; a condition expression; a row-varying per-measurementobservableParameters/noiseParametersplaceholder; replicate rows) andPybnfErrorfor a malformed problem (anobservableFormulasymbol that is not a model entity, or an ambiguous dose-response group).
- pybnf.petab.import_.read_problem_yaml(path)[source]¶
Hand-parse the minimal
problem.yamlshapewrite_problem_yaml()emits.Returns a dict with the table-file lists (
parameter_files/observable_files/measurement_files/condition_files/experiment_files) and amodelslist – one{model_id, location, language}entry permodel_filesentry, in declaration order (one or many, ADR-0041). For single-model convenience the first model is also surfaced asmodel_file/model_id/model_language. Dependency-free (no YAML library): the writer emits a flatkey:+ `` - item`` list shape and a two-levelmodel_filesblock, which a small indentation-aware scan reads exactly. The scan is order-independent, so a real v2problem.yamlthat listsmodel_filesfirst (our writer emits it last) reads identically.This is a pure reader: it records each model’s
languagebut does not enforce a policy on it. The supported-language scope (BNGL or SBML, ADR-0036) is enforced by the importer (_require_supported_model()), not here.
Exporting a job¶
PEtab v2 exporter: a PyBNF/BNGL job -> PEtab v2 artifacts (#407/#423; ADR-0025/27/28).
The exporter-first direction of the PEtab interop (ADR-0025): a working PyBNF
BNGL job and a native .conf are read and serialized to a PEtab v2 problem
(parameters.tsv / observables.tsv / measurements.tsv / conditions.tsv /
experiments.tsv / problem.yaml + a PEtab-clean copy of the model), rather than
generating BNGL from a declarative PEtab spec (the harder importer direction, deferred).
The reverse asset mappings live beside their importer twins
(parameters.petab_parameter_row, observables.petab_observable_row,
measurements.measurement_rows_from_data, conditions.build_*); this module is the
disposable glue: it reads the job (the stdlib ploop config parser, a focused BNGL
block reader, and pybnf.data.Data for the .exp) and writes the files.
Why a function is the measurement model. A fitted .exp column matches a BNGL
observable or a function (PyBNF forces print_functions=>1), and the
function is usually the measurement model. So an observable column exports to
observableFormula = <name> and a function column to observableFormula =
<name> too – always the bare model name, with the function carried verbatim in
the model file (ADR-0025). PEtab ids are prefixed (obs_/func_ for
observables, the unprefixed model name for parameters) to keep the PEtab-id namespace
disjoint from the model-entity namespace.
New-era only (ADR-0028/0031): export is transcription. PEtab v2 interop is a new-era
feature, and the exporter reads only the new-era surface – both the objective
(objective / noise_model, never the retired objfunc, no implicit default;
ADR-0031) and the data linkage (model: / experiment: / data: /
condition: / observable:; ADR-0028). An experiment: is a PEtab Experiment
(experimentId = the experiment name) carrying its data: replicates as measurement
rows; a condition: is a PEtab Condition; an observable: renames a data column
before classification. export_job refuses a legacy (edition 1) job
(_require_modern_edition()) and a legacy data linkage – model = X : Y.exp /
mutant / param_scan (_require_new_era_data()) – rather than reverse-mapping
it. The gate is on the exporter alone; the fitter still runs legacy confs unchanged.
Conditions/experiments (ADR-0027/0028). A condition: referenced by an experiment
becomes a PEtab Condition/Experiment via the surrogate-base <p>__REF rename of a
fit-and-perturbed parameter (see pybnf.petab.conditions,
build_experiment_conditions()); a shared condition emits its
rows once. A dose-response (parameter scan) experiment takes the dual shape (ADR-0046):
each dose becomes a Condition setting the swept parameter + an Experiment, measured at the
scan time (inf for the steady-state default => PEtab time=inf, or a finite t_end:).
The objective and prior surfaces map to PEtab as far as PEtab v2 can express them
(ADR-0023/0031 reversed): the Gaussian/Laplace likelihoods with a _SD-column,
fixed, or column-mean sigma (chi_sq/sos/sod/ave_norm_sos), and the
uniform/log-uniform/normal/laplace prior families with their log forms.
A BNGL (.bngl) or SBML (.xml) model is exported in its own native language
(ADR-0040): a BNGL model PEtab-cleaned, an SBML model carried byte-verbatim with its
observables emitted as observableFormula expressions from the conf measurement-model
layer (the mirror of the ADR-0036 import). A job may declare more than one model
(ADR-0041): each experiment: names the model it simulates, that model’s id is stamped
on the experiment’s measurement rows (the modelId link), free parameters bind across
the union of every model’s ids, and problem.yaml lists every model in its own language
(BNGL + SBML may mix). Everything else raises NotImplementedError (the boundary is in
code, not silent): an objective PEtab cannot represent (neg_bin* – removed from v2;
lognormal – log10 vs PEtab natural log; a free-parameter or relative sigma;
direct_pass/kl/wasserstein); the no-prior var/logvar; a .con/.prop
Constraint; an Antimony (.ant) model. The
oracle is petab’s full default_validation_tasks via Problem.from_yaml + the native
BnglModel loader (ADR-0026), wired into the tests; see ADR-0025/0027/0028/0036/0040.
- pybnf.petab.export.clean_model_for_petab(text)[source]¶
Return a PEtab-clean copy of a BNGL model: the
begin actionsblock stripped.New-era BNGL binds free parameters by id (ADR-0034), so the source model already carries bare parameter ids with real nominal values – exactly what PEtab estimates. “PEtab-clean” therefore collapses to dropping the
begin actionsblock (PEtab drives simulation via the measurement times / experiments, not the model’s own actions); the reaction network and thebegin functionsblock – which carry the measurement model – are carried verbatim. A fit-and-mutated parameter keeps its model name (v1) here as a plain nominal-valued parameter (always overridden by its Condition); only the parameter table carries the surrogatev1__REF(ADR-0027).A legacy
<name>__FREEmarker in the model code is rejected: new-era binds by id, so a model still carrying one was not modernized, and shipping it would dangle an undefinedv1__FREEsymbol in PEtab. The error names the bind-by-id contract rather than letting the PEtab oracle reject it opaquely. The scan ignores#line comments – a comment may legitimately mention the retiredKD1__FREEform as a counter-example (receptor_v2.bngldoes), which is documentation, not a dangling binding.
- pybnf.petab.export.export_job(conf_path, out_dir, inline_functions=False)[source]¶
Export the PyBNF job at
conf_pathto a PEtab v2 problem inout_dir.Reads the job’s data/conditions/observables from the new-era surface (ADR-0028): a
model:declaration, namedexperiment:lines carrying theirdata:files (a PEtab Experiment; multiple files = replicates),condition:perturbations (a PEtab Condition), andobservable:column-header overrides. Writesparameters.tsv,observables.tsv,measurements.tsv, a PEtab-clean copy of the BNGL model,problem.yaml, and – when some experiment applies a condition –conditions.tsv/experiments.tsv. Returns theout_dirpath.inline_functions(defaultFalse) is the opt-in expression mode (ADR-0035): when set, a fitted function column emits its body as anobservableFormulaexpression (translated to PEtab math, requires thepybnf[petab]extra) instead of the bare model name, producing a model-portable problem and the round-trip oracle the importer’s synthesis is graded against. The default stays bare-name, lossless, byte-stable, andpetab-free; an observable column is never inlined.New-era only (ADR-0028 Chunk 5c, “refuse legacy everything”): a job that binds data the legacy way (
model = X : Y.exp/mutant/param_scan) is refused, as is a legacy-edition job (_require_modern_edition()). RaisesNotImplementedErrorat every documented boundary andPybnfErrorfor a malformed/unsupported job (see the module docstring).
- pybnf.petab.export.write_problem_yaml(path, models, has_conditions=False, has_experiments=False)[source]¶
Write a PEtab v2
problem.yamlreferencing the tables and the model(s).modelsis a list of(model_id, location, language)tuples (ADR-0041): one entry for a single-model job (byte-identical to the pre-multi-model output), or N entries in declaration order for a multi-model job.languageis each model’s PEtab language (bnglorsbml, ADR-0040) – emitted verbatim so every model declares its own native language (a BNGL + SBML mix is just two entries).