Source code for qupulse.pulses.parameters
"""This module defines parameter constriants.
"""
from abc import abstractmethod
from typing import Optional, Union, Dict, Any, Iterable, Set, List, Mapping, AbstractSet
from numbers import Real
import warnings
import sympy
import numpy
from qupulse.serialization import AnonymousSerializable
from qupulse.expressions import Expression
from qupulse.parameter_scope import Scope, ParameterNotProvidedException
from qupulse.utils.sympy import sympify
__all__ = ["ParameterNotProvidedException", "ParameterConstraintViolation", "ParameterConstraint"]
[docs]class ParameterConstraint(AnonymousSerializable):
"""A parameter constraint like 't_2 < 2.7' that can be used to set bounds to parameters."""
def __init__(self, relation: Union[str, sympy.Expr]):
super().__init__()
if isinstance(relation, str) and '==' in relation:
# The '==' operator is interpreted by sympy as exactly, however we need a symbolical evaluation
self._expression = sympy.Eq(*sympify(relation.split('==')))
else:
self._expression = sympify(relation)
if not isinstance(self._expression, sympy.logic.boolalg.Boolean):
raise ValueError('Constraint is not boolean')
self._expression = Expression(self._expression)
@property
def affected_parameters(self) -> Set[str]:
return set(self._expression.variables)
[docs] def is_fulfilled(self, parameters: Mapping[str, Any], volatile: AbstractSet[str] = frozenset()) -> bool:
"""
Args:
parameters: These parameters are checked.
volatile: For each of these parameters a warning is raised if they appear in a constraint
Raises:
:class:`qupulse.parameter_scope.ParameterNotProvidedException`: if a parameter is missing
Warnings:
ConstrainedParameterIsVolatileWarning: if a constrained parameter is volatile
"""
affected_parameters = self.affected_parameters
if not affected_parameters.issubset(parameters.keys()):
raise ParameterNotProvidedException((affected_parameters-parameters.keys()).pop())
for parameter in volatile & affected_parameters:
warnings.warn(ConstrainedParameterIsVolatileWarning(parameter_name=parameter, constraint=self))
return numpy.all(self._expression.evaluate_in_scope(parameters))
@property
def sympified_expression(self) -> sympy.Expr:
return self._expression.underlying_expression
def __eq__(self, other: 'ParameterConstraint') -> bool:
return self._expression.underlying_expression == other._expression.underlying_expression
def __str__(self) -> str:
if isinstance(self._expression.underlying_expression, sympy.Eq):
return '{}=={}'.format(self._expression.underlying_expression.lhs,
self._expression.underlying_expression.rhs)
else:
return str(self._expression.underlying_expression)
def __repr__(self):
return 'ParameterConstraint(%s)' % repr(str(self))
[docs] def get_serialization_data(self) -> str:
return str(self)
ConstraintLike = Union[sympy.Expr, str, ParameterConstraint]
class ParameterConstrainer:
"""A class that implements the testing of parameter constraints. It is used by the subclassing pulse templates."""
def __init__(self, *,
parameter_constraints: Optional[Iterable[ConstraintLike]]) -> None:
if parameter_constraints is None:
self._parameter_constraints = []
else:
self._parameter_constraints = [constraint if isinstance(constraint, ParameterConstraint)
else ParameterConstraint(constraint)
for constraint in parameter_constraints]
@property
def parameter_constraints(self) -> List[ParameterConstraint]:
return self._parameter_constraints
def validate_parameter_constraints(self, parameters: [str, Real], volatile: Set[str]) -> None:
"""
Raises a ParameterConstraintViolation exception if one of the constraints is violated.
Args:
parameters: These parameters are checked.
volatile: For each of these parameters a warning is raised if they appear in a constraint
Raises:
ParameterConstraintViolation: if one of the constraints is violated.
Warnings:
ConstrainedParameterIsVolatileWarning: via `ParameterConstraint.is_fulfilled`
"""
for constraint in self._parameter_constraints:
if not constraint.is_fulfilled(parameters, volatile=volatile):
raise ParameterConstraintViolation(constraint, parameters)
def validate_scope(self, scope: Scope):
volatile = scope.get_volatile_parameters().keys()
for constraint in self._parameter_constraints:
if not constraint.is_fulfilled(scope, volatile=volatile):
constrained_parameters = {parameter_name: scope[parameter_name]
for parameter_name in constraint.affected_parameters}
raise ParameterConstraintViolation(constraint, constrained_parameters)
@property
def constrained_parameters(self) -> AbstractSet[str]:
return set().union(*(c.affected_parameters for c in self._parameter_constraints))
[docs]class ParameterConstraintViolation(Exception):
def __init__(self, constraint: ParameterConstraint, parameters: Dict[str, Real]):
super().__init__("The constraint '{}' is not fulfilled.\nParameters: {}".format(constraint, parameters))
self.constraint = constraint
self.parameters = parameters
class InvalidParameterNameException(Exception):
def __init__(self, parameter_name: str):
self.parameter_name = parameter_name
def __str__(self) -> str:
return '{} is an invalid parameter name'.format(self.parameter_name)
class ConstrainedParameterIsVolatileWarning(RuntimeWarning):
def __init__(self, parameter_name: str, constraint: ParameterConstraint):
super().__init__(parameter_name, constraint)
@property
def parameter_name(self) -> str:
return self.args[0]
@property
def constraint(self) -> ParameterConstraint:
return self.args[1]
def __str__(self):
return ("The parameter '{parameter_name}' is constrained "
"by '{constraint}' but marked as volatile").format(parameter_name=self.parameter_name,
constraint=self.constraint)