diff --git a/glotaran/deprecation/modules/test/test_parameter_parameter_group.py b/glotaran/deprecation/modules/test/test_parameter_parameter_group.py index 426304d49..e13777338 100644 --- a/glotaran/deprecation/modules/test/test_parameter_parameter_group.py +++ b/glotaran/deprecation/modules/test/test_parameter_parameter_group.py @@ -6,7 +6,7 @@ from glotaran.examples.sequential import parameter -def test_parameter_group_to_csv(tmp_path: Path): +def test_parameter_group_to_csv_no_stderr(tmp_path: Path): """``ParameterGroup.to_csv`` raises deprecation warning and saves file.""" parameter_path = tmp_path / "test_parameter.csv" deprecation_warning_on_call_test_helper( @@ -14,14 +14,14 @@ def test_parameter_group_to_csv(tmp_path: Path): ) expected = dedent( """\ - label,value,minimum,maximum,vary,non-negative,expression - j.1,1.0,-inf,inf,False,False,None - j.0,0.0,-inf,inf,False,False,None - kinetic.1,0.5,-inf,inf,True,False,None - kinetic.2,0.3,-inf,inf,True,False,None - kinetic.3,0.1,-inf,inf,True,False,None - irf.center,0.3,-inf,inf,True,False,None - irf.width,0.1,-inf,inf,True,False,None + label,value,expression,minimum,maximum,non-negative,vary,standard-error + j.1,1.0,None,-inf,inf,False,False,None + j.0,0.0,None,-inf,inf,False,False,None + kinetic.1,0.5,None,-inf,inf,False,True,None + kinetic.2,0.3,None,-inf,inf,False,True,None + kinetic.3,0.1,None,-inf,inf,False,True,None + irf.center,0.3,None,-inf,inf,False,True,None + irf.width,0.1,None,-inf,inf,False,True,None """ ) diff --git a/glotaran/model/test/test_model.py b/glotaran/model/test/test_model.py index 5998fc516..7e99d6992 100644 --- a/glotaran/model/test/test_model.py +++ b/glotaran/model/test/test_model.py @@ -514,9 +514,9 @@ def test_model_markdown(): * **k1**: * *Label*: k1 * *Matrix*: - * *('s2', 's1')*: rates.1: **5.01000e-01** *(StdErr: 0e+00)* - * *('s3', 's2')*: rates.2: **2.02000e-02** *(StdErr: 0e+00)* - * *('s3', 's3')*: rates.3: **1.05000e-03** *(StdErr: 0e+00)* + * *('s2', 's1')*: rates.1: **5.01000e-01** *(StdErr: nan)* + * *('s3', 's2')*: rates.2: **2.02000e-02** *(StdErr: nan)* + * *('s3', 's3')*: rates.3: **1.05000e-03** *(StdErr: nan)* ## Initial Concentration @@ -532,8 +532,8 @@ def test_model_markdown(): * **irf1** (multi-gaussian): * *Label*: irf1 * *Type*: multi-gaussian - * *Center*: [irf.center: **1.30000e+00** *(StdErr: 0e+00)*] - * *Width*: [irf.width: **7.80000e+00** *(StdErr: 0e+00)*] + * *Center*: [irf.center: **1.30000e+00** *(StdErr: nan)*] + * *Width*: [irf.width: **7.80000e+00** *(StdErr: nan)*] * *Normalize*: True * *Backsweep*: False diff --git a/glotaran/parameter/parameter.py b/glotaran/parameter/parameter.py index b42d30027..309ce30e0 100644 --- a/glotaran/parameter/parameter.py +++ b/glotaran/parameter/parameter.py @@ -9,6 +9,7 @@ import numpy as np from numpy.typing._array_like import _SupportsArray +from glotaran.utils.ipython import MarkdownStr from glotaran.utils.sanitize import sanitize_parameter_list if TYPE_CHECKING: @@ -42,11 +43,12 @@ def __init__( self, label: str = None, full_label: str = None, - expression: str = None, - maximum: int | float = np.inf, - minimum: int | float = -np.inf, + expression: str | None = None, + maximum: float = np.inf, + minimum: float = -np.inf, non_negative: bool = False, - value: float | int = np.nan, + standard_error: float = np.nan, + value: float = np.nan, vary: bool = True, ): """Optimization Parameter supporting numpy array operations. @@ -58,15 +60,17 @@ def __init__( full_label : str The label of the parameter with its path in a parameter group prepended. , by default None - expression : str + expression : str | None Expression to calculate the parameters value from, e.g. if used in relation to another parameter. , by default None - maximum : int + maximum : float Upper boundary for the parameter to be varied to., by default np.inf - minimum : int + minimum : float Lower boundary for the parameter to be varied to., by default -np.inf non_negative : bool Whether the parameter should always be bigger than zero., by default False + standard_error: float + The standard error of the parameter. , by default ``np.nan`` value : float Value of the parameter, by default np.nan vary : bool @@ -79,7 +83,7 @@ def __init__( self.maximum = maximum self.minimum = minimum self.non_negative = non_negative - self.standard_error = 0.0 + self.standard_error = standard_error self.value = value self.vary = vary @@ -144,6 +148,57 @@ def from_list_or_value( param._set_options_from_dict(options) return param + @classmethod + def from_dict(cls, parameter_dict: dict[str, Any]) -> Parameter: + """Create a :class:`Parameter` from a dictionary. + + Expects a dictionary created by :method:`Parameter.as_dict`. + + Parameters + ---------- + parameter_dict : dict[str, Any] + The source dictionary. + + Returns + ------- + Parameter + The created :class:`Parameter` + """ + parameter_dict = {k.replace("-", "_"): v for k, v in parameter_dict.items()} + parameter_dict["full_label"] = parameter_dict["label"] + parameter_dict["label"] = parameter_dict["label"].split(".")[-1] + return cls(**parameter_dict) + + def as_dict(self, as_optimized: bool = True) -> dict[str, Any]: + """Create a dictionary containing the parameter properties. + + Note: + ----- + Intended for internal use. + + Parameters + ---------- + as_optimized : bool + Whether to include properties which are the result of optimization. + + Returns + ------- + dict[str, Any] + The created dictionary. + """ + parameter_dict = { + "label": self.full_label, + "value": self.value, + "expression": self.expression, + "minimum": self.minimum, + "maximum": self.maximum, + "non-negative": self.non_negative, + "vary": self.vary, + } + if as_optimized: + parameter_dict["standard-error"] = self.standard_error + return parameter_dict + def set_from_group(self, group: ParameterGroup): """Set all values of the parameter to the values of the corresponding parameter in the group. @@ -165,12 +220,12 @@ def set_from_group(self, group: ParameterGroup): self.value = p.value self.vary = p.vary - def _set_options_from_dict(self, options: dict): + def _set_options_from_dict(self, options: dict[str, Any]): """Set the parameter's options from a dictionary. Parameters ---------- - options : dict + options : dict[str, Any] A dictionary containing parameter options. """ if Keys.EXPR in options: @@ -220,7 +275,7 @@ def full_label(self, full_label: str): @property def non_negative(self) -> bool: - r"""Indicate if the parameter is non-negativ. + r"""Indicate if the parameter is non-negative. If true, the parameter will be transformed with :math:`p' = \log{p}` and :math:`p = \exp{p'}`. @@ -232,7 +287,7 @@ def non_negative(self) -> bool: Returns ------- bool - Whether the parameter is non-negativ. + Whether the parameter is non-negative. """ return self._non_negative if self.expression is None else False @@ -409,6 +464,50 @@ def set_value_from_optimization(self, value: float): """ self.value = np.exp(value) if self.non_negative else value + def markdown( + self, + all_parameter: ParameterGroup | None = None, + initial_parameter: ParameterGroup | None = None, + ) -> MarkdownStr: + """Get a markdown representation of the parameter. + + Parameters + ---------- + all_parameter : ParameterGroup | None + A parameter group containing the whole parameter set (used for expression lookup). + initial_parameter : ParameterGroup | None + The initial parameter. + + Returns + ------- + MarkdownStr + The parameter as markdown string. + """ + md = f"{self.full_label}" + + value = f"{self.value:.2e}" + if self.vary: + if self.standard_error is not np.nan: + value += f"±{self.standard_error}" + if initial_parameter is not None: + initial_value = initial_parameter.get(self.full_label).value + value += f", initial: {initial_value:.2e}" + md += f"({value})" + elif self.expression is not None: + expression = self.expression + if all_parameter is not None: + for match in PARAMETER_EXPRESION_REGEX.findall(expression): + label = match[0] + parameter = all_parameter.get(label) + expression = expression.replace( + "$" + label, f"_{parameter.markdown(all_parameter=all_parameter)}_" + ) + md += f"({value}={expression})" + else: + md += f"({value}, fixed)" + + return MarkdownStr(md) + def __getstate__(self): """Get state for pickle.""" return ( diff --git a/glotaran/parameter/parameter_group.py b/glotaran/parameter/parameter_group.py index 579065721..192985727 100644 --- a/glotaran/parameter/parameter_group.py +++ b/glotaran/parameter/parameter_group.py @@ -5,6 +5,7 @@ from copy import copy from textwrap import indent from typing import TYPE_CHECKING +from typing import Any from typing import Generator import asteval @@ -69,7 +70,7 @@ def __init__(self, label: str = None, root_group: ParameterGroup = None): @classmethod def from_dict( cls, - parameter_dict: dict[str, dict | list], + parameter_dict: dict[str, dict[str, Any] | list[float | list[Any]]], label: str = None, root_group: ParameterGroup = None, ) -> ParameterGroup: @@ -103,7 +104,7 @@ def from_dict( @classmethod def from_list( cls, - parameter_list: list[float | list], + parameter_list: list[float | list[Any]], label: str = None, root_group: ParameterGroup = None, ) -> ParameterGroup: @@ -111,7 +112,7 @@ def from_list( Parameters ---------- - parameter_list : list[float | list] + parameter_list : list[float | list[Any]] A parameter list containing parameters label : str The label of the group. @@ -121,7 +122,7 @@ def from_list( Returns ------- ParameterGroup - The created :class:`ParameterGroup` + The created :class:`ParameterGroup`. """ root = cls(label=label, root_group=root_group) @@ -146,6 +147,29 @@ def from_list( root.update_parameter_expression() return root + @classmethod + def from_parameter_dict_list(cls, parameter_dict_list: list[dict[str, Any]]) -> ParameterGroup: + """Create a :class:`ParameterGroup` from a list of parameter dictionaries. + + Parameters + ---------- + parameter_dict_list : list[dict[str, Any]] + A list of parameter dictionaries. + + Returns + ------- + ParameterGroup + The created :class:`ParameterGroup`. + """ + parameter_group = cls() + for parameter_dict in parameter_dict_list: + group = parameter_group.get_group_for_parameter_by_label( + parameter_dict["label"], create_if_not_exist=True + ) + group.add_parameter(Parameter.from_dict(parameter_dict)) + parameter_group.update_parameter_expression() + return parameter_group + @classmethod def from_dataframe(cls, df: pd.DataFrame, source: str = "DataFrame") -> ParameterGroup: """Create a :class:`ParameterGroup` from a :class:`pandas.DataFrame`. @@ -181,41 +205,11 @@ def from_dataframe(cls, df: pd.DataFrame, source: str = "DataFrame") -> Paramete if column_name in df and any(not isinstance(v, bool) for v in df[column_name]): raise ValueError(f"Column '{column_name}' in '{source}' has non boolean values") - root = cls() - - for i, full_label in enumerate(df["label"]): - path = full_label.split(".") - group = root - while len(path) > 1: - group_label = path.pop(0) - if group_label not in group: - group.add_group(ParameterGroup(label=group_label, root_group=group)) - group = group[group_label] - label = path.pop() - value = df["value"][i] - minimum = df["minimum"][i] if "minimum" in df else -np.inf - maximum = df["maximum"][i] if "maximum" in df else np.inf - non_negative = df["non-negative"][i] if "non-negative" in df else False - vary = df["vary"][i] if "vary" in df else True - expression = ( - df["expression"][i] - if "expression" in df and isinstance(df["expression"][i], str) - else None - ) - - parameter = Parameter( - label=label, - full_label=full_label, - value=value, - expression=expression, - maximum=maximum, - minimum=minimum, - non_negative=non_negative, - vary=vary, - ) - group.add_parameter(parameter) - root.update_parameter_expression() - return root + # clean NaN if expressions + if "expression" in df: + expressions = df["expression"].to_list() + df["expression"] = [expr if isinstance(expr, str) else None for expr in expressions] + return cls.from_parameter_dict_list(df.to_dict(orient="records")) @property def label(self) -> str | None: @@ -239,32 +233,69 @@ def root_group(self) -> ParameterGroup | None: """ return self._root_group - def to_dataframe(self) -> pd.DataFrame: + def to_parameter_dict_list(self, as_optimized: bool = True) -> list[dict[str, Any]]: + """Create list of parameter dictionaries from the group. + + Parameters + ---------- + as_optimized : bool + Whether to include properties which are the result of optimization. + + Returns + ------- + list[dict[str, Any]] + Alist of parameter dictionaries. + """ + return [p[1].as_dict(as_optimized=as_optimized) for p in self.all()] + + def to_dataframe(self, as_optimized: bool = True) -> pd.DataFrame: """Create a pandas data frame from the group. + Parameters + ---------- + as_optimized : bool + Whether to include properties which are the result of optimization. + Returns ------- pd.DataFrame The created data frame. """ - parameter_dict: dict[str, list[str | float | bool | None]] = { - "label": [], - "value": [], - "minimum": [], - "maximum": [], - "vary": [], - "non-negative": [], - "expression": [], - } - for label, parameter in self.all(): - parameter_dict["label"].append(label) - parameter_dict["value"].append(parameter.value) - parameter_dict["minimum"].append(parameter.minimum) - parameter_dict["maximum"].append(parameter.maximum) - parameter_dict["vary"].append(parameter.vary) - parameter_dict["non-negative"].append(parameter.non_negative) - parameter_dict["expression"].append(parameter.expression) - return pd.DataFrame(parameter_dict) + return pd.DataFrame(self.to_parameter_dict_list(as_optimized=as_optimized)) + + def get_group_for_parameter_by_label( + self, parameter_label: str, create_if_not_exist: bool = False + ) -> ParameterGroup: + """Get the group for a parameter by it's label. + + Parameters + ---------- + parameter_label : str + The parameter label. + create_if_not_exist : bool + Create the parameter group if not existent. + + Returns + ------- + ParameterGroup + The group of the parameter. + + Raises + ------ + KeyError + Raised if the group does not exist and `create_if_not_exist` is `False`. + """ + path = parameter_label.split(".") + group = self + while len(path) > 1: + group_label = path.pop(0) + if group_label not in group: + if create_if_not_exist: + group.add_group(ParameterGroup(label=group_label, root_group=group)) + else: + raise KeyError(f"Subgroup '{group_label}' does not exist.") + group = group[group_label] + return group @deprecate( deprecated_qual_name_usage=( diff --git a/glotaran/parameter/test/test_parameter.py b/glotaran/parameter/test/test_parameter.py index b39111061..4ca1a175d 100644 --- a/glotaran/parameter/test/test_parameter.py +++ b/glotaran/parameter/test/test_parameter.py @@ -6,7 +6,6 @@ import pytest from glotaran.io import load_parameters -from glotaran.io import save_parameters from glotaran.parameter import Parameter @@ -27,7 +26,7 @@ def test_parameter_label_error_wrong_label_pattern(label: str | int | float): Parameter(label=label) # type:ignore[arg-type] -def test_param_repr(): +def test_parameter_repr(): """Repr creates code to recreate the object.""" result = Parameter(label="foo", value=1.0, expression="$foo.bar", vary=False) result_short = Parameter(label="foo", value=1, expression="$foo.bar") @@ -39,24 +38,7 @@ def test_param_repr(): assert result_short.__repr__() == expected -def test_param_array(): - params = """ - - 5 - - 4 - - 3 - - 2 - - 1 - """ - - params = load_parameters(params, format_name="yml_str") - - assert len(list(params.all())) == 5 - - assert [p.label for _, p in params.all()] == [f"{i}" for i in range(1, 6)] - assert [p.value for _, p in params.all()] == list(range(1, 6))[::-1] - - -def test_param_scientific(): +def test_parameter_scientific_values(): values = [5e3, -4.2e-4, 3e-2, -2e6] params = """ - ["1", 5e3] @@ -70,7 +52,7 @@ def test_param_scientific(): assert [p.value for _, p in params.all()] == values -def test_param_label(): +def test_parameter_from_list(): params = """ - ["5", 1] - ["4", 2] @@ -84,30 +66,7 @@ def test_param_label(): assert [p.value for _, p in params.all()] == list(range(1, 4)) -def test_param_group_copy(): - params = """ - kinetic: - - ["5", 1, {non-negative: true, min: -1, max: 1, vary: false}] - - 4 - - 5 - j: - - 7 - - 8 - """ - params = load_parameters(params, format_name="yml_str") - copy = params.copy() - - for label, parameter in params.all(): - assert copy.has(label) - copied_parameter = copy.get(label) - assert parameter.value == copied_parameter.value - assert parameter.non_negative == copied_parameter.non_negative - assert parameter.minimum == copied_parameter.minimum - assert parameter.maximum == copied_parameter.maximum - assert parameter.vary == copied_parameter.vary - - -def test_param_options(): +def test_parameter_options(): params = """ - ["5", 1, {non-negative: false, min: -1, max: 1, vary: false}] - ["6", 4e2, {non-negative: true, min: -7e2, max: 8e2, vary: true}] @@ -135,7 +94,7 @@ def test_param_options(): assert params.get("7").vary -def test_param_block_options(): +def test_parameter_block_options(): params = """ block: - 1.0 @@ -148,49 +107,6 @@ def test_param_block_options(): assert params.get("block.2").vary -def test_nested_param_list(): - params = """ - kinetic: - - 3 - - 4 - - 5 - j: - - 7 - - 8 - """ - - params = load_parameters(params, format_name="yml_str") - - assert len(list(params.all())) == 5 - group = params["kinetic"] - assert len(list(group.all())) == 3 - assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 4)] - assert [p.value for _, p in group.all()] == list(range(3, 6)) - group = params["j"] - assert len(list(group.all())) == 2 - assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 3)] - assert [p.value for _, p in group.all()] == list(range(7, 9)) - - -def test_nested_param_group(): - params = """ - kinetic: - j: - - 7 - - 8 - - 9 - """ - - params = load_parameters(params, format_name="yml_str") - assert len(list(params.all())) == 3 - group = params["kinetic"] - assert len(list(group.all())) == 3 - group = group["j"] - assert len(list(group.all())) == 3 - assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 4)] - assert [p.value for _, p in group.all()] == list(range(7, 10)) - - def test_parameter_set_from_group(): """Parameter extracted from group has correct values""" group = load_parameters( @@ -254,64 +170,6 @@ def test_parameter_non_negative(): assert not np.allclose(6, maximum) -def test_parameter_group_to_array(): - params = """ - - ["1", 1, {non-negative: false, min: -1, max: 1, vary: false}] - - ["2", 4e2, {non-negative: true, min: 10, max: 8e2, vary: true}] - - ["3", 2e4] - """ - - params = load_parameters(params, format_name="yml_str") - - labels, values, lower_bounds, upper_bounds = params.get_label_value_and_bounds_arrays( - exclude_non_vary=False - ) - - assert len(labels) == 3 - assert len(values) == 3 - assert len(lower_bounds) == 3 - assert len(upper_bounds) == 3 - - assert labels == ["1", "2", "3"] - assert np.allclose(values, [1, np.log(4e2), 2e4]) - assert np.allclose(lower_bounds, [-1, np.log(10), -np.inf]) - assert np.allclose(upper_bounds, [1, np.log(8e2), np.inf]) - - ( - labels_only_vary, - values_only_vary, - lower_bounds_only_vary, - upper_bounds_only_vary, - ) = params.get_label_value_and_bounds_arrays(exclude_non_vary=True) - - assert len(labels_only_vary) == 2 - assert len(values_only_vary) == 2 - assert len(lower_bounds_only_vary) == 2 - assert len(upper_bounds_only_vary) == 2 - - assert labels_only_vary == ["2", "3"] - - -def test_update_parameter_group_from_array(): - params = """ - - ["1", 1, {non-negative: false, min: -1, max: 1, vary: false}] - - ["2", 4e2, {non-negative: true, min: 10, max: 8e2, vary: true}] - - ["3", 2e4] - """ - - params = load_parameters(params, format_name="yml_str") - - labels = ["1", "2", "3"] - values = [0, np.log(6e2), 42] - - params.set_from_label_and_value_arrays(labels, values) - - values[1] = np.exp(values[1]) - - for i in range(3): - assert params.get(f"{i+1}").value == values[i] - - @pytest.mark.parametrize( "case", [ @@ -488,97 +346,26 @@ def test_parameter_numpy_operations(): assert parm1 <= parm2 -TEST_CSV = """ -label, value, minimum, maximum, vary, non-negative, expression -rates.k1,0.050,0,5,True,True,None -rates.k2,None,-inf,inf,True,True,$rates.k1 * 2 -rates.k3,2.311,-inf,inf,True,True,None -pen.eq.1,1.000,-inf,inf,False,False,None -""" - - -def test_param_group_from_csv(tmpdir): - - csv_path = tmpdir.join("parameters.csv") - with open(csv_path, "w") as f: - f.write(TEST_CSV) - - params = load_parameters(csv_path) - - assert "rates" in params - - assert params.has("rates.k1") - p = params.get("rates.k1") - assert p.label == "k1" - assert p.value == 0.05 - assert p.minimum == 0 - assert p.maximum == 5 - assert p.vary - assert p.non_negative - assert p.expression is None - - assert params.has("rates.k2") - p = params.get("rates.k2") - assert p.label == "k2" - assert p.value == params.get("rates.k1") * 2 - assert p.minimum == -np.inf - assert p.maximum == np.inf - assert not p.vary - assert not p.non_negative - assert p.expression == "$rates.k1 * 2" - - assert params.has("rates.k3") - p = params.get("rates.k3") - assert p.label == "k3" - assert p.value == 2.311 - assert p.minimum == -np.inf - assert p.maximum == np.inf - assert p.vary - assert p.non_negative - assert p.expression is None - - assert "pen" in params - assert "eq" in params["pen"] - - assert params.has("pen.eq.1") - p = params.get("pen.eq.1") - assert p.label == "1" - assert p.value == 1.0 - assert p.minimum == -np.inf - assert p.maximum == np.inf - assert not p.vary - assert not p.non_negative - assert p.expression is None - - -def test_parameter_to_csv(tmpdir): - csv_path = tmpdir.join("parameters.csv") - params = load_parameters( - """ - b: - - ["1", 0.25, {vary: false, min: 0, max: 8}] - - ["2", 0.75, {expr: '1 - $b.1', non-negative: true}] - rates: - - ["total", 2] - - ["branch1", {expr: '$rates.total * $b.1'}] - - ["branch2", {expr: '$rates.total * $b.2'}] - """, - format_name="yml_str", +def test_parameter_dict_roundtrip(): + param = Parameter( + label="foo", + full_label="bar.foo", + expression="1", + maximum=2, + minimum=1, + non_negative=True, + value=42, + vary=False, ) - save_parameters(params, csv_path, "csv") - - with open(csv_path) as f: - print(f.read()) - params_from_csv = load_parameters(csv_path) - - for label, p in params.all(): - assert params_from_csv.has(label) - p_from_csv = params_from_csv.get(label) - assert p.label == p_from_csv.label - assert p.value == p_from_csv.value - assert p.minimum == p_from_csv.minimum - assert p.maximum == p_from_csv.maximum - assert p.vary == p_from_csv.vary - assert p.non_negative == p_from_csv.non_negative - assert p.expression == p_from_csv.expression + param_dict = param.as_dict() + param_from_dict = Parameter.from_dict(param_dict) + + assert param.label == param_from_dict.label + assert param.full_label == param_from_dict.full_label + assert param.expression == param_from_dict.expression + assert param.maximum == param_from_dict.maximum + assert param.minimum == param_from_dict.minimum + assert param.non_negative == param_from_dict.non_negative + assert param.value == param_from_dict.value + assert param.vary == param_from_dict.vary diff --git a/glotaran/parameter/test/test_parameter_group.py b/glotaran/parameter/test/test_parameter_group.py index 67f376c9b..aad30498d 100644 --- a/glotaran/parameter/test/test_parameter_group.py +++ b/glotaran/parameter/test/test_parameter_group.py @@ -1,106 +1,339 @@ -from IPython.core.formatters import format_display_data +from __future__ import annotations + +from textwrap import dedent + +import numpy as np from glotaran.io import load_parameters -from glotaran.parameter.parameter_group import ParameterGroup - -PARAMETERS_3C_BASE = """\ -irf: - - ["center", 1.3] - - ["width", 7.8] -j: - - ["1", 1, {"vary": False, "non-negative": False}] -""" - -PARAMETERS_3C_KINETIC = """\ -kinetic: - - ["1", 300e-3] - - ["2", 500e-4] - - ["3", 700e-5] -""" - -RENDERED_MARKDOWN = """\ - * __irf__: - - | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | - |-----------|-----------|------------|---------|---------|----------|------------------|----------| - | center | 1.3 | 0 | -inf | inf | True | False | None | - | width | 7.8 | 0 | -inf | inf | True | False | None | - - * __j__: - - | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | - |-----------|-----------|------------|---------|---------|----------|------------------|----------| - | 1 | 1 | 0 | -inf | inf | False | False | None | - - * __kinetic__: - - | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | - |-----------|-----------|------------|---------|---------|----------|------------------|----------| - | 1 | 0.3 | 0 | -inf | inf | True | False | None | - | 2 | 0.05 | 0 | -inf | inf | True | False | None | - | 3 | 0.007 | 0 | -inf | inf | True | False | None | - -""" # noqa: E501 - - -def test_param_group_markdown_is_order_independent(): - """Markdown output of ParameterGroup.markdown() is independent of initial order""" - PARAMETERS_3C_INITIAL1 = f"""{PARAMETERS_3C_BASE}\n{PARAMETERS_3C_KINETIC}""" - PARAMETERS_3C_INITIAL2 = f"""{PARAMETERS_3C_KINETIC}\n{PARAMETERS_3C_BASE}""" - - initial_parameters_ref = ParameterGroup.from_dict( - { - "j": [["1", 1, {"vary": False, "non-negative": False}]], - "kinetic": [ - ["1", 300e-3], - ["2", 500e-4], - ["3", 700e-5], - ], - "irf": [["center", 1.3], ["width", 7.8]], - } +from glotaran.io import save_parameters +from glotaran.parameter import ParameterGroup + + +def test_parameter_group_copy(): + params = """ + kinetic: + - ["5", 1, {non-negative: true, min: -1, max: 1, vary: false}] + - 4 + - 5 + j: + - 7 + - 8 + """ + params = load_parameters(params, format_name="yml_str") + copy = params.copy() + + for label, parameter in params.all(): + assert copy.has(label) + copied_parameter = copy.get(label) + assert parameter.value == copied_parameter.value + assert parameter.non_negative == copied_parameter.non_negative + assert parameter.minimum == copied_parameter.minimum + assert parameter.maximum == copied_parameter.maximum + assert parameter.vary == copied_parameter.vary + + +def test_parameter_group_from_list(): + params = """ + - 5 + - 4 + - 3 + - 2 + - 1 + """ + + params = load_parameters(params, format_name="yml_str") + + assert len(list(params.all())) == 5 + + assert [p.label for _, p in params.all()] == [f"{i}" for i in range(1, 6)] + assert [p.value for _, p in params.all()] == list(range(1, 6))[::-1] + + +def test_parameter_group_from_dict(): + params = """ + kinetic: + - 3 + - 4 + - 5 + j: + - 7 + - 8 + """ + + params = load_parameters(params, format_name="yml_str") + + assert len(list(params.all())) == 5 + group = params["kinetic"] + assert len(list(group.all())) == 3 + assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 4)] + assert [p.value for _, p in group.all()] == list(range(3, 6)) + group = params["j"] + assert len(list(group.all())) == 2 + assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 3)] + assert [p.value for _, p in group.all()] == list(range(7, 9)) + + +def test_parameter_group_from_dict_nested(): + params = """ + kinetic: + j: + - 7 + - 8 + - 9 + """ + + params = load_parameters(params, format_name="yml_str") + assert len(list(params.all())) == 3 + group = params["kinetic"] + assert len(list(group.all())) == 3 + group = group["j"] + assert len(list(group.all())) == 3 + assert [p.label for _, p in group.all()] == [f"{i}" for i in range(1, 4)] + assert [p.value for _, p in group.all()] == list(range(7, 10)) + + +def test_parameter_group_to_array(): + params = """ + - ["1", 1, {non-negative: false, min: -1, max: 1, vary: false}] + - ["2", 4e2, {non-negative: true, min: 10, max: 8e2, vary: true}] + - ["3", 2e4] + """ + + params = load_parameters(params, format_name="yml_str") + + labels, values, lower_bounds, upper_bounds = params.get_label_value_and_bounds_arrays( + exclude_non_vary=False ) - initial_parameters1 = load_parameters(PARAMETERS_3C_INITIAL1, format_name="yml_str") - initial_parameters2 = load_parameters(PARAMETERS_3C_INITIAL2, format_name="yml_str") + assert len(labels) == 3 + assert len(values) == 3 + assert len(lower_bounds) == 3 + assert len(upper_bounds) == 3 + + assert labels == ["1", "2", "3"] + assert np.allclose(values, [1, np.log(4e2), 2e4]) + assert np.allclose(lower_bounds, [-1, np.log(10), -np.inf]) + assert np.allclose(upper_bounds, [1, np.log(8e2), np.inf]) + + ( + labels_only_vary, + values_only_vary, + lower_bounds_only_vary, + upper_bounds_only_vary, + ) = params.get_label_value_and_bounds_arrays(exclude_non_vary=True) + + assert len(labels_only_vary) == 2 + assert len(values_only_vary) == 2 + assert len(lower_bounds_only_vary) == 2 + assert len(upper_bounds_only_vary) == 2 + + assert labels_only_vary == ["2", "3"] + + +def test_parameter_group_set_from_label_and_value_arrays(): + params = """ + - ["1", 1, {non-negative: false, min: -1, max: 1, vary: false}] + - ["2", 4e2, {non-negative: true, min: 10, max: 8e2, vary: true}] + - ["3", 2e4] + """ + + params = load_parameters(params, format_name="yml_str") - assert initial_parameters1.markdown() == RENDERED_MARKDOWN - assert initial_parameters2.markdown() == RENDERED_MARKDOWN - assert initial_parameters_ref.markdown() == RENDERED_MARKDOWN + labels = ["1", "2", "3"] + values = [0, np.log(6e2), 42] + params.set_from_label_and_value_arrays(labels, values) -def test_param_group_repr(): - """Repr creates code to recreate the object with from_dict.""" - result = ParameterGroup.from_dict({"foo": {"bar": [["1", 1.0], ["2", 2.0], ["3", 3.0]]}}) - result_short = ParameterGroup.from_dict({"foo": {"bar": [1, 2, 3]}}) - expected = "ParameterGroup.from_dict({'foo': {'bar': [['1', 1.0], ['2', 2.0], ['3', 3.0]]}})" + values[1] = np.exp(values[1]) - assert result == result_short - assert result_short.__repr__() == expected - assert result.__repr__() == expected - assert result == eval(result.__repr__()) + for i in range(3): + assert params.get(f"{i+1}").value == values[i] -def test_param_group_repr_from_list(): - """Repr creates code to recreate the object with from_list.""" - result = ParameterGroup.from_list([["1", 2.3], ["2", 3.0]]) - result_short = ParameterGroup.from_list([2.3, 3.0]) - expected = "ParameterGroup.from_list([['1', 2.3], ['2', 3.0]])" +def test_parameter_group_from_csv(tmpdir): + + TEST_CSV = dedent( + """\ + label, value, minimum, maximum, vary, non-negative, expression + rates.k1,0.050,0,5,True,True,None + rates.k2,None,-inf,inf,True,True,$rates.k1 * 2 + rates.k3,2.311,-inf,inf,True,True,None + pen.eq.1,1.000,-inf,inf,False,False,None + """ + ) + + csv_path = tmpdir.join("parameters.csv") + with open(csv_path, "w") as f: + f.write(TEST_CSV) + + params = load_parameters(csv_path) + + assert "rates" in params + + assert params.has("rates.k1") + p = params.get("rates.k1") + assert p.label == "k1" + assert p.value == 0.05 + assert p.minimum == 0 + assert p.maximum == 5 + assert p.vary + assert p.non_negative + assert p.expression is None + + assert params.has("rates.k2") + p = params.get("rates.k2") + assert p.label == "k2" + assert p.value == params.get("rates.k1") * 2 + assert p.minimum == -np.inf + assert p.maximum == np.inf + assert not p.vary + assert not p.non_negative + assert p.expression == "$rates.k1 * 2" + + assert params.has("rates.k3") + p = params.get("rates.k3") + assert p.label == "k3" + assert p.value == 2.311 + assert p.minimum == -np.inf + assert p.maximum == np.inf + assert p.vary + assert p.non_negative + assert p.expression is None + + assert "pen" in params + assert "eq" in params["pen"] + + assert params.has("pen.eq.1") + p = params.get("pen.eq.1") + assert p.label == "1" + assert p.value == 1.0 + assert p.minimum == -np.inf + assert p.maximum == np.inf + assert not p.vary + assert not p.non_negative + assert p.expression is None + + +def test_parameter_group_to_csv(tmpdir): + csv_path = tmpdir.join("parameters.csv") + params = load_parameters( + """ + b: + - ["1", 0.25, {vary: false, min: 0, max: 8}] + - ["2", 0.75, {expr: '1 - $b.1', non-negative: true}] + rates: + - ["total", 2] + - ["branch1", {expr: '$rates.total * $b.1'}] + - ["branch2", {expr: '$rates.total * $b.2'}] + """, + format_name="yml_str", + ) + for _, p in params.all(): + p.standard_error = 42 + + save_parameters(params, csv_path, "csv") + wanted = dedent( + """\ + label,value,expression,minimum,maximum,non-negative,vary,standard-error + b.1,0.25,None,0.0,8.0,False,False,42 + b.2,0.75,1 - $b.1,-inf,inf,False,False,42 + rates.total,2.0,None,-inf,inf,False,True,42 + rates.branch1,0.5,$rates.total * $b.1,-inf,inf,False,False,42 + rates.branch2,1.5,$rates.total * $b.2,-inf,inf,False,False,42 + """ + ) + + with open(csv_path) as f: + got = f.read() + print(got) + assert got == wanted + params_from_csv = load_parameters(csv_path) + + for label, p in params.all(): + assert params_from_csv.has(label) + p_from_csv = params_from_csv.get(label) + assert p.label == p_from_csv.label + assert p.value == p_from_csv.value + assert p.minimum == p_from_csv.minimum + assert p.maximum == p_from_csv.maximum + assert p.vary == p_from_csv.vary + assert p.non_negative == p_from_csv.non_negative + assert p.expression == p_from_csv.expression + + +def test_parameter_group_to_from_parameter_dict_list(): + parameter_group = load_parameters( + """ + b: + - ["1", 0.25, {vary: false, min: 0, max: 8}] + - ["2", 0.75, {expr: '1 - $b.1', non-negative: true}] + rates: + - ["total", 2] + - ["branch1", {expr: '$rates.total * $b.1'}] + - ["branch2", {expr: '$rates.total * $b.2'}] + """, + format_name="yml_str", + ) + + parameter_dict_list = parameter_group.to_parameter_dict_list() + parameter_group_from_dict_list = ParameterGroup.from_parameter_dict_list(parameter_dict_list) + + for label, wanted in parameter_group.all(): + got = parameter_group_from_dict_list.get(label) + + assert got.label == wanted.label + assert got.full_label == wanted.full_label + assert got.expression == wanted.expression + assert got.maximum == wanted.maximum + assert got.minimum == wanted.minimum + assert got.non_negative == wanted.non_negative + assert got.value == wanted.value + assert got.vary == wanted.vary + + +def test_parameter_group_to_from_df(): + parameter_group = load_parameters( + """ + b: + - ["1", 0.25, {vary: false, min: 0, max: 8}] + - ["2", 0.75, {expr: '1 - $b.1', non-negative: true}] + rates: + - ["total", 2] + - ["branch1", {expr: '$rates.total * $b.1'}] + - ["branch2", {expr: '$rates.total * $b.2'}] + """, + format_name="yml_str", + ) + + for _, p in parameter_group.all(): + p.standard_error = 42 - assert result == result_short - assert result.__repr__() == expected - assert result_short.__repr__() == expected - assert result == eval(result.__repr__()) + parameter_df = parameter_group.to_dataframe() + for column in [ + "label", + "value", + "standard-error", + "expression", + "minimum", + "maximum", + "non-negative", + "vary", + ]: + assert column in parameter_df -def test_param_group_ipython_rendering(): - """Autorendering in ipython""" - param_group = ParameterGroup.from_dict({"foo": {"bar": [["1", 1.0], ["2", 2.0], ["3", 3.0]]}}) - rendered_obj = format_display_data(param_group)[0] + assert all(parameter_df["standard-error"] == 42) - assert "text/markdown" in rendered_obj - assert rendered_obj["text/markdown"].startswith(" * __foo__") + parameter_group_from_df = ParameterGroup.from_dataframe(parameter_df) - rendered_markdown_return = format_display_data(param_group.markdown())[0] + for label, wanted in parameter_group.all(): + got = parameter_group_from_df.get(label) - assert "text/markdown" in rendered_markdown_return - assert rendered_markdown_return["text/markdown"].startswith(" * __foo__") + assert got.label == wanted.label + assert got.full_label == wanted.full_label + assert got.expression == wanted.expression + assert got.maximum == wanted.maximum + assert got.minimum == wanted.minimum + assert got.non_negative == wanted.non_negative + assert got.value == wanted.value + assert got.vary == wanted.vary diff --git a/glotaran/parameter/test/test_parameter_group_rendering.py b/glotaran/parameter/test/test_parameter_group_rendering.py new file mode 100644 index 000000000..a38b49e38 --- /dev/null +++ b/glotaran/parameter/test/test_parameter_group_rendering.py @@ -0,0 +1,106 @@ +from IPython.core.formatters import format_display_data + +from glotaran.io import load_parameters +from glotaran.parameter.parameter_group import ParameterGroup + +PARAMETERS_3C_BASE = """\ +irf: + - ["center", 1.3] + - ["width", 7.8] +j: + - ["1", 1, {"vary": False, "non-negative": False}] +""" + +PARAMETERS_3C_KINETIC = """\ +kinetic: + - ["1", 300e-3] + - ["2", 500e-4] + - ["3", 700e-5] +""" + +RENDERED_MARKDOWN = """\ + * __irf__: + + | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | + |-----------|-----------|------------|---------|---------|----------|------------------|----------| + | center | 1.3 | nan | -inf | inf | True | False | None | + | width | 7.8 | nan | -inf | inf | True | False | None | + + * __j__: + + | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | + |-----------|-----------|------------|---------|---------|----------|------------------|----------| + | 1 | 1 | nan | -inf | inf | False | False | None | + + * __kinetic__: + + | _Label_ | _Value_ | _StdErr_ | _Min_ | _Max_ | _Vary_ | _Non-Negative_ | _Expr_ | + |-----------|-----------|------------|---------|---------|----------|------------------|----------| + | 1 | 0.3 | nan | -inf | inf | True | False | None | + | 2 | 0.05 | nan | -inf | inf | True | False | None | + | 3 | 0.007 | nan | -inf | inf | True | False | None | + +""" # noqa: E501 + + +def test_param_group_markdown_is_order_independent(): + """Markdown output of ParameterGroup.markdown() is independent of initial order""" + PARAMETERS_3C_INITIAL1 = f"""{PARAMETERS_3C_BASE}\n{PARAMETERS_3C_KINETIC}""" + PARAMETERS_3C_INITIAL2 = f"""{PARAMETERS_3C_KINETIC}\n{PARAMETERS_3C_BASE}""" + + initial_parameters_ref = ParameterGroup.from_dict( + { + "j": [["1", 1, {"vary": False, "non-negative": False}]], + "kinetic": [ + ["1", 300e-3], + ["2", 500e-4], + ["3", 700e-5], + ], + "irf": [["center", 1.3], ["width", 7.8]], + } + ) + + initial_parameters1 = load_parameters(PARAMETERS_3C_INITIAL1, format_name="yml_str") + initial_parameters2 = load_parameters(PARAMETERS_3C_INITIAL2, format_name="yml_str") + + assert str(initial_parameters1.markdown()) == RENDERED_MARKDOWN + assert str(initial_parameters2.markdown()) == RENDERED_MARKDOWN + assert str(initial_parameters_ref.markdown()) == RENDERED_MARKDOWN + + +def test_param_group_repr(): + """Repr creates code to recreate the object with from_dict.""" + result = ParameterGroup.from_dict({"foo": {"bar": [["1", 1.0], ["2", 2.0], ["3", 3.0]]}}) + result_short = ParameterGroup.from_dict({"foo": {"bar": [1, 2, 3]}}) + expected = "ParameterGroup.from_dict({'foo': {'bar': [['1', 1.0], ['2', 2.0], ['3', 3.0]]}})" + + assert result == result_short + assert result_short.__repr__() == expected + assert result.__repr__() == expected + assert result == eval(result.__repr__()) + + +def test_param_group_repr_from_list(): + """Repr creates code to recreate the object with from_list.""" + result = ParameterGroup.from_list([["1", 2.3], ["2", 3.0]]) + result_short = ParameterGroup.from_list([2.3, 3.0]) + expected = "ParameterGroup.from_list([['1', 2.3], ['2', 3.0]])" + + assert result == result_short + assert result.__repr__() == expected + assert result_short.__repr__() == expected + assert result == eval(result.__repr__()) + + +def test_param_group_ipython_rendering(): + """Autorendering in ipython""" + param_group = ParameterGroup.from_dict({"foo": {"bar": [["1", 1.0], ["2", 2.0], ["3", 3.0]]}}) + rendered_obj = format_display_data(param_group)[0] + + assert "text/markdown" in rendered_obj + assert rendered_obj["text/markdown"].startswith(" * __foo__") + + rendered_markdown_return = format_display_data(param_group.markdown())[0] + + assert "text/markdown" in rendered_markdown_return + assert rendered_markdown_return["text/markdown"].startswith(" * __foo__")