diff --git a/changes.d/750.feature b/changes.d/750.feature new file mode 100644 index 000000000..9e745d690 --- /dev/null +++ b/changes.d/750.feature @@ -0,0 +1,5 @@ +Promote ``qupulse.expression`` to a subpackage and create ``qupulse.expression.protocol`` with protocol classes that define the expression interface that is supposed to be used by qupulse. +The ```sympy`` based implementation is moved to ``qupulse.expressions.sympy`` and imported in ``qupulse.expressions``. + +The intended use is to be able to use less powerful but faster implementations of the ``Expression`` protocol where appropriate. +In this first iteration, qupulse still relies on internals of the ``sympy`` based implementation in many places which is to be removed in the future. diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 9553f0e3f..9c1463917 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -33,7 +33,7 @@ Parameters As mentioned above, all pulse templates may depend on parameters. During pulse template initialization the parameters simply are the free variables of expressions that occur in the pulse template. For example the :class:`.FunctionPulseTemplate` has expressions for its duration and the voltage time dependency i.e. the underlying function. Some pulse templates provided means to constrain parameters by accepting a list of :class:`.ParameterConstraint` which encapsulate comparative expressions that must evaluate to true for a given parameter set to successfully instantiate a pulse from the pulse template. This can be used to encode physical or logical parameter boundaries at pulse level. -The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.Expression` class which wraps `sympy `_ for string evaluation. +The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.sympy.Expression` class which wraps `sympy `_ for string evaluation by default. Other more performant or secure backends can potentially be implemented by conforming to the :class:`.protocol.Expression`. Parameters can be mapped to arbitrary expressions via :class:`.mapping_pulse_template.MappingPulseTemplate`. One use case can be deriving pulse parameters from physical quantities. diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py new file mode 100644 index 000000000..52aa5f325 --- /dev/null +++ b/qupulse/expressions/__init__.py @@ -0,0 +1,76 @@ +"""This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol` +that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow +default implementation with a faster less expressive backend. + +The default implementation is in :py:mod:`.expressions.sympy`. + +There is are wrapper classes for finding non-protocol uses of expression in :py:mod:`.expressions.wrapper`. Define +``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages. +""" + +from typing import Type, TypeVar +from numbers import Real +import os + +import numpy as np +import sympy as sp + +from . import sympy, protocol, wrapper + + +__all__ = ["Expression", "ExpressionVector", "ExpressionScalar", + "NonNumericEvaluation", "ExpressionVariableMissingException"] + + +Expression: Type[protocol.Expression] = sympy.Expression +ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar +ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector + + +if os.environ.get('QUPULSE_EXPRESSION_WRAPPER', None): # pragma: no cover + Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, + sympy.ExpressionScalar, + sympy.ExpressionVector) + + +ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar) + + +class ExpressionVariableMissingException(Exception): + """An exception indicating that a variable value was not provided during expression evaluation. + + See also: + qupulse.expressions.Expression + """ + + def __init__(self, variable: str, expression: Expression) -> None: + super().__init__() + self.variable = variable + self.expression = expression + + def __str__(self) -> str: + return f"Could not evaluate <{self.expression}>: A value for variable <{self.variable}> is missing!" + + +class NonNumericEvaluation(Exception): + """An exception that is raised if the result of evaluate_numeric is not a number. + + See also: + qupulse.expressions.Expression.evaluate_numeric + """ + + def __init__(self, expression: Expression, non_numeric_result, call_arguments): + self.expression = expression + self.non_numeric_result = non_numeric_result + self.call_arguments = call_arguments + + def __str__(self) -> str: + if isinstance(self.non_numeric_result, np.ndarray): + dtype = self.non_numeric_result.dtype + + if dtype == np.dtype('O'): + dtypes = set(map(type, self.non_numeric_result.flat)) + return f"The result of evaluate_numeric is an array with the types {dtypes} which is not purely numeric" + else: + dtype = type(self.non_numeric_result) + return f"The result of evaluate_numeric is of type {dtype} which is not a number" diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py new file mode 100644 index 000000000..667337c3d --- /dev/null +++ b/qupulse/expressions/protocol.py @@ -0,0 +1,105 @@ +"""This module contains the interface / protocol descriptions of ``Expression``, ``ExpressionScalar`` and +``ExpressionVector``.""" + +from typing import Mapping, Union, Sequence, Hashable, Any, Protocol +from numbers import Real + +import numpy as np + + +class Ordered(Protocol): + def __lt__(self, other): + pass + + def __le__(self, other): + pass + + def __gt__(self, other): + pass + + def __ge__(self, other): + pass + + +class Scalar(Protocol): + def __add__(self, other): + pass + + def __sub__(self, other): + pass + + def __mul__(self, other): + pass + + def __truediv__(self, other): + pass + + def __floordiv__(self, other): + pass + + def __ceil__(self): + pass + + def __floor__(self): + pass + + def __float__(self): + pass + + def __int__(self): + pass + + def __abs__(self): + pass + + +class Expression(Hashable, Protocol): + """This protocol defines how Expressions are allowed to be used in qupulse.""" + + def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: + """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be + any mapping.) + Args: + scope: + + Returns: + + """ + + def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression': + """Substitute a part of the expression for another""" + + def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: + """Evaluate to a time dependent expression or a constant.""" + + @property + def variables(self) -> Sequence[str]: + """ Get all free variables in the expression. + + Returns: + A collection of all free variables occurring in the expression. + """ + raise NotImplementedError() + + @classmethod + def make(cls, + expression_or_dict, + numpy_evaluation=None) -> 'Expression': + """Backward compatible expression generation to allow creation from dict.""" + raise NotImplementedError() + + @property + def underlying_expression(self) -> Any: + """Return some internal unspecified representation""" + raise NotImplementedError() + + def get_serialization_data(self): + raise NotImplementedError() + + +class ExpressionScalar(Expression, Scalar, Ordered, Protocol): + pass + + +class ExpressionVector(Expression, Protocol): + pass diff --git a/qupulse/expressions.py b/qupulse/expressions/sympy.py similarity index 88% rename from qupulse/expressions.py rename to qupulse/expressions/sympy.py index bbfad3eed..eeb64ee97 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions/sympy.py @@ -18,7 +18,9 @@ get_most_simple_representation, get_variables, evaluate_lamdified_exact_rational from qupulse.utils.types import TimeType -__all__ = ["Expression", "ExpressionVariableMissingException", "ExpressionScalar", "ExpressionVector", "ExpressionLike"] +import qupulse.expressions + +__all__ = ["Expression", "ExpressionScalar", "ExpressionVector"] _ExpressionType = TypeVar('_ExpressionType', bound='Expression') @@ -60,9 +62,9 @@ def _parse_evaluate_numeric_vector(vector_result: numpy.ndarray) -> numpy.ndarra if not issubclass(vector_result.dtype.type, allowed_scalar): obj_types = set(map(type, vector_result.flat)) if all(issubclass(obj_type, sympy.Integer) for obj_type in obj_types): - result = vector_result.astype(numpy.int64) + vector_result = vector_result.astype(numpy.int64) elif all(issubclass(obj_type, (sympy.Integer, sympy.Float)) for obj_type in obj_types): - result = vector_result.astype(float) + vector_result = vector_result.astype(float) else: raise ValueError("Could not parse vector result", vector_result) return vector_result @@ -98,7 +100,7 @@ def _parse_evaluate_numeric_arguments(self, eval_args: Mapping[str, Number]) -> # we forward qupulse errors, I down like this raise else: - raise ExpressionVariableMissingException(key_error.args[0], self) from key_error + raise qupulse.expressions.ExpressionVariableMissingException(key_error.args[0], self) from key_error def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: """Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be @@ -129,7 +131,7 @@ def evaluate_symbolic(self, substitutions: Mapping[Any, Any]) -> 'Expression': def _evaluate_to_time_dependent(self, scope: Mapping) -> Union['Expression', Number, numpy.ndarray]: try: return self.evaluate_numeric(**scope, t=sympy.symbols('t')) - except NonNumericEvaluation as non_num: + except qupulse.expressions.NonNumericEvaluation as non_num: return ExpressionScalar(non_num.non_numeric_result) except TypeError: return self.evaluate_symbolic(scope) @@ -212,7 +214,7 @@ def evaluate_in_scope(self, scope: Mapping) -> numpy.ndarray: try: return _parse_evaluate_numeric_vector(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err def get_serialization_data(self) -> Sequence[str]: serialized_items = list(map(get_most_simple_representation, self._expression_items)) @@ -444,7 +446,7 @@ def evaluate_with_exact_rationals(self, scope: Mapping) -> Union[Number, numpy.n try: return _parse_evaluate_numeric(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: parsed_kwargs = self._parse_evaluate_numeric_arguments(scope) @@ -453,50 +455,4 @@ def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: try: return _parse_evaluate_numeric(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err - - -class ExpressionVariableMissingException(Exception): - """An exception indicating that a variable value was not provided during expression evaluation. - - See also: - qupulse.expressions.Expression - """ - - def __init__(self, variable: str, expression: Expression) -> None: - super().__init__() - self.variable = variable - self.expression = expression - - def __str__(self) -> str: - return "Could not evaluate <{}>: A value for variable <{}> is missing!".format( - str(self.expression), self.variable) - - -class NonNumericEvaluation(Exception): - """An exception that is raised if the result of evaluate_numeric is not a number. - - See also: - qupulse.expressions.Expression.evaluate_numeric - """ - - def __init__(self, expression: Expression, non_numeric_result: Any, call_arguments: Mapping): - self.expression = expression - self.non_numeric_result = non_numeric_result - self.call_arguments = call_arguments - - def __str__(self) -> str: - if isinstance(self.non_numeric_result, numpy.ndarray): - dtype = self.non_numeric_result.dtype - - if dtype == numpy.dtype('O'): - dtypes = set(map(type, self.non_numeric_result.flat)) - "The result of evaluate_numeric is an array with the types {} " \ - "which is not purely numeric".format(dtypes) - else: - dtype = type(self.non_numeric_result) - return "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(dtype) - - -ExpressionLike = TypeVar('ExpressionLike', str, Number, sympy.Expr, ExpressionScalar) + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py new file mode 100644 index 000000000..1052c1174 --- /dev/null +++ b/qupulse/expressions/wrapper.py @@ -0,0 +1,117 @@ +"""This module contains the function :py:``make_wrappers`` to define wrapper classes for expression protocol implementations +which only implements methods of the protocol. +It is used for finding code that relies on expression implementation details.""" + +import math +from typing import Sequence, Any, Mapping, Union, Tuple +from numbers import Real + +import numpy as np + +from qupulse.expressions import protocol, sympy + + +def make_wrappers(expr: type, expr_scalar: type, expr_vector: type) -> Tuple[type, type, type]: + """Create wrappers for expression base, scalar and vector types that only expose the methods defined in the + corresponding expression protocol classes. + + The vector is currently not implemented. + + Args: + expr: Expression base type of the implementation + expr_scalar: Expression scalar type of the implementation + expr_vector: Expression vector type of the implementation + + Returns: + A tuple of (base, scalar, vector) types that wrap the given types. + """ + + class ExpressionWrapper(protocol.Expression): + def __init__(self, x): + self._wrapped: protocol.Expression = expr(x) + + @classmethod + def make(cls, expression_or_dict, numpy_evaluation=None) -> 'ExpressionWrapper': + return cls(expression_or_dict) + + @property + def underlying_expression(self) -> Any: + return self._wrapped.underlying_expression + + def __hash__(self) -> int: + return hash(self._wrapped) + + def __eq__(self, other): + return self._wrapped == getattr(other, '_wrapped', other) + + @property + def variables(self) -> Sequence[str]: + return self._wrapped.variables + + def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: + return self._wrapped.evaluate_in_scope(scope) + + def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'ExpressionWrapper': + """Substitute a part of the expression for another""" + return ExpressionWrapper(self._wrapped.evaluate_symbolic(substitutions)) + + def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: + """Evaluate to a time dependent expression or a constant.""" + return self._wrapped.evaluate_time_dependent(scope) + + def get_serialization_data(self): + return self._wrapped.get_serialization_data() + + class ExpressionScalarWrapper(ExpressionWrapper, protocol.ExpressionScalar): + def __init__(self, x): + ExpressionWrapper.__init__(self, 0) + self._wrapped: protocol.ExpressionScalar = expr_scalar(x) + + # Scalar + def __add__(self, other): + return ExpressionScalarWrapper(self._wrapped + getattr(other, '_wrapped', other)) + + def __sub__(self, other): + return ExpressionScalarWrapper(self._wrapped - getattr(other, '_wrapped', other)) + + def __mul__(self, other): + return ExpressionScalarWrapper(self._wrapped * getattr(other, '_wrapped', other)) + + def __truediv__(self, other): + return ExpressionScalarWrapper(self._wrapped / getattr(other, '_wrapped', other)) + + def __floordiv__(self, other): + return ExpressionScalarWrapper(self._wrapped // getattr(other, '_wrapped', other)) + + def __ceil__(self): + return ExpressionScalarWrapper(math.ceil(self._wrapped)) + + def __floor__(self): + return ExpressionScalarWrapper(math.floor(self._wrapped)) + + def __float__(self): + return float(self._wrapped) + + def __int__(self): + return int(self._wrapped) + + def __abs__(self): + return ExpressionScalarWrapper(abs(self._wrapped)) + + # Ordered + def __lt__(self, other): + return self._wrapped < getattr(other, '_wrapped', other) + + def __le__(self, other): + return self._wrapped <= getattr(other, '_wrapped', other) + + def __gt__(self, other): + return self._wrapped > getattr(other, '_wrapped', other) + + def __ge__(self, other): + return self._wrapped >= getattr(other, '_wrapped', other) + + class ExpressionVectorWrapper(ExpressionWrapper): + pass + + return ExpressionWrapper, ExpressionScalarWrapper, ExpressionVectorWrapper diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index c39aa846e..2997bbd46 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -1,4 +1,4 @@ -from typing import Optional, Set, Dict, Union, List, Any, Tuple +from typing import Optional, Set, Dict, Union, List, Any, Tuple, Mapping import itertools import numbers import collections @@ -113,7 +113,7 @@ def __init__(self, template: PulseTemplate, *, template = template.template self.__template: PulseTemplate = template - self.__parameter_mapping = FrozenDict(parameter_mapping) + self.__parameter_mapping: Mapping[str, Expression] = FrozenDict(parameter_mapping) self.__external_parameters = set(itertools.chain(*(expr.variables for expr in self.__parameter_mapping.values()))) self.__external_parameters |= self.constrained_parameters self.__measurement_mapping = measurement_mapping @@ -260,7 +260,7 @@ def map_parameter_values(self, parameters: Dict[str, numbers.Real], A new dictionary with mapped numeric values. """ self._validate_parameters(parameters=parameters, volatile=volatile) - return {parameter: mapping_function.evaluate_numeric(**parameters) + return {parameter: mapping_function.evaluate_in_scope(parameters) for parameter, mapping_function in self.__parameter_mapping.items()} def map_scope(self, scope: Scope) -> MappedScope: diff --git a/qupulse/pulses/measurement.py b/qupulse/pulses/measurement.py index c9d1f9ba2..2a12575f9 100644 --- a/qupulse/pulses/measurement.py +++ b/qupulse/pulses/measurement.py @@ -2,7 +2,7 @@ from numbers import Real import itertools -from qupulse.expressions import Expression +from qupulse.expressions import Expression, ExpressionScalar from qupulse.utils.types import MeasurementWindow from qupulse.parameter_scope import Scope @@ -15,8 +15,8 @@ def __init__(self, measurements: Optional[List[MeasurementDeclaration]]): self._measurement_windows = [] else: self._measurement_windows = [(name, - begin if isinstance(begin, Expression) else Expression(begin), - length if isinstance(length, Expression) else Expression(length)) + begin if isinstance(begin, Expression) else ExpressionScalar(begin), + length if isinstance(length, Expression) else ExpressionScalar(length)) for name, begin, length in measurements] for _, _, length in self._measurement_windows: if (length < 0) is True: @@ -73,8 +73,8 @@ def measurement_parameters(self) -> AbstractSet[str]: def measurement_declarations(self) -> List[MeasurementDeclaration]: """Return the measurements that are directly declared on `self`. Does _not_ visit eventual child objects.""" return [(name, - begin.original_expression, - length.original_expression) + begin, + length) for name, begin, length in self._measurement_windows] @property diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index 8b19d152a..6b76bb49f 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -145,7 +145,7 @@ def build_waveform(self, parameters: Dict[str, numbers.Real], waveform = MultiChannelWaveform.from_parallel(sub_waveforms) if self._duration: - expected_duration = self._duration.evaluate_numeric(**parameters) + expected_duration = self._duration.evaluate_in_scope(parameters) if not isclose(expected_duration, waveform.duration): raise ValueError('The duration does not ' diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py index 5972e4740..0075a98dc 100644 --- a/qupulse/pulses/point_pulse_template.py +++ b/qupulse/pulses/point_pulse_template.py @@ -26,8 +26,8 @@ class PointPulseEntry(TableEntry): def instantiate(self, parameters: Dict[str, numbers.Real], num_channels: int) -> Sequence[PointWaveformEntry]: - t = self.t.evaluate_numeric(**parameters) - vs = self.v.evaluate_numeric(**parameters) + t = self.t.evaluate_in_scope(parameters) + vs = self.v.evaluate_in_scope(parameters) if isinstance(vs, numbers.Number): vs = (vs,) * num_channels @@ -71,7 +71,7 @@ def build_waveform(self, for channel in self.defined_channels): return None - if self.duration.evaluate_numeric(**parameters) == 0: + if self.duration.evaluate_in_scope(parameters) == 0: return None mapped_channels = tuple(channel_mapping[c] for c in self._channels) diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 973536e81..f8b631add 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -259,6 +259,9 @@ def get_entries_instantiated(self, parameters: Dict[str, numbers.Real]) \ duration = max(instantiated[-1].t for instantiated in instantiated_entries.values()) + if duration == 0: + return {} + # ensure that all channels have equal duration for channel, instantiated in instantiated_entries.items(): final_entry = instantiated[-1] @@ -321,9 +324,6 @@ def build_waveform(self, if not instantiated: return None - if self.duration.evaluate_numeric(**parameters) == 0: - return None - waveforms = [TableWaveform.from_table(*ch_instantiated) for ch_instantiated in instantiated] diff --git a/setup.cfg b/setup.cfg index 8c68a1adf..2101b343b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ install_requires = sympy>=1.1.1 numpy cached_property;python_version<'3.8' + typing-extensions;python_version<'3.8' frozendict lazy_loader test_suite = tests diff --git a/tests/expressions/__init__.py b/tests/expressions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/expression_tests.py b/tests/expressions/expression_tests.py similarity index 96% rename from tests/expression_tests.py rename to tests/expressions/expression_tests.py index 26693821c..194434935 100644 --- a/tests/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -1,464 +1,465 @@ -import pickle -import unittest -import sys - -import numpy as np -import sympy.abc -from sympy import sympify, Eq - -from qupulse.expressions import Expression, ExpressionVariableMissingException, NonNumericEvaluation, ExpressionScalar, ExpressionVector -from qupulse.utils.types import TimeType - -class ExpressionTests(unittest.TestCase): - def test_make(self): - self.assertTrue(Expression.make('a') == 'a') - self.assertTrue(Expression.make('a + b') == 'a + b') - self.assertTrue(Expression.make(9) == 9) - - self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector) - - self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar) - self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector) - - -class ExpressionVectorTests(unittest.TestCase): - def test_evaluate_numeric(self) -> None: - e = ExpressionVector(['a * b + c', 'a + d']) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]), - e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_evaluate_numeric_2d(self) -> None: - e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]), - e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_partial_evaluation(self): - e = ExpressionVector(['a * b + c', 'a + d']) - - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - - expected = ExpressionVector([2 * 1.5 - 7, '2 + d']) - evaluated = e.evaluate_symbolic(params) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_symbolic_evaluation(self): - e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - - expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]]) - evaluated = e.evaluate_symbolic(params) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_numeric_expression(self): - numbers = np.linspace(1, 2, num=5) - - e = ExpressionVector(numbers) - - np.testing.assert_equal(e.underlying_expression, numbers) - - def test_eq(self): - e1 = ExpressionVector([1, 2]) - e2 = ExpressionVector(['1', '2']) - e3 = ExpressionVector(['1', 'a']) - e4 = ExpressionVector([1, 'a']) - e5 = ExpressionVector([1, 'a', 3]) - e6 = ExpressionVector([1, 1, '1']) - e7 = ExpressionVector(['a']) - - self.assertEqual(e1, e2) - self.assertEqual(e3, e4) - self.assertNotEqual(e4, e5) - - self.assertEqual(e1, [1, 2]) - self.assertNotEqual(e6, 1) - self.assertEqual(e7, ExpressionScalar('a')) - - def test_hash(self): - e1 = ExpressionVector([1, 2]) - e2 = ExpressionVector(['1', '2']) - e7 = ExpressionVector(['a']) - - s = ExpressionScalar('a') - self.assertEqual({e1, e7}, {e1, e2, e7, s}) - - def test_pickle(self): - expr = ExpressionVector([1, 'a + 5', 3]) - # populate lambdified - expr.evaluate_in_scope({'a': 3}) - dumped = pickle.dumps(expr) - loaded = pickle.loads(dumped) - self.assertEqual(expr, loaded) - - -class ExpressionScalarTests(unittest.TestCase): - def test_format(self): - expr = ExpressionScalar('17') - e_format = '{:.4e}'.format(expr) - self.assertEqual(e_format, "1.7000e+01") - - empty_format = "{}".format(expr) - self.assertEqual(empty_format, '17') - - expr_with_var = ExpressionScalar('17*a') - with self.assertRaises(TypeError): - # throw error on implicit float cast - '{:.4e}'.format(expr_with_var) - - empty_format = "{}".format(expr_with_var) - self.assertEqual(empty_format, '17*a') - - @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher") - def test_fstring(self) -> None: - src_code = """e = ExpressionScalar('2.0'); \ - self.assertEqual( f'{e}', str(e) ); \ - self.assertEqual( f'{e:.2f}', '%.2f' % e) - """ - exec(src_code) - - def test_evaluate_numeric(self) -> None: - e = ExpressionScalar('a * b + c') - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_evaluate_numpy(self): - e = ExpressionScalar('a * b + c') - params = { - 'a': 2*np.ones(4), - 'b': 1.5*np.ones(4), - 'c': -7*np.ones(4) - } - np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params)) - - e = ExpressionScalar('a * b + c') - params = { - 'a': np.array(2), - 'b': np.array(1.5), - 'c': np.array(-7) - } - np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params)) - - def test_indexing(self): - e = ExpressionScalar('a[i] * c') - - params = { - 'a': np.array([1, 2, 3]), - 'i': 1, - 'c': 2 - } - - self.assertEqual(e.evaluate_numeric(**params), 2 * 2) - params['a'] = [1, 2, 3] - self.assertEqual(e.evaluate_numeric(**params), 2 * 2) - params['a'] = np.array([[1, 2, 3], [4, 5, 6]]) - np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6])) - - def test_partial_evaluation(self) -> None: - e = ExpressionScalar('a * c') - params = {'c': 5.5} - evaluated = e.evaluate_symbolic(params) - expected = ExpressionScalar('a * 5.5') - self.assertEqual(expected.underlying_expression, evaluated.underlying_expression) - - def test_partial_evaluation_vectorized(self) -> None: - e = ExpressionScalar('a[i] * c') - - params = { - 'c': np.array([[1, 2], [3, 4]]) - } - - evaluated = e.evaluate_symbolic(params) - expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']]) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_evaluate_numeric_without_numpy(self): - e = Expression('a * b + c') - - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2j, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2, - 'b': 6, - 'c': -7 - } - self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2, - 'b': sympify('k'), - 'c': -7 - } - with self.assertRaises(NonNumericEvaluation): - e.evaluate_numeric(**params) - - def test_evaluate_symbolic(self): - e = ExpressionScalar('a * b + c') - params = { - 'a': 'd', - 'c': -7 - } - result = e.evaluate_symbolic(params) - expected = ExpressionScalar('d*b-7') - self.assertEqual(result, expected) - - def test_variables(self) -> None: - e = ExpressionScalar('4 ** pi + x * foo') - expected = sorted(['foo', 'x']) - received = sorted(e.variables) - self.assertEqual(expected, received) - - def test_variables_indexed(self): - e = ExpressionScalar('a[i] * c') - expected = sorted(['a', 'i', 'c']) - received = sorted(e.variables) - self.assertEqual(expected, received) - - def test_evaluate_variable_missing(self) -> None: - e = ExpressionScalar('a * b + c') - params = { - 'b': 1.5 - } - with self.assertRaises(ExpressionVariableMissingException): - e.evaluate_numeric(**params) - - def test_repr(self): - s = 'a * b' - e = ExpressionScalar(s) - self.assertEqual("ExpressionScalar('a * b')", repr(e)) - - def test_repr_original_expression_is_sympy(self): - # in this case we test that we get the original expression back if we do - # eval(repr(e)) - - org = sympy.sympify(3.1415) - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - org = sympy.abc.a * sympy.abc.b - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - org = sympy.sympify('3/17') - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - def test_str(self): - s = 'a * b' - e = ExpressionScalar(s) - self.assertEqual('a*b', str(e)) - - def test_original_expression(self): - s = 'a * b' - self.assertEqual(ExpressionScalar(s).original_expression, s) - - def test_hash(self): - expected = {ExpressionScalar(2), ExpressionScalar('a')} - sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')] - self.assertEqual(expected, set(sequence)) - - def test_undefined_comparison(self): - valued = ExpressionScalar(2) - unknown = ExpressionScalar('a') - - self.assertIsNone(unknown < 0) - self.assertIsNone(unknown > 0) - self.assertIsNone(unknown >= 0) - self.assertIsNone(unknown <= 0) - self.assertFalse(unknown == 0) - - self.assertIsNone(0 < unknown) - self.assertIsNone(0 > unknown) - self.assertIsNone(0 <= unknown) - self.assertIsNone(0 >= unknown) - self.assertFalse(0 == unknown) - - self.assertIsNone(unknown < valued) - self.assertIsNone(unknown > valued) - self.assertIsNone(unknown >= valued) - self.assertIsNone(unknown <= valued) - self.assertFalse(unknown == valued) - - valued, unknown = unknown, valued - self.assertIsNone(unknown < valued) - self.assertIsNone(unknown > valued) - self.assertIsNone(unknown >= valued) - self.assertIsNone(unknown <= valued) - self.assertFalse(unknown == valued) - valued, unknown = unknown, valued - - self.assertFalse(unknown == valued) - - def test_defined_comparison(self): - small = ExpressionScalar(2) - large = ExpressionScalar(3) - - self.assertIs(small < small, False) - self.assertIs(small > small, False) - self.assertIs(small <= small, True) - self.assertIs(small >= small, True) - self.assertIs(small == small, True) - - self.assertIs(small < large, True) - self.assertIs(small > large, False) - self.assertIs(small <= large, True) - self.assertIs(small >= large, False) - self.assertIs(small == large, False) - - self.assertIs(large < small, False) - self.assertIs(large > small, True) - self.assertIs(large <= small, False) - self.assertIs(large >= small, True) - self.assertIs(large == small, False) - - def test_number_comparison(self): - valued = ExpressionScalar(2) - - self.assertIs(valued < 3, True) - self.assertIs(valued > 3, False) - self.assertIs(valued <= 3, True) - self.assertIs(valued >= 3, False) - - self.assertIs(valued == 3, False) - self.assertIs(valued == 2, True) - self.assertIs(3 == valued, False) - self.assertIs(2 == valued, True) - - self.assertIs(3 < valued, False) - self.assertIs(3 > valued, True) - self.assertIs(3 <= valued, False) - self.assertIs(3 >= valued, True) - - def assertExpressionEqual(self, lhs: Expression, rhs: Expression): - self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs)) - - def test_number_math(self): - a = ExpressionScalar('a') - b = 3.3 - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_symbolic_math(self): - a = ExpressionScalar('a') - b = ExpressionScalar('b') - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_sympy_math(self): - a = ExpressionScalar('a') - b = sympify('b') - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_is_nan(self): - self.assertTrue(ExpressionScalar('nan').is_nan()) - self.assertTrue(ExpressionScalar('0./0.').is_nan()) - - self.assertFalse(ExpressionScalar(456).is_nan()) - - def test_special_function_numeric_evaluation(self): - expr = Expression('erfc(t)') - data = [-1., 0., 1.] - expected = np.array([1.84270079, 1., 0.15729921]) - result = expr.evaluate_numeric(t=data) - - np.testing.assert_allclose(expected, result) - - def test_evaluate_with_exact_rationals(self): - expr = ExpressionScalar('1 / 3') - self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({})) - - expr = ExpressionScalar('a * (1 / 3)') - self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2})) - - expr = ExpressionScalar('dot(a, b) * (1 / 3)') - self.assertEqual(TimeType.from_fraction(10, 3), - expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]})) - - def test_pickle(self): - expr = ExpressionScalar('1 / a') - # populate lambdified - expr.evaluate_in_scope({'a': 7}) - dumped = pickle.dumps(expr) - loaded = pickle.loads(dumped) - self.assertEqual(expr, loaded) - - -class ExpressionExceptionTests(unittest.TestCase): - def test_expression_variable_missing(self): - variable = 's' - expression = ExpressionScalar('s*t') - - self.assertEqual(str(ExpressionVariableMissingException(variable, expression)), - "Could not evaluate : A value for variable is missing!") - - def test_non_numeric_evaluation(self): - expression = ExpressionScalar('a*b') - call_arguments = dict() - - expected = "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(float) - self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected) - - expected = "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(np.zeros(1).dtype) - self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected) +import pickle +import unittest +import sys + +import numpy as np +import sympy.abc +from sympy import sympify, Eq + +from qupulse.expressions.sympy import Expression, ExpressionScalar, ExpressionVector +from qupulse.expressions import ExpressionVariableMissingException, NonNumericEvaluation +from qupulse.utils.types import TimeType + +class ExpressionTests(unittest.TestCase): + def test_make(self): + self.assertTrue(Expression.make('a') == 'a') + self.assertTrue(Expression.make('a + b') == 'a + b') + self.assertTrue(Expression.make(9) == 9) + + self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector) + + self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar) + self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector) + + +class ExpressionVectorTests(unittest.TestCase): + def test_evaluate_numeric(self) -> None: + e = ExpressionVector(['a * b + c', 'a + d']) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]), + e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_evaluate_numeric_2d(self) -> None: + e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]), + e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_partial_evaluation(self): + e = ExpressionVector(['a * b + c', 'a + d']) + + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + + expected = ExpressionVector([2 * 1.5 - 7, '2 + d']) + evaluated = e.evaluate_symbolic(params) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_symbolic_evaluation(self): + e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + + expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]]) + evaluated = e.evaluate_symbolic(params) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_numeric_expression(self): + numbers = np.linspace(1, 2, num=5) + + e = ExpressionVector(numbers) + + np.testing.assert_equal(e.underlying_expression, numbers) + + def test_eq(self): + e1 = ExpressionVector([1, 2]) + e2 = ExpressionVector(['1', '2']) + e3 = ExpressionVector(['1', 'a']) + e4 = ExpressionVector([1, 'a']) + e5 = ExpressionVector([1, 'a', 3]) + e6 = ExpressionVector([1, 1, '1']) + e7 = ExpressionVector(['a']) + + self.assertEqual(e1, e2) + self.assertEqual(e3, e4) + self.assertNotEqual(e4, e5) + + self.assertEqual(e1, [1, 2]) + self.assertNotEqual(e6, 1) + self.assertEqual(e7, ExpressionScalar('a')) + + def test_hash(self): + e1 = ExpressionVector([1, 2]) + e2 = ExpressionVector(['1', '2']) + e7 = ExpressionVector(['a']) + + s = ExpressionScalar('a') + self.assertEqual({e1, e7}, {e1, e2, e7, s}) + + def test_pickle(self): + expr = ExpressionVector([1, 'a + 5', 3]) + # populate lambdified + expr.evaluate_in_scope({'a': 3}) + dumped = pickle.dumps(expr) + loaded = pickle.loads(dumped) + self.assertEqual(expr, loaded) + + +class ExpressionScalarTests(unittest.TestCase): + def test_format(self): + expr = ExpressionScalar('17') + e_format = '{:.4e}'.format(expr) + self.assertEqual(e_format, "1.7000e+01") + + empty_format = "{}".format(expr) + self.assertEqual(empty_format, '17') + + expr_with_var = ExpressionScalar('17*a') + with self.assertRaises(TypeError): + # throw error on implicit float cast + '{:.4e}'.format(expr_with_var) + + empty_format = "{}".format(expr_with_var) + self.assertEqual(empty_format, '17*a') + + @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher") + def test_fstring(self) -> None: + src_code = """e = ExpressionScalar('2.0'); \ + self.assertEqual( f'{e}', str(e) ); \ + self.assertEqual( f'{e:.2f}', '%.2f' % e) + """ + exec(src_code) + + def test_evaluate_numeric(self) -> None: + e = ExpressionScalar('a * b + c') + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_evaluate_numpy(self): + e = ExpressionScalar('a * b + c') + params = { + 'a': 2*np.ones(4), + 'b': 1.5*np.ones(4), + 'c': -7*np.ones(4) + } + np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params)) + + e = ExpressionScalar('a * b + c') + params = { + 'a': np.array(2), + 'b': np.array(1.5), + 'c': np.array(-7) + } + np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params)) + + def test_indexing(self): + e = ExpressionScalar('a[i] * c') + + params = { + 'a': np.array([1, 2, 3]), + 'i': 1, + 'c': 2 + } + + self.assertEqual(e.evaluate_numeric(**params), 2 * 2) + params['a'] = [1, 2, 3] + self.assertEqual(e.evaluate_numeric(**params), 2 * 2) + params['a'] = np.array([[1, 2, 3], [4, 5, 6]]) + np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6])) + + def test_partial_evaluation(self) -> None: + e = ExpressionScalar('a * c') + params = {'c': 5.5} + evaluated = e.evaluate_symbolic(params) + expected = ExpressionScalar('a * 5.5') + self.assertEqual(expected.underlying_expression, evaluated.underlying_expression) + + def test_partial_evaluation_vectorized(self) -> None: + e = ExpressionScalar('a[i] * c') + + params = { + 'c': np.array([[1, 2], [3, 4]]) + } + + evaluated = e.evaluate_symbolic(params) + expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']]) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_evaluate_numeric_without_numpy(self): + e = Expression('a * b + c') + + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2j, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2, + 'b': 6, + 'c': -7 + } + self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2, + 'b': sympify('k'), + 'c': -7 + } + with self.assertRaises(NonNumericEvaluation): + e.evaluate_numeric(**params) + + def test_evaluate_symbolic(self): + e = ExpressionScalar('a * b + c') + params = { + 'a': 'd', + 'c': -7 + } + result = e.evaluate_symbolic(params) + expected = ExpressionScalar('d*b-7') + self.assertEqual(result, expected) + + def test_variables(self) -> None: + e = ExpressionScalar('4 ** pi + x * foo') + expected = sorted(['foo', 'x']) + received = sorted(e.variables) + self.assertEqual(expected, received) + + def test_variables_indexed(self): + e = ExpressionScalar('a[i] * c') + expected = sorted(['a', 'i', 'c']) + received = sorted(e.variables) + self.assertEqual(expected, received) + + def test_evaluate_variable_missing(self) -> None: + e = ExpressionScalar('a * b + c') + params = { + 'b': 1.5 + } + with self.assertRaises(ExpressionVariableMissingException): + e.evaluate_numeric(**params) + + def test_repr(self): + s = 'a * b' + e = ExpressionScalar(s) + self.assertEqual("ExpressionScalar('a * b')", repr(e)) + + def test_repr_original_expression_is_sympy(self): + # in this case we test that we get the original expression back if we do + # eval(repr(e)) + + org = sympy.sympify(3.1415) + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + org = sympy.abc.a * sympy.abc.b + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + org = sympy.sympify('3/17') + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + def test_str(self): + s = 'a * b' + e = ExpressionScalar(s) + self.assertEqual('a*b', str(e)) + + def test_original_expression(self): + s = 'a * b' + self.assertEqual(ExpressionScalar(s).original_expression, s) + + def test_hash(self): + expected = {ExpressionScalar(2), ExpressionScalar('a')} + sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')] + self.assertEqual(expected, set(sequence)) + + def test_undefined_comparison(self): + valued = ExpressionScalar(2) + unknown = ExpressionScalar('a') + + self.assertIsNone(unknown < 0) + self.assertIsNone(unknown > 0) + self.assertIsNone(unknown >= 0) + self.assertIsNone(unknown <= 0) + self.assertFalse(unknown == 0) + + self.assertIsNone(0 < unknown) + self.assertIsNone(0 > unknown) + self.assertIsNone(0 <= unknown) + self.assertIsNone(0 >= unknown) + self.assertFalse(0 == unknown) + + self.assertIsNone(unknown < valued) + self.assertIsNone(unknown > valued) + self.assertIsNone(unknown >= valued) + self.assertIsNone(unknown <= valued) + self.assertFalse(unknown == valued) + + valued, unknown = unknown, valued + self.assertIsNone(unknown < valued) + self.assertIsNone(unknown > valued) + self.assertIsNone(unknown >= valued) + self.assertIsNone(unknown <= valued) + self.assertFalse(unknown == valued) + valued, unknown = unknown, valued + + self.assertFalse(unknown == valued) + + def test_defined_comparison(self): + small = ExpressionScalar(2) + large = ExpressionScalar(3) + + self.assertIs(small < small, False) + self.assertIs(small > small, False) + self.assertIs(small <= small, True) + self.assertIs(small >= small, True) + self.assertIs(small == small, True) + + self.assertIs(small < large, True) + self.assertIs(small > large, False) + self.assertIs(small <= large, True) + self.assertIs(small >= large, False) + self.assertIs(small == large, False) + + self.assertIs(large < small, False) + self.assertIs(large > small, True) + self.assertIs(large <= small, False) + self.assertIs(large >= small, True) + self.assertIs(large == small, False) + + def test_number_comparison(self): + valued = ExpressionScalar(2) + + self.assertIs(valued < 3, True) + self.assertIs(valued > 3, False) + self.assertIs(valued <= 3, True) + self.assertIs(valued >= 3, False) + + self.assertIs(valued == 3, False) + self.assertIs(valued == 2, True) + self.assertIs(3 == valued, False) + self.assertIs(2 == valued, True) + + self.assertIs(3 < valued, False) + self.assertIs(3 > valued, True) + self.assertIs(3 <= valued, False) + self.assertIs(3 >= valued, True) + + def assertExpressionEqual(self, lhs: Expression, rhs: Expression): + self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs)) + + def test_number_math(self): + a = ExpressionScalar('a') + b = 3.3 + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_symbolic_math(self): + a = ExpressionScalar('a') + b = ExpressionScalar('b') + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_sympy_math(self): + a = ExpressionScalar('a') + b = sympify('b') + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_is_nan(self): + self.assertTrue(ExpressionScalar('nan').is_nan()) + self.assertTrue(ExpressionScalar('0./0.').is_nan()) + + self.assertFalse(ExpressionScalar(456).is_nan()) + + def test_special_function_numeric_evaluation(self): + expr = Expression('erfc(t)') + data = [-1., 0., 1.] + expected = np.array([1.84270079, 1., 0.15729921]) + result = expr.evaluate_numeric(t=data) + + np.testing.assert_allclose(expected, result) + + def test_evaluate_with_exact_rationals(self): + expr = ExpressionScalar('1 / 3') + self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({})) + + expr = ExpressionScalar('a * (1 / 3)') + self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2})) + + expr = ExpressionScalar('dot(a, b) * (1 / 3)') + self.assertEqual(TimeType.from_fraction(10, 3), + expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]})) + + def test_pickle(self): + expr = ExpressionScalar('1 / a') + # populate lambdified + expr.evaluate_in_scope({'a': 7}) + dumped = pickle.dumps(expr) + loaded = pickle.loads(dumped) + self.assertEqual(expr, loaded) + + +class ExpressionExceptionTests(unittest.TestCase): + def test_expression_variable_missing(self): + variable = 's' + expression = ExpressionScalar('s*t') + + self.assertEqual(str(ExpressionVariableMissingException(variable, expression)), + "Could not evaluate : A value for variable is missing!") + + def test_non_numeric_evaluation(self): + expression = ExpressionScalar('a*b') + call_arguments = dict() + + expected = "The result of evaluate_numeric is of type {} " \ + "which is not a number".format(float) + self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected) + + expected = "The result of evaluate_numeric is of type {} " \ + "which is not a number".format(np.zeros(1).dtype) + self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected) diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py index 40414ccd5..46cf99ae8 100644 --- a/tests/pulses/table_pulse_template_tests.py +++ b/tests/pulses/table_pulse_template_tests.py @@ -219,15 +219,22 @@ def test_external_constraints(self): table.build_waveform(parameters=dict(v=1., w=2, t=0.1, x=1.2, y=1, h=2), channel_mapping={0: 0, 1: 1}) - def test_get_entries_instantiated_one_entry_float_float(self) -> None: + def test_get_entries_instantiated_empty(self): table = TablePulseTemplate({0: [(0, 2)]}) + self.assertEqual({}, table.get_entries_instantiated(dict())) + + def test_get_entries_instantiated_one_entry_float_float(self) -> None: + table = TablePulseTemplate({0: [(1, 2)]}) instantiated_entries = table.get_entries_instantiated(dict())[0] - self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries) + self.assertEqual([(0, 2, HoldInterpolationStrategy()), (1, 2, HoldInterpolationStrategy())], + instantiated_entries) def test_get_entries_instantiated_one_entry_float_declaration(self) -> None: - table = TablePulseTemplate({0: [(0, 'foo')]}) + table = TablePulseTemplate({0: [(1, 'foo')]}) instantiated_entries = table.get_entries_instantiated({'foo': 2})[0] - self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries) + self.assertEqual([(0, 2, HoldInterpolationStrategy()), + (1, 2, HoldInterpolationStrategy())], + instantiated_entries) def test_get_entries_instantiated_two_entries_float_float_declaration_float(self) -> None: table = TablePulseTemplate({0: [('foo', -2.)]})