Skip to content

Commit

Permalink
🩹 Show validation problem if parameters are missing values (default: …
Browse files Browse the repository at this point in the history
…NaN) (#1076)

* 👌 Don't set default parameter value to zero

* 🧹 Improved typing and exception raising

* 🩹 Fix degraded label with nesting over 2

* ✨ Added missing_parameter_value_labels property to ParameterGroup

* ✨ Added missing parameters check to model validation

* 👌 Improved validation output

* 📚🚧 Added change to changelog

* ♻️ Refactored by Sourcery

Co-authored-by: Sourcery AI <>
  • Loading branch information
s-weigand authored May 20, 2022
1 parent 0da383e commit 114da78
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 19 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

- 🩹 Fix Crash in optimization_group_calculator_linked when using guidance spectra (#950)
- 🩹 ParameterGroup.get degrades full_label of nested Parameters with nesting over 2 (#1043)
- 🩹 Show validation problem if parameters are missing values (default: NaN) (#1076)

### 📚 Documentation

Expand Down
21 changes: 15 additions & 6 deletions glotaran/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ def get_dataset_groups(self) -> dict[str, DatasetGroup]:
if group not in groups:
try:
groups[group] = DatasetGroup(model=self.dataset_group_models[group])
except KeyError:
raise ValueError(f"Unknown dataset group '{group}'")
except KeyError as e:
raise ValueError(f"Unknown dataset group '{group}'") from e
groups[group].dataset_models[dataset_model.label] = dataset_model
return groups

Expand Down Expand Up @@ -367,7 +367,7 @@ def is_groupable(self, parameters: ParameterGroup, data: dict[str, xr.DataArray]
}
return len(global_dimensions) == 1 and len(model_dimensions) == 1

def problem_list(self, parameters: ParameterGroup = None) -> list[str]:
def problem_list(self, parameters: ParameterGroup | None = None) -> list[str]:
"""
Returns a list with all problems in the model and missing parameters if specified.
Expand All @@ -388,9 +388,18 @@ def problem_list(self, parameters: ParameterGroup = None) -> list[str]:
for item in items.values():
problems += item.validate(self, parameters=parameters)

if parameters is not None and len(parameters.missing_parameter_value_labels) != 0:
label_prefix = "\n - "
problems.append(
f"Parameter definition is missing values for the labels:"
f"{label_prefix}{label_prefix.join(parameters.missing_parameter_value_labels)}"
)

return problems

def validate(self, parameters: ParameterGroup = None, raise_exception: bool = False) -> str:
def validate(
self, parameters: ParameterGroup = None, raise_exception: bool = False
) -> MarkdownStr:
"""
Returns a string listing all problems in the model and missing parameters if specified.
Expand All @@ -403,14 +412,14 @@ def validate(self, parameters: ParameterGroup = None, raise_exception: bool = Fa
result = ""

if problems := self.problem_list(parameters):
result = f"Your model has {len(problems)} problems:\n"
result = f"Your model has {len(problems)} problem{'s' if len(problems) > 1 else ''}:\n"
for p in problems:
result += f"\n * {p}"
if raise_exception:
raise ModelError(result)
else:
result = "Your model is valid."
return result
return MarkdownStr(result)

def valid(self, parameters: ParameterGroup = None) -> bool:
"""Returns `True` if the number problems in the model is 0, else `False`
Expand Down
53 changes: 53 additions & 0 deletions glotaran/model/test/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import xarray as xr
from IPython.core.formatters import format_display_data

from glotaran.io import load_parameters
from glotaran.model import DatasetModel
from glotaran.model import Megacomplex
from glotaran.model import megacomplex
Expand Down Expand Up @@ -337,6 +338,58 @@ def test_model_validity(test_model: Model, model_error: Model, parameter: Parame
assert len(model_error.problem_list(parameter)) == 10


def test_model_validate_missing_parameters():
"""Show list of missing parameters as a problem."""

model_dict = {
"default_megacomplex": "decay-sequential",
"megacomplex": {
"megacomplex_sequential_decay": {
"type": "decay-sequential",
"compartments": ["species_1", "species_2", "species_3", "species_4"],
"rates": [
"b.missing_value_1",
"b.missing_value_2",
"b.2",
"kinetic.j.missing_value_3",
],
"dimension": "time",
}
},
"dataset": {
"dataset_1": {
"group": "default",
"megacomplex": ["megacomplex_sequential_decay"],
}
},
}
model = Model.from_dict(model_dict)
parameters = load_parameters(
dedent(
"""\
b:
- ["missing_value_1",]
- ["missing_value_2"]
- ["2", 0.75]
kinetic:
j:
- ["missing_value_3"]
"""
),
format_name="yml_str",
)
expected = dedent(
"""\
Your model has 1 problem:
* Parameter definition is missing values for the labels:
- b.missing_value_1
- b.missing_value_2
- kinetic.j.missing_value_3"""
)
assert str(model.validate(parameters)) == expected


def test_items(test_model: Model):

assert "m1" in test_model.megacomplex
Expand Down
2 changes: 1 addition & 1 deletion glotaran/parameter/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def from_list_or_value(
else:
values = sanitize_parameter_list(value)
param.label = _retrieve_item_from_list_by_type(values, str, label)
param.value = float(_retrieve_item_from_list_by_type(values, (int, float), 0))
param.value = float(_retrieve_item_from_list_by_type(values, (int, float), np.nan))
options = _retrieve_item_from_list_by_type(values, dict, None)

if default_options:
Expand Down
29 changes: 22 additions & 7 deletions glotaran/parameter/parameter_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import contextlib
from copy import copy
from textwrap import indent
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -132,10 +133,8 @@ def from_list(

for i, item in enumerate(parameter_list):
if isinstance(item, (str, int, float)):
try:
with contextlib.suppress(ValueError):
item = float(item)
except Exception:
pass
if isinstance(item, (float, int, list)):
root.add_parameter(
Parameter.from_list_or_value(item, label=str(i + 1), default_options=defaults)
Expand Down Expand Up @@ -444,14 +443,14 @@ def get(self, label: str) -> Parameter: # type:ignore[override]
for element in path:
try:
group = group[element]
except KeyError:
raise ParameterNotFoundException(path, label)
except KeyError as e:
raise ParameterNotFoundException(path, label) from e
try:
parameter = group._parameters[label]
parameter.full_label = full_label
return parameter
except KeyError:
raise ParameterNotFoundException(path, label)
except KeyError as e:
raise ParameterNotFoundException(path, label) from e

def copy(self) -> ParameterGroup:
"""Create a copy of the :class:`ParameterGroup`.
Expand Down Expand Up @@ -491,6 +490,7 @@ def all(
"""
root = f"{root}{self.label}{separator}" if root is not None else ""
for label, p in self._parameters.items():
p.full_label = f"{root}{label}"
yield (f"{root}{label}", p)
for _, l in self.items():
yield from l.all(root=root, separator=separator)
Expand Down Expand Up @@ -585,6 +585,21 @@ def update_parameter_expression(self):
)
parameter.value = value

@property
def missing_parameter_value_labels(self) -> list[str]:
"""List of full labels where the value is a NaN.
This property is used to validate that all parameters have starting values.
Returns
-------
str
List full labels with missing value.
"""
parameter_df = self.to_dataframe(as_optimized=False)
parameter_nan_value_mask = parameter_df["value"].isna()
return parameter_df[parameter_nan_value_mask]["label"].to_list()

def markdown(self, float_format: str = ".3e") -> MarkdownStr:
"""Format the :class:`ParameterGroup` as markdown string.
Expand Down
27 changes: 27 additions & 0 deletions glotaran/parameter/test/test_parameter_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ def test_parameter_group_from_dict_nested():

assert params.get("kinetic.j.1").full_label == "kinetic.j.1"

roundtrip_df = ParameterGroup.from_dataframe(params.to_dataframe()).to_dataframe()
assert all(roundtrip_df.label == params.to_dataframe().label)


def test_parameter_group_to_array():
params = """
Expand Down Expand Up @@ -339,3 +342,27 @@ def test_parameter_group_to_from_df():
assert got.non_negative == wanted.non_negative
assert got.value == wanted.value
assert got.vary == wanted.vary


def test_missing_parameter_value_labels():
"""Full labels of all parameters with missing values (NaN) get listed."""
parameter_group = load_parameters(
dedent(
"""\
b:
- ["missing_value_1",]
- ["missing_value_2"]
- ["2", 0.75]
kinetic:
j:
- ["missing_value_3"]
"""
),
format_name="yml_str",
)

assert parameter_group.missing_parameter_value_labels == [
"b.missing_value_1",
"b.missing_value_2",
"kinetic.j.missing_value_3",
]
8 changes: 3 additions & 5 deletions glotaran/project/scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ def problem_list(self) -> list[str]:
"""
return self.model.problem_list(self.parameters)

def validate(self) -> str:
def validate(self) -> MarkdownStr:
"""Return a string listing all problems in the model and missing parameters.
Returns
-------
str
MarkdownStr
A user-friendly string containing all the problems of a model if any.
Defaults to 'Your model is valid.' if no problems are found.
"""
Expand All @@ -150,9 +150,7 @@ def markdown(self):
"""
model_markdown_str = self.model.markdown(parameters=self.parameters)

markdown_str = "\n\n"
markdown_str += "__Scheme__\n\n"

markdown_str = "\n\n__Scheme__\n\n"
if self.non_negative_least_squares is not None:
markdown_str += f"* *non_negative_least_squares*: {self.non_negative_least_squares}\n"
markdown_str += (
Expand Down

0 comments on commit 114da78

Please sign in to comment.