diff --git a/petab/v2/core.py b/petab/v2/core.py index 22453878..fb206502 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( @@ -1820,12 +1824,66 @@ 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 not self.has_map_objective + def get_priors(self) -> dict[str, Distribution]: """Get prior distributions. - :returns: The prior distributions for the estimated parameters. + Note that this will default to uniform distributions over the + parameter bounds for parameters without an explicit prior. + + :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: Mapping of parameter IDs to distributions for sampling + startpoints. """ - 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: 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