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-free csv parse of parameters.tsv into PetabParameterRow records. When the later importer chunks pull in the petab library for the SBML model and the observableFormula sympy layer, this is swapped for petab’s parameter_df reader with no change below.

  • The mapping (free_parameter_from_row) – the asset: a PetabParameterRow -> FreeParameter, driven by the prior-family registry (ADR-0010). It synthesizes the equivalent legacy *_var keyword and builds the FreeParameter through 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 parameterScale column (removed in v2); everything is in linear space, and the parameter’s PyBNF Scale is derived from the prior family instead (a log-* prior -> Log10).

  • Priors are priorDistribution / priorParameters (renamed from v1’s objectivePrior*); a single prior, used for the objective only.

  • log-normal / log-laplace parameters 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 own v1.distributions classes (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’s theta <= 0) – maps to a half-bounded box, a single reflecting wall (ADR-0047, #432). Both directions are the ub->inf limit 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 from Problem.parameter_df records.

lower_bound / upper_bound / nominal_value are None when the column is absent or blank; prior_distribution is None for an estimated parameter with no explicit prior (PEtab v2 defaults that to a uniform over the bounds). prior_parameters is 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). Raises NotImplementedError at the remaining PEtab/PyBNF boundary (estimate=false fixed parameters) and PybnfError for malformed rows (unknown prior type, wrong parameter count, reversed bounds).

pybnf.petab.parameters.free_parameters_from_file(path)[source]

Read parameters.tsv at path and map it to FreeParameter objects.

pybnf.petab.parameters.free_parameters_from_table(rows)[source]

Map the estimated rows of a parameters table to FreeParameter objects.

estimate=false rows are skipped (they are constants, not free parameters), so this returns one FreeParameter per estimated row. A caller that builds a job from these must apply the fixed rows’ nominalValues itself, as pybnf.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 FreeParameter back to a PetabParameterRow.

The exact reverse of free_parameter_from_row(): a native .conf free parameter and a PEtab row land on the same object, so this read backwards is the two-adapter proof in the export direction. parameter_id defaults to the free parameter’s name – new-era binds a free parameter to its model parameter by id (ADR-0034), so the name is the parameterId; a caller that has resolved the model parameter name authoritatively (the exporter, which renames a fit-and-mutated parameter to its <p>__REF surrogate) 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 no priorDistribution is written (PEtab v2 defaults an estimated, prior-less parameter to uniform-over-bounds, so the row round-trips).

  • loguniform_var – the same linear bounds, but priorDistribution is stated (log-uniform; PEtab’s default uniform is linear) with priorParameters = (p1, p2).

  • normal_var / laplace_var / cauchy_var / gamma_var and the log forms of normal/laplace – a two-parameter prior: priorParameters are (p1, p2) ((loc, scale) / (shape, scale)), in natural log for the log families (PyBNF parameterizes in log10, so they are scaled back by ln 10).

  • exponential_var / chisquare_var / rayleigh_var – a one-parameter prior: priorParameters is 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 / logvar point-start keywords and the log forms of the five catalog families (no PEtab log- spelling) raise NotImplementedError – surfaced in code, not mis-exported. So does a Uniform family carrying the native u flag, 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.tsv into PetabParameterRow records.

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 rows to path as a PEtab v2 parameters.tsv.

Always writes parameterId/estimate/lowerBound/upperBound. The two optional chunks are appended only when some row needs them, so a plain uniform_var job with no declared start keeps the four-column chunk-1 shape:

  • nominalValue – the fit’s start point, the reverse of the importer reading it onto FreeParameter.value and emitting a start_point line (#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 leaves nominalValue optional 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-free csv parse of observables.tsv into PetabObservableRow records. When the later observableFormula chunk pulls in the petab library, this is swapped for petab’s observable_df reader with no change below.

  • The mapping (noise_model_from_row) – the asset: a PetabObservableRow -> (NoiseModel, SigmaSource), built through the ordinary Gaussian / Laplace constructors.

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 exactly normal / log-normal / laplace / log-laplace (default normal). PEtab’s log is the natural log, so the log forms map to LN (ADR-0022), not LOG10.

  • 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)

normal

Gaussian(LINEAR, MEDIAN)

log-normal

Gaussian(LN, MEDIAN) (natural log)

laplace

Laplace(LINEAR, MEDIAN)

log-laplace

Laplace(LN, MEDIAN) (natural log)

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 from Problem.observable_df records.

noise_distribution is None when the column is absent or blank; the mapping applies the PEtab v2 default (normal). observable_formula – the model-output expression – is recorded for the deferred observableFormula chunk but is not consumed by the noise mapping (this is the noise half only).

observable_transformation (lin / log / log10, or None when 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 a log10 residual, since v2’s log-normal is 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).

noiseDistribution selects the family and (with the natural-log log- prefixes) its additive scale (normal -> Gaussian(LINEAR), log-normal -> Gaussian(LN), laplace -> Laplace(LINEAR), log-laplace -> Laplace(LN)); a re-injected observableTransformation (issue #499) overrides the scale (log10 -> LOG10, log -> LN), the only channel for a log10 residual. The prediction is the median (PEtab default). noiseFormula becomes the sigma-source: a number -> ConstantSigma, a bare noise-parameter id -> FreeParameterSigma.

Raises NotImplementedError for a non-trivial noiseFormula expression (the deferred sympy layer) and PybnfError for a malformed row (unknown noiseDistribution / observableTransformation spelling, a transformation that contradicts a log noiseDistribution, missing noiseFormula).

pybnf.petab.observables.noise_models_from_file(path)[source]

Read observables.tsv at path and 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 the LikelihoodObjective(overrides=...) map the native noise_model lines 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_name is the BNGL observable/function name (an .exp column header); kind is 'observable' or 'function'. The observableId is the prefixed name (obs_<name> / func_<name>). kind == 'measurement' is a conf-declared measurement model (ADR-0036): the observableId is model_name verbatim (no prefix – it is already a PEtab id) and observable_formula (the conf formula) is required.

observableFormula is 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 and petab-free. The exporter’s opt-in inlining mode (ADR-0035) overrides it for a function column by passing observable_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 pass observable_formula only for kind == 'function'.

noise_distribution is the PEtab family the job’s objective maps to (ADR-0023 reversed: gaussian -> normal, laplace -> laplace). noise_source is the PEtab representation of the objective’s sigma source (ADR-0021 reversed), one of:

  • ('placeholder', None) – a per-point _SD data column: a declared noise placeholder (noiseFormula = noisePlaceholders = noiseParameter1_<id>) whose per-point value the measurements’ noiseParameters column 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 numeric noiseFormula with no placeholder: a fix_at constant (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-math noiseFormula over free-parameter ids + constants, emitted verbatim with no placeholder. The inverse of the importer’s expression-noiseFormula classification, so a whole-fit FormulaSigma round-trips.

  • ('free_param', id) – a free-parameter (estimated) sigma (FreeParameterSigma, #439): the bare noise-parameter id as the noiseFormula, no placeholder, declared estimated in the parameter table and an observation-layer nuisance (not a model entity). The importer reads a bare-id noiseFormula back to a fit source (ADR-0044), so a per-observable estimated sigma round-trips.

  • ('per_measurement', expr) – a row-varying placeholder sigma (PerMeasurementFormulaSigma, ADR-0045): the noiseFormula expression carries a per-measurement placeholder (noiseParameter1_<id>) whose value differs row to row, supplied by the measurements’ noiseParameters column. The noiseFormula is emitted verbatim and the placeholder(s) it references declared in noisePlaceholders – the inverse of the importer routing a row-varying noiseParameters id to the binding table.

pybnf.petab.observables.read_observable_table(path)[source]

Read a PEtab v2 observables.tsv into PetabObservableRow records.

Dependency-free (stdlib csv). noisePlaceholders is recorded (it marks a named noiseFormula placeholder whose value the measurements’ noiseParameters column supplies – a per-observable estimated sigma, ADR-0037). observableTransformation is 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 rows to path as a PEtab v2 observables.tsv.

The optional observablePlaceholders column (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 (wide Data -> 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 wide Data replicates per (experiment, model), the exact inverse of measurement_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 _SD companion column from the per-point noiseParameters).

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_id is '' for a base time-course with no condition changes (PEtab’s “model as is”); model_id is the optional model->data link (ADR-0041) – the modelId of the model that produced the row ('' for a single-model job, where the column is omitted). noise_parameters is the per-point numeric noise value (the _SD cell) feeding the observable’s single declared noise placeholder, or None when the column carries no number. noise_parameter_id is the alternative: a parameter id in the noiseParameters column (Boehm’s sd_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_parameters is the semicolon-split tokens of the observableParameters column (the n-th token binds observableParameter${n}_${observableId} in the observableFormula / 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_tokens is the analogous semicolon-split tokens of the noiseParameters column (the n-th binds noiseParameter${n}_${observableId} – a multi-parameter noiseFormula like Raia’s noiseParameter1_X + noiseParameter2_X * y or Fiedler’s noiseParameter1_X * noiseParameter2_X, ADR-0075). The single-token special cases keep their dedicated fields for byte-identical backward compatibility: a lone numeric token also sets noise_parameters (the per-point _SD value), a lone id token also sets noise_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 rows back to the wide Data replicates per (experiment, model) – the inverse of measurement_rows_from_data().

observable_id_to_column maps a PEtab observableId (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-unique time values become column 0 and each measured observable becomes a value column, NaN-filled where a (time, observable) cell is absent (the forward pivot skips NaN, so this restores the ragged grid). When any row in the group carries a noiseParameters value, each value column gets a <col><sd_suffix> companion rebuilt from those per-point values (the _SD source a chi_sq re-export reads back); a group with no noiseParameters (a fixed / column-mean sigma objective) gets no _SD columns.

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 carries modelId = '' on every row, so its grouping is identical to keying on experimentId alone.

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-th Data (the first grid is the full one; later grids hold only the cells that repeat). This is the exact inverse of the forward export’s for 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 .exp files (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_id being '' for the “model as is” base time course and model_id '' for a single-model job. Raises PybnfError if a row names an observableId absent 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 Data to long rows.

The dual of measurement_rows_from_data() for a Parameter Scan .exp whose 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’s swept_param cell is looked up in experiment_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 raises PybnfError rather than tagging the row with a neighbour’s experiment.

The measurement time is the scan’s fixed scan_time – a scalar, inf for a steady-state scan (PEtab time=inf) or a finite t_end: for a fixed-endpoint scan (ADR-0046), not a data column. column_to_observable_id and the <col><sd_suffix> noise companion behave as in the time-course pivot (sd_suffix=None disables per-point noise); the swept-parameter column is not in the map, so it is never emitted as a measurement. model_id is the optional model->data link (ADR-0041), stamped on every row ('' for a single-model job). noise_values is 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. key is a bare time for a one-replicate group and (zero_based_replicate, time) when the group has repeated cells (ADR-0083):

  • noise (row_varying_noise) – each measurement row’s noiseParameters token(s) bind noiseParameter${n}_<observable_id> at that row’s time (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) – the PerMeasurementFormulaSigma source;

  • observable (row_varying_obs) – the n-th observableParameters token binds observableParameter${n}_<observable_id> at that row’s time (the PerMeasurementModel scale/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 match data_from_measurement_rows() so each experiment gets its own sidecar. Replicate rows are dealt with _deal_replicates(), the exact same partition used by data_from_measurement_rows(), so each sidecar token follows the row into the corresponding .exp file. 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 Data to long measurement rows.

column_to_observable_id maps a Data column header (a model observable/function name, e.g. x) to its PEtab observableId (e.g. obs_x); only those columns become measurements. For each such column its <col><sd_suffix> companion (if present) supplies the per-point noiseParameters value. sd_suffix is 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 _SD companion (or none). None (a column’s suffix, or the whole argument) disables per-point numeric noise (noiseParameters left blank) – used when the objective’s sigma source is not a data column (a fixed / column-mean / formula sigma carried inline in noiseFormula), so a stray _SD column does not produce a noiseParameters override with no placeholder to bind to. model_id is the optional model->data link (ADR-0041): the modelId of the model the experiment simulates, stamped on every row ('' for a single-model job, where the column is omitted on write). NaN cells are skipped (a ragged long table round trips through PyBNF’s NaN-skipping objective). Rows are grouped by observable, then ordered by the independent variable as it appears in data.

measurement_params (ADR-0045) is one replicate’s per-measurement binding-table slice {column: {placeholder: {time: token}}} (selected from a replicate-aware measurement_params: sidecar by the exporter, ADR-0083), the source of a row-varying placeholder’s per-row token. For a column whose sidecar carries noiseParameter1_<id> the row’s token becomes its noiseParameters id (a PerMeasurementFormulaSigma noise source); for observableParameter{n}_<id> the n-th token becomes the row’s observableParameters tuple (a PerMeasurementModel scale/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 numeric noiseParameters value 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 _SD companion. A per-row sidecar noise token for the same column would bind the same placeholder, so that pairing raises NotImplementedError instead of letting the token win silently.

Raises NotImplementedError if the independent variable is not time (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 whose noiseParameters is 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 whose noiseParameters is 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-token noiseParameters tuple is constant across all their rows (ADR-0075) – the noise-side sibling of observable_parameters_by_observable().

The n-th token binds noiseParameter${n}_${observableId} in a multi-parameter noiseFormula (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 whose observableParameters tuple is constant across all of that observable’s rows (ADR-0044) – the sibling of noise_parameter_ids_by_observable().

A constant-per-observable observableParameters is a per-observable scale/offset: the n-th token binds observableParameter${n}_${observableId} and is substituted into the observableFormula / noiseFormula (an id stays a free symbol that resolves from the PSet, a number inlines – ADR-0044). Observables with a blank observableParameters are 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.tsv into PetabMeasurementRow records.

Dependency-free (stdlib csv), mirroring parameters.read_parameter_table. experimentId is optional (blank -> '', the base time course); modelId is the optional model->data link (blank -> '', a single-model job; ADR-0041); time and measurement are required; noiseParameters is the optional per-point _SD value (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 – inf for the steady-state default (the common case), or a finite t_end. This recovers each such group back to a swept-axis Data per 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-era experiment: (its .exp driving 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>__REF expression 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 = -inf pre-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 by reconstruct_preequilibrated_dose_responses() (which runs first) or left to the time-course reconstruction, which recovers a -inf + finite pair as preequilibrate: + 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; datas is its list of replicate grids, _dose_response_datas(); scan_time is inf for 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 leading time = -inf steady-state period under a shared pre-equilibration condition, then a time = 0 measurement period applying an optional shared wash condition plus a per-dose condition cond_<stem>_<i> that sets one swept parameter to dose i. This recovers each such group back to a swept-axis Data per 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-era experiment: <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 finite time = -T period instead of -inf; it is recognized only when every other period starts at exactly time = 0 (so T is the equilibration’s duration), and the scan carries equil_t_end = T.

Detection. An experiment is a pre-equilibrated dose-response point when (a) it has exactly one time = -inf period, or a finite leading time = -T < 0 period followed only by periods at time = 0 (a single pre-equilibration condition), (b) its measurements all share one time (the scan time), and (c) the condition cond_<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 (mirroring reconstruct_dose_responses()).

Other sources (#904). A problem PyBNF did not write names its dose conditions freely – petab.v2.petab1to2 turns a v1 preequilibrationConditionId + simulationConditionId pair into experiment experiment__<pre>___<sim> with periods -inf -> <pre> and 0 -> <sim>. So when cond_<eid> is absent, the measurement period’s lone condition, applied at time = 0, is the dose if it sets exactly one numeric target – the rule reconstruct_dose_responses() applies to a plain dose (there is no wash to tell apart) – and that target is in sweepable, the model parameters (a condition setting a species amount is a bolus, which a parameter_scan cannot 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 own preequilibrate: + 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 a condition: 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 / wash are condition names, wash None when the measurement period applies no shared condition; scan_time is inf for steady state; equil_t_end is None for a steady-state equilibration; datas is 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 become preequilibrate: / condition: lines via conditions_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 whose noiseParameters parameter 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-token noiseParameters tuple 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 whose observableParameters tuple 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 rows to path as a PEtab v2 measurements.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-empty model_id (a multi-model job); a single-model job stamps ''.

  • observableParameters (a row-varying observable scale/offset, ADR-0045) – emitted when some row carries an observable_parameters tuple, its tokens semicolon-joined.

The noiseParameters cell is the per-row parameter-id token of a row-varying noise placeholder (the sidecar source, ADR-0045) when set, else the numeric _SD value (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 conditionId prefix 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_value is 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 period CheckInitialChangeSymbols inspects.

class pybnf.petab.conditions.PetabMappingRow(petab_id: str, model_id: str)[source]

One row of a PEtab v2 mapping table: a petabEntityId -> modelEntityId alias.

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 condition targetId directly. The mapping row aliases a synthesized SId petab_id (species_target_id(), used as the target) to the verbatim BNGL model_id pattern; petab’s CheckValidConditionTargets admits 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: none condition it synthesizes for a time = -inf period 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_values becomes its own Condition setting the swept parameter and a single-period Experiment at time=0 (the dose is an initial condition; the measurement occurs later, at scan_time). dose_values is 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) where experiment_ids[i] is the experimentId for dose_values[i]; the exporter keys each data row to the id of that row’s own dose.

surrogate is 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 pins p = p__REF for every p in M, exactly as the time-course, wildtype and pre-equilibration builders do (#892). Without the pin a PEtab tool simulates every dose at p’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 (a condition: is named once; N experiment: records may reference it, so a shared condition emits its rows once).

experiments is a list of (experiment_name, condition_name_or_None) in declaration order. conditions maps a condition name to its perturbations [(var, op, val), ...] (val a float). fit_params is the set of model-parameter names that are fit; nominal_of(condition, var) returns a fixed parameter’s numeric nominal in the model condition belongs to (or None for 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 set M) – fit parameters perturbed by some referenced condition (an unused condition contributes nothing), unioned with extra_surrogate (fit params perturbed by a pre-equilibration condition in the same job – build_preequilibration_conditions()’s contribution, threaded in by the orchestrator so M stays problem-global across both experiment shapes). They are renamed to <p>__REF in the parameter table and pinned in every experiment’s Condition: M is 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 (conditionId cond_<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 pin p = p__REF for each p in M the condition does not itself set. Plus a shared synthesized base condition cond_wildtype (pinning all of M) when M is non-empty and some experiment is wildtype.

  • experiment_to_id – {experiment_name: experimentId}: the name for a conditioned experiment, or for a wildtype one when M is non-empty; '' (“model as is”) for a wildtype experiment when M is 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 leading time = -inf steady-state period under the shared pre-equilibration condition, then a time = 0 measurement period that applies BOTH the shared measurement (wash) condition AND a per-dose condition cond_<stem>_<i> setting the swept parameter to dose i. 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 exactly build_dose_response_conditions()’; the pre-equilibration and wash conditions are the job’s named condition: sets, whose species setConcentration targets are emitted through species_id_of (ADR-0062). Measurements are tagged <stem>_<i> at the scan time – identical to a plain dose-response, so dose_response_measurement_rows() pivots them unchanged.

experiments is 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_values is 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 a time = -T period 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_ids dedups a pre-equilibration or wash condition already emitted by another experiment shape.

surrogate is 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 -inf period sets every p in M: its own value where the condition perturbs p, else the base pin p = 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 inline setParameter and 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) where experiment_ids_by_name[name] is the ordered [<stem>_0, <stem>_1, ...] list (aligned with that experiment’s dose_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 = -inf period under the pre-equilibration condition (equilibrate to steady state, unmeasured) followed by a time = 0 period under the measurement condition (the data grid is measured there). A fixed-duration equilibration (equil_t_end: T) leads with a finite time = -T period instead, so the equilibration runs for exactly T before the measured period starts at 0 (#896, equilibration_period_time()). experiments is a list of (name, preequil_cond, measurement_cond_or_None, equil_t_end_or_None); conditions maps 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 for build_experiment_conditions() (the fit-vs-fixed split a target needs is carried by surrogate, below – a target in M is a surrogate-handled fit param, the rest are fixed).

surrogate is the problem-global M – 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 into M via build_experiment_conditions()’s extra_surrogate, so both builders share one M). The shared _condition_rows_for() re-pins all of M on 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 absolute k = 0.5) and every other period re-pins the base value (k = k__REF).

existing_condition_ids is the set of conditionId values build_experiment_conditions() already emitted (its time-course conditions plus the synthesized cond_wildtype base 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 conditionId on the time = 0 period when M is empty, else the synthesized base condition WILDTYPE_CONDITION_ID (re-pinning every removed fit param at its base value – the same base build_experiment_conditions() pins for a wildtype time course, emitted once and shared). The importer maps that base back to “no condition:” (a wash-out), so the round trip is preserved.

species_id_of ({pattern: petab_id}, ADR-0062) maps a species setConcentration wash 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: none pre-equilibration condition (an empty perturbation list, #906, ADR-0150) equilibrates the model as it stands, so it has no rows of its own: its -inf period takes the wash-out’s treatment – a blank conditionId when M is empty (PEtab v2’s “the model as is”), else WILDTYPE_CONDITION_ID, which re-pins M at base.

pybnf.petab.conditions.condition_name_from_id(condition_id)[source]

The new-era condition: name for a PEtab conditionId, or None.

None for an absent/blank id (the model as is). Otherwise the cond_ prefix is stripped (an externally-authored id without the prefix passes through unchanged, defensively) – except for WILDTYPE_CONDITION_ID, which keeps its literal id. By the time an id reaches here the importer has dropped a cond_wildtype that is only the exporter’s synthesized base (drop_synthesized_wildtype()), so one that is still present carries real targets. It is not named wildtype: the exporter reserves that name and would refuse to export the imported job, whereas a condition named cond_wildtype re-exports as cond_cond_wildtype and 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_params is the set of model-parameter names that are fit-and-perturbed (the <p>__REF surrogates, recovered from the parameter table by the orchestrator). Base pins (a row whose targetValue is 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_a and a, or cond_cond_wildtype and cond_wildtype). applied is the set of conditionIds some measured experiment applies (None: treat every id as applied). If two or more of the colliding ids are applied, PybnfError is 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 species setConcentration wash/bolus).

free_names (the estimated parameter-table ids) and fixed_params ({id: value} for the fixed ones) resolve a parameter-valued targetValue – a per-condition estimated initial condition (ADR-0076): a target set to a free-parameter id becomes a parameter reference (val a 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_ID to 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 a cond_wildtype made 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 blank conditionId, the PEtab spelling of “the model as is” that the reconstruction below already reads. A cond_wildtype with any other row carries real targets – a condition some other tool wrote, or a PyBNF condition named wildtype exported before the exporter reserved that name – and is left in place, to import under its literal id (condition_name_from_id()). Before #905 every cond_wildtype was 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_params is the set of model parameters with a <p>__REF surrogate.

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 exactly 0 (the measured period); anything else raises ValueError.

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 (no equil_t_end:, the ADR-0052 default) is -inf: equilibrate to steady state. A fixed duration T is 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 exactly T (#896). The exporter has already checked T is 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 in taken (a set of condition names and ids) nor, prefixed cond_, an id in it.

pybnf.petab.conditions.is_species_target(target)[source]

True iff a condition target is a BNGL species pattern (a setConcentration wash / 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) on t in [0, T] and restarts the clock at 0 for the measured phase (pset.py::_append_preequilibration_actions and the bngsim SBML backend’s _begin_preequilibration); a PEtab v2 leading period at -T runs 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 time symbol in a parameters, functions, reaction rules, or compartments block (time() in a function or rate law, or the index of a tfun(..., time) table function), and a lowercase tfun call with no index at all (bngsim indexes it by time by default). The legacy uppercase TFUN(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 PEtab targetValue string.

An absolute set (=) is the bare number, regardless of target kind. A relative op (* / + -) needs the base value:

  • Fit target – pass surrogate (the <p>__REF symbol): 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 with nominal is None (an expression-RHS / unknown nominal) raises NotImplementedError – evaluating a BNGL expression tree is simulation-grade work, out of scope (ADR-0026 precedent).

A parameter-reference value (val a string free-parameter id, not a number – a per-condition estimated initial condition, ADR-0076) emits that id verbatim as the targetValue for 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 = -inf period that applies no condition at a synthesized none condition (#906, ADR-0150).

In PEtab v2 a blank conditionId means “the model as is”. On the -inf period of a pre-equilibration that is an equilibration with nothing changed – a PyBNF perturbations: none condition applied as preequilibrate: – which is not the same thing as no pre-equilibration at all, the meaning None has 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 id cond_<name> for one name (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 declares name as a none condition. A period whose id condition_name_from_id() maps to None counts as applying no condition: a blank id, which includes the exporter’s base condition once drop_synthesized_wildtype() has blanked it (only when every row is a base pin). A cond_wildtype with 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 a condition: (a wash-out).

Returns (experiment_rows, name); name is None, and the rows are unchanged, when no period applies no condition.

pybnf.petab.conditions.read_condition_table(path)[source]

Read a PEtab v2 conditions.tsv into PetabConditionRow records (stdlib csv; targetValue kept as the raw string).

pybnf.petab.conditions.read_experiment_table(path)[source]

Read a PEtab v2 experiments.tsv into PetabExperimentRow records (stdlib csv; time coerced to float).

pybnf.petab.conditions.read_mapping_table(path)[source]

Read a PEtab v2 mapping table into PetabMappingRow records (stdlib csv).

Columns petabEntityId / modelEntityId; the modelEntityId is kept as its raw string (a BNGL species pattern, not an identifier). A row missing a petabEntityId raises.

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 -inf period into the synthesized perturbations: none condition name_unperturbed_equilibrations() names, and a named condition whose rows are all base pins p = p__REF into a none condition of its own. In PyBNF a none condition 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_params is the set M of parameters estimated through a <p>__REF surrogate; only the experiments in measured_ids are checked, since only they reach the conf.

  • A first period that leaves a parameter of M unset. Each p in 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 -inf period 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 a p to its model-file value, is left to #948.

  • A pins-only named condition after an earlier period changed what it pins. p = p__REF is the identity only while nothing earlier in the experiment set p. After a pre-equilibration that set k = 2, PEtab’s pin restores k to its estimate, which a none condition would not do. Importing such a re-pin exactly is #948. (The exporter’s pins-only cond_wildtype in that position is the older #948 case and is left as it is.)

Raises NotImplementedError naming 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 -T may carry measurements at times in [-T, 0), taken while the system equilibrates. PyBNF’s preequilibrate: + equil_t_end: T equilibration 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). times are 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 to species_<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 rows to path as a PEtab v2 conditions.tsv.

pybnf.petab.conditions.write_experiment_table(rows, path)[source]

Write experiment rows to path as a PEtab v2 experiments.tsv.

pybnf.petab.conditions.write_mapping_table(rows, path)[source]

Write mapping rows to path as a PEtab v2 mapping table (petabEntityId -> modelEntityId), the species-pattern alias of the species-amount condition targets (#477).

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 vectorized numpy callable (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 (the PEtab-math -> BNGL function body printer the importer once used to inject a begin functions entry).

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 body to a PEtab math expression string.

The exporter’s inlining mode (ADR-0035): a fitted function column emits its body as observableFormula instead of the bare name. The body is BNGL, and BNGL does not read arithmetic the way PEtab math does (-k^2 is (-k)^2 and a^b^c is (a^b)^c in BNGL, the opposite in PEtab), so it is parsed with BioNetGen’s grammar and printed as PEtab math with explicit parentheses (_bngl_math, #908). A g() reference to another global function or an observable becomes the bare symbol g (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_file only name the function in error messages.

Raises PybnfError on a missing petab extra, a malformed body, a body using an operator BioNetGen’s simulators refuse, an unknown free symbol, or a formula that disagrees with the body; NotImplementedError on 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, an if() 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 expression to (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 to compile_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_params is the set/iterable of declared free-parameter names. data_columns is 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))^2 references the free parameters vmax/km and the data columns x/y), which the model evaluates per row and sums (ExpressionModel.execute). With no bound data data_columns is 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).

backend selects the lambdify target: 'numpy' (the default) feeds the gradient-free score-column path (de / am / dream); 'jax' produces a JAX-traceable callable so job_type = hmc can jax.grad the user’s expression (ADR-0059 item 2). The parse, symbol validation, and ordered_names are 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 PybnfError on a missing petab extra, 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 observableFormula to (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 numpy callable 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_symbols is the model’s expression namespace (BNGL ParamList, or SBML species u parameters; ADR-0026/0036); detail overrides 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. Reuses sympify_petab + the shared free-symbol validation, then lambdifys with the numpy backend.

Raises PybnfError on a missing petab extra, an unparseable formula, or an unknown free symbol; NotImplementedError on 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 of compile_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.diff gives the partials exactly (the formula is already a parsed sympy tree), so the chain rule is the exact derivative of the same callable compile_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 sorted ordered_names the value callable expects. Every derivative callable takes the same positional arguments in the same ``ordered_names`` order as compile_petab_formula()’s value callable, so the caller binds one argument list once and evaluates f and every ∂f/∂symbol from 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 PybnfError on a missing petab extra, an unparseable formula, or an unknown free symbol; NotImplementedError on a per-measurement placeholder symbol the value compiler would also reject (it does not, for a registered PerMeasurementModel).

pybnf.petab.formula.formula_free_symbols(formula)[source]

The sorted free-symbol names of a PEtab math formula – the parameters a FormulaSigma (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 expression noiseFormula requires the fit to declare, before the callable is compiled. Raises PybnfError on a missing petab extra or an unparseable formula.

pybnf.petab.formula.inline_constants(formula, constants)[source]

Substitute fixed-parameter constants into a PEtab math observableFormula.

A PEtab observableFormula may reference a fixed parameter that lives only in the PEtab parameters table, not in the model file (Boehm’s specC17 = 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).

constants is 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 through sympy (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 of formula the original string is returned verbatim (the bare-name / model-only common case never reaches sympy, so the demo round trip is byte-stable).

Raises PybnfError on a missing petab extra, 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 observableFormula references down to the species/parameters they are defined over (#465 and #795, ADR-0036).

An SBML assignmentRule variable="X" makes X an algebraic function of other model entities, recomputed every step – never a simulation-output column and value-less, so the measurement layer cannot resolve X as a symbol (#464). But its RHS is resolvable, and SBML authors define exactly these convenience observables (the D2D Epo_cells := Epo_EpoRi + dEpoi), so observable: Epo_cells, formula: Epo_cells should 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 existing compile_petab_formula() + the chain-rule gradient (MeasurementModel.prediction_sensitivity()) handle unchanged: the rule’s species sensitivities flow in automatically.

The second construct is an initialAssignment on a parameter or compartment (#795). SBML lets one supersede the declared value/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’s beta_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, because MeasurementModel.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 as None.

derived maps every derived entity id to a DerivedSymbol carrying its kind, its RHS as a PEtab-math infix string (the stdlib _sbml scanner’s serialization) or None, and the clause naming why a None cannot 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 through sympy (never a string tokenizer – ADR-0033), guarded by the same numeric round-trip self-check as the exporter.

Raises PybnfError on a missing petab extra, an unparseable formula/RHS, a reference to a definition the scanner could not carry (None RHS), 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 PEtab observableParameter${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 an observableFormula; a sigma source for a noiseFormula).

substitutions maps 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 through sympy (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 reaches sympy). A placeholder not in substitutions is left in place (the caller validates it downstream).

Raises PybnfError on a missing petab extra, 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 (every optimizer + sampler; the check checker excluded) and writes one runnable imported_<jt>.conf per 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 every experiment: 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 drops method (no PEtab home), import defaults it to ode – 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_path into out_dir.

Reads the problem’s tables + model, reconstructs the experiments’ data, and writes a new-era PyBNF job: the .exp data 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 .conf files. The problem (parameters/priors, observables/noise, measurements, conditions/experiments) is recovered exactly, from every file each problem.yaml key lists (#902); the run-recipe (job_type, method, settings) is supplied by the caller (see the module docstring). Returns the out_dir path.

Each model copy is verbatim except for marked estimate = false overrides (#907, ADR-0149). A parameters-table row with estimate = false that names a model parameter fixes it at the row’s nominalValue, 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 raises PybnfError.

job_type is the SEARCH method token, or 'all' to emit one imported_<jt>.conf per registered optimizer + sampler. method (default 'ode') is the per-experiment SIMULATION method; method_overrides (a {experiment_name: method} map) sets per-experiment values. settings overrides 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 optional pybnf[petab] extra. A constant-per-observable observableParameters scale/offset and an expression noiseFormula are 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 + a parameter_scan experiment; a steady-state measurement with no swept axis (time = inf under a single condition) imports as an ordinary experiment whose .exp time is inf, 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 species setConcentration wash targets are aliased through the mapping table – is reconstructed into a preequilibrate: + condition: parameter_scan experiment (the species patterns recovered from the mapping). Raises NotImplementedError at the remaining PEtab/PyBNF boundaries (a model language other than bngl/sbml; the five unsupported prior families; a log-normal/log-laplace noise distribution; a multi-symbol condition expression – a single parameter-valued targetValue is a per-condition estimated initial condition and imports, ADR-0076; a row-varying per-measurement observableParameters/noiseParameters placeholder; a fixed SBML parameter an initial assignment, event assignment or algebraic rule also sets, or a fixed BNGL parameter the model’s actions set) and PybnfError for a malformed problem (an observableFormula symbol that is not a model entity, an ambiguous dose-response group, an id defined twice across a table’s files, a problem.yaml shape 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.yaml without 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 a models list – one {model_id, location, language} entry per model_files entry, in declaration order (one or many, ADR-0041). For single-model convenience the first model is also surfaced as model_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 - item lines, model_files last), the column-0 - item lists petab.v2.petab1to2 writes, keys in any order, and the one-line flow list key: [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 raises PybnfError naming the line or key rather than being skipped: a scalar where a list belongs, a flow list continued over several lines, a flow-form model_files entry, a key the PEtab v2 schema does not allow, a key or model given twice, a file listed twice under one key, a format_version other than 2 (#902). The scan used to skip what it did not recognize, so condition_files: [conditions.tsv] read as no condition table at all.

This is a pure reader: it records each model’s language but 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 simulate calls) while keeping generate_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’s generate_network key (#901). The exported model then carries its own cap, so import_.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 – the begin actions block and any loose action after end model – and the kept line goes in a minimal begin actions block at the end of the file, where the fitter writes it. setOption and 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 the begin functions block – 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 surrogate v1__REF (ADR-0027).

generate_network_options is the job’s generate_network key and experiment_methods maps the experiments on this model to their method: (_experiment_methods_by_model()); the None defaults mean no key and network-based experiments. model_file names the model in error messages.

A legacy <name>__FREE marker 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 undefined v1__FREE symbol 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 retired KD1__FREE form as a counter-example (receptor_v2.bngl does), 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_path to a PEtab v2 problem in out_dir.

Reads the job’s data/conditions/observables from the new-era surface (ADR-0028): a model: declaration, named experiment: lines carrying their data: files (a PEtab Experiment; multiple files = replicates), condition: perturbations (a PEtab Condition), and observable: column-header overrides. Writes parameters.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 the out_dir path.

inline_functions (default False) is the opt-in expression mode (ADR-0035): when set, a fitted function column emits its body as an observableFormula expression (translated to PEtab math, requires the pybnf[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, and petab-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()). Raises NotImplementedError at every documented boundary and PybnfError for 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.yaml referencing the tables and the model(s).

models is 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. language is each model’s PEtab language (bngl or sbml, ADR-0040) – emitted verbatim so every model declares its own native language (a BNGL + SBML mix is just two entries). has_mapping adds the mapping_files entry for the species-amount mapping table (ADR-0062).