Source code for glotaran.parameter.parameters

"""The parameters class."""

from __future__ import annotations

from collections.abc import Generator
from textwrap import indent
from typing import TYPE_CHECKING
from typing import Any

import asteval
import numpy as np
import pandas as pd
from tabulate import tabulate

from glotaran.io import load_parameters
from glotaran.parameter.parameter import Parameter
from glotaran.utils.ipython import MarkdownStr
from glotaran.utils.sanitize import pretty_format_numerical

if TYPE_CHECKING:
    from glotaran.parameter.parameter_history import ParameterHistory


[docs] class ParameterNotFoundException(Exception): """Raised when a Parameter is not found.""" def __init__(self, label: str): # noqa: D107 super().__init__(f"Cannot find parameter {label}")
[docs] class Parameters: """A container for :class:`Parameter`.""" loader = load_parameters def __init__(self, parameters: dict[str, Parameter]): """Create :class:`Parameters`. Parameters ---------- parameters : dict[str, Parameter] A parameter list containing parameters Returns ------- 'Parameters' The created :class:`Parameters`. """ self._parameters: dict[str, Parameter] = parameters self._evaluator = asteval.Interpreter(symtable=asteval.make_symbol_table(parameters=self)) self.source_path = "parameters.csv" self.update_parameter_expression()
[docs] @classmethod def from_list( cls, parameter_list: list[float | int | str | list[Any] | dict[str, Any]] ) -> Parameters: """Create :class:`Parameters` from a list. Parameters ---------- parameter_list : list[float | list[Any]] A parameter list containing parameters Returns ------- Parameters The created :class:`Parameters`. .. # noqa: D414 """ defaults: dict[str, Any] | None = next( (item for item in parameter_list if isinstance(item, dict)), None ) parameters = {} for i, item in enumerate(item for item in parameter_list if not isinstance(item, dict)): if not isinstance(item, list): item = [item] if not any(isinstance(v, str) for v in item): item += [f"{i+1}"] parameter = Parameter.from_list(item, default_options=defaults) parameters[parameter.label] = parameter return cls(parameters)
[docs] @classmethod def from_dict( cls, parameter_dict: dict[str, dict[str, Any] | list[float | list[Any]]], ) -> Parameters: """Create a :class:`Parameters` from a dictionary. Parameters ---------- parameter_dict: dict[str, dict[str, Any] | list[float | list[Any]]] A parameter dictionary containing parameters. Returns ------- Parameters The created :class:`Parameters` .. # noqa: D414 """ parameters = {} for label, param_def, default in flatten_parameter_dict(parameter_dict): parameter = Parameter.from_list(param_def, default_options=default) label += f".{parameter.label}" parameter.label = label parameters[label] = parameter return cls(parameters)
[docs] @classmethod def from_parameter_dict_list(cls, parameter_dict_list: list[dict[str, Any]]) -> Parameters: """Create :class:`Parameters` from a list of parameter dictionaries. Parameters ---------- parameter_dict_list : list[dict[str, Any]] A list of parameter dictionaries. Returns ------- Parameters The created :class:`Parameters`. .. # noqa: D414 """ parameters = {} for parameter_dict in parameter_dict_list: parameter = Parameter(**parameter_dict) parameters[parameter.label] = parameter return cls(parameters)
[docs] @classmethod def from_dataframe(cls, df: pd.DataFrame, source: str = "DataFrame") -> Parameters: """Create a :class:`Parameters` from a :class:`pandas.DataFrame`. Parameters ---------- df : pd.DataFrame The source data frame. source : str Optional name of the source file, used for error messages. Raises ------ ValueError Raised if the columns 'label' or 'value' doesn't exist. Also raised if the columns 'minimum', 'maximum' or 'values' contain non numeric values or if the columns 'non-negative' or 'vary' are no boolean. Returns ------- Parameters The created parameter group. .. # noqa: D414 """ for column_name in ["label", "value"]: if column_name not in df: raise ValueError(f"Missing required column '{column_name}' in '{source}'.") for column_name in filter(lambda x: x in df.columns, ["minimum", "maximum", "value"]): if any(not np.isreal(v) for v in df[column_name]): raise ValueError(f"Column '{column_name}' in '{source}' has non numeric values.") for column_name in filter(lambda x: x in df.columns, ["non_negative", "vary"]): df[column_name] = [v != 0 if isinstance(v, int) else v for v in df[column_name]] if any(not isinstance(v, bool) for v in df[column_name]): raise ValueError(f"Column '{column_name}' in '{source}' has non boolean values.") # 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"))
[docs] def to_dataframe(self) -> pd.DataFrame: """Create a pandas data frame from the group. Returns ------- pd.DataFrame The created data frame. """ return pd.DataFrame(self.to_parameter_dict_list())
[docs] def to_parameter_dict_list(self) -> list[dict[str, Any]]: """Create list of parameter dictionaries from the group. Returns ------- list[dict[str, Any]] A list of parameter dictionaries. """ return [p.as_dict() for p in self.all()]
[docs] def to_parameter_dict_or_list(self, serialize_parameters: bool = False) -> dict | list: """Convert to a dict or list of parameter definitions. Parameters ---------- serialize_parameters : bool If true, the parameters will be serialized into list representation. Returns ------- dict | list A dict or list of parameter definitions. """ if all("." not in p.label for p in self.all()): return list(self.all()) parameter_dict: dict[str, Any] = {} for parameter in self.all(): path = parameter.label.split(".") nodes = path[:-2] node = parameter_dict for n in nodes: if n not in node: node[n] = {} node = node[n] upper_node = path[-2] if upper_node not in node: node[upper_node] = [] node[upper_node].append( parameter.as_list(label_short=True) if serialize_parameters else parameter ) return parameter_dict
[docs] def set_from_history(self, history: ParameterHistory, index: int): """Update the :class:`Parameters` with values from a parameter history. Parameters ---------- history : ParameterHistory The parameter history. index : int The history index. """ self.set_from_label_and_value_arrays( # Omit 0th element with `iteration` label history.parameter_labels[1:], history.get_parameters(index)[1:], )
[docs] def copy(self) -> Parameters: """Create a copy of the :class:`Parameters`. Returns ------- Parameters : A copy of the :class:`Parameters`. .. # noqa: D414 """ return Parameters( {label: parameter.copy() for label, parameter in self._parameters.items()} )
[docs] def all(self) -> Generator[Parameter, None, None]: """Iterate over all parameters. Yields ------ Parameter A parameter in the parameters. """ yield from self._parameters.values()
[docs] def has(self, label: str) -> bool: """Check if a parameter with the given label is in the group or in a subgroup. Parameters ---------- label : str The label of the parameter, with its path in a :class:`ParameterGroup` prepended. Returns ------- bool Whether a parameter with the given label exists in the group. """ return label in self._parameters
[docs] def get(self, label: str) -> Parameter: """Get a :class:`Parameter` by its label. Parameters ---------- label : str The label of the parameter, with its path in a :class:`ParameterGroup` prepended. Returns ------- Parameter The parameter. Raises ------ ParameterNotFoundException Raised if no parameter with the given label exists. """ try: return self._parameters[label] except KeyError as error: raise ParameterNotFoundException(label) from error
[docs] def update_parameter_expression(self): """Update all parameters which have an expression. Raises ------ ValueError Raised if an expression evaluates to a non-numeric value. """ for parameter in self.all(): if parameter.expression is not None: value = self._evaluator(parameter.transformed_expression) if not isinstance(value, (int, float)): raise ValueError( f"Expression '{parameter.expression}' of parameter '{parameter.label}' " f"evaluates to non numeric value '{value}'." ) parameter.value = value
[docs] def get_label_value_and_bounds_arrays( self, exclude_non_vary: bool = False ) -> tuple[list[str], np.ndarray, np.ndarray, np.ndarray]: """Return a arrays of all parameter labels, values and bounds. Parameters ---------- exclude_non_vary: bool If true, parameters with `vary=False` are excluded. Returns ------- tuple[list[str], np.ndarray, np.ndarray, np.ndarray] A tuple containing a list of parameter labels and an array of the values, lower and upper bounds. """ self.update_parameter_expression() labels = [] values = [] lower_bounds = [] upper_bounds = [] for parameter in self.all(): if not exclude_non_vary or parameter.vary: labels.append(parameter.label) value, minimum, maximum = parameter.get_value_and_bounds_for_optimization() values.append(value) lower_bounds.append(minimum) upper_bounds.append(maximum) return labels, np.asarray(values), np.asarray(lower_bounds), np.asarray(upper_bounds)
[docs] def set_from_label_and_value_arrays(self, labels: list[str], values: np.ndarray): """Update the parameter values from a list of labels and values. Parameters ---------- labels : list[str] A list of parameter labels. values : np.ndarray An array of parameter values. Raises ------ ValueError Raised if the size of the labels does not match the stize of values. """ if len(labels) != len(values): raise ValueError( f"Length of labels({len(labels)}) not equal to length of values({len(values)})." ) for label, value in zip(labels, values): self.get(label).set_value_from_optimization(value) self.update_parameter_expression()
[docs] def markdown(self, float_format: str = ".3e") -> MarkdownStr: """Format the :class:`ParameterGroup` as markdown string. This is done by recursing the nested :class:`ParameterGroup` tree. Parameters ---------- float_format: str Format string for floating point numbers, by default ".3e" Returns ------- MarkdownStr : The markdown representation as string. """ return param_dict_to_markdown(self.to_parameter_dict_or_list(), float_format=float_format)
def _repr_markdown_(self) -> str: """Create a markdown representation. Special method used by ``ipython`` to render markdown. Returns ------- str : The markdown representation as string. """ return str(self.markdown()) @property def labels(self) -> list[str]: """List of all labels. Returns ------- list[str] """ return sorted(p.label for p in self.all()) def __str__(self) -> str: """Representation used by print and str.""" return str(self.markdown()) def __repr__(self) -> str: """Representation debug.""" params = [f"{p.label!r}: {repr(p)}" for p in self.all()] return f"Parameters({{{', '.join(params)}}})" def __eq__(self, other: object) -> bool: """==""" # noqa: D400 if isinstance(other, Parameters): return self.labels == other.labels and all( self.get(label)._deep_equals(other.get(label)) for label in self.labels ) raise NotImplementedError( "Parameters can only be compared with instances of Parameters, " f"not with {type(other).__qualname__!r}." )
[docs] def flatten_parameter_dict( parameter_dict: dict, ) -> Generator[tuple[str, list[Any], dict[str, Any] | None], None, None]: """Flatten a parameter dictionary. Parameters ---------- parameter_dict: dict The parameter dictionary. Yields ------ tuple[str, list[Any], dict | None The concatenated keys, the parameter definition and default options. """ for key, value in parameter_dict.items(): if isinstance(value, dict): for sub_key, sub_value, sub_dict in flatten_parameter_dict(value): yield f"{key}.{sub_key}", sub_value, sub_dict elif isinstance(value, list): sub_dict: dict[str, Any] | None = next( # type:ignore[no-redef] (item for item in value if isinstance(item, dict)), None ) for index, list_value in enumerate( (list_value for list_value in value if not isinstance(list_value, dict)), start=1 ): if not isinstance(list_value, list): list_value = [str(index), list_value] elif not any(isinstance(v, str) for v in list_value): list_value += [str(index)] yield key, list_value, sub_dict
[docs] def param_dict_to_markdown( parameters: dict | list, float_format: str = ".3e", depth: int = 0, label: str | None = None, ) -> MarkdownStr: """Format the :class:`Parameters` as markdown string. This is done by recursing the nested :class:`Parameters` tree. Parameters ---------- parameters: dict | list The parameter dict or list. float_format: str Format string for floating point numbers, by default ".3e" depth: int The depth of the parameter dict. label: str | None The label of the parameter dict. Returns ------- MarkdownStr : The markdown representation as string. """ node_indentation = " " * depth return_string = "" table_header = [ "_Label_", "_Value_", "_Standard Error_", "_t-value_", "_Minimum_", "_Maximum_", "_Vary_", "_Non-Negative_", "_Expression_", ] if label is not None: return_string += f"{node_indentation}* __{label}__:\n" if isinstance(parameters, list): parameter_rows = [ [ parameter.label_short, parameter.value, parameter.standard_error, repr(pretty_format_numerical(parameter.value / parameter.standard_error)), parameter.minimum, parameter.maximum, parameter.vary, parameter.non_negative, f"`{parameter.expression}`", ] for parameter in parameters ] parameter_table = indent( tabulate( parameter_rows, headers=table_header, tablefmt="github", missingval="None", floatfmt=float_format, ), f" {node_indentation}", ) return_string += f"\n{parameter_table}\n\n" else: for label, child in sorted(parameters.items()): return_string += str( param_dict_to_markdown( child, float_format=float_format, depth=depth + 1, label=label ) ) return MarkdownStr(return_string.replace("'", " "))