Skip to content

Commit fd3334d

Browse files
dweindlclaude
andcommitted
Skip log-scaled parameters with a lower bound of 0 in petab1to2
PEtab v1 allows a lower bound of 0 for log-scaled parameters if an initialization prior is given. On log scale, this would be `-inf`, for which `reparametrize_log` raises, and neither the v1 default prior nor `parameterScaleUniform` would be proper. Instead of failing the whole conversion, keep such parameters on linear scale and warn. Generated with AI assistance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 83d3402 commit fd3334d

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

‎petab/v2/petab1to2.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def petab1to2(
8888
8989
Further log-scaled parameters are reparametrized if they share
9090
a placeholder with reparametrized parameters in the measurement table.
91+
Parameters with a lower bound of ``0`` are not reparametrized.
9192
9293
:raises ValueError:
9394
If the input is invalid or, if ``validate`` is ``True``, if the input
@@ -763,6 +764,8 @@ def _reparametrize_log_scaled(
763764
# parameters with parameter-scale priors
764765
required = {10: set(), "e": set()}
765766
selected = {10: set(), "e": set()}
767+
# selected parameters that cannot be reparametrized due to their bounds
768+
zero_bound = []
766769
for par_id, row in df.iterrows():
767770
pscale = row.get(v1.C.PARAMETER_SCALE, v1.C.LIN)
768771
if pscale not in (v1.C.LOG, v1.C.LOG10):
@@ -776,6 +779,18 @@ def _reparametrize_log_scaled(
776779
# A prior over the unscaled parameter. Its conversion already
777780
# preserves the objective function.
778781
continue
782+
if (
783+
row.get(v1.C.LOWER_BOUND, 1) <= 0
784+
or row.get(v1.C.UPPER_BOUND, 1) <= 0
785+
):
786+
# v1 allows a lower bound of 0 for log-scaled parameters.
787+
# This would be -inf on log scale, where neither the v1 default
788+
# prior nor a parameterScaleUniform prior are proper.
789+
if int(row[v1.C.ESTIMATE]) and (
790+
reparametrize == "all" or prior_type is not None
791+
):
792+
zero_bound.append(par_id)
793+
continue
779794

780795
prior = None
781796
if not int(row[v1.C.ESTIMATE]):
@@ -815,6 +830,17 @@ def _reparametrize_log_scaled(
815830
):
816831
selected[base].add(par_id)
817832

833+
if zero_bound:
834+
warnings.warn(
835+
f"Log-scaled parameters {', '.join(zero_bound)} were not "
836+
"reparametrized, because their bounds are not positive. "
837+
"They are estimated on linear scale, and their parameter-scale "
838+
"priors were converted to the corresponding priors of the "
839+
"unscaled parameters, which changes the maximum a posteriori "
840+
"estimate.",
841+
stacklevel=4,
842+
)
843+
818844
# Placeholders can only be transformed if all their overrides are
819845
# reparametrized or positive numbers. Add log-scaled parameters that
820846
# share a placeholder with selected parameters. If that is not possible,

‎tests/v2/test_conversion.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,70 @@ def test_petab1to2_reparametrize_priors(
363363
np.testing.assert_allclose(v2_pdf, v1_pdf, rtol=1e-12)
364364

365365

366+
@pytest.mark.filterwarnings(
367+
"ignore:.*Parameter scales are not supported in PEtab v2.*:UserWarning",
368+
"ignore:Initialisation priors.*:UserWarning",
369+
)
370+
@pytest.mark.parametrize(
371+
"prior_type,prior_parameters,reparametrize",
372+
[
373+
(v1.C.PARAMETER_SCALE_NORMAL, "-1;0.5", "priors"),
374+
(v1.C.PARAMETER_SCALE_NORMAL, "-1;0.5", "all"),
375+
(np.nan, np.nan, "all"),
376+
],
377+
)
378+
def test_petab1to2_reparametrize_zero_lower_bound(
379+
tmp_path, prior_type, prior_parameters, reparametrize
380+
):
381+
"""Parameters with a lower bound of 0 are skipped."""
382+
# v1 allows a lower bound of 0 for log-scaled parameters
383+
# if there is an initialization prior
384+
k1 = _K1 | {
385+
v1.C.PARAMETER_SCALE: v1.C.LOG10,
386+
v1.C.LOWER_BOUND: 0.0,
387+
v1.C.INITIALIZATION_PRIOR_TYPE: v1.C.PARAMETER_SCALE_UNIFORM,
388+
v1.C.INITIALIZATION_PRIOR_PARAMETERS: "-3;2",
389+
v1.C.OBJECTIVE_PRIOR_TYPE: prior_type,
390+
v1.C.OBJECTIVE_PRIOR_PARAMETERS: prior_parameters,
391+
}
392+
yaml_file = _write_v1_problem(
393+
tmp_path,
394+
observables=[
395+
{
396+
v1.C.OBSERVABLE_ID: "obs_B",
397+
v1.C.OBSERVABLE_FORMULA: "B",
398+
v1.C.NOISE_FORMULA: "0.1",
399+
}
400+
],
401+
measurements=[
402+
{
403+
v1.C.OBSERVABLE_ID: "obs_B",
404+
v1.C.SIMULATION_CONDITION_ID: "c0",
405+
v1.C.TIME: 1.0,
406+
v1.C.MEASUREMENT: 0.3,
407+
}
408+
],
409+
parameters=[k1],
410+
)
411+
assert not v1.lint_problem(v1.Problem.from_yaml(yaml_file))
412+
413+
with pytest.warns(UserWarning, match=r"k1 were not reparametrized"):
414+
problem = petab1to2(yaml_file, reparametrize=reparametrize)
415+
416+
assert [p.id for p in problem.parameters] == ["k1"]
417+
par = problem["k1"]
418+
assert (par.lb, par.ub) == (0.0, _K1[v1.C.UPPER_BOUND])
419+
if pd.isna(prior_type):
420+
# as with reparametrize="none"
421+
assert par.prior_distribution == v2.C.UNIFORM
422+
assert par.prior_parameters == [0.0, _K1[v1.C.UPPER_BOUND]]
423+
else:
424+
assert par.prior_distribution == v2.C.LOG_NORMAL
425+
sbml_model = problem.model.sbml_model
426+
assert sbml_model.getInitialAssignmentBySymbol("k1") is None
427+
assert sbml_model.getAssignmentRuleByVariable("k1") is None
428+
429+
366430
def _write_v1_problem_with_overrides(
367431
tmp_path, parameters: list[dict], overrides: list[str]
368432
) -> str:

0 commit comments

Comments
 (0)