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: an estimate = false row is not a
FreeParameter and is refused here. The importer applies it instead
(pybnf.petab.import_.import_job()): a fixed model parameter’s nominalValue is
written into the imported model copy (#907, ADR-0149), and any other fixed row is inlined
as a constant where the tables reference it.
- 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 constants, not free parameters), so this returns oneFreeParameterper estimated row. A caller that builds a job from these must apply the fixed rows’ nominalValues itself, aspybnf.petab.import_.import_job()does (#907): PEtab gives a fixed row’s value precedence over the model file.
- 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. So does a Uniform family carrying the nativeuflag, whose box seeds the first population without constraining the search: PEtab’s bounds are hard, so there is nothing to write it as (#736, see_petab_uniform_row()).
- 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 two optional chunks are appended only when some row needs them, so a plainuniform_varjob with no declared start keeps the four-column chunk-1 shape:nominalValue– the fit’s start point, the reverse of the importer reading it ontoFreeParameter.valueand emitting astart_pointline (#583). It was omitted while nothing produced it; since #583 it is the point the job starts from, and dropping it silently moved a re-imported fit back to a sampled draw (#719). A row without one writes a blank cell (PEtab v2 leavesnominalValueoptional for an estimated parameter).priorDistribution/priorParameters– an explicit prior (PEtab v2 defaults a prior-less estimated parameter to uniform-over-bounds).
An unbounded location-scale family writes blank bounds.
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 re-injected log10 channel (issue #499, ADR-0073). Because v2 has no log10 noiseDistribution,
a v1 observableTransformation = log10 observable has no faithful v2 home – and
petab.v2.petab1to2 silently drops it, importing the observable as a linear Gaussian and
scoring the wrong objective. The scale-preserving converter (pybnf.petab.convert)
re-injects observableTransformation as a preserved extra column, and this adapter reads
it to override the additive scale (log10 -> LOG10, log -> LN, lin -> unchanged),
selecting the family’s scale from the transformation, not just the family from
noiseDistribution. LOG10 matches PyBNF’s native lognormal token; natural-log Gaussian matches
the explicit lnnormal token (issue #509, ADR-0084), while natural-log Laplace stays reachable
through this structural adapter only.
The two-adapter equivalence is exact for both Gaussian log bases and the linear families,
structural only for natural-log Laplace. 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). log-normal matches native lnnormal
(Gaussian(LN, MEDIAN); issue #509), deliberately distinct from log10 lognormal.
log-laplace has no native .conf token and is validated structurally against the kernel’s
analytic NLL; the engine remains a superset of the native grammar.
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_transformation: 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).observable_transformation(lin/log/log10, orNonewhen absent) is the v1 residual-scale column PEtab v2 removed, re-injected as a preserved extra column by the scale-preserving converter (issue #499). The mapping reads it to pick the noise family’s additive scale – the only channel for alog10residual, since v2’slog-normalis natural log.
- 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 (with the natural-loglog-prefixes) its additive scale (normal->Gaussian(LINEAR),log-normal->Gaussian(LN),laplace->Laplace(LINEAR),log-laplace->Laplace(LN)); a re-injectedobservableTransformation(issue #499) overrides the scale (log10-> LOG10,log-> LN), the only channel for a log10 residual. 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 (unknownnoiseDistribution/observableTransformationspelling, a transformation that contradicts a lognoiseDistribution, 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). A column mean that differs between experiments uses it too, each row carrying its experiment’s mean (#894).('constant', value)– a fixed sigma written inline as a numericnoiseFormulawith no placeholder: afix_atconstant (sos-> 1,sod-> 1) or an observable’s column mean when every experiment has the same one (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).observableTransformationis recorded too – the v1 residual-scale column the scale-preserving converter re-injects (issue #499). 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 swept-axis Data (one per replicate, #903).
- 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 = (), noise_param_tokens: 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.noise_param_tokensis the analogous semicolon-split tokens of thenoiseParameterscolumn (the n-th bindsnoiseParameter${n}_${observableId}– a multi-parameter noiseFormula like Raia’snoiseParameter1_X + noiseParameter2_X * yor Fiedler’snoiseParameter1_X * noiseParameter2_X, ADR-0075). The single-token special cases keep their dedicated fields for byte-identical backward compatibility: a lone numeric token also setsnoise_parameters(the per-point_SDvalue), a lone id token also setsnoise_parameter_id(Boehm’s per-observable estimated sigma).()for a blank 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, swept_param, experiment_id_of_dose, scan_time, sd_suffix='_SD', model_id='', noise_values=None)[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 is the swept parameter, not time. Each data row is one measured dose, tagged with the experiment of its own dose: the row’sswept_paramcell is looked up inexperiment_id_of_dose({dose: experimentId}, built by the exporter over the experiment’s whole dose axis). The fitter pairs a row with the simulation at the row’s own dose (Objective._sim_row_for), so a replicate whose doses are reordered, missing or extra is tagged correctly (#895); the row’s position in the file plays no part. A dose absent from the map raisesPybnfErrorrather than tagging the row with a neighbour’s experiment.The measurement
timeis 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 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).noise_valuesis the time-course pivot’s per-experiment numeric noise (#894): the scan’s one column mean, written on every dose.
- 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: {key: token}}}}, collecting both row-varying frontiers into the one sidecar shape.keyis a baretimefor a one-replicate group and(zero_based_replicate, time)when the group has repeated cells (ADR-0083):noise (
row_varying_noise) – each measurement row’snoiseParameterstoken(s) bindnoiseParameter${n}_<observable_id>at that row’stime(the n-th token to the n-th placeholder: one for a single-id row-varying sigma, ADR-0045; several for a multi-parameter one, ADR-0075) – 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. Replicate rows are dealt with_deal_replicates(), the exact same partition used bydata_from_measurement_rows(), so each sidecar token follows the row into the corresponding.expfile. The two placeholder families (noise vs observable) coexist under one column with distinct placeholder keys. The single-replicate case keeps bare time keys so its original four-column sidecar stays byte-compatible.
- pybnf.petab.measurements.measurement_rows_from_data(data, column_to_observable_id, experiment_id='', sd_suffix='_SD', model_id='', measurement_params=None, noise_values=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 one replicate’s per-measurement binding-table slice{column: {placeholder: {time: token}}}(selected from a replicate-awaremeasurement_params:sidecar by the exporter, ADR-0083), 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.noise_values({column: sigma}, #894) gives a column one numericnoiseParametersvalue for every row of this experiment: a column-mean sigma whose mean differs between experiments, exported through a noise placeholder. Such a column reads no_SDcompanion. A per-row sidecar noise token for the same column would bind the same placeholder, so that pairing raisesNotImplementedErrorinstead of letting the token win silently.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.noise_parameters_by_observable(rows)[source]¶
{observable_id: (token, ...)}for observables whose multi-tokennoiseParameterstuple is constant across all their rows (ADR-0075) – the noise-side sibling ofobservable_parameters_by_observable().The n-th token binds
noiseParameter${n}_${observableId}in a multi-parameternoiseFormula(Raia’s affineσ_abs + σ_rel * y), substituted in by the importer (an id stays a free symbol, a number/fixed-parameter inlines). Single-token observables (the Boehm / per-point / row-varying-single-id shapes) are absent – they keep their ADR-0037/0045 paths.
- 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 swept-axisDataper replicate (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 it has exactly one experiments-table row (a single period applying a single condition), that 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).One period, one condition (#904). A dose point is a single period: an experiment with a
time = -infpre-equilibration period, or with two conditions applied at the same time, is never a plain dose point, whatever its last row sets. Such an experiment is either claimed byreconstruct_preequilibrated_dose_responses()(which runs first) or left to the time-course reconstruction, which recovers a-inf+ finite pair aspreequilibrate:+condition:and refuses the shapes it cannot express. Before #904 the detector read a flat{experimentId: conditionId}map in which the last row won – the pre-#442 flattening bug of_experiments– and imported a pre-equilibrated dose as a plain one, or a two-condition period as its last condition alone.Returns
(dose_responses, remaining_rows, consumed_condition_ids, consumed_experiment_ids):dose_responses– a list of{name, model_id, swept_param, scan_time, datas}(one per reconstructed scan;datasis its list of replicate grids,_dose_response_datas();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.reconstruct_preequilibrated_dose_responses(measurement_rows, condition_rows, experiment_rows, observable_id_to_column, sd_suffix='_SD', *, sweepable)[source]¶
Detect + reconstruct pre-equilibrated dose-response groups – the inverse of the exporter’s
build_preequilibrated_dose_response_conditions(#477; ADR-0062), the combination of ADR-0052 pre-equilibration and ADR-0046 dose-response.A pre-equilibrated dose-response is represented in PEtab v2 as N two-period experiments
<stem>_<i>: a leadingtime = -infsteady-state period under a shared pre-equilibration condition, then atime = 0measurement period applying an optional shared wash condition plus a per-dose conditioncond_<stem>_<i>that sets one swept parameter to dosei. This recovers each such group back to a swept-axisDataper replicate (column 0 = the swept parameter, its values the doses) plus the pre-equilibration + wash condition names and the scan endpoint – the form a new-eraexperiment: <stem>, preequilibrate: <pre>[, condition: <wash>], type: parameter_scan[, t_end: <t>]re-exports.A fixed-duration equilibration (
equil_t_end: T, #896) leads with a finitetime = -Tperiod instead of-inf; it is recognized only when every other period starts at exactlytime = 0(soTis the equilibration’s duration), and the scan carriesequil_t_end = T.Detection. An experiment is a pre-equilibrated dose-response point when (a) it has exactly one
time = -infperiod, or a finite leadingtime = -T < 0period followed only by periods attime = 0(a single pre-equilibration condition), (b) its measurements all share one time (the scan time), and (c) the conditioncond_<eid>(the exporter’s per-dose naming) is applied at the measurement period and sets exactly one numeric target (the dose). The other measurement-period conditions are the shared wash. Points group by their experimentId stem (<stem>_<i>->stem) and modelId; within a group the swept parameter, scan time, pre-equilibration condition, and wash condition must agree (else the group is ambiguous -> raises). A steady-state (time = inf) point is always a scan; a finite-time point needs the<stem>_<i>name (mirroringreconstruct_dose_responses()).Other sources (#904). A problem PyBNF did not write names its dose conditions freely –
petab.v2.petab1to2turns a v1preequilibrationConditionId+simulationConditionIdpair into experimentexperiment__<pre>___<sim>with periods-inf -> <pre>and0 -> <sim>. So whencond_<eid>is absent, the measurement period’s lone condition, applied attime = 0, is the dose if it sets exactly one numeric target – the rulereconstruct_dose_responses()applies to a plain dose (there is no wash to tell apart) – and that target is insweepable, the model parameters (a condition setting a species amount is a bolus, which aparameter_scancannot sweep). Such a group is claimed only when it is one scan; otherwise its experiments are left to the time-course reconstruction, which imports each one exactly as its ownpreequilibrate:+condition:experiment. A dose condition that any other experiments-table row also applies (another experiment, or a scan’s own pre-equilibration) is kept as acondition:line rather than consumed. Before #904 these experiments fell through to the plain dose detector, which read only their last row and dropped the pre-equilibration.Returns
(scans, remaining_rows, consumed_condition_ids, consumed_experiment_ids):scans– a list of{name, model_id, preequilibrate, wash, swept_param, scan_time, equil_t_end, datas}(preequilibrate/washare condition names,washNonewhen the measurement period applies no shared condition;scan_timeisinffor steady state;equil_t_endisNonefor a steady-state equilibration;datasis the list of replicate grids,_dose_response_datas());remaining_rows– the measurement rows NOT in any pre-equilibrated scan;consumed_condition_ids– only the per-dose condition ids no other experiments-table row applies (the shared pre-equilibration + wash conditions are NOT consumed: they becomepreequilibrate:/condition:lines viaconditions_from_rows);consumed_experiment_ids– every<stem>_<i>experiment id (dropped from the time-course / plain-pre-equilibration 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_noise_param_ids(rows)[source]¶
The set of
observable_ids whose multi-tokennoiseParameterstuple differs across rows – a row-varying multi-parameter noise (Fiedler’s per-gel scale × a shared sigma), bound per data point from the binding table (ADR-0075).The multi-token companion of
row_varying_noise_ids()(which covers the single-id case). A constant-per-observable tuple (noise_parameters_by_observable()) and every single-token cell are absent.
- 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 – when it
holds only base pins; one with real targets imports under its literal id (#905).
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.
- class pybnf.petab.conditions.PetabMappingRow(petab_id: str, model_id: str)[source]¶
One row of a PEtab v2 mapping table: a
petabEntityId->modelEntityIdalias.The exporter’s use (#477) is the species-amount condition target: a BNGL species pattern (
A(),IGF1(ds,hs,label~hot)) is not a valid PEtab identifier (it carries parens/commas/tildes), so it cannot be a conditiontargetIddirectly. The mapping row aliases a synthesized SIdpetab_id(species_target_id(), used as the target) to the verbatim BNGLmodel_idpattern; petab’sCheckValidConditionTargetsadmits a mapping petab_id whose model_id is a state variable (a species) as a condition target.
- pybnf.petab.conditions.REF_MARKER = '__REF'¶
The surrogate-base marker (a double-underscore suffix, like PyBNF’s
__FREE).
- pybnf.petab.conditions.SPECIES_ID_PREFIX = 'species_'¶
The prefix a synthesized species-amount target id carries (
species_<sanitized-pattern>).
- pybnf.petab.conditions.UNPERTURBED_CONDITION_NAME = 'unperturbed'¶
The name the importer gives the
perturbations: nonecondition it synthesizes for atime = -infperiod that applies no condition (#906, ADR-0150);unperturbed_2,unperturbed_3, … when the problem already uses it.
- pybnf.petab.conditions.build_dose_response_conditions(stem, swept_param, dose_values, scan_time, surrogate=frozenset({}))[source]¶
Build the conditions/experiments for a dose-response Parameter Scan.
Each dose of the exported dose axis
dose_valuesbecomes 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).dose_valuesis the axis the fitter scans – the sorted union of every replicate’s doses (#895), built by the exporter – not one data file’s rows. Returns(condition_rows, experiment_rows, experiment_ids)whereexperiment_ids[i]is the experimentId fordose_values[i]; the exporter keys each data row to the id of that row’s own dose.surrogateis the problem-global surrogate set M (ADR-0027). Its parameters are out of the parameter table, so every simulation must re-supply them: each per-dose Condition therefore also pinsp = p__REFfor everypin M, exactly as the time-course, wildtype and pre-equilibration builders do (#892). Without the pin a PEtab tool simulates every dose atp’s model-file value instead of its estimate. The swept parameter is never pinned: the dose sets it, as the fitter’s scan does.
- 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(condition, var)returns a fixed parameter’s numeric nominal in the modelconditionbelongs to (orNonefor an expression/unknown) – per condition, because two models of a multi-model job may give a same-named fixed parameter different values (#897).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_preequilibrated_dose_response_conditions(experiments, conditions, nominal_of, species_id_of=None, existing_condition_ids=frozenset({}), surrogate=frozenset({}))[source]¶
Build the conditions/experiments for pre-equilibrated dose-response experiments – the preincubate -> wash -> dose-scan protocol (#477; ADR-0062), the combination of ADR-0052’s two-period pre-equilibration and ADR-0046’s dose-response scan.
Each scanned experiment maps to N (one per dose) two-period PEtab Experiments
<stem>_<i>: a leadingtime = -infsteady-state period under the shared pre-equilibration condition, then atime = 0measurement period that applies BOTH the shared measurement (wash) condition AND a per-dose conditioncond_<stem>_<i>setting the swept parameter to dosei. A period carrying two condition ids is a native PEtab v2 shape (repeated experiment-table rows at the same(experimentId, time)); the two conditions’ targets are disjoint – the swept parameter is never a wash target. The per-dose conditions are exactlybuild_dose_response_conditions()’; the pre-equilibration and wash conditions are the job’s namedcondition:sets, whose speciessetConcentrationtargets are emitted throughspecies_id_of(ADR-0062). Measurements are tagged<stem>_<i>at the scan time – identical to a plain dose-response, sodose_response_measurement_rows()pivots them unchanged.experimentsis a list of(name, preequilibrate_cond, wash_cond_or_None, swept_param, dose_values, scan_time, equil_t_end_or_None)in declaration order;dose_valuesis the exported dose axis (the sorted union of the replicates’ doses, #895), and a fixed-duration equilibration (equil_t_end: T) leads each dose’s experiment with atime = -Tperiod in place of-inf(#896,equilibration_period_time()).species_id_of({pattern: petab_id}) maps a species pattern to its mapping-table id;existing_condition_idsdedups a pre-equilibration or wash condition already emitted by another experiment shape.surrogateis the problem-global surrogate set M (ADR-0027, #892). Its parameters are out of the parameter table, so the simulation must be given them before it starts. The pre-equilibration condition is emitted through_condition_rows_for(), so the-infperiod sets everypin M: its own value where the condition perturbsp, else the base pinp = p__REF. A later period keeps a value it does not change. That is PEtab v2’s rule (a period’s changes persist; libpetab turns them into SBML events), and it is PyBNF’s too: the fitter applies the pre-equilibration condition as an inlinesetParameterand never undoes it, so a fit parameter the pre-equilibration condition sets keeps that value through the scan. So a wash-free measurement period carries only the per-dose condition. A synthesized base there would re-pin such a parameter to its estimate, which the fit does not do. A wash condition is emitted through_condition_rows_for()as well, so it also re-pins M. That is harmless for a parameter the pre-equilibration left at its base, and wrong for one it set: the orchestrator refuses the latter, and a swept parameter in M, whose pin would collide with the dose. The per-dose condition sets only the swept parameter. PEtab v2 forbids two conditions of one period from sharing a target (CheckValidConditionTargets), so it cannot carry the wash’s pins as well.Returns
(condition_rows, experiment_rows, experiment_ids_by_name)whereexperiment_ids_by_name[name]is the ordered[<stem>_0, <stem>_1, ...]list (aligned with that experiment’sdose_values) for tagging its measurements.
- pybnf.petab.conditions.build_preequilibration_conditions(experiments, conditions, nominal_of, surrogate=frozenset({}), existing_condition_ids=frozenset({}), species_id_of=None)[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). A fixed-duration equilibration (equil_t_end: T) leads with a finitetime = -Tperiod instead, so the equilibration runs for exactlyTbefore the measured period starts at 0 (#896,equilibration_period_time()).experimentsis a list of(name, preequil_cond, measurement_cond_or_None, equil_t_end_or_None);conditionsmaps a condition name to its perturbations[(var, op, val), ...];nominal_of(condition, var)returns a fixed parameter’s numeric nominal in the model the condition belongs to, as forbuild_experiment_conditions()(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.species_id_of({pattern: petab_id}, ADR-0062) maps a speciessetConcentrationwash target to its mapping-table id, threaded through_condition_rows_for()so a pre-equilibration or measurement (wash) condition can perturb a species amount.A
perturbations: nonepre-equilibration condition (an empty perturbation list, #906, ADR-0150) equilibrates the model as it stands, so it has no rows of its own: its-infperiod takes the wash-out’s treatment – a blankconditionIdwhenMis empty (PEtab v2’s “the model as is”), elseWILDTYPE_CONDITION_ID, which re-pinsMat base.
- pybnf.petab.conditions.condition_name_from_id(condition_id)[source]¶
The new-era
condition:name for a PEtabconditionId, orNone.Nonefor an absent/blank id (the model as is). Otherwise thecond_prefix is stripped (an externally-authored id without the prefix passes through unchanged, defensively) – except forWILDTYPE_CONDITION_ID, which keeps its literal id. By the time an id reaches here the importer has dropped acond_wildtypethat is only the exporter’s synthesized base (drop_synthesized_wildtype()), so one that is still present carries real targets. It is not namedwildtype: the exporter reserves that name and would refuse to export the imported job, whereas a condition namedcond_wildtypere-exports ascond_cond_wildtypeand imports back under the same name (#905).
- pybnf.petab.conditions.condition_names_and_ids(condition_rows, experiment_rows)[source]¶
Every condition id either table uses, and the conf name each one imports as.
- pybnf.petab.conditions.conditions_from_rows(condition_rows, surrogate_params, species_by_id=None, free_names=frozenset({}), fixed_params=None, applied=None)[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). Base pins (a row whosetargetValueis exactly a surrogate name) are dropped as machinery, so the synthesized wildtype base – only pins – yields no condition (the importer has already dropped it,drop_synthesized_wildtype()); 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).Two conditionIds can map to one name (
cond_aanda, orcond_cond_wildtypeandcond_wildtype).appliedis the set of conditionIds some measured experiment applies (None: treat every id as applied). If two or more of the colliding ids are applied,PybnfErroris raised rather than merging their targets into one condition. Otherwise one id is kept under the shared name – the applied one, or with none applied the first in table order – and the others are left out: a condition no experiment applies cannot change the fit (libpetab only warns about one).species_by_id({petab_id: pattern}, ADR-0062) inverts the mapping table: a target that is a mapping species id recovers its BNGL pattern and a verbatim=value (a speciessetConcentrationwash/bolus).free_names(the estimated parameter-table ids) andfixed_params({id: value}for the fixed ones) resolve a parameter-valuedtargetValue– a per-condition estimated initial condition (ADR-0076): a target set to a free-parameter id becomes a parameter reference (vala string naming that free parameter), a target set to a fixed one inlines its numeric value.
- pybnf.petab.conditions.drop_synthesized_wildtype(condition_rows, experiment_rows, surrogate_params)[source]¶
Remove the exporter’s synthesized base condition from an imported problem (#905).
The exporter writes
WILDTYPE_CONDITION_IDto re-pin every fit-and-perturbed parameter at its base value (p = p__REF) on a wildtype time course or a wash-out measurement period (ADR-0027/0052). The importer has always read those rows as “no condition” (see_is_base_pin()for when that is exact), so acond_wildtypemade only of base pins (or with no rows at all) means “no condition”: its rows are dropped and every experiments-table period that applies it gets a blankconditionId, the PEtab spelling of “the model as is” that the reconstruction below already reads. Acond_wildtypewith any other row carries real targets – a condition some other tool wrote, or a PyBNF condition namedwildtypeexported before the exporter reserved that name – and is left in place, to import under its literal id (condition_name_from_id()). Before #905 everycond_wildtypewas treated as the base, so a real one was dropped whole and its experiments were fitted against the unperturbed model.Returns
(condition_rows, experiment_rows), the inputs unchanged unless the base was dropped.surrogate_paramsis the set of model parameters with a<p>__REFsurrogate.
- pybnf.petab.conditions.equil_t_end_from_period_time(time)[source]¶
The inverse of
equilibration_period_time():-inf->None(steady state), a finite-T < 0->T. The caller checks that the period is followed by one at exactly0(the measured period); anything else raisesValueError.
- pybnf.petab.conditions.equilibration_period_time(equil_t_end)[source]¶
The PEtab v2 start time of a pre-equilibration experiment’s leading (unmeasured) period.
None(noequil_t_end:, the ADR-0052 default) is-inf: equilibrate to steady state. A fixed durationTis the finite time-T: PEtab v2 runs a period from its start time until the next period starts, and the measured period starts at 0, so the equilibration runs for exactlyT(#896). The exporter has already checkedTis finite and positive (export._read_experiments).
- pybnf.petab.conditions.free_condition_name(taken, base='unperturbed')[source]¶
The first of
base,base_2,base_3, … that is neither intaken(a set of condition names and ids) nor, prefixedcond_, an id in it.
- pybnf.petab.conditions.is_species_target(target)[source]¶
True iff a condition target is a BNGL species pattern (a
setConcentrationwash / bolus, ADR-0062), not a bare parameter/compartment id – detected by the(a pattern always carries and a PEtab/BNGL identifier never does.
- pybnf.petab.conditions.model_time_reads(text, language)[source]¶
How a model reads the simulation time, as a list of short descriptions (empty when it does not): what makes it non-autonomous, so that shifting its clock changes its result.
PyBNF runs a fixed-duration equilibration (
equil_t_end: T) ontin[0, T]and restarts the clock at 0 for the measured phase (pset.py::_append_preequilibration_actionsand the bngsim SBML backend’s_begin_preequilibration); a PEtab v2 leading period at-Truns on[-T, 0]. The two are the same simulation exactly when nothing in the model reads the time (#896), which is what this checks:BNGL – the
timesymbol in a parameters, functions, reaction rules, or compartments block (time()in a function or rate law, or the index of atfun(..., time)table function), and a lowercasetfuncall with no index at all (bngsim indexes it by time by default). The legacy uppercaseTFUN(counter, 'file')is indexed by an observable, so it is a state read, not a time read.SBML – a
<csymbol>for time anywhere in the document (a kinetic law, rule, initial assignment, or event trigger).
- pybnf.petab.conditions.mutation_target_value(op, val, *, nominal=None, surrogate=None, target=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).
A parameter-reference value (
vala string free-parameter id, not a number – a per-condition estimated initial condition, ADR-0076) emits that id verbatim as thetargetValuefor an absolute set: it is PEtab-legal (the referenced id is a parameter-table entry, and the fixed target is not, so no id is in both tables – the surrogate split is not needed). A relative op on a parameter reference is a multi-symbol expression, deferred (NotImplementedError).
- pybnf.petab.conditions.name_unperturbed_equilibrations(condition_rows, experiment_rows)[source]¶
Point each
time = -infperiod that applies no condition at a synthesizednonecondition (#906, ADR-0150).In PEtab v2 a blank
conditionIdmeans “the model as is”. On the-infperiod of a pre-equilibration that is an equilibration with nothing changed – a PyBNFperturbations: nonecondition applied aspreequilibrate:– which is not the same thing as no pre-equilibration at all, the meaningNonehas in the period readers. Before #906 the two collapsed, and the experiment started from the seed species instead of the steady state. So each such period is given the idcond_<name>for onename(free_condition_name()) that collides with no condition name or id in the problem; every reader downstream then sees an ordinary named pre-equilibration, and the importer declaresnameas anonecondition. A period whose idcondition_name_from_id()maps toNonecounts as applying no condition: a blank id, which includes the exporter’s base condition oncedrop_synthesized_wildtype()has blanked it (only when every row is a base pin). Acond_wildtypewith a real target keeps its id and stays a named pre-equilibration (#905). A blank id on a finite period is left alone: there it is simply the absence of acondition:(a wash-out).Returns
(experiment_rows, name);nameisNone, and the rows are unchanged, when no period applies no condition.
- 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.read_mapping_table(path)[source]¶
Read a PEtab v2 mapping table into
PetabMappingRowrecords (stdlibcsv).Columns
petabEntityId/modelEntityId; themodelEntityIdis kept as its raw string (a BNGL species pattern, not an identifier). A row missing apetabEntityIdraises.
- pybnf.petab.conditions.refuse_inexact_unperturbed_periods(condition_rows, experiment_rows, surrogate_params, measured_ids)[source]¶
Refuse the periods the importer would read as the model as is when PEtab does not (#906, ADR-0150).
The importer turns a blank
-infperiod into the synthesizedperturbations: noneconditionname_unperturbed_equilibrations()names, and a named condition whose rows are all base pinsp = p__REFinto anonecondition of its own. In PyBNF anonecondition leaves every parameter as it stands: a fit parameter at its trial value, and whatever an earlier period of the experiment set. Two PEtab readings differ from that, and are refused here, before anything is dropped or renamed. The rows are the tables as read;surrogate_paramsis the set M of parameters estimated through a<p>__REFsurrogate; only the experiments inmeasured_idsare checked, since only they reach the conf.A first period that leaves a parameter of M unset. Each
pin M is a condition target, not a parameter-table entry, so until a period sets it PEtab runs it at the model file’s value, where PyBNF would run the trial value. So a-infperiod that is blank or applies a pins-only condition, and a first measured period that applies a pins-only named condition, must pin all of M. The exporter’s own periods always do. The exact import, which would set such apto its model-file value, is left to #948.A pins-only named condition after an earlier period changed what it pins.
p = p__REFis the identity only while nothing earlier in the experiment setp. After a pre-equilibration that setk = 2, PEtab’s pin restoreskto its estimate, which anonecondition would not do. Importing such a re-pin exactly is #948. (The exporter’s pins-onlycond_wildtypein that position is the older #948 case and is left as it is.)
Raises
NotImplementedErrornaming the experiment and the parameters.
- pybnf.petab.conditions.refuse_measurements_inside_fixed_equilibration(experiment, equil_t_end, times)[source]¶
Refuse an imported measurement taken during a fixed-duration equilibration period.
A PEtab v2 experiment can be measured at any time from its first period’s start, so a problem whose leading period starts at
-Tmay carry measurements at times in[-T, 0), taken while the system equilibrates. PyBNF’spreequilibrate:+equil_t_end: Tequilibration is unmeasured, and its measured phase starts at the intervention (t = 0), so such a measurement has no PyBNF representation. Imported as it is, it would land on the measured phase’s time grid, where bngsim starts integrating at the earliest sample time and so scores every measurement of the experiment late (#896).timesare the experiment’s measurement times.
- pybnf.petab.conditions.species_target_id(pattern)[source]¶
A PEtab v2 SId aliasing a BNGL species pattern for the mapping table’s
petabEntityId.A BNGL pattern (
A(),IGF1(ds,hs,label~hot)) is not a valid PEtab identifier, so it is sanitized tospecies_<A-Za-z0-9_>: every run of non-identifier characters collapses to a single_and leading/trailing_are trimmed. Content-derived and deterministic – the same pattern always maps to the same id, so an import -> re-export is byte-stable regardless of condition order. Two distinct patterns that sanitize alike would collide; the exporter detects that and raises (_species_id_map()).
- pybnf.petab.conditions.surrogate_name(model_param)[source]¶
The surrogate-base parameter id for a fit-and-mutated model parameter.
- pybnf.petab.conditions.write_condition_table(rows, path)[source]¶
Write condition
rowstopathas a PEtab v2conditions.tsv.
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 body is BNGL, not PEtab math, so it is read with BioNetGen’s grammar (_bngl_math, #908), printed as PEtab math with explicit parentheses, and guarded by a numeric check of the printed formula against BioNetGen’s reading of the body (_assert_matches_bngl()).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, *, function_name=None, model_file=None)[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. The body is BNGL, and BNGL does not read arithmetic the way PEtab math does (-k^2is(-k)^2anda^b^cis(a^b)^cin BNGL, the opposite in PEtab), so it is parsed with BioNetGen’s grammar and printed as PEtab math with explicit parentheses (_bngl_math, #908). Ag()reference to another global function or an observable becomes the bare symbolg(PEtab math has no user zero-argument functions). Every free symbol is validated against the model namespace (parameters u observables u functions), and the printed formula is checked numerically against BioNetGen’s reading of the body (_assert_matches_bngl()) – a wrong observableFormula is worse than a refused one (ADR-0035).function_name/model_fileonly name the function in error messages.Raises
PybnfErroron a missingpetabextra, a malformed body, a body using an operator BioNetGen’s simulators refuse, an unknown free symbol, or a formula that disagrees with the body;NotImplementedErroron a construct PEtab math cannot express exactly (rint,time(),mratio,TFUN, a call with arguments to a local function, a negative literal raised to a power, anif()whose condition is not a comparison) and on 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_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.inline_derived_symbols(formula, derived, *, observable_id=None)[source]¶
Inline the derived SBML entities a measurement
observableFormulareferences down to the species/parameters they are defined over (#465 and #795, 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.The second construct is an
initialAssignmenton a parameter or compartment (#795). SBML lets one supersede the declaredvalue/size, so the attribute is a placeholder and the entity has no value of its own. Inlining is what makes such a model work rather than merely fail politely: an observable over Bertozzi’sbeta_N, defined as(R0_ * gamma_) / N_, resolves to an expression over the three parameters, and if the fit estimates one of them the measurement tracks the fit, becauseMeasurementModel.materialize()resolves a symbol from the parameter set before the constant snapshot. An initial assignment is only inlinable when every entity it reads holds still for the whole simulation, since the substituted expression is read at a measurement time while the assignment fixed a value at t=0; the scanner applies that gate and records a non-inlinable definition asNone.derivedmaps every derived entity id to aDerivedSymbolcarrying itskind, its RHS as a PEtab-math infix string (the stdlib_sbmlscanner’s serialization) orNone, and the clause naming why aNonecannot be inlined. A formula that references no derived entity 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 definition the scanner could not carry (NoneRHS), a cyclic dependency, 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.
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 copy of the model that is verbatim except for marked
estimate = false overrides (new-era binds free parameters by id, ADR-0034, so the model
needs no re-instrumentation; see Fixed parameters below) – 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; a table the problem splits over several files is read in
full and re-exports as one file, #902); 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: block
and one-line flow lists, every file a key lists, and a refusal for any shape it cannot read
(#902). 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 (except for marked
estimate = false overrides, below), 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 (save the marked overrides below) 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). A time = inf
measurement with no swept axis (a single condition, e.g. Blasi) needs no reconstruction at
all: the time is written verbatim into the .exp, and the fitter reads an all-inf time
column as a steady-state experiment – a relaxation to equilibrium (ADR-0086, #521). 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.)
Fixed parameters (#907, ADR-0149). A parameters-table row with estimate = false fixes
its parameter at the row’s nominalValue, and PEtab gives that value precedence over the
model file; libpetab, AMICI and pyPESTO all simulate with it. When the row names a model
parameter (a BNGL begin parameters entry, or an SBML global <parameter>), the importer
therefore writes the nominalValue into its copy of every model that declares the parameter –
only where the file disagrees: a numeric value already equal to it is left byte-identical,
while a different number, a BNGL expression, or a missing SBML value is replaced. Each edit is
marked with a comment in the copy, listed in the conf header, and printed; the source files are
never touched, and nothing about it is a warning, because it is what the problem says. A fixed
row that names no model entity keeps the inlining path (a formula constant, a fixed sigma, a
condition targetValue). Refused: a fixed row with no nominalValue, a fixed row naming a model
entity other than a parameter or an SBML rule target (libpetab’s lint rejects both), an SBML
parameter an initial assignment, event assignment or algebraic rule also sets, and a BNGL
parameter the model’s own actions set (either way the written value would not be its value for
the whole simulation). The exporter never writes an estimate =
false row: a parameter it does not fit stays in the exported model at the model file’s value,
which is what PEtab uses for a parameter absent from the table, so a re-export of an imported
job carries the edited model and no fixed row – the same problem.
- class pybnf.petab.import_.ImportedExperiment(name, condition, preequilibrate, data_files, model_location, measparams_file, t_end, equil_t_end)¶
- condition¶
Alias for field number 1
- data_files¶
Alias for field number 3
- equil_t_end¶
Alias for field number 7
- 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 copy of each 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, from every file eachproblem.yamlkey lists (#902); the run-recipe (job_type,method,settings) is supplied by the caller (see the module docstring). Returns theout_dirpath.Each model copy is verbatim except for marked
estimate = falseoverrides (#907, ADR-0149). A parameters-table row withestimate = falsethat names a model parameter fixes it at the row’snominalValue, which PEtab gives precedence over the model file. Where the model file has a different value (or, in BNGL, an expression; in SBML, no value), the copy is edited to the table’s value and the edit is marked with a comment in the copy, listed in the conf header, and printed, one line per override. A model that already agrees is copied byte for byte, and the source files are never touched: an edited copy whose destination is its own source file raisesPybnfError.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 (except for the overrides above), 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; a steady-state measurement with no swept axis (time = infunder a single condition) imports as an ordinary experiment whose.exptime isinf, which the fitter reads as a relaxation to equilibrium (ADR-0086); a pre-equilibrated dose-response (ADR-0062, the preincubate -> wash -> dose-scan protocol) – N two-period experiments whose speciessetConcentrationwash targets are aliased through the mapping table – is reconstructed into apreequilibrate:+condition:parameter_scanexperiment (the species patterns recovered from the mapping). 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 multi-symbol condition expression – a single parameter-valuedtargetValueis a per-condition estimated initial condition and imports, ADR-0076; a row-varying per-measurementobservableParameters/noiseParametersplaceholder; a fixed SBML parameter an initial assignment, event assignment or algebraic rule also sets, or a fixed BNGL parameter the model’s actions set) andPybnfErrorfor a malformed problem (anobservableFormulasymbol that is not a model entity, an ambiguous dose-response group, an id defined twice across a table’s files, aproblem.yamlshape the reader cannot read – #902, or a fixed row with no nominalValue or naming a model entity PEtab does not allow in the parameters table).
- pybnf.petab.import_.read_problem_yaml(path)[source]¶
Read a PEtab v2
problem.yamlwithout a YAML library.Returns a dict with the table-file lists (
parameter_files/observable_files/measurement_files/condition_files/experiment_files/mapping_files, each holding every file listed, in order) 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.A small indentation-aware scan reads the shapes a problem file is written in: our own writer’s (
key:then two-space-indented- itemlines,model_fileslast), the column-0- itemlistspetab.v2.petab1to2writes, keys in any order, and the one-line flow listkey: [a.tsv, b.tsv]([]included). Items may be quoted,#comments are dropped, a model entry’s fields other than location and language are passed over with everything nested beneath them, and a leading directive or---and a closing...are allowed. Whatever else it meets raisesPybnfErrornaming the line or key rather than being skipped: a scalar where a list belongs, a flow list continued over several lines, a flow-formmodel_filesentry, a key the PEtab v2 schema does not allow, a key or model given twice, a file listed twice under one key, aformat_versionother than 2 (#902). The scan used to skip what it did not recognize, socondition_files: [conditions.tsv]read as no condition table at all.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), natural-log
Gaussian (lnnormal -> PEtab log-normal), 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); a mean-centred lnnormal, whether the line says
location = mean or the job says noise_location = mean (#898); the no-prior
var/logvar; a u-flagged
Uniform, whose box seeds the draw without constraining the search (#736); a time_error
measurement-time marginalization, whose latent sampling time a PEtab measurement row’s single
exact time cannot carry (#738); a fixed-duration equilibration (equil_t_end:) on a
model that reads the simulation time, whose clock a PEtab period cannot restart (#896); a
postprocess script (#899); a BNGL action the fit runs ahead of its experiments that can
change their starting state, such as setParameter (#900); a .con/.prop
Constraint; an Antimony (.ant) model. The job’s generate_network cap is written into
the exported BNGL model, as the fitter synthesizes it (#901). 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, model_file='model.bngl', generate_network_options=None, experiment_methods=None)[source]¶
Return a PEtab-clean copy of a BNGL model: its actions removed, and the network definition the fit used (
generate_network) kept.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 drops the simulation actions (PEtab drives simulation via the measurement times / experiments, not the model’s own
simulatecalls) while keepinggenerate_network– a network-definition / compilation directive, not a simulation action. That directive carries the model’s finiteness cap (max_stoich/max_agg/max_iter); dropping it would silently turn a model that is finite only under the cap into one that network-generates unbounded, with no error or warning (#485). The line written is the one the fitter runs (_exported_generate_network_line()): the model’s own, or the one the fitter synthesizes from the job’sgenerate_networkkey (#901). The exported model then carries its own cap, soimport_.py(which copies the model byte-verbatim) round-trips it and any BNG2.pl / PyBNF consumer stays finite; with no line to keep, the actions disappear entirely.The model is read with the fitter’s own scanner (
BNGLModel), so the lines removed are exactly the ones the fit reads as actions – thebegin actionsblock and any loose action afterend model– and the kept line goes in a minimalbegin actionsblock at the end of the file, where the fitter writes it.setOptionand its siblings stay where they are (the fitter keeps them in the model text too), as do a comment outside the actions block and a protocol block. An action the export cannot drop without changing the fit – one that sets a parameter or species, saves a snapshot, or reads a file – raises rather than vanishing (_require_droppable_actions(), #900). 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).generate_network_optionsis the job’sgenerate_networkkey andexperiment_methodsmaps the experiments on this model to theirmethod:(_experiment_methods_by_model()); theNonedefaults mean no key and network-based experiments.model_filenames the model in error messages.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, has_mapping=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).has_mappingadds themapping_filesentry for the species-amount mapping table (ADR-0062).