Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🩹 Show validation problem if parameters are missing values (default: NaN) #1076

Merged
merged 8 commits into from
May 20, 2022
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):
jsnel marked this conversation as resolved.
Show resolved Hide resolved
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