# -*- coding: utf-8 -*-
"""
This file contains Qudi methods for handling real-world values with units.
.. Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
.. distribution and on <https://github.com/Ulm-IQO/qudi-core/>
..
.. This file is part of qudi.
..
.. Qudi is free software: you can redistribute it and/or modify it under the terms of
.. the GNU Lesser General Public License as published by the Free Software Foundation,
.. either version 3 of the License, or (at your option) any later version.
..
.. Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
.. without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
.. See the GNU Lesser General Public License for more details.
..
.. You should have received a copy of the GNU Lesser General Public License along with qudi.
.. If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = ['ScalarConstraint']
from typing import Union, Optional, Tuple, Callable, Any
from qudi.util.helpers import is_float, is_integer
[docs]
class ScalarConstraint:
""" """
[docs]
def __init__(
self,
default: Union[int, float],
bounds: Tuple[Union[int, float], Union[int, float]],
increment: Optional[Union[int, float]] = None,
enforce_int: Optional[bool] = False,
checker: Optional[Callable[[Union[int, float]], bool]] = None,
) -> None:
""" """
self._enforce_int = bool(enforce_int)
self.check_value_type(default)
for value in bounds:
self.check_value_type(value)
if increment is not None:
self.check_value_type(increment)
if checker is not None and not callable(checker):
raise TypeError(
'checker must be either None or a callable accepting a single scalar '
'and returning a valid-flag bool or raising ValueError'
)
self._default = default
self._minimum, self._maximum = sorted(bounds)
self._increment = increment
self._checker = checker
if not self.is_valid(self._default):
raise ValueError(f'invalid default value ({self._default}) encountered')
@property
def bounds(self) -> Tuple[Union[int, float], Union[int, float]]:
return self._minimum, self._maximum
@property
def minimum(self) -> Union[int, float]:
return self._minimum
@property
def maximum(self) -> Union[int, float]:
return self._maximum
@property
def default(self) -> Union[int, float]:
return self._default
@property
def increment(self) -> Union[None, int, float]:
return self._increment
@property
def enforce_int(self) -> bool:
return self._enforce_int
[docs]
def check(self, value: Union[int, float]) -> None:
self.check_value_type(value)
self.check_value_range(value)
self.check_custom(value)
[docs]
def is_valid(self, value: Union[int, float]) -> bool:
try:
self.check(value)
except (ValueError, TypeError):
return False
return True
[docs]
def clip(self, value: Union[int, float]) -> Union[int, float]:
return min(self._maximum, max(self._minimum, value))
[docs]
def copy(self) -> object:
return ScalarConstraint(
default=self.default,
bounds=self.bounds,
increment=self.increment,
enforce_int=self.enforce_int,
checker=self._checker,
)
[docs]
def check_custom(self, value: Any) -> None:
if (self._checker is not None) and (not self._checker(value)):
raise ValueError(f'Custom checker failed to validate value "{value}"')
[docs]
def check_value_range(self, value: Union[int, float]) -> None:
if not (self._minimum <= value <= self._maximum):
raise ValueError(f'Value "{value}" is out of bounds {self.bounds}')
[docs]
def check_value_type(self, value: Any) -> None:
if self._enforce_int:
if not is_integer(value):
raise TypeError(f'values must be int type (received {value})')
else:
if not (is_integer(value) or is_float(value)):
raise TypeError(f'values must be int or float type (received {value})')
def __repr__(self) -> str:
cls = self.__class__.__name__
module = self.__class__.__module__
return (
f'{module}.{cls}('
f'default={self.default}, '
f'bounds={self.bounds}, '
f'increment={self.increment}, '
f'enforce_int={self.enforce_int}, '
f'checker={self._checker})'
)
def __copy__(self):
return self.copy()
def __deepcopy__(self, memodict={}):
new_obj = self.copy()
memodict[id(self)] = new_obj
return new_obj
# Backwards compatibility properties:
@default.setter
def default(self, value: Union[int, float]):
if not self.is_valid(value):
raise ValueError(f'invalid default value ({value}) encountered')
self._default = value
@property
def min(self) -> Union[int, float]:
return self._minimum
@min.setter
def min(self, value: Union[int, float]):
self._minimum = value
@property
def max(self) -> Union[int, float]:
return self._maximum
@max.setter
def max(self, value: Union[int, float]):
self._maximum = value
@property
def step(self) -> Union[None, int, float]:
return self._increment
@step.setter
def step(self, value: Union[None, int, float]):
self._increment = value