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

Serveral refactorings in glotaran.Parameter #910

Merged
merged 18 commits into from
Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ad18248
Added Parameter to and from dict methods.
joernweissenborn Nov 20, 2021
0064f88
Moved markdown related parameter group tests to test_parameter_group_…
joernweissenborn Nov 20, 2021
4cc77d2
Refactored parameter tests.
joernweissenborn Nov 20, 2021
a76ad61
Added parameter to and from dictonionary list.
joernweissenborn Nov 20, 2021
02f5a9e
Added more extensive test for paramater csv.
joernweissenborn Nov 20, 2021
f6f5026
Added `as_optimized` to parameter(group) as dict and dataframe functi…
joernweissenborn Nov 21, 2021
95aac8c
Fixed deprecation test.
joernweissenborn Nov 21, 2021
ef65e56
Added `Parameter.markdown`
joernweissenborn Nov 21, 2021
5e2d1e2
Update glotaran/parameter/parameter_group.py
joernweissenborn Nov 21, 2021
0d10f4b
♻️ Refactored by Sourcery (less if nesting)
Nov 21, 2021
36123af
🩹 Fixed renamed method get_group_for_parameter_by_label only in def
s-weigand Nov 21, 2021
bed48e8
🧹 Removed redunent int since mypy sees int as subclass of float
s-weigand Nov 21, 2021
9d5ea14
♻️ Changed stderr type from 'float|None' to float and default to np.nan
s-weigand Nov 21, 2021
9402e55
🩹🧪 Fixed tests due to changed rendering
s-weigand Nov 21, 2021
5172afa
👌 Improved typing where dict and list were used w/o types
s-weigand Nov 21, 2021
053e356
🧹👌 Fixed up left over tying issues in parameters
s-weigand Nov 21, 2021
6c418bb
🧹 Fixed typo
s-weigand Nov 21, 2021
4168edd
🧹 Apply minor naming suggestions from code review
s-weigand Nov 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,0.0
j.0,0.0,None,-inf,inf,False,False,0.0
kinetic.1,0.5,None,-inf,inf,False,True,0.0
kinetic.2,0.3,None,-inf,inf,False,True,0.0
kinetic.3,0.1,None,-inf,inf,False,True,0.0
irf.center,0.3,None,-inf,inf,False,True,0.0
irf.width,0.1,None,-inf,inf,False,True,0.0
"""
)

Expand Down
108 changes: 103 additions & 5 deletions glotaran/parameter/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -42,10 +43,11 @@ def __init__(
self,
label: str = None,
full_label: str = None,
expression: str = None,
expression: str | None = None,
maximum: int | float = np.inf,
minimum: int | float = -np.inf,
non_negative: bool = False,
standard_error: float | None = None,
value: float | int = np.nan,
vary: bool = True,
):
Expand All @@ -58,7 +60,7 @@ 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
Expand All @@ -67,6 +69,8 @@ def __init__(
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 | None
The standard error of the parameter.
value : float
Value of the parameter, by default np.nan
vary : bool
Expand All @@ -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

Expand Down Expand Up @@ -144,6 +148,55 @@ def from_list_or_value(
param._set_options_from_dict(options)
return param

@classmethod
def from_dict(cls, parameter_dict: dict) -> Parameter:
"""Create a :class:`Parameter` from a dictionary.

Expects a dictionary created by :method:`Parameter.as_dict`.

Parameters
----------
parameter_dict : dict
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:
"""Create a dictionary containing the parameter properties.

Intended for internal use.

Parameters
----------
as_optimized : bool
Whether to include properties which are the result of optimization.

Returns
-------
dict
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.

Expand Down Expand Up @@ -342,7 +395,7 @@ def transformed_expression(self) -> str | None:
return self._transformed_expression

@property
def standard_error(self) -> float:
def standard_error(self) -> float | None:
"""Standard error of the optimized parameter.

Returns
Expand All @@ -353,7 +406,7 @@ def standard_error(self) -> float:
return self._stderr

@standard_error.setter
def standard_error(self, standard_error: float):
def standard_error(self, standard_error: float | None):
self._stderr = standard_error

@property
Expand Down Expand Up @@ -409,6 +462,51 @@ 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 None:
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})"
else:
if 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 (
Expand Down
140 changes: 85 additions & 55 deletions glotaran/parameter/parameter_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def from_list(
Returns
-------
ParameterGroup
The created :class:`ParameterGroup`
The created :class:`ParameterGroup`.
"""
root = cls(label=label, root_group=root_group)

Expand All @@ -146,6 +146,29 @@ def from_list(
root.update_parameter_expression()
return root

@classmethod
def from_parameter_dict_list(cls, parameter_dict_list: list[dict]) -> ParameterGroup:
"""Create a :class:`ParameterGroup` from a list of parameter dictionaries.

Parameters
----------
parameter_dict_list : list[dict]
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_paramter_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`.
Expand Down Expand Up @@ -181,41 +204,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:
Expand All @@ -239,32 +232,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]:
"""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]
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=(
Expand Down
Loading