Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions petab/v1/distributions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Probability distributions used by PEtab."""
"""Probability distributions used by PEtab.

This module provides a set of univariate probability distributions
that can be used for sampling and evaluating the probability density
function (PDF) and cumulative distribution function (CDF).
Most of these distributions also support log transformations and truncation.

Not all distributions that can be represented by these classes are valid
as PEtab parameter prior or noise distributions.
"""

from __future__ import annotations

Expand All @@ -19,9 +28,14 @@

__all__ = [
"Distribution",
"Cauchy",
"ChiSquare",
"Exponential",
"Gamma",
"Laplace",
"Normal",
"Rayleigh",
"Uniform",
"Laplace",
]


Expand Down
53 changes: 53 additions & 0 deletions petab/v2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
)
from typing_extensions import Self

from ..v1.distributions import *
from ..v1.lint import is_valid_identifier
from ..v1.math import petab_math_str, sympify_petab
from . import C, get_observable_df
Expand Down Expand Up @@ -150,6 +151,26 @@ class PriorDistribution(str, Enum):
f"{set(C.PRIOR_DISTRIBUTIONS)} vs { {e.value for e in PriorDistribution} }"
)

_prior_to_cls = {
PriorDistribution.CAUCHY: Cauchy,
PriorDistribution.CHI_SQUARED: ChiSquare,
PriorDistribution.EXPONENTIAL: Exponential,
PriorDistribution.GAMMA: Gamma,
PriorDistribution.LAPLACE: Laplace,
PriorDistribution.LOG10_NORMAL: Normal,
PriorDistribution.LOG_LAPLACE: Laplace,
PriorDistribution.LOG_NORMAL: Normal,
PriorDistribution.LOG_UNIFORM: Uniform,
PriorDistribution.NORMAL: Normal,
PriorDistribution.RAYLEIGH: Rayleigh,
PriorDistribution.UNIFORM: Uniform,
}

assert not (_mismatch := set(PriorDistribution) ^ set(_prior_to_cls)), (
"PriorDistribution enum does not match _prior_to_cls. "
f"Mismatches: {_mismatch}"
)


class Observable(BaseModel):
"""Observable definition."""
Expand Down Expand Up @@ -929,6 +950,38 @@ def _validate(self) -> Self:

return self

@property
def prior_dist(self) -> Distribution:
"""Get the pior distribution of the parameter."""
if self.estimate is False:
raise ValueError(f"Parameter `{self.id}' is not estimated.")

if self.prior_distribution is None:
return Uniform(self.lb, self.ub)

if not (cls := _prior_to_cls.get(self.prior_distribution)):
raise ValueError(
f"Prior distribution `{self.prior_distribution}' not "
"supported."
)

if str(self.prior_distribution).startswith("log-"):
log = True
elif str(self.prior_distribution).startswith("log10-"):
log = 10
else:
log = False

if cls == Exponential:
# `Exponential.__init__` does not accept the `log` parameter
if log is not False:
raise ValueError(
"Exponential distribution does not support log "
"transformation."
)
return cls(*self.prior_parameters, trunc=[self.lb, self.ub])
return cls(*self.prior_parameters, log=log, trunc=[self.lb, self.ub])


class ParameterTable(BaseModel):
"""PEtab parameter table."""
Expand Down
28 changes: 17 additions & 11 deletions petab/v2/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

import numpy as np
import pandas as pd
import sympy as sp
from pydantic import AnyUrl, BaseModel, Field
Expand All @@ -22,10 +23,10 @@
observables,
parameter_mapping,
parameters,
sampling,
yaml,
)
from ..v1.core import concat_tables, get_visualization_df
from ..v1.distributions import Distribution
from ..v1.models.model import Model, model_factory
from ..v1.yaml import get_path_prefix
from ..v2.C import * # noqa: F403
Expand Down Expand Up @@ -726,24 +727,29 @@ def get_optimization_to_simulation_parameter_mapping(self, **kwargs):
)
)

def sample_parameter_startpoints(self, n_starts: int = 100, **kwargs):
"""Create 2D array with starting points for optimization
def get_priors(self) -> dict[str, Distribution]:
"""Get prior distributions.

See :py:func:`petab.sample_parameter_startpoints`.
:returns: The prior distributions for the estimated parameters.
"""
return sampling.sample_parameter_startpoints(
self.parameter_df, n_starts=n_starts, **kwargs
)
return {
p.id: p.prior_dist
for p in self.parameter_table.parameters
if p.estimate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to drop non-estimated parameter priors, but then users might see unexpected posterior values. e.g. if they perform MAP then fix the parameters to those values and recompute, there will be an undocumented change in the posterior value because their estimate column has changed.
So perhaps priors + fixed parameters should be an error or warning?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I am not sure. It's effectively a different problem then (kind of like changing your prior to a Dirac delta?), so that a change in the posterior should be expected, isn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, a change can be expected, but then priors should be treated as an error since they are dropped, or?

Copy link
Member Author

@dweindl dweindl Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then priors should be treated as an error since they are dropped, or?

I'd say it shouldn't be an error.

It's the same as with bounds for fixed parameters.

Those are explicitly allowed to be specified with estimate=false (specs):

  • lowerBound [NUMERIC]

    Lower bound of the parameter used for estimation. Optional, if estimate==false.

  • upperBound [NUMERIC]

    Upper bound of the parameter used for estimation. Optional, if estimate==false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, same issue there, but the worst thing that happens there is that the user looks into their parameter estimates and sees that they can't find the parameter that they thought they estimated. In the priors case, it would be difficult to notice the mistake.

But you're right, and since it's nice to toggle parameters on or off without having to change other columns, fine to not have an error. As you like, but a warning/info/debug for unused priors could still be nice

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be part of some (yet to be implemented) warning mode for validation then, but I'd still find it somewhat confusing to treat priors different than bounds in that situation. Maybe that's something to leave to the parameter estimation tools.

}

def sample_parameter_startpoints(self, n_starts: int = 100, **kwargs):
"""Create 2D array with starting points for optimization"""
priors = self.get_priors()
return np.vstack([p.sample(n_starts) for p in priors.values()]).T

def sample_parameter_startpoints_dict(
self, n_starts: int = 100
) -> list[dict[str, float]]:
"""Create dictionaries with starting points for optimization

See also :py:func:`petab.sample_parameter_startpoints`.

Returns:
A list of dictionaries with parameter IDs mapping to samples
:returns:
A list of dictionaries with parameter IDs mapping to sampled
parameter values.
"""
return [
Expand Down
20 changes: 20 additions & 0 deletions tests/v2/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
TARGET_VALUE,
UPPER_BOUND,
)
from petab.v2.core import *


def test_load_remote():
Expand Down Expand Up @@ -170,3 +171,22 @@ def test_modify_problem():
}
).set_index([PETAB_ENTITY_ID])
assert_frame_equal(problem.mapping_df, exp_mapping_df, check_dtype=False)


def test_sample_startpoint_shape():
"""Test startpoint sampling."""
problem = Problem()
problem += Parameter(id="p1", estimate=True, lb=1, ub=2)
problem += Parameter(
id="p2",
estimate=True,
lb=2,
ub=3,
prior_distribution="normal",
prior_parameters=[2.5, 0.5],
)
problem += Parameter(id="p3", estimate=False, nominal_value=1)

n_starts = 10
sp = problem.sample_parameter_startpoints(n_starts=n_starts)
assert sp.shape == (n_starts, 2)