Skip to content

PEtab import: observableTransformation=log10 dropped in v1->v2 conversion → log10 observable imports as linear gaussian #499

Description

@wshlavacek

Summary

A PEtab v1 observable with observableTransformation = log10 imports as a linear gaussian
noise model, so the fit optimizes the wrong objective (linear residual instead of log10 residual, and
no change-of-variables Jacobian). The log10 transformation is dropped during the v1→v2 conversion —
directly parallel to the parameterScale drop that petab1to2_preserve_scale (#491) was written to fix.

Root cause (verified on main)

petab.v2.petab1to2 drops the v1 observableTransformation column. Converting Perelson_Science1996
(v1 observable: observableTransformation = log10, noiseDistribution = normal) via
pybnf.petab.petab1to2_preserve_scale yields a v2 observables table with a blank noiseDistribution
and no transformation column:

observableId               observableFormula  noiseDistribution  noiseFormula ...
task0_model0_perelson1_V   V                  (blank)            noiseParameter1_...

import_job then reads noiseDistribution only (import_.py:_resolve_noiserow.noise_distribution;
_PETAB_DISTRIBUTION_TO_NOISE_MODEL @587 has no scale axis), so a blank/normal distribution imports as
Gaussian(LINEAR) — the emitted conf line is noise_model = gaussian, sigma = fit <p>.

Complication (v2 spec gap). PyBNF's v2 observables adapter (pybnf/petab/observables.py) documents
that PEtab v2 removed observableTransformation and folded it into noiseDistribution as natural-log
prefixes (log-normal = Gaussian(LN)), with no log10 form. But PyBNF's native lognormal conf
token is log10 (objective.py:731Gaussian(additive_on=LOG10)). So even if the converter emitted
log-normal, it would give natural-log, not the base-10 the v1 problem (and the paper) use. v1 log10
therefore has no faithful PEtab-v2 noiseDistribution representation.

Impact

Three of the six gradient-fittable Grein et al. 2026 benchmark problems have observableTransformation = log10 and are affected: Perelson_Science1996, Borghans_BiophysChem1997, Elowitz_Nature2000. The other
three (Boehm, Sneyd, Okuonghae) are lin and import correctly.

Worked example (Perelson): the linear import's global minimum is J_paper = 232.3 (OG ≈ 10 — the model,
a monotone decay, cannot beat σ̂ ≈ 4.9e5 on the linear scale). Correcting gaussian → lognormal by
hand (Gaussian(additive_on=LOG10)) gives -log_likelihood = 222.2807694, matching the reference
J* = 222.2807689 to OG = 5e-7. An independent roadrunner reconstruction confirms only the log10
scale plus the data Jacobian
Σ log(y_obs·ln10) reproduces J* (linear → 232.3, log10-without-Jacobian
→ −9.6, log10+Jacobian → 222.3). PyBNF's lognormal lnL already includes that Jacobian, so once the
family is right the score is exact.

Proposed fix

Extend the scale-preserving conversion (as petab1to2_preserve_scale did for parameterScale, #491) to
re-inject observableTransformation, and have the importer route it to the native families:

v1 observableTransformation + noiseDistribution PyBNF family
lin + normal gaussian = Gaussian(LINEAR) (current)
log10 + normal native lognormal = Gaussian(LOG10)
log + normal Gaussian(LN) (the v2 log-normal mapping)
(analogously for laplace) laplace / log10-laplace / Laplace(LN)

i.e. select the noise family's additive scale from observableTransformation, not just the family
from noiseDistribution. This also closes the v2-spec gap that log-normal currently means only
natural-log while the native lognormal token means log10.

Context

Part of the subset-I benchmark build (sibling of #492#496; tracked in wshlavacek/BNGL-Models#3).
Affects Perelson/Borghans/Elowitz; hand-corrected in those job confs pending this fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions