Skip to content

feat(petab): scale-preserving v1→v2 conversion (petab1to2_preserve_scale)#491

Merged
wshlavacek merged 1 commit into
mainfrom
feat/petab-v1to2-preserve-scale
Jul 18, 2026
Merged

feat(petab): scale-preserving v1→v2 conversion (petab1to2_preserve_scale)#491
wshlavacek merged 1 commit into
mainfrom
feat/petab-v1to2-preserve-scale

Conversation

@wshlavacek

Copy link
Copy Markdown
Collaborator

What

Adds pybnf.petab.petab1to2_preserve_scale — a scale-preserving PEtab v1 → v2 converter.

Why

The official petab.v2.petab1to2 drops the v1 parameterScale column (PEtab v2 removed it by
design) and only warns. For a parameterScale = log10 estimated parameter carrying no objective
prior — the common case for a multi-decade kinetic parameter — the converted v2 problem is read as a
linear uniform_var over the raw bounds. That is the same argmin, but a far harder,
worse-conditioned optimization than the log10 search the modeler specified — so a tool that imports
the converted problem naively optimizes a different (much harder) problem than intended.

Fix

petab1to2_preserve_scale runs the standard converter, then re-injects the dropped estimation scale
in the v2-native form: each bare log/log10 estimated parameter gets
priorDistribution = log-uniform over its bounds, which PyBNF imports as a loguniform_var on the
Log10 scale. Because PyBNF's optimizer objective excludes the prior (base.py), this sets only the
search scale and initial sampling — not the objective — so the fit stays the pure-MLE problem v1
specified. Parameters petab1to2 already folded into a prior (parameterScale*Normallog-normal,
…) and linear parameters are left untouched.

import_job stays a pure v2 importer — the conversion is an explicit, named step, not a
reach-back to v1 in the read path.

Tests

tests/test_petab_convert.py (17 cases): a log/log10 param gains log-uniform over its bounds, a
linear param does not, an existing prior is never clobbered, and the float64-column coercion is
pinned. Full tests/test_petab_import.py + test_petab_convert.py green (122 passed).

Verified end to end

On the PEtab benchmark collection: Boehm/Perelson/Sneyd now import loguniform_var (0 linear) and
gradient-fit in log10; Borghans, previously a CVODES blowup from a linear box-center start over
10 decades, now fits. Intended as the opt-in migration petab1to2 itself should offer upstream.

…cale)

petab.v2.petab1to2 drops the v1 parameterScale column (removed in v2) with only a warning, so a bare parameterScale=log10 estimated parameter converts to a linear uniform_var -- the same argmin but a far harder, worse-conditioned optimization than the log10 search the modeler specified. This wrapper runs the standard converter, then re-injects the dropped scale in the v2-native form (priorDistribution=log-uniform over the bounds), which PyBNF imports as loguniform_var on the Log10 scale. The optimizer objective excludes the prior, so this sets only the search scale and initial sampling, keeping the pure-MLE problem v1 specified. Priors petab1to2 already carried (parameterScale*Normal etc.) and linear parameters are left untouched. import_job stays a pure v2 importer.
@wshlavacek
wshlavacek merged commit c62d921 into main Jul 18, 2026
8 checks passed
wshlavacek added a commit that referenced this pull request Jul 19, 2026
…vables import as lognormal (#499, ADR-0073)

petab.v2.petab1to2 drops a v1 observableTransformation=log10 (it collapses log10-normal to a blank noiseDistribution), so a log10 observable imported as a linear gaussian and the fit optimized the WRONG objective -- a linear residual with no change-of-variables Jacobian instead of the log10 residual the problem (and the paper) specify. Affects Perelson_Science1996, Borghans_BiophysChem1997, Elowitz_Nature2000 and other multi-decade-signal problems.

Directly parallel to the parameterScale drop petab1to2_preserve_scale (#491) already fixes:

- petab1to2_preserve_scale now ALSO re-injects observableTransformation as a preserved extra column on the converted v2 observables table (v2 lint-clean; other tools ignore it). Since v2 has no faithful log10 noiseDistribution, this extra column is the only channel for a log10 residual -- the observable-axis twin of the log-uniform parameter-scale re-injection.
- The importer (import_.py) and the observables.py adapter select the noise family's ADDITIVE SCALE from observableTransformation, not just the family from noiseDistribution: log10 + normal -> the native lognormal family (Gaussian(LOG10, MEDIAN), which already carries the log10-space residual AND the Jacobian) -> objective = lognormal / a noise_model = lognormal line; log + normal -> Gaussian(LN). Guards against a transformation that contradicts a log noiseDistribution; families with no native token (natural-log, any log laplace) still raise NotImplementedError. A linear problem is byte-for-byte unchanged.

ADR-0073 scopes this as an exception to ADR-0023 (which correctly rejected reading observableTransformation for NATIVE v2 problems): the column is a PyBNF-specific converter channel, not a claim that observableTransformation is standard v2.

Verified end to end (v1 -> convert -> import -> the built objective's noise is Gaussian(LOG10)) plus the boundary raises. 1271 passed across the petab/objective/noise/config suites; ruff clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant