Skip to content

Commit f0c3075

Browse files
dweindldilpath
andauthored
Add v2.Problem.has_{map,ml}_objective (#463)
* Add `v2.Problem.has_{map,ml}_objective` To check for the type of objective function encoded in the PEtab problem. * No implicit prior in Parameter.prior_dist * Separate startpoints and priors --------- Co-authored-by: Dilan Pathirana <[email protected]>
1 parent fd1fa76 commit f0c3075

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

petab/v2/core.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,13 +1026,17 @@ def _validate(self) -> Self:
10261026
return self
10271027

10281028
@property
1029-
def prior_dist(self) -> Distribution:
1030-
"""Get the prior distribution of the parameter."""
1031-
if self.estimate is False:
1029+
def prior_dist(self) -> Distribution | None:
1030+
"""Get the prior distribution of the parameter.
1031+
1032+
:return: The prior distribution of the parameter, or None if no prior
1033+
distribution is set.
1034+
"""
1035+
if not self.estimate:
10321036
raise ValueError(f"Parameter `{self.id}' is not estimated.")
10331037

10341038
if self.prior_distribution is None:
1035-
return Uniform(self.lb, self.ub)
1039+
return None
10361040

10371041
if not (cls := _prior_to_cls.get(self.prior_distribution)):
10381042
raise ValueError(
@@ -1820,12 +1824,66 @@ def x_fixed_indices(self) -> list[int]:
18201824
"""Parameter table non-estimated parameter indices."""
18211825
return [i for i, p in enumerate(self.parameters) if not p.estimate]
18221826

1827+
@property
1828+
def has_map_objective(self) -> bool:
1829+
"""Whether this problem encodes a maximum a posteriori (MAP) objective.
1830+
1831+
A PEtab problem is considered to have a MAP objective if there is a
1832+
prior distribution specified for at least one estimated parameter.
1833+
1834+
:returns: ``True`` if MAP objective, ``False`` otherwise.
1835+
"""
1836+
return any(
1837+
p.prior_distribution is not None
1838+
for p in self.parameters
1839+
if p.estimate
1840+
)
1841+
1842+
@property
1843+
def has_ml_objective(self) -> bool:
1844+
"""Whether this problem encodes a maximum likelihood (ML) objective.
1845+
1846+
A PEtab problem is considered to have an ML objective if there are no
1847+
prior distributions specified for any estimated parameters.
1848+
1849+
:returns: ``True`` if ML objective, ``False`` otherwise.
1850+
"""
1851+
return not self.has_map_objective
1852+
18231853
def get_priors(self) -> dict[str, Distribution]:
18241854
"""Get prior distributions.
18251855
1826-
:returns: The prior distributions for the estimated parameters.
1856+
Note that this will default to uniform distributions over the
1857+
parameter bounds for parameters without an explicit prior.
1858+
1859+
:returns: The prior distributions for the estimated parameters in case
1860+
the problem has a MAP objective, an empty dictionary otherwise.
1861+
"""
1862+
if not self.has_map_objective:
1863+
return {}
1864+
1865+
return {
1866+
p.id: p.prior_dist if p.prior_distribution else Uniform(p.lb, p.ub)
1867+
for p in self.parameters
1868+
if p.estimate
1869+
}
1870+
1871+
def get_startpoint_distributions(self) -> dict[str, Distribution]:
1872+
"""Get distributions for sampling startpoints.
1873+
1874+
The distributions are the prior distributions for estimated parameters
1875+
that have a prior distribution defined, and uniform distributions
1876+
over the parameter bounds for estimated parameters without an explicit
1877+
prior.
1878+
1879+
:returns: Mapping of parameter IDs to distributions for sampling
1880+
startpoints.
18271881
"""
1828-
return {p.id: p.prior_dist for p in self.parameters if p.estimate}
1882+
return {
1883+
p.id: p.prior_dist if p.prior_distribution else Uniform(p.lb, p.ub)
1884+
for p in self.parameters
1885+
if p.estimate
1886+
}
18291887

18301888
def sample_parameter_startpoints(self, n_starts: int = 100, **kwargs):
18311889
"""Create 2D array with starting points for optimization"""

petab/v2/lint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ def run(self, problem: Problem) -> ValidationIssue | None:
843843

844844
# TODO: check distribution parameter domains more specifically
845845
try:
846-
if parameter.estimate:
846+
if parameter.estimate and parameter.prior_dist is not None:
847847
# .prior_dist fails for non-estimated parameters
848848
_ = parameter.prior_dist.sample(1)
849849
except Exception as e:

tests/v2/test_core.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,3 +866,22 @@ def test_mapping_validation():
866866

867867
# identity mapping is valid
868868
Mapping(petab_id="valid_id", model_id="valid_id", name="some name")
869+
870+
871+
def test_objective_type():
872+
"""Test that MAP and ML problems are recognized correctly."""
873+
problem = Problem()
874+
problem += Parameter(id="par1", lb=0, ub=100, estimate=True)
875+
assert problem.has_ml_objective is True
876+
assert problem.has_map_objective is False
877+
878+
problem += Parameter(
879+
id="par2",
880+
lb=0,
881+
ub=100,
882+
estimate=True,
883+
prior_distribution="normal",
884+
prior_parameters=[50, 10],
885+
)
886+
assert problem.has_map_objective is True
887+
assert problem.has_ml_objective is False

0 commit comments

Comments
 (0)