PyBNF measurement models (pybnf.measurement)¶
The pybnf.measurement package holds PyBNF’s measurement models –
the post-simulation observation layer that transforms a simulation’s output
trajectory (plus the current parameter values) into the quantity compared
against data, a PEtab observableFormula (see the Measurement Model entry
in CONTEXT.md). Each measurement model is materialized into the simulated
Data before the objective scores it, never by editing the model file, so
the layer is backend- and language-agnostic. It is the M2 peer of
pybnf.priors and pybnf.noise (ADR-0036).
For the user-facing view – how a PEtab observableFormula becomes a
measurement model and round-trips through import/export – see PEtab interoperability.
The measurement-model observation layer (issue #407, ADR-0036).
A PEtab observableFormula is a measurement model – a function from the simulation
output trajectory (+ the current parameter values) to the quantity compared against data.
It is an observation-layer concept, not part of the dynamical model. PyBNF evaluates it as
a post-simulation transform over the output trajectory, never by editing a model file:
MeasurementModel– one named measurement model: anobservable_idand anobservableFormula(PEtab math), compiled lazily to a vectorizednumpycallable over the trajectory’s columns + the PSet.MeasurementLayer– the ordered collection and the(sim_data_dict, pset_values) -> sim_data_dicttransform the objective applies before it scores (the empty layer is an exact no-op; ADR-0036 §2).
Because it sits downstream of simulation, the layer is backend-agnostic (RoadRunner,
bngsim, the legacy BNG stack all just produce a trajectory) and language-agnostic (one
mechanism for BNGL and SBML), and it carries the model file verbatim. It is the missing
M2 peer to Prior (ADR-0010) and the noise model (ADR-0011).
petab/sympy (the optional pybnf[petab] extra) is imported lazily, only when a
formula is compiled (the first MeasurementModel.materialize()); the bare-name
observableFormula common case never becomes a MeasurementModel and stays
dependency-free (ADR-0019/0036).
- class pybnf.measurement.base.MeasurementLayer(models=())[source]¶
The ordered measurement models + the
(sim_data_dict, pset_values)transform.apply()walks every(model, suffix)Datain the simulation output and materializes eachMeasurementModel’s column into it in place, before the objective’s by-name column match. The empty layer is an exact no-op – the byte-identical default for every job with no expression measurement model (ADR-0036 §2). Adding a column is additive (existing columns are untouched); anobservableIdthat shadows an existing output column raises rather than silently overwriting it.
- class pybnf.measurement.base.MeasurementModel(observable_id, formula, allowed_symbols, constants=None)[source]¶
One named PEtab measurement model:
observable_id=formulaevaluated post-sim.formulais a PEtab mathobservableFormulaover the model’s expression namespace (allowed_symbols– the BNGLParamListor SBML species u parameters, ADR-0026/0036).constantscarries fixed model-parameter values (a numeric BNGL parameter RHS, or an SBMLparameter/compartmentvalue) snapshotted by the loader, for symbols that are neither an output column nor a free parameter.At
materialize()each free symbol resolves, in order, to a trajectory column (species / observable / global function /time– vectorized over the time axis), a PSet value (a free / estimated parameter – broadcast), or a fixed constant (broadcast). The compiled callable is built lazily on first use and excluded from pickling (alambdifyd callable is not picklable, and the objective carrying the layer is scattered to workers; ADR-0036 §5 – the compile-once-per-worker pattern).- materialize(data, pset_values)[source]¶
Evaluate this measurement model over one trajectory
data-> a column vector.datais aData(one(model, suffix)simulation output);pset_valuesis the{name: value}map of the current PSet. Returns a 1-Dnumpyarray of lengthdata.data.shape[0](the number of output rows). A formula over only scalars (no trajectory column) is broadcast to that length.
- prediction_sensitivity(data, sim_row, pset_values, raw_sens, index)[source]¶
The native-space
∂(materialized column)/∂θ(a(len(index),)vector) of this measurement model at one trajectory row (#455/#385) – the gradient sibling ofmaterialize(), resolving each symbol the same way.A materialized observable is
f(symbol values)over sim-output columns + the PSet, so by the chain rule∂(col)/∂θ = Σ_symbol (∂f/∂symbol)·(∂symbol/∂θ):a sim-output column symbol chains through its sensitivity,
∂f/∂col · raw_sens(col, row)–raw_sens(column_name, row)is the #447 tensor read at that row with anyData-level normalization already folded in (the assembly’s accessor), so a formula over the raw species/observables of any backend (SBML/Antimony species, ADR-0036) differentiates through their forward sensitivities;a directly-named free parameter (a free symbol resolved from the PSet that the fit declares) contributes
∂f/∂paramstraight to that parameter’s column – a parameter that enters the observation model, e.g. a scale/offset estimated as a fit parameter;a fixed model constant (a snapshotted numeric parameter) and the independent variable contribute nothing.
Both paths sum, so a parameter that both appears in the formula and moves the simulation gets its explicit
∂f/∂paramterm plus the indirect∂f/∂col·∂col/∂paramterms – the complete total derivative. MirrorsPerMeasurementModel.prediction_sensitivity()(the row-varying sibling), without the per-row placeholder branch a constant-per-observable model never has.
- class pybnf.measurement.base.PerMeasurementModel(observable_id, formula, allowed_symbols, constants=None)[source]¶
A measurement model whose
observableFormulareferences a row-varying PEtab per-measurement placeholder, bound per data point from the experimental data’s binding table (the observable-side sibling ofPerMeasurementFormulaSigma, ADR-0045).A
MeasurementModelcovers a constant-per-observable scale/offset: it is pre-materialized once over the simulation trajectory byMeasurementLayer. When theobservableParameterstoken instead differs row to row – a per-condition or per-timepoint scale – there is no single column to materialize: the value must be evaluated per data point, after the sim<->data match, with that row’s token in hand. So this model is not placed in the layer; it is registered on the objective (_per_measurement_models) and evaluated in the objective’s prediction step, whereexp_rowis known.At
value()each free symbol of the (cached, lambdified) formula resolves to: a placeholder -> the row’s token fromexp_data.measurement_params[col_name][placeholder][exp_row](a number inlines, a parameter id resolves from the PSet – a per-row estimated nuisance, ADR-0034); a simulation-output column -> that column’s value atsim_row(unlikeMeasurementModel, which is vectorized over the whole column, this reads one matched point); a PSet value; or a fixed model constant. The compiled callable is built lazily and dropped on pickling (the compile-once-per-worker pattern, ADR-0036 §5); the binding table rides the experimentalData, so it survives scatter independently of this model.- prediction_sensitivity(sim_data, sim_row, exp_data, exp_row, col_name, pset_values, raw_sens, index)[source]¶
The native-space
∂pred/∂θ(an(len(index),)vector) of this per-measurement measurement model at one matched point (#453/#385) – the gradient sibling ofvalue(), resolving each symbol the same way.raw_sens(column_name, sim_row)supplies∂(that sim column as _prediction sees it)/∂θ(the #447 sensitivity tensor, with anyData-level normalization already folded in);indexmaps a free-parameter name to its column in the returned vector. The prediction isf(symbol values), so by the chain rule∂pred/∂θ = Σ_symbol (∂f/∂symbol)·(∂symbol/∂θ):a sim-output column symbol chains through its sensitivity (
∂f/∂col · raw_sens);a placeholder bound to a free-parameter id, or a directly-named free parameter, contributes
∂f/∂symbolstraight to that parameter’s column – a per-row estimated nuisance / scale (ADR-0034). Unlike a free σ (layer D, which is model-unbound and lands only on the scalar path), such a parameter genuinely enters∂pred/∂θ, so it belongs in the residual-Jacobian (the column it lands in is a square);a numeric token, a fixed model constant, or the independent variable contributes nothing.
Both paths sum, so a free parameter that appears in the formula and moves the simulation (named directly while also driving a referenced column) gets both its explicit
∂f/∂paramterm and the indirect∂f/∂col·∂col/∂paramterms – the complete total derivative.
- value(sim_data, sim_row, exp_data, exp_row, col_name, pset_values)[source]¶
The measurement-model prediction for one matched data point – a scalar.
sim_data/sim_rowlocate the matched simulation point (trajectory columns read there);exp_data/exp_row/col_namelocate the data row (its placeholder token(s) read fromexp_data.measurement_params[col_name]);pset_valuesis the objective’s{name: value}PSet map (free parameters + per-row estimated nuisances).