From 6a656ce58d20f84fd59600572072ac30422fbe2f Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 13 Oct 2025 16:11:34 +0200 Subject: [PATCH 1/3] Add `v2.Problem.{get_output_parameters,get_x_nominal_dict}` * Move `get_output_parameters` from v2.lint to v2.Problem * Add `Problem.get_x_nominal_dict` * Test --- petab/v1/observables.py | 2 +- petab/v2/core.py | 81 +++++++++++++++++++++++++++++++++++++++++ petab/v2/lint.py | 67 ++-------------------------------- tests/v2/test_core.py | 74 +++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 65 deletions(-) diff --git a/petab/v1/observables.py b/petab/v1/observables.py index 411c2a4c..38c539c7 100644 --- a/petab/v1/observables.py +++ b/petab/v1/observables.py @@ -84,7 +84,7 @@ def get_output_parameters( ) -> list[str]: """Get output parameters - Returns IDs of parameters used in observable and noise formulas that are + Returns IDs of parameters used in observable or noise formulas that are not defined in the model. Arguments: diff --git a/petab/v2/core.py b/petab/v2/core.py index 6d117f92..ac6102f7 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -8,6 +8,7 @@ import tempfile import traceback from abc import abstractmethod +from collections import OrderedDict from collections.abc import Sequence from enum import Enum from itertools import chain @@ -1691,6 +1692,27 @@ def get_x_nominal(self, free: bool = True, fixed: bool = True) -> list: return self._apply_mask(v, free=free, fixed=fixed) + def get_x_nominal_dict( + self, free: bool = True, fixed: bool = True + ) -> dict[str, float]: + """Get parameter nominal values as dict. + + :param free: + Whether to return free parameters, i.e. parameters to estimate. + :param fixed: + Whether to return fixed parameters, i.e. parameters not to + estimate. + :returns: + A dictionary mapping parameter IDs to their nominal values. + """ + return dict( + zip( + self.get_x_ids(free=free, fixed=fixed), + self.get_x_nominal(free=free, fixed=fixed), + strict=True, + ) + ) + @property def x_nominal(self) -> list: """Parameter table nominal values""" @@ -2257,6 +2279,65 @@ def get_measurements_for_experiment( if measurement.experiment_id == experiment.id ] + def get_output_parameters( + self, observables: bool = True, noise: bool = True + ) -> list[str]: + """Get output parameters. + + Returns IDs of symbols used in observable and noise formulas that are + not observables and that are not defined in the model. + + :param observables: + Include parameters from observableFormulas + :param noise: + Include parameters from noiseFormulas + :returns: + List of output parameter IDs, including any placeholder parameters. + """ + # collect free symbols from observable and noise formulas, + # skipping observable IDs + candidates = set() + if observables: + candidates |= { + str_sym + for o in self.observables + if o.formula is not None + for sym in o.formula.free_symbols + if (str_sym := str(sym)) != o.id + } + if noise: + candidates |= { + str_sym + for o in self.observables + if o.noise_formula is not None + for sym in o.noise_formula.free_symbols + if (str_sym := str(sym)) != o.id + } + + output_parameters = OrderedDict() + + # filter out symbols that are defined in the model or mapped to + # such symbols + for candidate in sorted(candidates): + if self.model.symbol_allowed_in_observable_formula(candidate): + continue + + # does it map to a model entity? + for mapping in self.mappings: + if ( + mapping.petab_id == candidate + and mapping.model_id is not None + ): + if self.model.symbol_allowed_in_observable_formula( + mapping.model_id + ): + break + else: + # no mapping to a model entity, so it is an output parameter + output_parameters[candidate] = None + + return list(output_parameters.keys()) + class ModelFile(BaseModel): """A file in the PEtab problem configuration.""" diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 20f5dfc1..c1d7a4df 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -396,7 +396,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: if problem.model else set() ) - allowed_targets |= set(get_output_parameters(problem)) + allowed_targets |= set(problem.get_output_parameters()) allowed_targets |= { m.petab_id for m in problem.mappings if m.model_id is not None } @@ -932,7 +932,7 @@ def get_valid_parameters_for_parameter_table( parameter_ids[mapping.petab_id] = None # add output parameters from observable table - output_parameters = get_output_parameters(problem) + output_parameters = problem.get_output_parameters() for p in output_parameters: if p not in invalid: parameter_ids[p] = None @@ -1007,8 +1007,7 @@ def append_overrides(overrides): {"noise": True, "observables": True}, ), ): - output_parameters = get_output_parameters( - problem, + output_parameters = problem.get_output_parameters( **formula_type, ) placeholders = get_placeholders( @@ -1034,66 +1033,6 @@ def append_overrides(overrides): return parameter_ids -def get_output_parameters( - problem: Problem, - observables: bool = True, - noise: bool = True, -) -> list[str]: - """Get output parameters - - Returns IDs of symbols used in observable and noise formulas that are - not observables and that are not defined in the model. - - Arguments: - problem: The PEtab problem - observables: Include parameters from observableFormulas - noise: Include parameters from noiseFormulas - - Returns: - List of output parameter IDs, including any placeholder parameters. - """ - # collect free symbols from observable and noise formulas, - # skipping observable IDs - candidates = set() - if observables: - candidates |= { - str_sym - for o in problem.observables - if o.formula is not None - for sym in o.formula.free_symbols - if (str_sym := str(sym)) != o.id - } - if noise: - candidates |= { - str_sym - for o in problem.observables - if o.noise_formula is not None - for sym in o.noise_formula.free_symbols - if (str_sym := str(sym)) != o.id - } - - output_parameters = OrderedDict() - - # filter out symbols that are defined in the model or mapped to - # such symbols - for candidate in sorted(candidates): - if problem.model.symbol_allowed_in_observable_formula(candidate): - continue - - # does it map to a model entity? - for mapping in problem.mappings: - if mapping.petab_id == candidate and mapping.model_id is not None: - if problem.model.symbol_allowed_in_observable_formula( - mapping.model_id - ): - break - else: - # no mapping to a model entity, so it is an output parameter - output_parameters[candidate] = None - - return list(output_parameters.keys()) - - def get_placeholders( problem: Problem, observables: bool = True, diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index e38f31f1..4522b7c7 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -739,3 +739,77 @@ def make_yaml(id_line: str) -> str: f.write(make_yaml("")) problem = Problem.from_yaml(filepath) assert problem.id is None + + +def test_parameter_accessors(): # pylint: disable=W0621 + """ + Test the petab.Problem functions to get parameter values. + """ + petab_problem = Problem() + petab_problem += Parameter( + id="par1", lb=0, ub=100, nominal_value=7, estimate=True + ) + petab_problem += Parameter( + id="par2", lb=0.1, ub=100, nominal_value=8, estimate=True + ) + petab_problem += Parameter( + id="par3", lb=0.1, ub=200, nominal_value=9, estimate=False + ) + + assert petab_problem.x_ids == ["par1", "par2", "par3"] + assert petab_problem.x_free_ids == ["par1", "par2"] + assert petab_problem.x_fixed_ids == ["par3"] + assert petab_problem.lb == [0, 0.1, 0.1] + assert petab_problem.ub == [100, 100, 200] + assert petab_problem.x_nominal == [7, 8, 9] + assert petab_problem.x_nominal_free == [7, 8] + assert petab_problem.x_nominal_fixed == [9] + + assert ( + petab_problem.get_x_nominal_dict() + == petab_problem.get_x_nominal_dict(free=True, fixed=True) + == { + "par1": 7, + "par2": 8, + "par3": 9, + } + ) + assert petab_problem.get_x_nominal_dict(free=True, fixed=False) == { + "par1": 7, + "par2": 8, + } + assert petab_problem.get_x_nominal_dict(free=False, fixed=True) == { + "par3": 9, + } + + +def test_get_output_parameters(): + """Test Problem.get_output_parameters""" + petab_problem = Problem() + assert petab_problem.get_output_parameters() == [] + + petab_problem += Parameter(id="p1", lb=0, ub=100, estimate=True) + petab_problem.models.append(SbmlModel.from_antimony("p2 = 1")) + assert petab_problem.get_output_parameters() == [] + + petab_problem += Observable( + id="obs1", formula="p1 + p2", noise_formula="p1 * p2" + ) + assert petab_problem.get_output_parameters() == ["p1"] + + petab_problem += Observable( + id="obs1", + formula="p3 + p4", + noise_formula="p3 * p5", + ) + assert ( + petab_problem.get_output_parameters() + == petab_problem.get_output_parameters(observables=True, noise=True) + == ["p1", "p3", "p4", "p5"] + ) + assert petab_problem.get_output_parameters( + observables=True, noise=False + ) == ["p1", "p3", "p4"] + assert petab_problem.get_output_parameters( + observables=False, noise=True + ) == ["p1", "p3", "p5"] From 91f692318f7945c3c2d88cbab28c096804798dd0 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 15 Oct 2025 15:36:40 +0200 Subject: [PATCH 2/3] Update petab/v2/core.py Co-authored-by: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> --- petab/v2/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index ac6102f7..2087cbdd 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -2314,7 +2314,7 @@ def get_output_parameters( if (str_sym := str(sym)) != o.id } - output_parameters = OrderedDict() + output_parameters = [] # filter out symbols that are defined in the model or mapped to # such symbols @@ -2334,9 +2334,9 @@ def get_output_parameters( break else: # no mapping to a model entity, so it is an output parameter - output_parameters[candidate] = None + output_parameters.append(candidate) - return list(output_parameters.keys()) + return output_parameters class ModelFile(BaseModel): From 30cce79e0fa0684c4425be4c9e3962b73a6ccb6e Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 15 Oct 2025 15:39:21 +0200 Subject: [PATCH 3/3] observable --- petab/v2/core.py | 7 +++---- petab/v2/lint.py | 14 +++++++------- tests/v2/test_core.py | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 2087cbdd..2f98c8e4 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -8,7 +8,6 @@ import tempfile import traceback from abc import abstractmethod -from collections import OrderedDict from collections.abc import Sequence from enum import Enum from itertools import chain @@ -2280,14 +2279,14 @@ def get_measurements_for_experiment( ] def get_output_parameters( - self, observables: bool = True, noise: bool = True + self, observable: bool = True, noise: bool = True ) -> list[str]: """Get output parameters. Returns IDs of symbols used in observable and noise formulas that are not observables and that are not defined in the model. - :param observables: + :param observable: Include parameters from observableFormulas :param noise: Include parameters from noiseFormulas @@ -2297,7 +2296,7 @@ def get_output_parameters( # collect free symbols from observable and noise formulas, # skipping observable IDs candidates = set() - if observables: + if observable: candidates |= { str_sym for o in self.observables diff --git a/petab/v2/lint.py b/petab/v2/lint.py index c1d7a4df..a80599a9 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -996,15 +996,15 @@ def append_overrides(overrides): for formula_type, placeholder_sources in ( ( # Observable formulae - {"observables": True, "noise": False}, + {"observable": True, "noise": False}, # can only contain observable placeholders - {"noise": False, "observables": True}, + {"noise": False, "observable": True}, ), ( # Noise formulae - {"observables": False, "noise": True}, + {"observable": False, "noise": True}, # can contain noise and observable placeholders - {"noise": True, "observables": True}, + {"noise": True, "observable": True}, ), ): output_parameters = problem.get_output_parameters( @@ -1035,7 +1035,7 @@ def append_overrides(overrides): def get_placeholders( problem: Problem, - observables: bool = True, + observable: bool = True, noise: bool = True, ) -> list[str]: """Get all placeholder parameters from observable table observableFormulas @@ -1043,7 +1043,7 @@ def get_placeholders( Arguments: problem: The PEtab problem - observables: Include parameters from observableFormulas + observable: Include parameters from observableFormulas noise: Include parameters from noiseFormulas Returns: @@ -1054,7 +1054,7 @@ def get_placeholders( # {observable,noise}Parameters placeholders = [] for o in problem.observables: - if observables: + if observable: placeholders.extend(map(str, o.observable_placeholders)) if noise: placeholders.extend(map(str, o.noise_placeholders)) diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index 4522b7c7..9ff92bb5 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -804,12 +804,12 @@ def test_get_output_parameters(): ) assert ( petab_problem.get_output_parameters() - == petab_problem.get_output_parameters(observables=True, noise=True) + == petab_problem.get_output_parameters(observable=True, noise=True) == ["p1", "p3", "p4", "p5"] ) assert petab_problem.get_output_parameters( - observables=True, noise=False + observable=True, noise=False ) == ["p1", "p3", "p4"] assert petab_problem.get_output_parameters( - observables=False, noise=True + observable=False, noise=True ) == ["p1", "p3", "p5"]