Source code for imgreg.util.params

"""
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, Type, Union, cast

import matplotlib.pyplot as plt
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) -> None: 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) -> None: 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) -> None: 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) -> None: 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