from abc import ABCMeta, abstractmethod, abstractproperty
from typing import Optional, Union, Dict, Tuple, Any, Iterable
import logging
"""RELATED THIRD PARTY IMPORTS"""
"""LOCAL IMPORTS"""
from qctoolkit.serialization import Serializable, Serializer
from qctoolkit.expressions import Expression
logger = logging.getLogger(__name__)
__all__ = ["Parameter", "ParameterDeclaration", "ConstantParameter", "ParameterNotProvidedException", "ParameterValueIllegalException"]
[docs]class Parameter(Serializable, metaclass = ABCMeta):
"""A parameter for pulses.
Parameter specifies a concrete value which is inserted instead
of the parameter declaration reference in a PulseTemplate if it satisfies
the minimum and maximum boundary of the corresponding ParameterDeclaration.
Implementations of Parameter may provide a single constant value or
obtain values by computation (e.g. from measurement results).
"""
def __init__(self) -> None:
super().__init__(None)
@abstractmethod
[docs] def get_value(self) -> float:
"""Compute and return the parameter value."""
@abstractproperty
def requires_stop(self) -> bool:
"""Return True if the evaluation of this Parameter instance requires a stop in execution/sequencing, e.g., because it
depends on data that is only measured in during the next execution."""
def __float__(self) -> float:
return float(self.get_value())
[docs]class ConstantParameter(Parameter):
"""A pulse parameter with a constant value."""
def __init__(self, value: float) -> None:
super().__init__()
self.__value = value
[docs] def get_value(self) -> float:
return self.__value
@property
def requires_stop(self) -> bool:
return False
def __repr__(self) -> str:
return "<ConstantParameter {0}>".format(self.__value)
[docs] def get_serialization_data(self, serializer: Serializer) -> Dict[str, Any]:
return dict(type=serializer.get_type_identifier(self), constant=self.__value)
@staticmethod
[docs] def deserialize(serializer: Serializer, constant: float) -> 'ConstantParameter':
return ConstantParameter(constant)
class MappedParameter(Parameter):
"""A pulse parameter whose value is derived from other parameters via some mathematical expression."""
def __init__(self, expression: Expression, dependencies: Optional[Dict[str, Parameter]]=dict()) -> None:
super().__init__()
self.__expression = expression
self.dependencies = dependencies
def __collect_dependencies(self) -> Iterable[Parameter]:
try:
return {dependency_name: self.dependencies[dependency_name] for dependency_name in self.__expression.variables()}
except KeyError as e:
raise ParameterNotProvidedException(str(e)) from e
def get_value(self) -> float:
if self.requires_stop:
raise Exception("Cannot evaluate MappedParameter because at least one dependency cannot be evaluated.")
dependencies = self.__collect_dependencies()
variables = {k: float(dependencies[k]) for k in dependencies}
return self.__expression.evaluate(**variables)
@property
def requires_stop(self) -> bool:
try:
return any([p.requires_stop for p in self.__collect_dependencies().values()])
except:
raise
def __repr__self(self) -> str:
return "<MappedParameter {0} depending on {1}>".format(self.__expression, self.__dependencies)
def get_serialization_data(self, serializer: Serializer) -> Dict[str, Any]:
return dict(type=serializer.get_type_identifier(self),
expression=serializer._serialize_subpulse(self.__expression))
@staticmethod
def deserialize(serializer: Serializer, expression: str) -> 'MappedParameter':
return MappedParameter(serializer.deserialize(expression))
#class ParameterValueProvider(metaclass = ABCMeta):
#
# @abstractmethod
# def get_value(self, parameters: Dict[str, Parameter]) -> float:
# pass
[docs]class ParameterDeclaration(Serializable):
"""A declaration of a parameter required by a pulse template.
PulseTemplates may declare parameters to allow for variations of values in an otherwise
static pulse structure. ParameterDeclaration represents a declaration of such a parameter
and allows for the definition of boundaries and a default value for a parameter.
"""
BoundaryValue = Union[float, 'ParameterDeclaration']
def __init__(self, name: str, min: BoundaryValue = float('-inf'), max: BoundaryValue = float('+inf'), default: Optional[float] = None) -> None:
"""Creates a ParameterDeclaration object.
Args:
min (float, ParameterDeclaration): An optional real number or ParameterDeclaration object specifying the minimum value allowed.
max (float, ParameterDeclaration): An optional real number or ParameterDeclaration object specifying the maximum value allowed.
default (float): An optional real number specifying a default value for the declared pulse template parameter.
"""
super().__init__(None)
self.__name = name
self.__min_value = float('-inf')
self.__max_value = float('+inf')
self.__default_value = default # type: Optional[float]
self.min_value = min # type: BoundaryValue
self.max_value = max # type: BoundaryValue
self.__assert_values_valid()
def __assert_values_valid(self) -> None:
if self.absolute_min_value > self.absolute_max_value:
raise ValueError("Max value ({0}) is less than min value ({1}).".format(self.max_value, self.min_value))
if isinstance(self.min_value, ParameterDeclaration):
if self.min_value.absolute_max_value > self.absolute_max_value:
raise ValueError("Max value ({0}) is less than min value ({1}).".format(self.max_value, self.min_value))
if isinstance(self.max_value, ParameterDeclaration):
if self.max_value.absolute_min_value < self.absolute_min_value:
raise ValueError("Max value ({0}) is less than min value ({1}).".format(self.max_value, self.min_value))
if self.default_value is not None and self.absolute_min_value > self.default_value:
raise ValueError("Default value ({0}) is less than min value ({1}).".format(self.default_value, self.min_value))
if self.default_value is not None and self.absolute_max_value < self.default_value:
raise ValueError("Default value ({0}) is greater than max value ({1}).".format(self.__default_value, self.__max_value))
@property
def name(self) -> str:
return self.__name
@property
def min_value(self) -> BoundaryValue:
"""Return this ParameterDeclaration's minimum value or reference."""
return self.__min_value
@min_value.setter
def min_value(self, value: BoundaryValue) -> None:
"""Set this ParameterDeclaration's minimum value or reference."""
old_value = self.__min_value
self.__min_value = value
try:
if (isinstance(value, ParameterDeclaration) and
(isinstance(value.max_value, ParameterDeclaration) or
value.absolute_max_value == float('+inf'))):
value.__internal_set_max_value(self)
self.__assert_values_valid()
except:
self.__min_value = old_value
raise
def __internal_set_min_value(self, value: BoundaryValue) -> None:
old_value = self.__min_value
self.__min_value = value
try:
self.__assert_values_valid()
except:
self.__min_value = old_value
raise
@property
def max_value(self) -> BoundaryValue:
"""Return this ParameterDeclaration's maximum value or reference."""
return self.__max_value
@max_value.setter
def max_value(self, value: BoundaryValue) -> None:
"""Set this ParameterDeclaration's maximum value or reference."""
old_value = self.__max_value
self.__max_value = value
try:
if (isinstance(value, ParameterDeclaration) and
(isinstance(value.min_value, ParameterDeclaration) or
value.absolute_min_value == float('-inf'))):
value.__internal_set_min_value(self)
self.__assert_values_valid()
except:
self.__max_value = old_value
raise
def __internal_set_max_value(self, value: BoundaryValue) -> None:
old_value = self.__max_value
self.__max_value = value
try:
self.__assert_values_valid()
except:
self.__max_value = old_value
raise
@property
def default_value(self) -> Optional[float]:
"""Return this ParameterDeclaration's default value."""
return self.__default_value
@property
def absolute_min_value(self) -> float:
"""Return this ParameterDeclaration's minimum value.
If the minimum value of this ParameterDeclaration instance is a reference to another
instance, references are resolved until a concrete value or None is obtained.
"""
if isinstance(self.min_value, ParameterDeclaration):
return self.min_value.absolute_min_value
else:
return self.min_value
@property
def absolute_max_value(self) -> float:
"""Return this ParameterDeclaration's maximum value.
If the maximum value of this ParameterDeclaration instance is a reference to another
instance, references are resolved until a concrete value or None is obtained.
"""
if isinstance(self.max_value, ParameterDeclaration):
return self.max_value.absolute_max_value
else:
return self.max_value
[docs] def is_parameter_valid(self, p: Parameter) -> bool:
"""Checks whether a given parameter satisfies this ParameterDeclaration.
A parameter is valid if all of the following statements hold:
- If the declaration specifies a minimum value, the parameter's value must be greater or equal
- If the declaration specifies a maximum value, the parameter's value must be less or equal
"""
parameter_value = float(p)
is_valid = True
is_valid &= self.absolute_min_value <= parameter_value
is_valid &= self.absolute_max_value >= parameter_value
return is_valid
[docs] def get_value(self, parameters: Dict[str, Parameter]) -> float:
value = self.__get_value_internal(parameters)
if not self.__check_parameter_set_valid(parameters):
raise ParameterValueIllegalException(self, value)
return value
def __check_parameter_set_valid(self, parameters: Dict[str, Parameter]) -> bool:
parameter_value = self.__get_value_internal(parameters)
# get actual instantiated values for boundaries.
min_value = self.min_value
if isinstance(min_value, ParameterDeclaration):
min_value = min_value.__get_value_internal(parameters)
max_value = self.max_value
if isinstance(max_value, ParameterDeclaration):
max_value = max_value.__get_value_internal(parameters)
return min_value <= parameter_value and max_value >= parameter_value
def __get_value_internal(self, parameters: Dict[str, Parameter]) -> float:
try:
return float(parameters[self.name]) # float() wraps get_value for Parameters and works for normal floats also
except KeyError:
if self.default_value is not None:
return self.default_value
else:
raise ParameterNotProvidedException(self.name)
def __str__(self) -> str:
min_value_str = self.absolute_min_value
if isinstance(self.min_value, ParameterDeclaration):
min_value_str = "Parameter '{0}' (min {1})".format(self.min_value.name, min_value_str)
max_value_str = self.absolute_max_value
if isinstance(self.max_value, ParameterDeclaration):
max_value_str = "Parameter '{0}' (max {1})".format(self.max_value.name, max_value_str)
return "{4} '{0}', range ({1}, {2}), default {3}".format(self.name, min_value_str, max_value_str, self.default_value, type(self))
def __compute_compare_key(self) -> Tuple[str, Union[float, str], Union[float, str], Optional[float]]:
min_value = self.min_value
if isinstance(min_value, ParameterDeclaration):
min_value = min_value.name
max_value = self.max_value
if isinstance(max_value, ParameterDeclaration):
max_value = max_value.name
return (self.name, min_value, max_value, self.default_value)
def __repr__(self) -> str:
return "<"+self.__str__()+">"
def __eq__(self, other) -> bool:
return (isinstance(other, ParameterDeclaration) and
self.__compute_compare_key() == other.__compute_compare_key())
def __hash__(self) -> int:
return hash(self.__compute_compare_key())
[docs] def get_serialization_data(self, serializer: Serializer) -> Dict[str, Any]:
data = dict()
min_value = self.min_value
if isinstance(min_value, ParameterDeclaration):
min_value = min_value.name
max_value = self.max_value
if isinstance(max_value, ParameterDeclaration):
max_value = max_value.name
data['name'] = self.name
data['min_value'] = min_value
data['max_value'] = max_value
data['default_value'] = self.default_value
data['type'] = serializer.get_type_identifier(self)
return data
@staticmethod
[docs] def deserialize(serializer: Serializer,
name: str,
min_value: Union[str, float],
max_value: Union[str, float],
default_value: float) -> 'ParameterDeclaration':
if isinstance(min_value, str):
min_value = float("-inf")
if isinstance(max_value, str):
max_value = float("+inf")
return ParameterDeclaration(name, min=min_value, max=max_value, default=default_value)
[docs]class ParameterNotProvidedException(Exception):
"""Indicates that a required parameter value was not provided."""
def __init__(self, parameter_name: str) -> None:
super().__init__()
self.parameter_name = parameter_name
def __str__(self) -> str:
return "No value was provided for parameter '{0}' and no default value was specified.".format(self.parameter_name)
[docs]class ParameterValueIllegalException(Exception):
"""Indicates that the value provided for a parameter is illegal, i.e., is outside the parameter's bounds or of wrong type."""
def __init__(self, parameter_declaration: ParameterDeclaration, parameter_value: float) -> None:
super().__init__()
self.parameter_value = parameter_value
self.parameter_declaration = parameter_declaration
def __str__(self) -> str:
return "The value {0} provided for parameter {1} is illegal (min = {2}, max = {3})".format(
self.parameter_value, self.parameter_declaration.name, self.parameter_declaration.min_value,
self.parameter_declaration.max_value)