From d54a003dbb68b1998b3662d747001ba87ec7daba Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Fri, 20 Dec 2024 17:26:37 +0100 Subject: [PATCH 1/8] v2: More validation * Check priors * Check observables * Fix missing prior parameters after v1->v2 conversion of uniform priors * Fix style --- petab/v2/core.py | 3 + petab/v2/lint.py | 150 +++++++++++++++++++++++++++++++++++++----- petab/v2/petab1to2.py | 17 +++++ 3 files changed, 155 insertions(+), 15 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 37797610..e658d1ef 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -890,6 +890,9 @@ def _validate_id(cls, v): @field_validator("prior_parameters", mode="before") @classmethod def _validate_prior_parameters(cls, v): + if isinstance(v, float) and np.isnan(v): + return [] + if isinstance(v, str): v = v.split(C.PARAMETER_SEPARATOR) elif not isinstance(v, Sequence): diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 0fb055e8..faba5d15 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -14,6 +14,9 @@ import pandas as pd import sympy as sp +from ..v1.visualize.lint import validate_visualization_df +from ..v2.C import * +from .core import PriorDistribution from .problem import Problem logger = logging.getLogger(__name__) @@ -37,6 +40,8 @@ "CheckUnusedExperiments", "CheckObservablesDoNotShadowModelEntities", "CheckUnusedConditions", + "CheckAllObservablesDefined", + "CheckPriorDistribution", "lint_problem", "default_validation_tasks", ] @@ -77,8 +82,12 @@ def __post_init__(self): def __str__(self): return f"{self.level.name}: {self.message}" - def _get_task_name(self): - """Get the name of the ValidationTask that raised this error.""" + @staticmethod + def _get_task_name() -> str | None: + """Get the name of the ValidationTask that raised this error. + + Expected to be called from below a `ValidationTask.run`. + """ import inspect # walk up the stack until we find the ValidationTask.run method @@ -88,6 +97,7 @@ def _get_task_name(self): task = frame.f_locals["self"] if isinstance(task, ValidationTask): return task.__class__.__name__ + return None @dataclass @@ -222,6 +232,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: f"Missing files: {', '.join(missing_files)}" ) + return None + class CheckModel(ValidationTask): """A task to validate the model of a PEtab problem.""" @@ -234,6 +246,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: # TODO get actual model validation messages return ValidationError("Model is invalid.") + return None + class CheckMeasuredObservablesDefined(ValidationTask): """A task to check that all observables referenced by the measurements @@ -252,10 +266,13 @@ def run(self, problem: Problem) -> ValidationIssue | None: "measurement table but not defined in observable table." ) + return None + class CheckOverridesMatchPlaceholders(ValidationTask): """A task to check that the number of observable/noise parameters - in the measurements match the number of placeholders in the observables.""" + in the measurements matches the number of placeholders in the observables. + """ def run(self, problem: Problem) -> ValidationIssue | None: observable_parameters_count = { @@ -320,18 +337,20 @@ def run(self, problem: Problem) -> ValidationIssue | None: if messages: return ValidationError("\n".join(messages)) + return None + class CheckPosLogMeasurements(ValidationTask): """Check that measurements for observables with log-transformation are positive.""" def run(self, problem: Problem) -> ValidationIssue | None: - from .core import NoiseDistribution as nd + from .core import NoiseDistribution as ND # noqa: N813 log_observables = { o.id for o in problem.observable_table.observables - if o.noise_distribution in [nd.LOG_NORMAL, nd.LOG_LAPLACE] + if o.noise_distribution in [ND.LOG_NORMAL, ND.LOG_LAPLACE] } if log_observables: for m in problem.measurement_table.measurements: @@ -342,6 +361,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: f"positive, but {m.measurement} <= 0 for {m}" ) + return None + class CheckMeasuredExperimentsDefined(ValidationTask): """A task to check that all experiments referenced by measurements @@ -369,6 +390,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: + str(missing_experiments) ) + return None + class CheckValidConditionTargets(ValidationTask): """Check that all condition table targets are valid.""" @@ -418,6 +441,32 @@ def run(self, problem: Problem) -> ValidationIssue | None: f"{invalid} at time {period.time}." ) period_targets |= condition_targets + return None + + +class CheckAllObservablesDefined(ValidationTask): + """A task to validate that all observables in the measurement table are + defined in the observable table.""" + + def run(self, problem: Problem) -> ValidationIssue | None: + if problem.measurement_df is None: + return None + + measurement_df = problem.measurement_df + observable_df = problem.observable_df + used_observables = set(measurement_df[OBSERVABLE_ID].values) + defined_observables = ( + set(observable_df.index.values) + if observable_df is not None + else set() + ) + if undefined_observables := (used_observables - defined_observables): + return ValidationError( + f"Observables {undefined_observables} are used in the" + "measurements table but are not defined in observables table." + ) + + return None class CheckUniquePrimaryKeys(ValidationTask): @@ -429,7 +478,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: # check for uniqueness of all primary keys counter = Counter(c.id for c in problem.condition_table.conditions) - duplicates = {id for id, count in counter.items() if count > 1} + duplicates = {id_ for id_, count in counter.items() if count > 1} if duplicates: return ValidationError( @@ -437,7 +486,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: ) counter = Counter(o.id for o in problem.observable_table.observables) - duplicates = {id for id, count in counter.items() if count > 1} + duplicates = {id_ for id_, count in counter.items() if count > 1} if duplicates: return ValidationError( @@ -445,7 +494,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: ) counter = Counter(e.id for e in problem.experiment_table.experiments) - duplicates = {id for id, count in counter.items() if count > 1} + duplicates = {id_ for id_, count in counter.items() if count > 1} if duplicates: return ValidationError( @@ -453,13 +502,15 @@ def run(self, problem: Problem) -> ValidationIssue | None: ) counter = Counter(p.id for p in problem.parameter_table.parameters) - duplicates = {id for id, count in counter.items() if count > 1} + duplicates = {id_ for id_, count in counter.items() if count > 1} if duplicates: return ValidationError( f"Parameter table contains duplicate IDs: {duplicates}" ) + return None + class CheckObservablesDoNotShadowModelEntities(ValidationTask): """A task to check that observable IDs do not shadow model entities.""" @@ -479,6 +530,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: f"Observable IDs {shadowed_entities} shadow model entities." ) + return None + class CheckExperimentTable(ValidationTask): """A task to validate the experiment table of a PEtab problem.""" @@ -498,6 +551,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: if messages: return ValidationError("\n".join(messages)) + return None + class CheckExperimentConditionsExist(ValidationTask): """A task to validate that all conditions in the experiment table exist @@ -526,6 +581,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: if messages: return ValidationError("\n".join(messages)) + return None + class CheckAllParametersPresentInParameterTable(ValidationTask): """Ensure all required parameters are contained in the parameter table @@ -573,6 +630,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: + str(extraneous) ) + return None + class CheckValidParameterInConditionOrParameterTable(ValidationTask): """A task to check that all required and only allowed model parameters are @@ -646,9 +705,11 @@ def run(self, problem: Problem) -> ValidationIssue | None: "the condition table and the parameter table." ) + return None + class CheckUnusedExperiments(ValidationTask): - """A task to check for experiments that are not used in the measurements + """A task to check for experiments that are not used in the measurement table.""" def run(self, problem: Problem) -> ValidationIssue | None: @@ -668,9 +729,11 @@ def run(self, problem: Problem) -> ValidationIssue | None: "measurements table." ) + return None + class CheckUnusedConditions(ValidationTask): - """A task to check for conditions that are not used in the experiments + """A task to check for conditions that are not used in the experiment table.""" def run(self, problem: Problem) -> ValidationIssue | None: @@ -692,6 +755,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: "experiments table." ) + return None + class CheckVisualizationTable(ValidationTask): """A task to validate the visualization table of a PEtab problem.""" @@ -700,14 +765,64 @@ def run(self, problem: Problem) -> ValidationIssue | None: if problem.visualization_df is None: return None - from ..v1.visualize.lint import validate_visualization_df - if validate_visualization_df(problem): return ValidationIssue( level=ValidationIssueSeverity.ERROR, message="Visualization table is invalid.", ) + return None + + +class CheckPriorDistribution(ValidationTask): + """A task to validate the prior distribution of a PEtab problem.""" + + _num_pars = { + PriorDistribution.CAUCHY: 2, + PriorDistribution.CHI_SQUARED: 1, + PriorDistribution.EXPONENTIAL: 1, + PriorDistribution.GAMMA: 2, + PriorDistribution.LAPLACE: 2, + PriorDistribution.LOG10_NORMAL: 2, + PriorDistribution.LOG_LAPLACE: 2, + PriorDistribution.LOG_NORMAL: 2, + PriorDistribution.LOG_UNIFORM: 2, + PriorDistribution.NORMAL: 2, + PriorDistribution.RAYLEIGH: 1, + PriorDistribution.UNIFORM: 2, + } + + def run(self, problem: Problem) -> ValidationIssue | None: + messages = [] + for parameter in problem.parameter_table.parameters: + if parameter.prior_distribution is None: + continue + + if parameter.prior_distribution not in PRIOR_DISTRIBUTIONS: + messages.append( + f"Prior distribution `{parameter.prior_distribution}' " + f"for parameter `{parameter.id}' is not valid." + ) + continue + + if ( + exp_num_par := self._num_pars[parameter.prior_distribution] + ) != len(parameter.prior_parameters): + messages.append( + f"Prior distribution `{parameter.prior_distribution}' " + f"for parameter `{parameter.id}' requires " + f"{exp_num_par} parameters, but got " + f"{len(parameter.prior_parameters)} " + f"({parameter.prior_parameters})." + ) + + # TODO: check distribution parameter domains + + if messages: + return ValidationError("\n".join(messages)) + + return None + def get_valid_parameters_for_parameter_table( problem: Problem, @@ -752,7 +867,7 @@ def get_valid_parameters_for_parameter_table( if mapping.model_id and mapping.model_id in parameter_ids.keys(): parameter_ids[mapping.petab_id] = None - # add output parameters from observables table + # add output parameters from observable table output_parameters = get_output_parameters(problem) for p in output_parameters: if p not in invalid: @@ -781,7 +896,7 @@ def get_required_parameters_for_parameter_table( problem: Problem, ) -> Set[str]: """ - Get set of parameters which need to go into the parameter table + Get the set of parameters that need to go into the parameter table Arguments: problem: The PEtab problem @@ -965,4 +1080,9 @@ def get_placeholders( # TODO: atomize checks, update to long condition table, re-enable # CheckVisualizationTable(), # TODO validate mapping table + CheckValidParameterInConditionOrParameterTable(), + CheckAllObservablesDefined(), + CheckAllParametersPresentInParameterTable(), + CheckValidConditionTargets(), + CheckPriorDistribution(), ] diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index c788f116..ebe6b2c9 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -455,4 +455,21 @@ def update_prior(row): errors="ignore", ) + # if uniform, we need to explicitly set the parameters + def update_prior_pars(row): + prior_type = row.get(v2.C.PRIOR_DISTRIBUTION) + prior_pars = row.get(v2.C.PRIOR_PARAMETERS) + + if prior_type not in (v2.C.UNIFORM, v2.C.LOG_UNIFORM) or not pd.isna( + prior_pars + ): + return prior_pars + + return ( + f"{row[v2.C.LOWER_BOUND]}{v2.C.PARAMETER_SEPARATOR}" + f"{row[v2.C.UPPER_BOUND]}" + ) + + df[v2.C.PRIOR_PARAMETERS] = df.apply(update_prior_pars, axis=1) + return df From d3d6e9e05b729656a828ea10b2c2bca5d053c626 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 13:07:04 +0200 Subject: [PATCH 2/8] fix serialization / validation --- petab/v2/core.py | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index e658d1ef..31806ed6 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -503,9 +503,13 @@ class ExperimentPeriod(BaseModel): @field_validator("condition_ids", mode="before") @classmethod def _validate_ids(cls, condition_ids): + if condition_ids is None: + return [] + for condition_id in condition_ids: - if not is_valid_identifier(condition_id): - raise ValueError(f"Invalid ID: {condition_id}") + # condition_id may be empty + if condition_id and not is_valid_identifier(condition_id): + raise ValueError(f"Invalid ID: `{condition_id}'") return condition_ids @@ -854,17 +858,23 @@ class Parameter(BaseModel): #: Parameter ID. id: str = Field(alias=C.PARAMETER_ID) #: Lower bound. - lb: float | None = Field(alias=C.LOWER_BOUND, default=None) + lb: Annotated[float | None, BeforeValidator(_convert_nan_to_none)] = Field( + alias=C.LOWER_BOUND, default=None + ) #: Upper bound. - ub: float | None = Field(alias=C.UPPER_BOUND, default=None) + ub: Annotated[float | None, BeforeValidator(_convert_nan_to_none)] = Field( + alias=C.UPPER_BOUND, default=None + ) #: Nominal value. - nominal_value: float | None = Field(alias=C.NOMINAL_VALUE, default=None) + nominal_value: Annotated[ + float | None, BeforeValidator(_convert_nan_to_none) + ] = Field(alias=C.NOMINAL_VALUE, default=None) #: Is the parameter to be estimated? estimate: bool = Field(alias=C.ESTIMATE, default=True) #: Type of parameter prior distribution. - prior_distribution: PriorDistribution | None = Field( - alias=C.PRIOR_DISTRIBUTION, default=None - ) + prior_distribution: Annotated[ + PriorDistribution | None, BeforeValidator(_convert_nan_to_none) + ] = Field(alias=C.PRIOR_DISTRIBUTION, default=None) #: Prior distribution parameters. prior_parameters: list[float] = Field( alias=C.PRIOR_PARAMETERS, default_factory=list @@ -921,12 +931,17 @@ def _validate_estimate_before(cls, v): def _serialize_estimate(self, estimate: bool, _info): return str(estimate).lower() - @field_validator("lb", "ub", "nominal_value") - @classmethod - def _convert_nan_to_none(cls, v): - if isinstance(v, float) and np.isnan(v): - return None - return v + @field_serializer("prior_distribution") + def _serialize_prior_distribution( + self, prior_distribution: PriorDistribution | None, _info + ): + if prior_distribution is None: + return "" + return str(prior_distribution) + + @field_serializer("prior_parameters") + def _serialize_prior_parameters(self, prior_parameters: list[str], _info): + return C.PARAMETER_SEPARATOR.join(prior_parameters) @model_validator(mode="after") def _validate(self) -> Self: From 0786a8f86d6d3edc3cc7e1a70cb399f03f0ff2dd Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 13:21:58 +0200 Subject: [PATCH 3/8] .. --- petab/v2/lint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/petab/v2/lint.py b/petab/v2/lint.py index faba5d15..6330f233 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -14,7 +14,6 @@ import pandas as pd import sympy as sp -from ..v1.visualize.lint import validate_visualization_df from ..v2.C import * from .core import PriorDistribution from .problem import Problem @@ -765,6 +764,8 @@ def run(self, problem: Problem) -> ValidationIssue | None: if problem.visualization_df is None: return None + from ..v1.visualize.lint import validate_visualization_df + if validate_visualization_df(problem): return ValidationIssue( level=ValidationIssueSeverity.ERROR, From 4550d56cfefa23128e1606e4e687f8a49209d0be Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 18:57:36 +0200 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> --- petab/v2/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 31806ed6..1d73ba7b 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -503,12 +503,11 @@ class ExperimentPeriod(BaseModel): @field_validator("condition_ids", mode="before") @classmethod def _validate_ids(cls, condition_ids): - if condition_ids is None: + if condition_ids in [None, []]: return [] for condition_id in condition_ids: - # condition_id may be empty - if condition_id and not is_valid_identifier(condition_id): + if is_valid_identifier(condition_id): raise ValueError(f"Invalid ID: `{condition_id}'") return condition_ids @@ -900,7 +899,7 @@ def _validate_id(cls, v): @field_validator("prior_parameters", mode="before") @classmethod def _validate_prior_parameters(cls, v): - if isinstance(v, float) and np.isnan(v): + if pd.isna(v): return [] if isinstance(v, str): From 9d173f84c6252a5dbed8ead79082ab03bf9602a3 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 19:28:39 +0200 Subject: [PATCH 5/8] review --- petab/v2/core.py | 25 ++++++++++++++++++++----- petab/v2/lint.py | 12 +++++++++++- petab/v2/problem.py | 4 ++-- tests/v2/test_core.py | 2 +- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 1d73ba7b..9f3b115d 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -73,8 +73,11 @@ def _not_nan(v: float, info: ValidationInfo) -> float: def _convert_nan_to_none(v): + """Convert NaN or "" to None.""" if isinstance(v, float) and np.isnan(v): return None + if isinstance(v, str) and v == "": + return None return v @@ -503,12 +506,17 @@ class ExperimentPeriod(BaseModel): @field_validator("condition_ids", mode="before") @classmethod def _validate_ids(cls, condition_ids): - if condition_ids in [None, []]: + if condition_ids in [None, "", [], [""]]: + # unspecified, or "use-model-as-is" return [] for condition_id in condition_ids: - if is_valid_identifier(condition_id): - raise ValueError(f"Invalid ID: `{condition_id}'") + # The empty condition ID for "use-model-as-is" has been handled + # above. Having a combination of empty and non-empty IDs is an + # error, since the targets of conditions to be combined must be + # disjoint. + if not is_valid_identifier(condition_id): + raise ValueError(f"Invalid {C.CONDITION_ID}: `{condition_id}'") return condition_ids @@ -899,7 +907,7 @@ def _validate_id(cls, v): @field_validator("prior_parameters", mode="before") @classmethod def _validate_prior_parameters(cls, v): - if pd.isna(v): + if isinstance(v, float) and np.isnan(v): return [] if isinstance(v, str): @@ -969,7 +977,7 @@ def _validate(self) -> Self: @property def prior_dist(self) -> Distribution: - """Get the pior distribution of the parameter.""" + """Get the prior distribution of the parameter.""" if self.estimate is False: raise ValueError(f"Parameter `{self.id}' is not estimated.") @@ -997,6 +1005,13 @@ def prior_dist(self) -> Distribution: "transformation." ) return cls(*self.prior_parameters, trunc=[self.lb, self.ub]) + + if cls == Uniform: + # `Uniform.__init__` does not accept the `trunc` parameter + low = max(self.prior_parameters[0], self.lb) + high = min(self.prior_parameters[1], self.ub) + return cls(low, high, log=log) + return cls(*self.prior_parameters, log=log, trunc=[self.lb, self.ub]) diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 6330f233..2558ea3c 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -817,7 +817,17 @@ def run(self, problem: Problem) -> ValidationIssue | None: f"({parameter.prior_parameters})." ) - # TODO: check distribution parameter domains + # TODO: check distribution parameter domains more specifically + try: + if parameter.estimate: + # .prior_dist fails for non-estimated parameters + _ = parameter.prior_dist.sample(1) + except Exception as e: + messages.append( + f"Prior parameters `{parameter.prior_parameters}' " + f"for parameter `{parameter.id}' are invalid " + f"(hint: {e})." + ) if messages: return ValidationError("\n".join(messages)) diff --git a/petab/v2/problem.py b/petab/v2/problem.py index 01903b16..52baf724 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -1121,8 +1121,8 @@ def model_dump(self, **kwargs) -> dict[str, Any]: 'id': 'par', 'lb': 0.0, 'nominal_value': None, - 'prior_distribution': None, - 'prior_parameters': [], + 'prior_distribution': '', + 'prior_parameters': '', 'ub': 1.0}]} """ res = { diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index 074c0d2d..2aba25e4 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -212,7 +212,7 @@ def test_period(): with pytest.raises(ValidationError, match="got inf"): ExperimentPeriod(time="inf", condition_ids=["p1"]) - with pytest.raises(ValidationError, match="Invalid ID"): + with pytest.raises(ValidationError, match="Invalid conditionId"): ExperimentPeriod(time=1, condition_ids=["1_condition"]) with pytest.raises(ValidationError, match="type=missing"): From 52a1812de7592f8a8d84c687bf7c550062b8fdcb Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 19:30:39 +0200 Subject: [PATCH 6/8] -- --- petab/v2/petab1to2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index ebe6b2c9..bc7398fc 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -460,15 +460,15 @@ def update_prior_pars(row): prior_type = row.get(v2.C.PRIOR_DISTRIBUTION) prior_pars = row.get(v2.C.PRIOR_PARAMETERS) - if prior_type not in (v2.C.UNIFORM, v2.C.LOG_UNIFORM) or not pd.isna( + if prior_type in (v2.C.UNIFORM, v2.C.LOG_UNIFORM) and pd.isna( prior_pars ): - return prior_pars + return ( + f"{row[v2.C.LOWER_BOUND]}{v2.C.PARAMETER_SEPARATOR}" + f"{row[v2.C.UPPER_BOUND]}" + ) - return ( - f"{row[v2.C.LOWER_BOUND]}{v2.C.PARAMETER_SEPARATOR}" - f"{row[v2.C.UPPER_BOUND]}" - ) + return prior_pars df[v2.C.PRIOR_PARAMETERS] = df.apply(update_prior_pars, axis=1) From 644b058f17b3c1a8c0619cad01b628fecfa9b223 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 19:40:27 +0200 Subject: [PATCH 7/8] .. --- petab/v2/core.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index 9f3b115d..9ed3e269 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -906,11 +906,15 @@ def _validate_id(cls, v): @field_validator("prior_parameters", mode="before") @classmethod - def _validate_prior_parameters(cls, v): + def _validate_prior_parameters( + cls, v: str | list[str] | float | None | np.ndarray + ): if isinstance(v, float) and np.isnan(v): return [] if isinstance(v, str): + if v == "": + return [] v = v.split(C.PARAMETER_SEPARATOR) elif not isinstance(v, Sequence): v = [v] @@ -919,7 +923,7 @@ def _validate_prior_parameters(cls, v): @field_validator("estimate", mode="before") @classmethod - def _validate_estimate_before(cls, v): + def _validate_estimate_before(cls, v: bool | str): if isinstance(v, bool): return v From 03060ccb009b919da5e9a2795b74f3d4a0167158 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 28 Apr 2025 19:47:41 +0200 Subject: [PATCH 8/8] .. --- petab/v2/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/petab/v2/core.py b/petab/v2/core.py index 9ed3e269..1ee74ace 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -909,6 +909,9 @@ def _validate_id(cls, v): def _validate_prior_parameters( cls, v: str | list[str] | float | None | np.ndarray ): + if v is None: + return [] + if isinstance(v, float) and np.isnan(v): return []