From 7df4446850bf65a62c18266a1b85dd83cbc619e5 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 30 May 2022 10:51:01 +0200 Subject: [PATCH 01/12] Use evaluate_in_scope where appropriate --- qupulse/pulses/mapping_pulse_template.py | 6 +++--- qupulse/pulses/multi_channel_pulse_template.py | 2 +- qupulse/pulses/point_pulse_template.py | 6 +++--- qupulse/pulses/table_pulse_template.py | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index af5b64e00..453b62ef8 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 = 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 @@ -257,7 +257,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_parameter_objects(self, parameters: Dict[str, Parameter], diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index b6c220b05..a9e6aa5d3 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -137,7 +137,7 @@ def build_waveform(self, parameters: Dict[str, numbers.Real], waveform = MultiChannelWaveform(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 376e28be4..9f896a26f 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 4decd2576..289133da2 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -322,13 +322,12 @@ 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] - return MultiChannelWaveform.from_parallel(waveforms) + mc_waveform = MultiChannelWaveform.from_parallel(waveforms) + if mc_waveform.duration != 0: + return mc_waveform @staticmethod def from_array(times: np.ndarray, voltages: np.ndarray, channels: List[ChannelID]) -> 'TablePulseTemplate': From 6329e4aa4f854f72fcfcf75780964b1a5a07c670 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jan 2023 18:35:04 +0100 Subject: [PATCH 02/12] Restructure expression code and start specifying a clean interface --- qupulse/expressions/__init__.py | 59 ++++++++++ .../{expressions.py => expressions/legacy.py} | 64 ++--------- qupulse/expressions/protocol.py | 105 ++++++++++++++++++ 3 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 qupulse/expressions/__init__.py rename qupulse/{expressions.py => expressions/legacy.py} (88%) create mode 100644 qupulse/expressions/protocol.py diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py new file mode 100644 index 000000000..d2edd8a77 --- /dev/null +++ b/qupulse/expressions/__init__.py @@ -0,0 +1,59 @@ +from typing import Type, TypeVar +from numbers import Real + +import numpy as np +import sympy + +from . import legacy, protocol + + +__all__ = ["Expression", "ExpressionVector", "ExpressionScalar", + "NonNumericEvaluation", "ExpressionVariableMissingException"] + + +Expression: Type[protocol.Expression] = legacy.Expression +ExpressionScalar: Type[protocol.ExpressionScalar] = legacy.ExpressionScalar +ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector + + +ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.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.py b/qupulse/expressions/legacy.py similarity index 88% rename from qupulse/expressions.py rename to qupulse/expressions/legacy.py index bbfad3eed..eeb64ee97 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions/legacy.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/protocol.py b/qupulse/expressions/protocol.py new file mode 100644 index 000000000..58fbd0803 --- /dev/null +++ b/qupulse/expressions/protocol.py @@ -0,0 +1,105 @@ +"""This module contains the interface / protocol descriptions.""" + +from typing import Protocol +from typing import Mapping, Union, Sequence, Hashable, Any + +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): + 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): + pass + + +class ExpressionScalar(Expression, Scalar, Ordered, Protocol): + pass + + +class ExpressionVector(Expression, Protocol): + pass From 4c9c52608f2d76e3a899cb3eae03624dd04b8a9b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jan 2023 18:44:15 +0100 Subject: [PATCH 03/12] Catch zero duration waveforms already in Waveform instantiate method. --- qupulse/pulses/table_pulse_template.py | 7 ++++--- tests/pulses/table_pulse_template_tests.py | 15 +++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 86902da29..656127b90 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] @@ -324,9 +327,7 @@ def build_waveform(self, waveforms = [TableWaveform.from_table(*ch_instantiated) for ch_instantiated in instantiated] - mc_waveform = MultiChannelWaveform.from_parallel(waveforms) - if mc_waveform.duration != 0: - return mc_waveform + return MultiChannelWaveform.from_parallel(waveforms) @staticmethod def from_array(times: np.ndarray, voltages: np.ndarray, channels: List[ChannelID]) -> 'TablePulseTemplate': 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.)]}) From 28bc4cdd19499544ba2c9b0234b3de276c433728 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 12:30:37 +0100 Subject: [PATCH 04/12] Use typing-extensions in python version < 3.8 --- qupulse/expressions/protocol.py | 7 ++++++- setup.cfg | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 58fbd0803..0280747d3 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -1,6 +1,11 @@ """This module contains the interface / protocol descriptions.""" -from typing import Protocol +try: + from typing import Protocol +except ImportError: + # python version < 3.8 + from typing_extensions import Protocol + from typing import Mapping, Union, Sequence, Hashable, Any from numbers import Real diff --git a/setup.cfg b/setup.cfg index 68647e6f9..17185b685 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 From 5b9ee515dbf97e6e4844579b75dec877b4481da4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 20 Mar 2023 09:28:48 +0100 Subject: [PATCH 05/12] Add wrapper --- qupulse/expressions/__init__.py | 12 +- qupulse/expressions/wrapper.py | 101 +++ qupulse/pulses/measurement.py | 10 +- tests/expressions/__init__.py | 0 tests/{ => expressions}/expression_tests.py | 929 ++++++++++---------- 5 files changed, 582 insertions(+), 470 deletions(-) create mode 100644 qupulse/expressions/wrapper.py create mode 100644 tests/expressions/__init__.py rename tests/{ => expressions}/expression_tests.py (96%) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index d2edd8a77..391f98f72 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,15 @@ +"""This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`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. +""" + from typing import Type, TypeVar from numbers import Real import numpy as np import sympy -from . import legacy, protocol +from . import legacy, protocol, wrapper __all__ = ["Expression", "ExpressionVector", "ExpressionScalar", @@ -16,6 +21,11 @@ ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector +Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(legacy.Expression, + legacy.ExpressionScalar, + legacy.ExpressionVector) + + ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.Expr, ExpressionScalar) diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py new file mode 100644 index 000000000..eab995fa1 --- /dev/null +++ b/qupulse/expressions/wrapper.py @@ -0,0 +1,101 @@ +import functools +import inspect +import math +from typing import Sequence, Any, Mapping, Union +from numbers import Real + +import numpy as np + +from qupulse.expressions import protocol, legacy + + +def make_wrappers(expr, expr_scalar, expr_vector): + class ExpressionWrapper(protocol.Expression): + def __init__(self, x): + self._wrapped: protocol.Expression = expr(x) + + @classmethod + def make(cls, expression_or_dict, numpy_evaluation=None) -> 'Expression': + 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/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/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..756be0442 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.legacy 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) From 9957c7100ce5484e8c314f6c4b40f2c396bbb927 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 09:29:05 +0200 Subject: [PATCH 06/12] Rename legacy to sympy --- qupulse/expressions/__init__.py | 18 +++++++++--------- qupulse/expressions/{legacy.py => sympy.py} | 0 qupulse/expressions/wrapper.py | 2 +- tests/expressions/expression_tests.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename qupulse/expressions/{legacy.py => sympy.py} (100%) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index 391f98f72..d9823bc75 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -7,26 +7,26 @@ from numbers import Real import numpy as np -import sympy +import sympy as sp -from . import legacy, protocol, wrapper +from . import sympy, protocol, wrapper __all__ = ["Expression", "ExpressionVector", "ExpressionScalar", "NonNumericEvaluation", "ExpressionVariableMissingException"] -Expression: Type[protocol.Expression] = legacy.Expression -ExpressionScalar: Type[protocol.ExpressionScalar] = legacy.ExpressionScalar -ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector +Expression: Type[protocol.Expression] = sympy.Expression +ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar +ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector -Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(legacy.Expression, - legacy.ExpressionScalar, - legacy.ExpressionVector) +Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, + sympy.ExpressionScalar, + sympy.ExpressionVector) -ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.Expr, ExpressionScalar) +ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar) class ExpressionVariableMissingException(Exception): diff --git a/qupulse/expressions/legacy.py b/qupulse/expressions/sympy.py similarity index 100% rename from qupulse/expressions/legacy.py rename to qupulse/expressions/sympy.py diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index eab995fa1..91340caaa 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -6,7 +6,7 @@ import numpy as np -from qupulse.expressions import protocol, legacy +from qupulse.expressions import protocol, sympy def make_wrappers(expr, expr_scalar, expr_vector): diff --git a/tests/expressions/expression_tests.py b/tests/expressions/expression_tests.py index 756be0442..194434935 100644 --- a/tests/expressions/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -6,7 +6,7 @@ import sympy.abc from sympy import sympify, Eq -from qupulse.expressions.legacy import Expression, ExpressionScalar, ExpressionVector +from qupulse.expressions.sympy import Expression, ExpressionScalar, ExpressionVector from qupulse.expressions import ExpressionVariableMissingException, NonNumericEvaluation from qupulse.utils.types import TimeType From 60d6ccfd0d7e98d633765b13ccb819f1a1498b11 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:35:56 +0200 Subject: [PATCH 07/12] Add newspiece --- changes.d/750.feature | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes.d/750.feature 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. From 76d641ce751330edf99e30da20ab635ac9156137 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:42:46 +0200 Subject: [PATCH 08/12] Add docstring to wrapper module --- qupulse/expressions/wrapper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index 91340caaa..8ea9dfc18 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -1,5 +1,6 @@ -import functools -import inspect +"""This module contains 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 from numbers import Real From f416ced9c0fdbdf85ff6247728a8d84c61ac376b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:43:32 +0200 Subject: [PATCH 09/12] Drop python 3.7 support --- qupulse/expressions/protocol.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 0280747d3..b177f501c 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -1,13 +1,7 @@ -"""This module contains the interface / protocol descriptions.""" - -try: - from typing import Protocol -except ImportError: - # python version < 3.8 - from typing_extensions import Protocol - -from typing import Mapping, Union, Sequence, Hashable, Any +"""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 From 7054d7eeeb3f45c4cbcd469b9bf080e32c7bc40b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 11:11:31 +0200 Subject: [PATCH 10/12] Add more documentation --- qupulse/expressions/__init__.py | 15 ++++++++++++--- qupulse/expressions/protocol.py | 2 -- qupulse/expressions/wrapper.py | 25 ++++++++++++++++++++----- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index d9823bc75..acbf29431 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,18 @@ """This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`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. + +Currently, the + +The default implementation is in :py:``qupulse.expressions.sympy``. + +There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.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 @@ -21,9 +29,10 @@ ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector -Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, - sympy.ExpressionScalar, - 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) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index b177f501c..11f267790 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -53,8 +53,6 @@ def __abs__(self): pass - - class Expression(Hashable, Protocol): 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 diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index 8ea9dfc18..1052c1174 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -1,8 +1,9 @@ -"""This module contains 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.""" +"""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 +from typing import Sequence, Any, Mapping, Union, Tuple from numbers import Real import numpy as np @@ -10,13 +11,27 @@ from qupulse.expressions import protocol, sympy -def make_wrappers(expr, expr_scalar, expr_vector): +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) -> 'Expression': + def make(cls, expression_or_dict, numpy_evaluation=None) -> 'ExpressionWrapper': return cls(expression_or_dict) @property From a7224af7b469e957b03333214e52f4c8675fc11c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 5 Jun 2023 11:47:57 +0200 Subject: [PATCH 11/12] Fix typo and formatting --- qupulse/expressions/__init__.py | 2 -- qupulse/expressions/protocol.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index acbf29431..61224f2fd 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -2,8 +2,6 @@ that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow default implementation with a faster less expressive backend. -Currently, the - The default implementation is in :py:``qupulse.expressions.sympy``. There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.expressions.wrapper``. Define diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 11f267790..f6f90d459 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -55,7 +55,7 @@ def __abs__(self): class Expression(Hashable, Protocol): 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 + """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be any mapping.) Args: scope: @@ -69,6 +69,7 @@ def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression': 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. @@ -91,7 +92,7 @@ def underlying_expression(self) -> Any: raise NotImplementedError() def get_serialization_data(self): - pass + raise NotImplementedError() class ExpressionScalar(Expression, Scalar, Ordered, Protocol): From 37b02f9014e992aa9a8c5c6c266b9f06de6953d7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 11:08:59 +0200 Subject: [PATCH 12/12] Update, fix and slightly extend documentation regarding expressions --- doc/source/concepts/pulsetemplates.rst | 2 +- qupulse/expressions/__init__.py | 6 +++--- qupulse/expressions/protocol.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) 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 index 61224f2fd..52aa5f325 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,10 @@ -"""This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`typing.Protocol` +"""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:``qupulse.expressions.sympy``. +The default implementation is in :py:mod:`.expressions.sympy`. -There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.expressions.wrapper``. Define +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. """ diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index f6f90d459..667337c3d 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -54,6 +54,8 @@ def __abs__(self): 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.)