"""
Parameter classes implementing lazy evaluation and dependency resolution.
Author: Fabian A. Preiss
"""
from __future__ import annotations
import types
from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Tuple, Union, Type, cast
import matplotlib.pyplot as plt
import types
from typeguard import check_type
[docs]class ParameterError(Exception):
pass
[docs]class ParameterRegisterError(Exception):
pass
[docs]def interface_function_handle(parameter: Dict[Enum, Parameter]) -> Any:
r"""Construct value using parent `Parameter`\ s"""
raise NotImplementedError
# TODO
# parents/children internal access: Dict[Enum, Parameter]
# parents/children external access: Set[Parameter]
[docs]class Parameter:
r"""
Base `Parameter` class, aware of related `Parameter`\ s and lazy evaluating.
"""
def __init__(
self,
enum_id: Enum,
value_definition: Union[Type[Any], Callable[[Dict[Enum, Parameter]], Any]],
parent_parameters: Optional[Set[Parameter]] = None,
):
self.__enum_id = enum_id
self.__parents: Dict[Enum, Parameter] = (
{}
if parent_parameters is None
else {parent.enum_id: parent for parent in parent_parameters}
)
# children have to be set from outside user
self.__children: Dict[Enum, Parameter] = {}
self.__descendants: Dict[Enum, Parameter] = {}
self.__function_handle: Callable[[Dict[Enum, Parameter]], Any]
self.__type_info: Union[Type[Any], Callable[[Dict[Enum, Parameter]], Any]]
if isinstance(value_definition, types.FunctionType):
self.__type_info = value_definition.__annotations__.get("return", Any)
self.__function_handle = value_definition
else:
try:
self.__type_info = value_definition
except TypeError as err:
raise ParameterError(
"value_definition is neither a type or a function."
) from err
self.__function_handle = interface_function_handle
if self.__type_info is Any:
raise ParameterError(
"value_definition requires type informations other than Any."
)
self.__value: Any = None
self.__const: bool = False
def __gt__(self, other: Parameter) -> bool:
if type(self).__name__ > type(other).__name__:
return True
if type(self).__name__ < type(other).__name__:
return False
return self.enum_id.name > other.enum_id.name
def __lt__(self, other: Parameter) -> bool:
if type(self).__name__ > type(other).__name__:
return False
if type(self).__name__ < type(other).__name__:
return True
return self.enum_id.name < other.enum_id.name
@property
def enum_id(self) -> Enum:
"""Returns the Parameter ID as an enumeration."""
return self.__enum_id
@property
def type_info(self):
"""The type of this `Parameter`."""
return self.__type_info
@property
def const(self) -> bool:
"""Flag if the value can be overwritten once initialized."""
return self.__const
@const.setter
def const(self, const: bool) -> None:
if self.__const:
raise ParameterError("Parameter is already flagged as constant.")
self.__const = const
@property
def value(self):
"""The value of this `Parameter`."""
if self.__value is None:
self.__clear_descendants()
try:
self.__value = self.__function_handle(self.__parents)
except (NotImplementedError, KeyError) as err:
raise ParameterError(err)
return self.__value
@value.setter
def value(self, value: Any):
self.__clear_descendants()
try:
check_type("value", value, self.__type_info)
except TypeError as err:
raise ParameterError(
"interface_function_handle annotation does not match given value"
) from err
if self.__const and self.__value is not None:
raise ParameterError("Cannot overwrite constant parameter.")
self.__value = value
[docs] def clear(self, clear_descendants=True):
r"""Clear the value stored in this and dependent `Parameter`\ s."""
if clear_descendants:
self.__clear_descendants()
if not self.__const:
self.__value = None
@property
def parents(self) -> Dict[Enum, Parameter]:
r"""Parent `Parameter`\ s of this `Parameter`\ s instance."""
return self.__parents.copy()
@property
def children(self) -> Dict[Enum, Parameter]:
r"""`Parameter`\ s with a child relation to this `Parameter`\ s instance."""
return self.__children.copy()
[docs] def add_child(self, child: Parameter):
r"""Assign a Parameter a child relation"""
if child.enum_id in self.__parents:
raise ParameterError(
"Neighbour can't be parent and child at the same time."
)
self.__children[child.enum_id] = child
@property
def descendants(self) -> Dict[Enum, Parameter]:
r"""`Parameter`\ s with a descendant relation to this `Parameter`\ s instance."""
return self.__descendants.copy()
[docs] def add_descendant(self, descendant: Parameter):
r"""Assign a Parameter a child relation"""
if descendant.enum_id in self.__parents:
raise ParameterError(
f"Parameter {descendant.enum_id} can't be parent and descendant at the same time."
)
self.__descendants[descendant.enum_id] = descendant
def __clear_descendants(self):
for descendant_enum_id in self.__descendants:
self.__descendants[descendant_enum_id].clear(clear_descendants=False)
[docs]class ImageParameter(Parameter):
"""`Parameter` with additional display functionality for stored image."""
def __init__(
self,
enum_id: Enum,
value_definition: Union[Type[Any], Callable[[Dict[Enum, Parameter]], Any]],
parent_parameters: Optional[Set[Parameter]] = None,
):
super().__init__(enum_id, value_definition, parent_parameters)
self.__title = None
self.__bounds: Optional[Tuple[int, int, int, int]] = None
self.__bounds_lookup: Optional[Enum] = None
self.__cmap = "gray"
self.__aspect: Optional[float] = None
[docs] def display(self, axsp: Any = None):
"""
Display the stored image using matplotlib.
Parameters
----------
aspx : matplotlib.axes._subplots.AxesSubplot
AxesSubplot on which to draw
Notes
-----
Uses the `title`, `cmap` and `bounds` properties of the `ImageParameter` class.
"""
if axsp is None:
fig = plt.figure()
axsp = fig.subplots()
if self.__title is not None:
axsp.set_title(self.__title)
if self.bounds is None:
axsp.imshow(self.value, cmap=self.__cmap, aspect=self.__aspect)
else:
(r_lower, r_upper, c_lower, c_upper) = self.bounds
axsp.imshow(
self.value[r_lower:r_upper, c_lower:c_upper],
cmap=self.__cmap,
aspect=self.__aspect,
)
@property
def title(self) -> Optional[str]:
"""The image title used by the display function."""
return self.__title
@title.setter
def title(self, title):
self.__title = title
@property
def bounds(self) -> Optional[Tuple[int, int, int, int]]:
"""Cropping boundaries for the display function."""
if self.__bounds is None and self.__bounds_lookup is None:
return None
if self.__bounds is None:
return cast(
Optional[Tuple[int, int, int, int]],
self.parents[cast(Enum, self.__bounds_lookup)].value,
)
return self.__bounds
@bounds.setter
def bounds(self, bounds: Tuple[int, int, int, int]):
self.__bounds = bounds
@property
def cmap(self) -> str:
"""The colormap used by matplotlib."""
return self.__cmap
@cmap.setter
def cmap(self, cmap: str):
self.__cmap = cmap
@property
def aspect(self) -> Optional[float]:
"""The aspect ratio used by matplotlib."""
return self.__aspect
@aspect.setter
def aspect(self, aspect: float):
self.__aspect = aspect
[docs] def set_bounds_lookup(self, bounds_enum_id: Enum):
"""Reference a boundary Parameter using its `enum_id` as a lookup for cropping."""
self.__bounds_lookup = bounds_enum_id