From acbbc6870facf7e31f6ad983d2f03420bb36b23f Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 3 Dec 2025 09:37:19 +0100 Subject: [PATCH 1/4] Add `v2.Problem.has_{map,ml}_objective` To check for the type of objective function encoded in the PEtab problem. --- petab/v2/core.py | 35 +++++++++++++++++++++++++++++++++++ tests/v2/test_core.py | 19 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/petab/v2/core.py b/petab/v2/core.py index 22453878..ed31f508 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1820,9 +1820,44 @@ def x_fixed_indices(self) -> list[int]: """Parameter table non-estimated parameter indices.""" return [i for i, p in enumerate(self.parameters) if not p.estimate] + @property + def has_map_objective(self) -> bool: + """Whether this problem encodes a maximum a posteriori (MAP) objective. + + A PEtab problem is considered to have a MAP objective if there is a + prior distribution specified for at least one estimated parameter. + + :returns: ``True`` if MAP objective, ``False`` otherwise. + """ + return any( + p.prior_distribution is not None + for p in self.parameters + if p.estimate + ) + + @property + def has_ml_objective(self) -> bool: + """Whether this problem encodes a maximum likelihood (ML) objective. + + A PEtab problem is considered to have an ML objective if there are no + prior distributions specified for any estimated parameters. + + :returns: ``True`` if ML objective, ``False`` otherwise. + """ + return all( + p.prior_distribution is None for p in self.parameters if p.estimate + ) + def get_priors(self) -> dict[str, Distribution]: """Get prior distributions. + Note that this will default to uniform distributions over the + parameter bounds for parameters without an explicit prior. + + For checking whether this :class:`Problem` encodes a MAP or ML + objective, use :attr:`Problem.has_map_objective` or + :attr:`Problem.has_ml_objective`. + :returns: The prior distributions for the estimated parameters. """ return {p.id: p.prior_dist for p in self.parameters if p.estimate} diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index 060fbb32..2cbe3e46 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -866,3 +866,22 @@ def test_mapping_validation(): # identity mapping is valid Mapping(petab_id="valid_id", model_id="valid_id", name="some name") + + +def test_objective_type(): + """Test that MAP and ML problems are recognized correctly.""" + problem = Problem() + problem += Parameter(id="par1", lb=0, ub=100, estimate=True) + assert problem.has_ml_objective is True + assert problem.has_map_objective is False + + problem += Parameter( + id="par2", + lb=0, + ub=100, + estimate=True, + prior_distribution="normal", + prior_parameters=[50, 10], + ) + assert problem.has_map_objective is True + assert problem.has_ml_objective is False From 784f074c2c5a97dbe519de126c84a877860085b8 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 11 Dec 2025 09:21:12 +0100 Subject: [PATCH 2/4] Update petab/v2/core.py Co-authored-by: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> --- petab/v2/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index ed31f508..cc19f82b 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1844,9 +1844,7 @@ def has_ml_objective(self) -> bool: :returns: ``True`` if ML objective, ``False`` otherwise. """ - return all( - p.prior_distribution is None for p in self.parameters if p.estimate - ) + return not self.has_map_objective def get_priors(self) -> dict[str, Distribution]: """Get prior distributions. From a77f7669f3f77b5f11fbcb6bb2a138f45f620dab Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 11 Dec 2025 10:32:16 +0100 Subject: [PATCH 3/4] No implicit prior in Parameter.prior_dist --- petab/v2/core.py | 18 +++++++++++++----- petab/v2/lint.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index cc19f82b..2d7e5b88 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1026,13 +1026,17 @@ def _validate(self) -> Self: return self @property - def prior_dist(self) -> Distribution: - """Get the prior distribution of the parameter.""" - if self.estimate is False: + def prior_dist(self) -> Distribution | None: + """Get the prior distribution of the parameter. + + :return: The prior distribution of the parameter, or None if no prior + distribution is set. + """ + if not self.estimate: raise ValueError(f"Parameter `{self.id}' is not estimated.") if self.prior_distribution is None: - return Uniform(self.lb, self.ub) + return None if not (cls := _prior_to_cls.get(self.prior_distribution)): raise ValueError( @@ -1858,7 +1862,11 @@ def get_priors(self) -> dict[str, Distribution]: :returns: The prior distributions for the estimated parameters. """ - return {p.id: p.prior_dist for p in self.parameters if p.estimate} + return { + p.id: p.prior_dist if p.prior_distribution else Uniform(p.lb, p.ub) + for p in self.parameters + if p.estimate + } def sample_parameter_startpoints(self, n_starts: int = 100, **kwargs): """Create 2D array with starting points for optimization""" diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 1fe83144..687d58f2 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -843,7 +843,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: # TODO: check distribution parameter domains more specifically try: - if parameter.estimate: + if parameter.estimate and parameter.prior_dist is not None: # .prior_dist fails for non-estimated parameters _ = parameter.prior_dist.sample(1) except Exception as e: From b9e087c94319487831f1550d315b14b441bd8a33 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 11 Dec 2025 10:38:10 +0100 Subject: [PATCH 4/4] Separate startpoints and priors --- petab/v2/core.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 2d7e5b88..fb206502 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1856,11 +1856,28 @@ def get_priors(self) -> dict[str, Distribution]: Note that this will default to uniform distributions over the parameter bounds for parameters without an explicit prior. - For checking whether this :class:`Problem` encodes a MAP or ML - objective, use :attr:`Problem.has_map_objective` or - :attr:`Problem.has_ml_objective`. + :returns: The prior distributions for the estimated parameters in case + the problem has a MAP objective, an empty dictionary otherwise. + """ + if not self.has_map_objective: + return {} + + return { + p.id: p.prior_dist if p.prior_distribution else Uniform(p.lb, p.ub) + for p in self.parameters + if p.estimate + } + + def get_startpoint_distributions(self) -> dict[str, Distribution]: + """Get distributions for sampling startpoints. + + The distributions are the prior distributions for estimated parameters + that have a prior distribution defined, and uniform distributions + over the parameter bounds for estimated parameters without an explicit + prior. - :returns: The prior distributions for the estimated parameters. + :returns: Mapping of parameter IDs to distributions for sampling + startpoints. """ return { p.id: p.prior_dist if p.prior_distribution else Uniform(p.lb, p.ub)