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..2f98c8e4 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1691,6 +1691,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 +2278,65 @@ def get_measurements_for_experiment( if measurement.experiment_id == experiment.id ] + def get_output_parameters( + 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 observable: + 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 observable: + 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 = [] + + # 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.append(candidate) + + return output_parameters + class ModelFile(BaseModel): """A file in the PEtab problem configuration.""" diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 20f5dfc1..a80599a9 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 @@ -996,19 +996,18 @@ 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 = get_output_parameters( - problem, + output_parameters = problem.get_output_parameters( **formula_type, ) placeholders = get_placeholders( @@ -1034,69 +1033,9 @@ 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, + observable: bool = True, noise: bool = True, ) -> list[str]: """Get all placeholder parameters from observable table observableFormulas @@ -1104,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: @@ -1115,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 e38f31f1..9ff92bb5 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(observable=True, noise=True) + == ["p1", "p3", "p4", "p5"] + ) + assert petab_problem.get_output_parameters( + observable=True, noise=False + ) == ["p1", "p3", "p4"] + assert petab_problem.get_output_parameters( + observable=False, noise=True + ) == ["p1", "p3", "p5"]