From 4488d134df753fc79d3c91819b08e17e5db109e6 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 16 Aug 2018 14:22:33 +0200 Subject: [PATCH 01/15] MappingPT now always accepts partial parameter mappings; deprecated the allow_partial_parameter_mapping argument. parameters which are not mapped by the parameter_mapping dict passed to MappingPT are automatically mapped using identity mapping. --- qctoolkit/pulses/mapping_pulse_template.py | 34 +++++--------------- tests/pulses/mapping_pulse_template_tests.py | 13 ++------ 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/qctoolkit/pulses/mapping_pulse_template.py b/qctoolkit/pulses/mapping_pulse_template.py index ce4232f4d..570c8fcf6 100644 --- a/qctoolkit/pulses/mapping_pulse_template.py +++ b/qctoolkit/pulses/mapping_pulse_template.py @@ -2,6 +2,7 @@ from typing import Optional, Set, Dict, Union, List, Any, Tuple import itertools import numbers +import warnings from qctoolkit.utils.types import ChannelID from qctoolkit.expressions import Expression, ExpressionScalar @@ -30,16 +31,12 @@ def __init__(self, template: PulseTemplate, *, measurement_mapping: Optional[Dict[str, str]] = None, channel_mapping: Optional[Dict[ChannelID, ChannelID]] = None, parameter_constraints: Optional[List[str]]=None, - allow_partial_parameter_mapping: bool=False, + allow_partial_parameter_mapping: Optional[bool]=False, registry: PulseRegistryType=None) -> None: """Standard constructor for the MappingPulseTemplate. - Mappings that are not specified are defaulted to identity mappings. Channels and measurement names of the - encapsulated template can be mapped partially by default. F.i. if channel_mapping only contains one of two - channels the other channel name is mapped to itself. - However, if a parameter mapping is specified and one or more parameters are not mapped a MissingMappingException - is raised. To allow partial mappings and enable the same behaviour as for the channel and measurement name - mapping allow_partial_parameter_mapping must be set to True. + Mappings that are not specified are defaulted to identity mappings. F.i. if channel_mapping only contains one of + two channels the other channel name is mapped to itself. Furthermore parameter constrains can be specified. :param template: The encapsulated pulse template whose parameters, measurement names and channels are mapped @@ -47,8 +44,10 @@ def __init__(self, template: PulseTemplate, *, :param measurement_mapping: mappings for other measurement names are inserted :param channel_mapping: mappings for other channels are auto inserted :param parameter_constraints: - :param allow_partial_parameter_mapping: + :param allow_partial_parameter_mapping: deprecated """ + if allow_partial_parameter_mapping: + warnings.warn("The allow_partial_parameter_mapping argument is deprecated and will be ignored. Partial parameter mappings are always possible.", category=DeprecationWarning) PulseTemplate.__init__(self, identifier=identifier) ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints) @@ -61,10 +60,7 @@ def __init__(self, template: PulseTemplate, *, if mapped_internal_parameters - internal_parameters: raise UnnecessaryMappingException(template, mapped_internal_parameters - internal_parameters) elif missing_parameter_mappings: - if allow_partial_parameter_mapping: - parameter_mapping.update({p: p for p in missing_parameter_mappings}) - else: - raise MissingMappingException(template, internal_parameters - mapped_internal_parameters) + parameter_mapping.update({p: p for p in missing_parameter_mappings}) parameter_mapping = dict((k, Expression(v)) for k, v in parameter_mapping.items()) measurement_mapping = dict() if measurement_mapping is None else measurement_mapping @@ -307,20 +303,6 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]: return expressions -class MissingMappingException(Exception): - """Indicates that no mapping was specified for some parameter declaration of a - SequencePulseTemplate's subtemplate.""" - - def __init__(self, template: PulseTemplate, key: Union[str,Set[str]]) -> None: - super().__init__() - self.key = key - self.template = template - - def __str__(self) -> str: - return "The template {} needs a mapping function for parameter(s) {}".\ - format(self.template, self.key) - - class UnnecessaryMappingException(Exception): """Indicates that a mapping was provided that does not correspond to any of a SequencePulseTemplate's subtemplate's parameter declarations and is thus obsolete.""" diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index 88f10a4b9..337d149c5 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -1,8 +1,7 @@ import unittest import itertools -from qctoolkit.pulses.mapping_pulse_template import MissingMappingException,\ - UnnecessaryMappingException, MappingPulseTemplate,\ +from qctoolkit.pulses.mapping_pulse_template import UnnecessaryMappingException, MappingPulseTemplate,\ AmbiguousMappingException, MappingCollisionException from qctoolkit.pulses.parameters import ParameterNotProvidedException from qctoolkit.pulses.parameters import ConstantParameter, ParameterConstraintViolation, ParameterConstraint @@ -21,10 +20,6 @@ def test_init_exceptions(self): template = DummyPulseTemplate(parameter_names={'foo', 'bar'}, defined_channels={'A'}, measurement_names={'B'}) parameter_mapping = {'foo': 't*k', 'bar': 't*l'} - with self.assertRaises(MissingMappingException): - MappingPulseTemplate(template, parameter_mapping={}) - with self.assertRaises(MissingMappingException): - MappingPulseTemplate(template, parameter_mapping={'bar': 'kneipe'}) with self.assertRaises(UnnecessaryMappingException): MappingPulseTemplate(template, parameter_mapping=dict(**parameter_mapping, foobar='asd')) @@ -224,12 +219,8 @@ def test_build_sequence(self): def test_requires_stop(self): pass -class PulseTemplateParameterMappingExceptionsTests(unittest.TestCase): - def test_missing_mapping_exception_str(self) -> None: - dummy = DummyPulseTemplate() - exception = MissingMappingException(dummy, 'foo') - self.assertIsInstance(str(exception), str) +class PulseTemplateParameterMappingExceptionsTests(unittest.TestCase): def test_unnecessary_mapping_exception_str(self) -> None: dummy = DummyPulseTemplate() From 09d62e7989c849a83b2ecfb0924b3a8246c6ba51 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 16 Aug 2018 14:54:31 +0200 Subject: [PATCH 02/15] MappingPT now optionally places unmapped parameters into namespace. Added mapping_namespace argument to MappingPT. All parameters of the inner template not explicitely mapped by the parameter_mapping provided to MappingPT are placed into the provided namespace by mapping them to parameters named ___. If the parameter already was in a namespace (i.e. had a prefix ending with ___) this is replaced by the new namespace/prefix. --- qctoolkit/pulses/mapping_pulse_template.py | 29 ++++++++++++-------- tests/pulses/mapping_pulse_template_tests.py | 8 ++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/qctoolkit/pulses/mapping_pulse_template.py b/qctoolkit/pulses/mapping_pulse_template.py index 570c8fcf6..3914fb67b 100644 --- a/qctoolkit/pulses/mapping_pulse_template.py +++ b/qctoolkit/pulses/mapping_pulse_template.py @@ -31,12 +31,16 @@ def __init__(self, template: PulseTemplate, *, measurement_mapping: Optional[Dict[str, str]] = None, channel_mapping: Optional[Dict[ChannelID, ChannelID]] = None, parameter_constraints: Optional[List[str]]=None, - allow_partial_parameter_mapping: Optional[bool]=False, + mapping_namespace: Optional[str]=None, + allow_partial_parameter_mapping: Optional[bool]=False, # deprecated; todo: remove registry: PulseRegistryType=None) -> None: """Standard constructor for the MappingPulseTemplate. Mappings that are not specified are defaulted to identity mappings. F.i. if channel_mapping only contains one of two channels the other channel name is mapped to itself. + All parameters that are not explicitly mapped in the parameter_mapping dictionary are mapped to themselves if + the mapping_namespace argument is not given or set to None. Otherwise, MappingPT maps these parameters are into + the namespace given by internal_namespace. Furthermore parameter constrains can be specified. :param template: The encapsulated pulse template whose parameters, measurement names and channels are mapped @@ -44,6 +48,7 @@ def __init__(self, template: PulseTemplate, *, :param measurement_mapping: mappings for other measurement names are inserted :param channel_mapping: mappings for other channels are auto inserted :param parameter_constraints: + :param mapping_namespace: Namespace into which unmapped parameters will be placed (i.e., prefix for parameter name) :param allow_partial_parameter_mapping: deprecated """ if allow_partial_parameter_mapping: @@ -51,16 +56,18 @@ def __init__(self, template: PulseTemplate, *, PulseTemplate.__init__(self, identifier=identifier) ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints) - if parameter_mapping is None: - parameter_mapping = dict((par, par) for par in template.parameter_names) - else: - mapped_internal_parameters = set(parameter_mapping.keys()) - internal_parameters = template.parameter_names - missing_parameter_mappings = internal_parameters - mapped_internal_parameters - if mapped_internal_parameters - internal_parameters: - raise UnnecessaryMappingException(template, mapped_internal_parameters - internal_parameters) - elif missing_parameter_mappings: - parameter_mapping.update({p: p for p in missing_parameter_mappings}) + parameter_mapping = dict() if parameter_mapping is None else parameter_mapping + mapped_internal_parameters = set(parameter_mapping.keys()) + internal_parameters = template.parameter_names + missing_parameter_mappings = internal_parameters - mapped_internal_parameters + if mapped_internal_parameters - internal_parameters: + raise UnnecessaryMappingException(template, mapped_internal_parameters - internal_parameters) + elif missing_parameter_mappings: + if not mapping_namespace: + mapping_namespace = "" + else: + mapping_namespace += "___" # namespace - parameter delimiter + parameter_mapping.update({p: mapping_namespace+p.rpartition('___')[2] for p in missing_parameter_mappings}) parameter_mapping = dict((k, Expression(v)) for k, v in parameter_mapping.items()) measurement_mapping = dict() if measurement_mapping is None else measurement_mapping diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index 337d149c5..f43772063 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -186,6 +186,14 @@ def test_integral(self) -> None: self.assertEqual({'default': Expression('2*f'), 'other': Expression('-3.2*f+2.3')}, pulse.integral) + def test_mapping_namespace(self) -> None: + template = DummyPulseTemplate(parameter_names={'foo', 'bar', 'outer___inner___hugo'}) + st = MappingPulseTemplate(template, mapping_namespace='scope') + self.assertEqual({'scope___foo', 'scope___bar', 'scope___hugo'}, st.parameter_names) + + st = MappingPulseTemplate(template, parameter_mapping={'foo': 'alpha+scope___beta'}, mapping_namespace='scope') + self.assertEqual({'alpha', 'scope___beta', 'scope___bar', 'scope___hugo'}, st.parameter_names) + class MappingPulseTemplateSequencingTests(unittest.TestCase): From 15a4514fc04c97ebdf37443a6e5983e94f03d795 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 12:41:43 +0200 Subject: [PATCH 03/15] Some comments to clarify structure of utils/sympy.py. --- qupulse/utils/sympy.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index dda823ff2..fdae2635a 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -15,6 +15,9 @@ Sympifyable = Union[str, Number, sympy.Expr, numpy.str_] +############################################################################## +### Utilities to automatically detect usage of indexed/subscripted symbols ### +############################################################################## class IndexedBasedFinder: """Acts as a symbol lookup and determines which symbols in an expression a subscripted.""" @@ -55,6 +58,17 @@ def __contains__(self, k) -> bool: return True +def get_subscripted_symbols(expression: str) -> set: + # track all symbols that are subscipted in here + indexed_base_finder = IndexedBasedFinder() + sympy.sympify(expression, locals=indexed_base_finder) + + return indexed_base_finder.indexed_base + +############################################################# +### "Built-in" length function for expressions in qupulse ### +############################################################# + class Len(sympy.Function): nargs = 1 @@ -70,6 +84,9 @@ def eval(cls, arg) -> Optional[sympy.Integer]: sympify_namespace = {'len': Len, 'Len': Len} +######################################### +### Functions for numpy compatability ### +######################################### def numpy_compatible_mul(*args) -> Union[sympy.Mul, sympy.Array]: if any(isinstance(a, sympy.NDimArray) for a in args): @@ -96,14 +113,9 @@ def to_numpy(sympy_array: sympy.NDimArray) -> numpy.ndarray: return numpy.asarray(sympy_array) return numpy.array(sympy_array.tolist()) - -def get_subscripted_symbols(expression: str) -> set: - # track all symbols that are subscipted in here - indexed_base_finder = IndexedBasedFinder() - sympy.sympify(expression, locals=indexed_base_finder) - - return indexed_base_finder.indexed_base - +####################################################################################################### +### Custom sympify method (which introduces all utility methods defined above into the sympy world) ### +####################################################################################################### def sympify(expr: Union[str, Number, sympy.Expr, numpy.str_], **kwargs) -> sympy.Expr: if isinstance(expr, numpy.str_): @@ -124,6 +136,10 @@ def sympify(expr: Union[str, Number, sympy.Expr, numpy.str_], **kwargs) -> sympy raise +############################################################################### +### Utility functions for expression manipulation/simplification/evaluation ### +############################################################################### + def get_most_simple_representation(expression: sympy.Expr) -> Union[str, int, float]: if expression.free_symbols: return str(expression) From 832b477fc2a521f9e7e71409298e3d76b9dcaa46 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 12:47:20 +0200 Subject: [PATCH 04/15] Attempt to enable namespace dot-notation (e.g. "foo.bar") in sympy expressions. Created customized version of the auto_symbol token transformation used by sympy.parse_expr and mocked that into sympy when calling sympify and co. Works well for sympify but not at all for lambdified and compiled expressions. An issue for lamdified expression is that the namespaced parameters will become kwargs and python cannot deal with the dot in there. anything to be done? WIP? probably just scrap this.. --- qupulse/utils/sympy.py | 129 ++++++++++++++++++++++++++++++------- tests/utils/sympy_tests.py | 18 +++++- 2 files changed, 122 insertions(+), 25 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index fdae2635a..1a841d2fe 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -6,8 +6,12 @@ import math import sympy +from sympy.parsing.sympy_parser import NAME, OP, iskeyword, Basic, Symbol, lambda_notation, repeated_decimals, \ + auto_number, factorial_notation import numpy +from unittest import mock + __all__ = ["sympify", "substitute_with_eval", "to_numpy", "get_variables", "get_free_symbols", "recursive_substitution", "evaluate_lambdified", "get_most_simple_representation"] @@ -19,6 +23,75 @@ ### Utilities to automatically detect usage of indexed/subscripted symbols ### ############################################################################## +## Custom auto_symbol transformation that deals with namespace dot notation (e.g. "foo.bar") + +def custom_auto_symbol_transform(tokens, local_dict, global_dict): + """Inserts calls to ``Symbol``/``Function`` for undefined variables.""" + result = [] + prev_tok = (None, None) + symbol_string = None + + tokens.append((None, None)) # so zip traverses all tokens + for tok, next_tok in zip(tokens, tokens[1:]): + tok_num, tok_val = tok + next_tok_num, next_tok_val = next_tok + + if symbol_string: + assert (tok_val == '.' or tok_num == NAME) + if tok_val == '.' or tok_num == NAME: + symbol_string += tok_val + if tok_val == '.' or (tok_num == NAME and next_tok_val == '.'): + continue + tok_num = NAME + tok_val = symbol_string + symbol_string = None + + if tok_num == NAME: + name = tok_val + + if (name in ['True', 'False', 'None'] + or iskeyword(name) + # Don't convert keyword arguments + or (prev_tok[0] == OP and prev_tok[1] in ('(', ',') + and next_tok_num == OP and next_tok_val == '=')): + result.append((NAME, name)) + continue + elif name in local_dict: + if isinstance(local_dict[name], Symbol) and next_tok_val == '(': + result.extend([(NAME, 'Function'), + (OP, '('), + (NAME, repr(str(local_dict[name]))), + (OP, ')')]) + else: + result.append((NAME, name)) + continue + elif name in global_dict: + obj = global_dict[name] + if isinstance(obj, (Basic, type)) or callable(obj): + result.append((NAME, name)) + continue + elif next_tok_val == '.': # symbol, not a function + symbol_string = str(name) + else: + result.extend([ + (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), + (OP, '('), + (NAME, repr(str(name))), + (OP, ')'), + ]) + else: + result.append((tok_num, tok_val)) + + prev_tok = (tok_num, tok_val) + + return result + +sympy_transformations = (lambda_notation, custom_auto_symbol_transform, + repeated_decimals, auto_number, factorial_notation) + + +## Utilities to automatically detect usage of indexed/subscripted symbols + class IndexedBasedFinder: """Acts as a symbol lookup and determines which symbols in an expression a subscripted.""" @@ -88,6 +161,8 @@ def eval(cls, arg) -> Optional[sympy.Integer]: ### Functions for numpy compatability ### ######################################### +## Functions for numpy compatability + def numpy_compatible_mul(*args) -> Union[sympy.Mul, sympy.Array]: if any(isinstance(a, sympy.NDimArray) for a in args): result = 1 @@ -117,23 +192,27 @@ def to_numpy(sympy_array: sympy.NDimArray) -> numpy.ndarray: ### Custom sympify method (which introduces all utility methods defined above into the sympy world) ### ####################################################################################################### + +## Custom sympify method (which introduces all utility methods defined above into the sympy world) + def sympify(expr: Union[str, Number, sympy.Expr, numpy.str_], **kwargs) -> sympy.Expr: if isinstance(expr, numpy.str_): # putting numpy.str_ in sympy.sympify behaves unexpected in version 1.1.1 # It seems to ignore the locals argument expr = str(expr) - try: - return sympy.sympify(expr, **kwargs, locals=sympify_namespace) - except TypeError as err: - if True:#err.args[0] == "'Symbol' object is not subscriptable": + with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): + try: + return sympy.sympify(expr, **kwargs, locals=sympify_namespace) + except TypeError as err: + if True:#err.args[0] == "'Symbol' object is not subscriptable": - indexed_base = get_subscripted_symbols(expr) - return sympy.sympify(expr, **kwargs, locals={**{k: sympy.IndexedBase(k) - for k in indexed_base}, - **sympify_namespace}) + indexed_base = get_subscripted_symbols(expr) + return sympy.sympify(expr, **kwargs, locals={**{k: sympy.IndexedBase(k) + for k in indexed_base}, + **sympify_namespace}) - else: - raise + else: + raise ############################################################################### @@ -209,25 +288,27 @@ def recursive_substitution(expression: sympy.Expr, def evaluate_compiled(expression: sympy.Expr, parameters: Dict[str, Union[numpy.ndarray, Number]], compiled: CodeType=None, mode=None) -> Tuple[any, CodeType]: - if compiled is None: - compiled = compile(sympy.printing.lambdarepr.lambdarepr(expression), - '', 'eval') - - if mode == 'numeric' or mode is None: - result = eval(compiled, parameters.copy(), _numpy_environment) - elif mode == 'exact': - result = eval(compiled, parameters.copy(), _sympy_environment) - else: - raise ValueError("Unknown mode: '{}'".format(mode)) + with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): + if compiled is None: + compiled = compile(sympy.printing.lambdarepr.lambdarepr(expression), + '', 'eval') + + if mode == 'numeric' or mode is None: + result = eval(compiled, parameters.copy(), _numpy_environment) + elif mode == 'exact': + result = eval(compiled, parameters.copy(), _sympy_environment) + else: + raise ValueError("Unknown mode: '{}'".format(mode)) - return result, compiled + return result, compiled def evaluate_lambdified(expression: Union[sympy.Expr, numpy.ndarray], variables: Sequence[str], parameters: Dict[str, Union[numpy.ndarray, Number]], lambdified) -> Tuple[Any, Any]: - lambdified = lambdified or sympy.lambdify(variables, expression, - [{'ceiling': numpy_compatible_ceiling}, 'numpy']) + with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): + lambdified = lambdified or sympy.lambdify(variables, expression, + [{'ceiling': numpy_compatible_ceiling}, 'numpy']) - return lambdified(**parameters), lambdified + return lambdified(**parameters), lambdified diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index effee275f..ab2166222 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -309,4 +309,20 @@ def test_get_most_simple_representation(self): st = get_most_simple_representation(qc_sympify('a + b')) self.assertIsInstance(st, str) - self.assertEqual(st, 'a + b') \ No newline at end of file + self.assertEqual(st, 'a + b') + + +class NamespaceTests(unittest.TestCase): + + def test_sympify_dot_namespace_notations(self) -> None: + expr = qc_sympify("qubit.a + qubit.spec2.a * 1.3") + expected = sympy.Add(sympy.Symbol('qubit.a'), sympy.Mul(sympy.Symbol('qubit.spec2.a'), sympy.RealNumber(1.3))) + self.assertEqual(expected, expr) + + def test_evaluate_lambdified_dot_namespace_notation(self) -> None: + res = evaluate_lambdified("qubit.a + qubit.spec2.a * 1.3", ["qubit.a", "qubit_spec2_a"], {"qubit_a": 2.1, "qubit_spec2_a": .1}, lambdified=None) + self.assertEqual(2.23, res) + + def test_evaluate_compiled_dot_namespace_notation(self) -> None: + res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit.a": 2.1, "qubit.spec2.a": .1}) + self.assertEqual(2.23, res) From c2016de15f7b7a5fa0cc4649309dbab7fe615a97 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 14:08:00 +0200 Subject: [PATCH 05/15] Custom sympy token transform now replaces "." with "___". Expression evaluation with sympy now works without issues, however, parameters passed into evaluation now have to have "___" as namespace separator. This could/should be automated away somewhere (parameter provider class?). --- qupulse/utils/sympy.py | 7 +++++-- tests/utils/sympy_tests.py | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index 1a841d2fe..e3f2cf485 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -37,8 +37,11 @@ def custom_auto_symbol_transform(tokens, local_dict, global_dict): next_tok_num, next_tok_val = next_tok if symbol_string: - assert (tok_val == '.' or tok_num == NAME) - if tok_val == '.' or tok_num == NAME: + if tok_val != '.' and tok_num != NAME: + raise SyntaxError("Not a valid namespaced sympy.symbol name") + if tok_val == '.': + symbol_string += '___' + elif tok_num == NAME: symbol_string += tok_val if tok_val == '.' or (tok_num == NAME and next_tok_val == '.'): continue diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index ab2166222..52b945d9e 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -316,13 +316,13 @@ class NamespaceTests(unittest.TestCase): def test_sympify_dot_namespace_notations(self) -> None: expr = qc_sympify("qubit.a + qubit.spec2.a * 1.3") - expected = sympy.Add(sympy.Symbol('qubit.a'), sympy.Mul(sympy.Symbol('qubit.spec2.a'), sympy.RealNumber(1.3))) + expected = sympy.Add(sympy.Symbol('qubit___a'), sympy.Mul(sympy.Symbol('qubit___spec2___a'), sympy.RealNumber(1.3))) self.assertEqual(expected, expr) def test_evaluate_lambdified_dot_namespace_notation(self) -> None: - res = evaluate_lambdified("qubit.a + qubit.spec2.a * 1.3", ["qubit.a", "qubit_spec2_a"], {"qubit_a": 2.1, "qubit_spec2_a": .1}, lambdified=None) - self.assertEqual(2.23, res) + res = evaluate_lambdified("qubit.a + qubit.spec2.a * 1.3", ["qubit___a", "qubit___spec2___a"], {"qubit___a": 2.1, "qubit___spec2___a": .1}, lambdified=None) + self.assertEqual(2.23, res[0]) def test_evaluate_compiled_dot_namespace_notation(self) -> None: - res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit.a": 2.1, "qubit.spec2.a": .1}) - self.assertEqual(2.23, res) + res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit___a": 2.1, "qubit___spec2___a": .1}) + self.assertEqual(2.23, res[0]) From 2a1c12977357162d59c4ebdee39dc9b3daf3b958 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 14:57:12 +0200 Subject: [PATCH 06/15] Evaluation methods transparently rename parameters from '.' to '___' notation. Now, users only see namespaced parameters as "foo.bar" while sympy only sees them as "foo___bar". --- qupulse/utils/sympy.py | 18 +++++++++++++----- tests/utils/sympy_tests.py | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index e3f2cf485..ee626ba6f 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -1,4 +1,4 @@ -from typing import Union, Dict, Tuple, Any, Sequence, Optional +from typing import Union, Dict, Tuple, Any, Sequence, Optional, Mapping from numbers import Number from types import CodeType @@ -25,7 +25,11 @@ ## Custom auto_symbol transformation that deals with namespace dot notation (e.g. "foo.bar") -def custom_auto_symbol_transform(tokens, local_dict, global_dict): + +sympy_internal_namespace_seperator = '___' + + +def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: Mapping[str, Any], global_dict: Mapping[str, Any]) -> None: """Inserts calls to ``Symbol``/``Function`` for undefined variables.""" result = [] prev_tok = (None, None) @@ -40,7 +44,7 @@ def custom_auto_symbol_transform(tokens, local_dict, global_dict): if tok_val != '.' and tok_num != NAME: raise SyntaxError("Not a valid namespaced sympy.symbol name") if tok_val == '.': - symbol_string += '___' + symbol_string += sympy_internal_namespace_seperator elif tok_num == NAME: symbol_string += tok_val if tok_val == '.' or (tok_num == NAME and next_tok_val == '.'): @@ -73,9 +77,10 @@ def custom_auto_symbol_transform(tokens, local_dict, global_dict): if isinstance(obj, (Basic, type)) or callable(obj): result.append((NAME, name)) continue - elif next_tok_val == '.': # symbol, not a function + + if next_tok_val == '.': # start of a namespaced symbol symbol_string = str(name) - else: + else: # single symbol/fct or end of a namespaced symbol result.extend([ (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), (OP, '('), @@ -291,6 +296,7 @@ def recursive_substitution(expression: sympy.Expr, def evaluate_compiled(expression: sympy.Expr, parameters: Dict[str, Union[numpy.ndarray, Number]], compiled: CodeType=None, mode=None) -> Tuple[any, CodeType]: + parameters = {k.replace('.', sympy_internal_namespace_seperator): v for k, v in parameters.items()} with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): if compiled is None: compiled = compile(sympy.printing.lambdarepr.lambdarepr(expression), @@ -310,6 +316,8 @@ def evaluate_lambdified(expression: Union[sympy.Expr, numpy.ndarray], variables: Sequence[str], parameters: Dict[str, Union[numpy.ndarray, Number]], lambdified) -> Tuple[Any, Any]: + variables = {v.replace('.', sympy_internal_namespace_seperator) for v in variables} + parameters = {k.replace('.', sympy_internal_namespace_seperator):v for k,v in parameters.items()} with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): lambdified = lambdified or sympy.lambdify(variables, expression, [{'ceiling': numpy_compatible_ceiling}, 'numpy']) diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index 52b945d9e..30c970ff4 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -320,9 +320,9 @@ def test_sympify_dot_namespace_notations(self) -> None: self.assertEqual(expected, expr) def test_evaluate_lambdified_dot_namespace_notation(self) -> None: - res = evaluate_lambdified("qubit.a + qubit.spec2.a * 1.3", ["qubit___a", "qubit___spec2___a"], {"qubit___a": 2.1, "qubit___spec2___a": .1}, lambdified=None) + res = evaluate_lambdified("qubit.a + qubit.spec2.a * 1.3", ["qubit.a", "qubit.spec2.a"], {"qubit.a": 2.1, "qubit.spec2.a": .1}, lambdified=None) self.assertEqual(2.23, res[0]) def test_evaluate_compiled_dot_namespace_notation(self) -> None: - res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit___a": 2.1, "qubit___spec2___a": .1}) + res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit.a": 2.1, "qubit.spec2.a": .1}) self.assertEqual(2.23, res[0]) From 49c628a14dff6c7b32b05bde0ca108b69d79c9ca Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 16:54:00 +0200 Subject: [PATCH 07/15] Fixes to custom sympy token transformation and affected code. - fixed transformation routine itself - caught some calls to sympy that were not mocked - switching between dot and internal underscore notation where required - added additional underscore to internal namespace seperator ('____') to prevent existing pulses using the 3-underscore notation to be wrongly mapped to dot notation - expanded utils.sympy_tests to also work on namespaced symbols (still errors left as of now, not complete 'coverage' yet) - removed tests that tested native sympy functionality (why were we testing this?) --- qupulse/utils/sympy.py | 48 +++++++++++++------- tests/utils/sympy_tests.py | 92 ++++++++++++++++++++++---------------- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index ee626ba6f..49f0768a8 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -26,11 +26,16 @@ ## Custom auto_symbol transformation that deals with namespace dot notation (e.g. "foo.bar") -sympy_internal_namespace_seperator = '___' +sympy_internal_namespace_seperator = '____' def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: Mapping[str, Any], global_dict: Mapping[str, Any]) -> None: - """Inserts calls to ``Symbol``/``Function`` for undefined variables.""" + """Inserts calls to ``Symbol``/``Function`` for undefined variables and deals with symbol namespaces. + + Original code taken from sympy and tweaked to allow for namespaced parameters following dot notation, e.g., ``foo.bar``. + A string ``foo.bar`` will be treated as a single symbol. To allow internal handling, dots are replaced by '____' + (4 underscores). + """ result = [] prev_tok = (None, None) symbol_string = None @@ -63,6 +68,9 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: and next_tok_num == OP and next_tok_val == '=')): result.append((NAME, name)) continue + elif next_tok_val == '.': + symbol_string = str(name) + continue elif name in local_dict: if isinstance(local_dict[name], Symbol) and next_tok_val == '(': result.extend([(NAME, 'Function'), @@ -78,15 +86,12 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: result.append((NAME, name)) continue - if next_tok_val == '.': # start of a namespaced symbol - symbol_string = str(name) - else: # single symbol/fct or end of a namespaced symbol - result.extend([ - (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), - (OP, '('), - (NAME, repr(str(name))), - (OP, ')'), - ]) + result.extend([ + (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), + (OP, '('), + (NAME, repr(str(name))), + (OP, ')'), + ]) else: result.append((tok_num, tok_val)) @@ -142,7 +147,8 @@ def __contains__(self, k) -> bool: def get_subscripted_symbols(expression: str) -> set: # track all symbols that are subscipted in here indexed_base_finder = IndexedBasedFinder() - sympy.sympify(expression, locals=indexed_base_finder) + with mock.patch.object(sympy.parsing.sympy_parser, 'standard_transformations', sympy_transformations): + sympy.sympify(expression, locals=indexed_base_finder) return indexed_base_finder.indexed_base @@ -229,7 +235,7 @@ def sympify(expr: Union[str, Number, sympy.Expr, numpy.str_], **kwargs) -> sympy def get_most_simple_representation(expression: sympy.Expr) -> Union[str, int, float]: if expression.free_symbols: - return str(expression) + return str(expression).replace(sympy_internal_namespace_seperator, '.') elif expression.is_Integer: return int(expression) elif expression.is_Float: @@ -239,19 +245,29 @@ def get_most_simple_representation(expression: sympy.Expr) -> Union[str, int, fl def get_free_symbols(expression: sympy.Expr) -> Sequence[sympy.Symbol]: + """Returns all free smybols in a sympy expression. + + Since these are sympy Symbol objects, possibly namespaced symbols will follow the underscore namespace notation, + i.e., `foo___bar`. + """ return tuple(symbol for symbol in expression.free_symbols if not isinstance(symbol, sympy.Indexed)) def get_variables(expression: sympy.Expr) -> Sequence[str]: - return tuple(map(str, get_free_symbols(expression))) + """Returns all free variables in a sympy expression. + + Returned are the names of the variables. Namespaced variables will follow the dot namespace notation, i.e. + `foo.bar`. + """ + return tuple(map(lambda x: str(x).replace(sympy_internal_namespace_seperator, '.'), get_free_symbols(expression))) def substitute_with_eval(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, numpy.ndarray, str]]) -> sympy.Expr: """Substitutes only sympy.Symbols. Workaround for numpy like array behaviour. ~Factor 3 slower compared to subs""" - substitutions = {k: v if isinstance(v, sympy.Expr) else sympify(v) + substitutions = {k.replace('.', sympy_internal_namespace_seperator): v if isinstance(v, sympy.Expr) else sympify(v) for k, v in substitutions.items()} for symbol in get_free_symbols(expression): @@ -281,7 +297,7 @@ def _recursive_substitution(expression: sympy.Expr, def recursive_substitution(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, numpy.ndarray, str]]) -> sympy.Expr: - substitutions = {sympy.Symbol(k): sympify(v) for k, v in substitutions.items()} + substitutions = {sympy.Symbol(k.replace('.',sympy_internal_namespace_seperator)): sympify(v) for k, v in substitutions.items()} for s in get_free_symbols(expression): substitutions.setdefault(s, s) return _recursive_substitution(expression, substitutions) diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index 30c970ff4..d4653bf96 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -12,6 +12,10 @@ a_ = IndexedBase(a) b_ = IndexedBase(b) +foo_bar = sympy.Symbol('foo____bar') +foo_bar_ = IndexedBase('foo____bar') +scope_n = sympy.Symbol('scope____n') + from qupulse.utils.sympy import sympify as qc_sympify, substitute_with_eval, recursive_substitution, Len,\ evaluate_lambdified, evaluate_compiled, get_most_simple_representation, get_variables, get_free_symbols @@ -22,6 +26,11 @@ (a*b, {'a': c}, b*c), (a*b, {'a': b, 'b': a}, a*b), (a*b, {'a': 1, 'b': 2}, 2), + (foo_bar*b, {'foo.bar': c}, b*c), + (foo_bar*b, {'b': scope_n}, scope_n*foo_bar), + (foo_bar*b, {'b': foo_bar, 'foo.bar': b}, foo_bar*b), + (foo_bar*scope_n, {'foo.bar': scope_n, 'scope.n': foo_bar}, foo_bar*scope_n), + (foo_bar*b, {'foo.bar': 5, 'b': 0.2}, 0.2*5) ] elem_func_substitution_cases = [ @@ -52,11 +61,14 @@ simple_sympify = [ ('a*b', a*b), ('a*6', a*6), - ('sin(a)', sin(a)) + ('sin(a)', sin(a)), + ('foo.bar*6', foo_bar*6), + ('sin(foo.bar)', sin(foo_bar)) ] complex_sympify = [ ('Sum(a, (i, 0, n))', Sum(a, (i, 0, n))), + ('Sum(foo.bar, (i, 0, scope.n))', Sum(foo_bar, (i, 0, scope_n))) ] len_sympify = [ @@ -65,7 +77,8 @@ ] index_sympify = [ - ('a[i]', a_[i]) + ('a[i]', a_[i]), + ('foo.bar[i]', foo_bar_[i]) ] @@ -73,29 +86,39 @@ eval_simple = [ (a*b, {'a': 2, 'b': 3}, 6), (a*b, {'a': 2, 'b': np.float32(3.5)}, 2*np.float32(3.5)), - (a+b, {'a': 3.4, 'b': 76.7}, 3.4+76.7) + (a+b, {'a': 3.4, 'b': 76.7}, 3.4+76.7), + (foo_bar+scope_n, {'foo.bar': 1.2, 'scope.n': 3.3}, 1.2+3.3), + (foo_bar*scope_n, {'foo.bar': 1.2, 'scope.n': np.float32(3.3)}, 1.2*np.float32(3.3)), + (foo_bar*scope_n, {'foo.bar': 1.2, 'scope.n': 3.3}, 1.2*3.3) ] eval_many_arguments = [ - (sum(sympy.symbols(list('a_' + str(i) for i in range(300)))), {'a_' + str(i): 1 for i in range(300)}, 300) + (sum(sympy.symbols(list('a_' + str(i) for i in range(300)))), {'a_' + str(i): 1 for i in range(300)}, 300), + (sum(sympy.symbols(list('scope____a_' + str(i) for i in range(300)))), {'scope.a_' + str(i): 1 for i in range(300)}, 300) ] eval_simple_functions = [ - (a*sin(b), {'a': 3.5, 'b': 1.2}, 3.5*math.sin(1.2)) + (a*sin(b), {'a': 3.5, 'b': 1.2}, 3.5*math.sin(1.2)), + (a*sin(foo_bar), {'a': 3.5, 'foo.bar': 1.2}, 3.5*math.sin(1.2)), ] eval_array_values = [ - (a*b, {'a': 2, 'b': np.array([3])}, np.array([6])), - (a*b, {'a': 2, 'b': np.array([3, 4, 5])}, np.array([6, 8, 10])), - (a*b, {'a': np.array([2, 3]), 'b': np.array([100, 200])}, np.array([200, 600])) + (a * b, {'a': 2, 'b': np.array([3])}, np.array([6])), + (a * b, {'a': 2, 'b': np.array([3, 4, 5])}, np.array([6, 8, 10])), + (a * b, {'a': np.array([2, 3]), 'b': np.array([100, 200])}, np.array([200, 600])), + (a * foo_bar, {'a': 2, 'foo.bar': np.array([3])}, np.array([6])), + (a * foo_bar, {'a': 2, 'foo.bar': np.array([3, 4, 5])}, np.array([6, 8, 10])), + (a * foo_bar, {'a': np.array([2, 3]), 'foo.bar': np.array([100, 200])}, np.array([200, 600])), ] eval_sum = [ (Sum(a_[i], (i, 0, Len(a) - 1)), {'a': np.array([1, 2, 3])}, 6), + (Sum(foo_bar_[i], (i, 0, Len(foo_bar) - 1)), {'foo.bar': np.array([1, 2, 3])}, 6), ] eval_array_expression = [ - (np.array([a*c, b*c]), {'a': 2, 'b': 3, 'c': 4}, np.array([8, 12])) + (np.array([a*c, b*c]), {'a': 2, 'b': 3, 'c': 4}, np.array([8, 12])), + (np.array([a*foo_bar, scope_n*foo_bar]), {'a': 2, 'scope.n': 3, 'foo.bar': 4}, np.array([8, 12])) ] @@ -108,59 +131,48 @@ def assertRaises(self, expected_exception, *args, **kwargs): class SympifyTests(TestCase): - def sympify(self, expression): - return sympy.sympify(expression) + + def sympify(self, expression) -> sympy.Expr: + return qc_sympify(expression) def assertEqual1(self, first, second, msg=None): if sympy.Eq(first, second): return raise self.failureException(msg=msg) - def test_simple_sympify(self): + def test_simple_sympify(self) -> None: for s, expected in simple_sympify: result = self.sympify(s) self.assertEqual(result, expected) - def test_complex_sympify(self): + def test_complex_sympify(self) -> None: for s, expected in complex_sympify: result = self.sympify(s) self.assertEqual(result, expected) - def test_len_sympify(self, expected_exception=AssertionError, msg="sympy.sympify does not know len"): - with self.assertRaises(expected_exception=expected_exception, msg=msg): - for s, expected in len_sympify: - result = self.sympify(s) - self.assertEqual(result, expected) - - def test_index_sympify(self, expected_exception=TypeError): - with self.assertRaises(expected_exception=expected_exception): - for s, expected in index_sympify: - result = self.sympify(s) - self.assertEqual(result, expected) - - -class SympifyWrapperTests(SympifyTests): - def sympify(self, expression): - return qc_sympify(expression) - - def test_len_sympify(self): - super().test_len_sympify(None) + def test_len_sympify(self) -> None: + for s, expected in len_sympify: + result = self.sympify(s) + self.assertEqual(result, expected) - def test_index_sympify(self): - super().test_index_sympify(None) + def test_index_sympify(self) -> None: + for s, expected in index_sympify: + result = self.sympify(s) + self.assertEqual(result, expected) class SubstitutionTests(TestCase): def substitute(self, expression: sympy.Expr, substitutions: dict): for key, value in substitutions.items(): if not isinstance(value, sympy.Expr): - substitutions[key] = sympy.sympify(value) - return expression.subs(substitutions, simultaneous=True).doit() + substitutions[key] = qc_sympify(value) + return recursive_substitution(expression, substitutions) + #return expression.subs(substitutions, simultaneous=True).doit() def test_simple_substitution_cases(self): for expr, subs, expected in simple_substitution_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result, msg=str((expr, subs, expected))) def test_elem_func_substitution_cases(self): for expr, subs, expected in elem_func_substitution_cases: @@ -311,12 +323,16 @@ def test_get_most_simple_representation(self): self.assertIsInstance(st, str) self.assertEqual(st, 'a + b') + sym = get_most_simple_representation(qc_sympify('b + foo.bar.test')) + self.assertIsInstance(sym, str) + self.assertEqual('b + foo.bar.test', sym) + class NamespaceTests(unittest.TestCase): def test_sympify_dot_namespace_notations(self) -> None: expr = qc_sympify("qubit.a + qubit.spec2.a * 1.3") - expected = sympy.Add(sympy.Symbol('qubit___a'), sympy.Mul(sympy.Symbol('qubit___spec2___a'), sympy.RealNumber(1.3))) + expected = sympy.Add(sympy.Symbol('qubit____a'), sympy.Mul(sympy.Symbol('qubit____spec2____a'), sympy.RealNumber(1.3))) self.assertEqual(expected, expr) def test_evaluate_lambdified_dot_namespace_notation(self) -> None: From da9f161874e39b4b60927d70ec46148841f0f5b7 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 16:59:10 +0200 Subject: [PATCH 08/15] Changed MappingPulseTemplate to dot namespace notation. --- qupulse/pulses/mapping_pulse_template.py | 4 ++-- tests/pulses/mapping_pulse_template_tests.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 7644ca5e1..2ea0622e7 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -67,8 +67,8 @@ def __init__(self, template: PulseTemplate, *, if not mapping_namespace: mapping_namespace = "" else: - mapping_namespace += "___" # namespace - parameter delimiter - parameter_mapping.update({p: mapping_namespace+p.rpartition('___')[2] for p in missing_parameter_mappings}) + mapping_namespace += "." # namespace - parameter delimiter + parameter_mapping.update({p: mapping_namespace+p.rpartition('.')[2] for p in missing_parameter_mappings}) parameter_mapping = dict((k, Expression(v)) for k, v in parameter_mapping.items()) measurement_mapping = dict() if measurement_mapping is None else measurement_mapping diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index 616a842a9..e757b9869 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -190,12 +190,12 @@ def test_integral(self) -> None: self.assertEqual({'default': Expression('2*f'), 'other': Expression('-3.2*f+2.3')}, pulse.integral) def test_mapping_namespace(self) -> None: - template = DummyPulseTemplate(parameter_names={'foo', 'bar', 'outer___inner___hugo'}) + template = DummyPulseTemplate(parameter_names={'foo', 'bar', 'outer.inner.hugo'}) st = MappingPulseTemplate(template, mapping_namespace='scope') - self.assertEqual({'scope___foo', 'scope___bar', 'scope___hugo'}, st.parameter_names) + self.assertEqual({'scope.foo', 'scope.bar', 'scope.hugo'}, st.parameter_names) - st = MappingPulseTemplate(template, parameter_mapping={'foo': 'alpha+scope___beta'}, mapping_namespace='scope') - self.assertEqual({'alpha', 'scope___beta', 'scope___bar', 'scope___hugo'}, st.parameter_names) + st = MappingPulseTemplate(template, parameter_mapping={'foo': 'alpha+scope.beta'}, mapping_namespace='scope') + self.assertEqual({'alpha', 'scope.beta', 'scope.bar', 'scope.hugo'}, st.parameter_names) class MappingPulseTemplateSequencingTest(MeasurementWindowTestCase): From 1560afa0354fcc5ebdd4de61de0aaa2f4d480d6f Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 2 Oct 2018 17:29:45 +0200 Subject: [PATCH 09/15] Increased test coverage for namespaced parameters. cleaned up tests file --- tests/utils/sympy_tests.py | 62 +++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index d4653bf96..d49dad25f 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -35,25 +35,34 @@ elem_func_substitution_cases = [ (a*b + sin(c), {'a': b, 'c': sympy.pi/2}, b**2 + 1), + (a*scope_n + sin(foo_bar), {'a': scope_n, 'foo.bar': sympy.pi/2}, scope_n**2 + 1) ] sum_substitution_cases = [ (a*b + Sum(c * k, (k, 0, n)), {'a': b, 'b': 2, 'k': 1, 'n': 2}, b*2 + c*(1 + 2)), + (a*foo_bar + Sum(c * scope_n, (scope_n, 0, n)), {'a': foo_bar, 'foo.bar': 2, 'scope.n': 1, 'n': 2}, foo_bar*2 + c*(1 + 2)), ] indexed_substitution_cases = [ - (a_[i]*b, {'b': 3}, a_[i]*3), - (a_[i]*b, {'a': sympy.Array([1, 2, 3])}, sympy.Array([1, 2, 3])[i]*b), - (sympy.Array([1, 2, 3])[i]*b, {'i': 1}, 2*b) + (a_[i] * b, {'b': 3}, a_[i] * 3), + (a_[i] * b, {'a': sympy.Array([1, 2, 3])}, sympy.Array([1, 2, 3])[i] * b), + (sympy.Array([1, 2, 3])[i] * b, {'i': 1}, 2 * b), + (foo_bar_[i]*scope_n, {'scope.n': 3}, foo_bar_[i]*3), + (foo_bar_[i]*scope_n, {'foo.bar': sympy.Array([1, 2, 3])}, sympy.Array([1, 2, 3])[i]*scope_n), + (sympy.Array([1, 2, 3])[foo_bar]*b, {'foo.bar': 1}, 2*b), ] vector_valued_cases = [ (a*b, {'a': sympy.Array([1, 2, 3])}, sympy.Array([1, 2, 3])*b), (a*b, {'a': sympy.Array([1, 2, 3]), 'b': sympy.Array([4, 5, 6])}, sympy.Array([4, 10, 18])), + (foo_bar*b, {'foo.bar': sympy.Array([1, 2, 3])}, sympy.Array([1, 2, 3])*b), + (foo_bar*b, {'foo.bar': sympy.Array([1, 2, 3]), 'b': sympy.Array([4, 5, 6])}, sympy.Array([4, 10, 18])), + (foo_bar*scope_n, {'foo.bar': sympy.Array([1, 2, 3]), 'scope.n': sympy.Array([4, 5, 6])}, sympy.Array([4, 10, 18])), ] full_featured_cases = [ (Sum(a_[i], (i, 0, Len(a) - 1)), {'a': sympy.Array([1, 2, 3])}, 6), + (Sum(foo_bar_[i], (i, 0, Len(foo_bar) - 1)), {'foo.bar': sympy.Array([1, 2, 3])}, 6), ] @@ -143,22 +152,22 @@ def assertEqual1(self, first, second, msg=None): def test_simple_sympify(self) -> None: for s, expected in simple_sympify: result = self.sympify(s) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_complex_sympify(self) -> None: for s, expected in complex_sympify: result = self.sympify(s) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_len_sympify(self) -> None: for s, expected in len_sympify: result = self.sympify(s) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_index_sympify(self) -> None: for s, expected in index_sympify: result = self.sympify(s) - self.assertEqual(result, expected) + self.assertEqual(expected, result) class SubstitutionTests(TestCase): @@ -166,8 +175,7 @@ def substitute(self, expression: sympy.Expr, substitutions: dict): for key, value in substitutions.items(): if not isinstance(value, sympy.Expr): substitutions[key] = qc_sympify(value) - return recursive_substitution(expression, substitutions) - #return expression.subs(substitutions, simultaneous=True).doit() + return recursive_substitution(expression, substitutions).doit() def test_simple_substitution_cases(self): for expr, subs, expected in simple_substitution_cases: @@ -177,36 +185,27 @@ def test_simple_substitution_cases(self): def test_elem_func_substitution_cases(self): for expr, subs, expected in elem_func_substitution_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sum_substitution_cases(self): for expr, subs, expected in sum_substitution_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_indexed_substitution_cases(self): - if type(self) is SubstitutionTests: - raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') - for expr, subs, expected in indexed_substitution_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_vector_valued_cases(self): - if type(self) is SubstitutionTests: - raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') - for expr, subs, expected in vector_valued_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result, msg="test: {}".format((expr, subs, expected))) def test_full_featured_cases(self): - if type(self) is SubstitutionTests: - raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') - for expr, subs, expected in full_featured_cases: result = self.substitute(expr, subs) - self.assertEqual(result, expected) + self.assertEqual(expected, result) class SubstituteWithEvalTests(SubstitutionTests): @@ -222,11 +221,6 @@ def test_full_featured_cases(self): super().test_full_featured_cases() -class RecursiveSubstitutionTests(SubstitutionTests): - def substitute(self, expression: sympy.Expr, substitutions: dict): - return recursive_substitution(expression, substitutions).doit() - - class GetFreeSymbolsTests(TestCase): def assert_symbol_sets_equal(self, expected, actual): self.assertEqual(len(expected), len(actual)) @@ -260,33 +254,33 @@ def evaluate(self, expression: Union[sympy.Expr, np.ndarray], parameters): def test_eval_simple(self): for expr, parameters, expected in eval_simple: result = self.evaluate(expr, parameters) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_eval_many_arguments(self, expected_exception=SyntaxError): with self.assertRaises(expected_exception): for expr, parameters, expected in eval_many_arguments: result = self.evaluate(expr, parameters) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_eval_simple_functions(self): for expr, parameters, expected in eval_simple_functions: result = self.evaluate(expr, parameters) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_eval_array_values(self): for expr, parameters, expected in eval_array_values: result = self.evaluate(expr, parameters) - np.testing.assert_equal(result, expected) + np.testing.assert_equal(expected, result) def test_eval_sum(self): for expr, parameters, expected in eval_sum: result = self.evaluate(expr, parameters) - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_eval_array_expression(self): for expr, parameters, expected in eval_array_expression: result = self.evaluate(expr, parameters) - np.testing.assert_equal(result, expected) + np.testing.assert_equal(expected, result) class CompiledEvaluationTest(EvaluationTests): From 564f117979bfe49748c1fd12049377a667e0672d Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Mon, 29 Oct 2018 11:44:52 +0100 Subject: [PATCH 10/15] Ensuring nice behavior of substitutions in expressions in integral() method of PulseTemplates. - added function substitute() in sympy and subs() in ExpressionScalar which symply wrap calls to sympy.Expr.subs but rename namespaced parameters - adapted integral() methods in PulseTemplates to accomodoate for those changes - added tests for namespaced parameters in integral() methods for PulseTemplates --- qupulse/expressions.py | 15 +++++++++-- qupulse/pulses/loop_pulse_template.py | 4 ++- qupulse/pulses/mapping_pulse_template.py | 14 ++-------- qupulse/pulses/point_pulse_template.py | 11 ++++---- qupulse/pulses/table_pulse_template.py | 8 +++--- qupulse/utils/sympy.py | 14 +++++++++- tests/expression_tests.py | 19 ++++++++++++- tests/pulses/loop_pulse_template_tests.py | 13 ++++++++- tests/pulses/mapping_pulse_template_tests.py | 9 +++++++ tests/pulses/point_pulse_template_tests.py | 12 ++++++++- tests/pulses/table_pulse_template_tests.py | 8 ++++++ tests/utils/sympy_tests.py | 28 +++++++++++++++++++- 12 files changed, 125 insertions(+), 30 deletions(-) diff --git a/qupulse/expressions.py b/qupulse/expressions.py index d85cffa79..379b7558c 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions.py @@ -14,7 +14,7 @@ from qupulse.serialization import AnonymousSerializable from qupulse.utils.sympy import sympify, to_numpy, recursive_substitution, evaluate_lambdified,\ - get_most_simple_representation, get_variables + get_most_simple_representation, get_variables, substitute, almost_equal __all__ = ["Expression", "ExpressionVariableMissingException", "ExpressionScalar", "ExpressionVector"] @@ -266,7 +266,10 @@ def __le__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> Union[ def __eq__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> bool: """Enable comparisons with Numbers""" - return self._sympified_expression == self._sympify(other) + sympified_other = self._sympify(other) + if self._sympified_expression == sympified_other: + return True + return almost_equal(self._sympified_expression, sympified_other) def __hash__(self) -> int: return hash(self._sympified_expression) @@ -316,6 +319,14 @@ def get_serialization_data(self) -> Union[str, float, int]: def is_nan(self) -> bool: return sympy.sympify('nan') == self._sympified_expression + def subs(self, substitutions: Dict[str, Union[str, 'ExpressionScalar', sympy.Expr, sympy.Symbol, Number]]) -> 'ExpressionScalar': + substitutions = substitutions.copy() + for p, e in substitutions.items(): + if isinstance(e, Expression): + substitutions[p] = e.underlying_expression + + return Expression(substitute(self.sympified_expression, substitutions)) + class ExpressionVariableMissingException(Exception): """An exception indicating that a variable value was not provided during expression evaluation. diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 992b3d20a..c8ee07b4c 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -174,7 +174,9 @@ def duration(self) -> ExpressionScalar: sum_index = sympy.symbols(self._loop_index) # replace loop_index with sum_index dependable expression - body_duration = self.body.duration.sympified_expression.subs({loop_index: self._loop_range.start.sympified_expression + sum_index*step_size}) + body_duration = self.body.duration.sympified_expression.subs( + {loop_index: self._loop_range.start.sympified_expression + sum_index*step_size} + ) # number of sum contributions step_count = sympy.ceiling((self._loop_range.stop.sympified_expression-self._loop_range.start.sympified_expression) / step_size) diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 2ea0622e7..1af1b60c2 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -314,18 +314,8 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]: internal_integral = self.__template.integral expressions = dict() - # sympy.subs() does not work if one of the mappings in the provided dict is an Expression object - # the following is an ugly workaround - # todo: make Expressions compatible with sympy.subs() - parameter_mapping = self.__parameter_mapping.copy() - for i in parameter_mapping: - if isinstance(parameter_mapping[i], ExpressionScalar): - parameter_mapping[i] = parameter_mapping[i].sympified_expression - - for channel in internal_integral: - expr = ExpressionScalar( - internal_integral[channel].sympified_expression.subs(parameter_mapping) - ) + for channel, channel_integral in internal_integral.items(): + expr = channel_integral.subs(self.__parameter_mapping) channel_out = channel if channel in self.__channel_mapping: channel_out = self.__channel_mapping[channel] diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py index 6ca029822..24bf81efa 100644 --- a/qupulse/pulses/point_pulse_template.py +++ b/qupulse/pulses/point_pulse_template.py @@ -148,15 +148,14 @@ def requires_stop(self, def integral(self) -> Dict[ChannelID, ExpressionScalar]: expressions = {channel: 0 for channel in self._channels} for first_entry, second_entry in zip(self._entries[:-1], self._entries[1:]): - substitutions = {'t0': ExpressionScalar(first_entry.t).sympified_expression, - 't1': ExpressionScalar(second_entry.t).sympified_expression} + substitutions = {'t0': (first_entry.t), + 't1': (second_entry.t)} for i, channel in enumerate(self._channels): - substitutions['v0'] = ExpressionScalar(first_entry.v[i]).sympified_expression - substitutions['v1'] = ExpressionScalar(second_entry.v[i]).sympified_expression - expressions[channel] += first_entry.interp.integral.sympified_expression.subs(substitutions) + substitutions['v0'] = (first_entry.v[i]) + substitutions['v1'] = (second_entry.v[i]) + expressions[channel] += first_entry.interp.integral.subs(substitutions) - expressions = {c: ExpressionScalar(expressions[c]) for c in expressions} return expressions diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 1f934b787..57ca35ad9 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -362,11 +362,11 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]: expr = 0 for first_entry, second_entry in zip(channel_entries[:-1], channel_entries[1:]): - substitutions = {'t0': ExpressionScalar(first_entry.t).sympified_expression, 'v0': ExpressionScalar(first_entry.v).sympified_expression, - 't1': ExpressionScalar(second_entry.t).sympified_expression, 'v1': ExpressionScalar(second_entry.v).sympified_expression} + substitutions = {'t0': first_entry.t, 'v0': first_entry.v, + 't1': second_entry.t, 'v1': second_entry.v} - expr += first_entry.interp.integral.sympified_expression.subs(substitutions) - expressions[channel] = ExpressionScalar(expr) + expr += first_entry.interp.integral.subs(substitutions) + expressions[channel] = expr return expressions diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index c6fb2617b..e0ac5c3f9 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -23,7 +23,7 @@ __all__ = ["sympify", "substitute_with_eval", "to_numpy", "get_variables", "get_free_symbols", "recursive_substitution", - "evaluate_lambdified", "get_most_simple_representation"] + "evaluate_lambdified", "get_most_simple_representation", 'substitute'] Sympifyable = Union[str, Number, sympy.Expr, numpy.str_] @@ -273,6 +273,18 @@ def get_variables(expression: sympy.Expr) -> Sequence[str]: return tuple(map(lambda x: str(x).replace(sympy_internal_namespace_seperator, '.'), get_free_symbols(expression))) +def substitute(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, str, Number]]) -> sympy.Expr: + inner_subs = dict() + for k, v in substitutions.items(): + if isinstance(k, sympy.Symbol): + k = k.name + k = k.replace('.', sympy_internal_namespace_seperator) + v = v if isinstance(v, sympy.Expr) else sympify(v) + inner_subs[k] = v + + return expression.subs(inner_subs) + + def substitute_with_eval(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, numpy.ndarray, str]]) -> sympy.Expr: """Substitutes only sympy.Symbols. Workaround for numpy like array behaviour. ~Factor 3 slower compared to subs""" diff --git a/tests/expression_tests.py b/tests/expression_tests.py index c6097a1dd..318d9836a 100644 --- a/tests/expression_tests.py +++ b/tests/expression_tests.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from sympy import sympify, Eq +from sympy import sympify, Eq, Symbol from qupulse.expressions import Expression, ExpressionVariableMissingException, NonNumericEvaluation, ExpressionScalar, ExpressionVector @@ -213,6 +213,12 @@ def test_variables_indexed(self): received = sorted(e.variables) self.assertEqual(expected, received) + def test_namespaced_variables(self) -> None: + e = ExpressionScalar('foo.bar[foo.index] * foo.hugo.ilse') + expected = sorted(['foo.bar', 'foo.index', 'foo.hugo.ilse']) + received = sorted(e.variables) + self.assertEqual(expected, received) + def test_evaluate_variable_missing(self) -> None: e = ExpressionScalar('a * b + c') params = { @@ -356,6 +362,17 @@ def test_special_function_numeric_evaluation(self): np.testing.assert_allclose(expected, result) + def test_subs(self) -> None: + expr = Expression('a + b * c - 0.07') + subs = { + Symbol('a'): '3.7', + 'b': '2/d', + 'c': Expression('1/d') + } + result = expr.subs(subs) + expected = Expression('3.7 + 2/(d*d) - 0.07') + self.assertEqual(expected, result) + class ExpressionExceptionTests(unittest.TestCase): def test_expression_variable_missing(self): diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index ba628f68f..bc7d0f3c0 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -168,7 +168,18 @@ def test_integral(self) -> None: pulse = ForLoopPulseTemplate(dummy, 'i', (1, 8, 2)) expected = {'A': ExpressionScalar('Sum(t1-3.1*(1+2*i), (i, 0, 3))'), - 'B': ExpressionScalar('Sum((1+2*i), (i, 0, 3))') } + 'B': ExpressionScalar('Sum((1+2*i), (i, 0, 3))')} + self.assertEqual(expected, pulse.integral) + + def test_integral_namespaced_params(self) -> None: + dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, + parameter_names={'time.t1', 'i'}, + integrals={'A': ExpressionScalar('time.t1-i*3.1+foo.c'), 'B': ExpressionScalar('i')}) + + pulse = ForLoopPulseTemplate(dummy, 'i', (1, 8, 2)) + + expected = {'A': ExpressionScalar('Sum(time.t1-3.1*(1+2*i)+foo.c, (i, 0, 3))'), + 'B': ExpressionScalar('Sum((1+2*i), (i, 0, 3))')} self.assertEqual(expected, pulse.integral) diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index e757b9869..fcaa4b54f 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -189,6 +189,15 @@ def test_integral(self) -> None: self.assertEqual({'default': Expression('2*f'), 'other': Expression('-3.2*f+2.3')}, pulse.integral) + def test_integral_namespaced_params(self) -> None: + dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, + parameter_names={'foo.k', 'foo.f', 'foo.b'}, + integrals={'A': Expression('2*foo.k'), 'other': Expression('-3.2*foo.f+foo.b')}) + pulse = MappingPulseTemplate(dummy, parameter_mapping={'foo.k': 'foo.f', 'foo.b': 2.3}, channel_mapping={'A': 'default'}, + mapping_namespace='pirate.arrr') + + self.assertEqual({'default': Expression('2*foo.f'), 'other': Expression('-3.2*pirate.arrr.f+2.3')}, pulse.integral) + def test_mapping_namespace(self) -> None: template = DummyPulseTemplate(parameter_names={'foo', 'bar', 'outer.inner.hugo'}) st = MappingPulseTemplate(template, mapping_namespace='scope') diff --git a/tests/pulses/point_pulse_template_tests.py b/tests/pulses/point_pulse_template_tests.py index ca1cac4fe..cd6b9e1ff 100644 --- a/tests/pulses/point_pulse_template_tests.py +++ b/tests/pulses/point_pulse_template_tests.py @@ -79,7 +79,7 @@ def test_integral(self) -> None: [(1, (2, 'b'), 'linear'), (3, (0, 0), 'jump'), (4, (2, 'c'), 'hold'), (5, (8, 'd'), 'hold')], [0, 'other_channel'] ) - self.assertEqual({0: ExpressionScalar(6), + self.assertEqual({0: ExpressionScalar(6.0), 'other_channel': ExpressionScalar('1.0*b + 2.0*c')}, pulse.integral) @@ -91,6 +91,16 @@ def test_integral(self) -> None: 1: ExpressionScalar('b*(0.5*t0 - 0.5) + c*(g - 4.0) + c*(-t0 + 4.0)')}, pulse.integral) + def test_integral_namespaced_params(self) -> None: + pulse = PointPulseTemplate( + [(1, ('2', 'foo.b'), 'linear'), ('time.t0', (0, 0), 'jump'), (4, (2, 'foo.c'), 'hold'), ('time.g', (8, 'foo.d'), 'hold')], + ['namespaced', 1] + ) + + self.assertEqual({'namespaced': ExpressionScalar('2.0*time.g - time.t0 - 1.0'), + 1: ExpressionScalar('foo.b*(0.5*time.t0 - 0.5) + foo.c*(time.g - 4.0) + foo.c*(-time.t0 + 4.0)')}, + pulse.integral) + class PointPulseTemplateSequencingTests(unittest.TestCase): diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py index 71258db9c..876bbc9da 100644 --- a/tests/pulses/table_pulse_template_tests.py +++ b/tests/pulses/table_pulse_template_tests.py @@ -428,6 +428,14 @@ def test_integral(self) -> None: 'other_channel': Expression(7), 'symbolic': Expression('(b-3)*a + 0.5 * (c-b)*(d+4)')}) + def test_integral_namespaced_params(self) -> None: + pulse = TablePulseTemplate(entries={'namespaced': [(3, 'foo.bar', 'hold'), ('hugo.ilse', 4, 'linear'), + ('foo.foo.bar', Expression('hugo.ilse.herbert'), 'hold')]}) + self.assertEqual(pulse.integral, {'namespaced': + Expression('foo.bar*(hugo.ilse - 3.0) + ' + '(0.5*foo.foo.bar - 0.5*hugo.ilse)*(hugo.ilse.herbert + 4.0)') + }) + class TablePulseTemplateConstraintTest(ParameterConstrainerTest): def __init__(self, *args, **kwargs): diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index 77aebcfdc..e10a9e2fa 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -19,7 +19,7 @@ from qupulse.utils.sympy import sympify as qc_sympify, substitute_with_eval, recursive_substitution, Len,\ evaluate_lambdified, evaluate_compiled, get_most_simple_representation, get_variables, get_free_symbols,\ - almost_equal + almost_equal, substitute ################################################### SUBSTITUTION ####################################################### @@ -346,3 +346,29 @@ def test_evaluate_lambdified_dot_namespace_notation(self) -> None: def test_evaluate_compiled_dot_namespace_notation(self) -> None: res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit.a": 2.1, "qubit.spec2.a": .1}) self.assertEqual(2.23, res[0]) + + +class SubstituteFunctionTests(unittest.TestCase): + + def test_substitute(self) -> None: + expr = qc_sympify('a + b * c - 0.07') + subs = { + a: '3.7', + b: qc_sympify('2/d'), + 'c': '1/d' + } + result = substitute(expr, subs) + expected = qc_sympify('3.7 + 2/(d*d) - 0.07') + self.assertEqual(expected, result) + + def test_substitute_namespaced(self) -> None: + expr = qc_sympify('foo.a + foo.b * foo.c - 0.07') + subs = { + sympy.Symbol('foo.a'): '3.7', + sympy.Symbol('foo____b'): qc_sympify('2/foo.d'), + 'foo.c': '1/foo.d' + } + result = substitute(expr, subs) + expected = qc_sympify('3.7 + 2/(foo.d*foo.d) - 0.07') + self.assertEqual(expected, result) + From 3ff811cd0483080eac91c255bf3ff4dcc74bb17b Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Mon, 29 Oct 2018 11:57:41 +0100 Subject: [PATCH 11/15] Added release notes for changes made in this branch. --- ReleaseNotes.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 7299c0298..5e633298b 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -7,10 +7,19 @@ - `AtomicMultichannelPulseTemplate`: - Add duration keyword argument & example (see MultiChannelTemplates notebook) - Make duration equality check approximate (numeric tolerance) + - `MappingPulseTemplate`: + - Add `mapping_namespace` keyword argument to allow mapping all subtemplate parameters into a namespace easily + - DEPRECATED: `allow_partial_parameter_mapping` argument. MappingPulseTemplate now always allows partial mapping. - Expressions: - Make ExpressionScalar hashable - Fix bug that prevented evaluation of expressions containing some special functions (`erfc`, `factorial`, etc.) + - ExpressionScalar equality operator now also returns true in case of numerical almost-equality + - Add method subs() to ExpressionScalar which wraps ExpressionScalar.sympified_expression.subs() + +- Utils/sympy: + - Add custom expression parsing step which allows usage of '.' in variable/symbol names to allow namespace notation + - Add method substitute() which wraps sympy.Expr.subs() but handles '.' in variable/symbol names gracefully ## 0.2 ## From 19941d6d96900d81beb1d43b02ce5f4d162db8b1 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Mon, 29 Oct 2018 13:07:45 +0100 Subject: [PATCH 12/15] Reverted sympy_tests.SubstitutionTests to behavior closer to current master. - had replaced expression.subs() with recursive_substitution() since the first one did not rename namespaced parameters, which is not replaced with substitute() which is the namespace-aware wrapper of expression.subs() - sympy.py: replaced a failure-catching if statement which should never occur with an assert() - added kwargs to utils.sympy.substitute() and ExpressionScalar.subs() --- qupulse/expressions.py | 6 ++++-- qupulse/utils/sympy.py | 7 +++---- tests/utils/sympy_tests.py | 37 ++++++++++--------------------------- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/qupulse/expressions.py b/qupulse/expressions.py index 379b7558c..daf2a5fe5 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions.py @@ -319,13 +319,15 @@ def get_serialization_data(self) -> Union[str, float, int]: def is_nan(self) -> bool: return sympy.sympify('nan') == self._sympified_expression - def subs(self, substitutions: Dict[str, Union[str, 'ExpressionScalar', sympy.Expr, sympy.Symbol, Number]]) -> 'ExpressionScalar': + def subs(self, + substitutions: Dict[str, Union[str, 'ExpressionScalar', sympy.Expr, sympy.Symbol, Number]], + **kwargs) -> 'ExpressionScalar': substitutions = substitutions.copy() for p, e in substitutions.items(): if isinstance(e, Expression): substitutions[p] = e.underlying_expression - return Expression(substitute(self.sympified_expression, substitutions)) + return Expression(substitute(self.sympified_expression, substitutions), **kwargs) class ExpressionVariableMissingException(Exception): diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index e0ac5c3f9..7a0961ad7 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -55,8 +55,7 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: next_tok_num, next_tok_val = next_tok if symbol_string: - if tok_val != '.' and tok_num != NAME: - raise SyntaxError("Not a valid namespaced sympy.symbol name") + assert(tok_val == '.' or tok_num == NAME) if tok_val == '.': symbol_string += sympy_internal_namespace_seperator elif tok_num == NAME: @@ -273,7 +272,7 @@ def get_variables(expression: sympy.Expr) -> Sequence[str]: return tuple(map(lambda x: str(x).replace(sympy_internal_namespace_seperator, '.'), get_free_symbols(expression))) -def substitute(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, str, Number]]) -> sympy.Expr: +def substitute(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr, str, Number]], **kwargs) -> sympy.Expr: inner_subs = dict() for k, v in substitutions.items(): if isinstance(k, sympy.Symbol): @@ -282,7 +281,7 @@ def substitute(expression: sympy.Expr, substitutions: Dict[str, Union[sympy.Expr v = v if isinstance(v, sympy.Expr) else sympify(v) inner_subs[k] = v - return expression.subs(inner_subs) + return expression.subs(inner_subs, **kwargs) def substitute_with_eval(expression: sympy.Expr, diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index e10a9e2fa..e8d0d6e50 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -176,7 +176,7 @@ def substitute(self, expression: sympy.Expr, substitutions: dict): for key, value in substitutions.items(): if not isinstance(value, sympy.Expr): substitutions[key] = qc_sympify(value) - return recursive_substitution(expression, substitutions).doit() + return substitute(expression, substitutions, simultaneous=True).doit() def test_simple_substitution_cases(self): for expr, subs, expected in simple_substitution_cases: @@ -194,16 +194,25 @@ def test_sum_substitution_cases(self): self.assertEqual(expected, result) def test_indexed_substitution_cases(self): + if type(self) is SubstitutionTests: + raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') + for expr, subs, expected in indexed_substitution_cases: result = self.substitute(expr, subs) self.assertEqual(expected, result) def test_vector_valued_cases(self): + if type(self) is SubstitutionTests: + raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') + for expr, subs, expected in vector_valued_cases: result = self.substitute(expr, subs) self.assertEqual(expected, result, msg="test: {}".format((expr, subs, expected))) def test_full_featured_cases(self): + if type(self) is SubstitutionTests: + raise unittest.SkipTest('sympy.Expr.subs does not handle simultaneous substitutions of indexed entities.') + for expr, subs, expected in full_featured_cases: result = self.substitute(expr, subs) self.assertEqual(expected, result) @@ -346,29 +355,3 @@ def test_evaluate_lambdified_dot_namespace_notation(self) -> None: def test_evaluate_compiled_dot_namespace_notation(self) -> None: res = evaluate_compiled("qubit.a + qubit.spec2.a * 1.3", {"qubit.a": 2.1, "qubit.spec2.a": .1}) self.assertEqual(2.23, res[0]) - - -class SubstituteFunctionTests(unittest.TestCase): - - def test_substitute(self) -> None: - expr = qc_sympify('a + b * c - 0.07') - subs = { - a: '3.7', - b: qc_sympify('2/d'), - 'c': '1/d' - } - result = substitute(expr, subs) - expected = qc_sympify('3.7 + 2/(d*d) - 0.07') - self.assertEqual(expected, result) - - def test_substitute_namespaced(self) -> None: - expr = qc_sympify('foo.a + foo.b * foo.c - 0.07') - subs = { - sympy.Symbol('foo.a'): '3.7', - sympy.Symbol('foo____b'): qc_sympify('2/foo.d'), - 'foo.c': '1/foo.d' - } - result = substitute(expr, subs) - expected = qc_sympify('3.7 + 2/(foo.d*foo.d) - 0.07') - self.assertEqual(expected, result) - From ca0ea795bda3078c5a4b1f47b67e509195625b27 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Mon, 19 Nov 2018 15:41:05 +0100 Subject: [PATCH 13/15] custom_auto_symbol_transformation that doesn't fail in sympy tests suite. (WIP) our tests break for subscriptable symbols now --- qupulse/utils/sympy.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index 7a0961ad7..082c74478 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -55,7 +55,6 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: next_tok_num, next_tok_val = next_tok if symbol_string: - assert(tok_val == '.' or tok_num == NAME) if tok_val == '.': symbol_string += sympy_internal_namespace_seperator elif tok_num == NAME: @@ -70,15 +69,14 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: name = tok_val if (name in ['True', 'False', 'None'] - or iskeyword(name) - # Don't convert keyword arguments - or (prev_tok[0] == OP and prev_tok[1] in ('(', ',') - and next_tok_num == OP and next_tok_val == '=')): + or iskeyword(name) + # # Don't convert attribute access + # or (prev_tok[0] == OP and prev_tok[1] == '.') + # Don't convert keyword arguments + or (prev_tok[0] == OP and prev_tok[1] in ('(', ',') + and next_tok_num == OP and next_tok_val == '=')): result.append((NAME, name)) continue - elif next_tok_val == '.': - symbol_string = str(name) - continue elif name in local_dict: if isinstance(local_dict[name], Symbol) and next_tok_val == '(': result.extend([(NAME, 'Function'), @@ -93,6 +91,9 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: if isinstance(obj, (Basic, type)) or callable(obj): result.append((NAME, name)) continue + elif next_tok_val == '.': + symbol_string = str(name) + continue result.extend([ (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), From 5744f4a6063b60859e1f026ce6736a9da5121b20 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Wed, 28 Nov 2018 13:14:31 +0100 Subject: [PATCH 14/15] fix parameter namespaces and indexing custom_auto_symbol_transformation now again ignores local/global symbols that are followed by a namespace separator (.) and interprets it as symbol --- qupulse/utils/sympy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index 082c74478..e8983248b 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -77,6 +77,9 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: and next_tok_num == OP and next_tok_val == '=')): result.append((NAME, name)) continue + elif next_tok_val == '.': + symbol_string = str(name) + continue elif name in local_dict: if isinstance(local_dict[name], Symbol) and next_tok_val == '(': result.extend([(NAME, 'Function'), @@ -91,9 +94,6 @@ def custom_auto_symbol_transform(tokens: Sequence[Tuple[int, str]], local_dict: if isinstance(obj, (Basic, type)) or callable(obj): result.append((NAME, name)) continue - elif next_tok_val == '.': - symbol_string = str(name) - continue result.extend([ (NAME, 'Symbol' if next_tok_val != '(' else 'Function'), From 39c2f4fead38b8bf2555b3fd0dd0ab026f75a19c Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Tue, 4 Dec 2018 10:26:34 +0100 Subject: [PATCH 15/15] More concise information for namespace mapping in MappingPT. --- qupulse/pulses/mapping_pulse_template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 1af1b60c2..e9238341e 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -40,8 +40,9 @@ def __init__(self, template: PulseTemplate, *, Mappings that are not specified are defaulted to identity mappings. F.i. if channel_mapping only contains one of two channels the other channel name is mapped to itself. All parameters that are not explicitly mapped in the parameter_mapping dictionary are mapped to themselves if - the mapping_namespace argument is not given or set to None. Otherwise, MappingPT maps these parameters are into - the namespace given by internal_namespace. + the mapping_namespace argument is not given or set to None. Otherwise, MappingPT maps these parameters into + the namespace given by mapping_namespace. All parameters that are already explicitely mapped in parameter_mapping + will not be affected in any way by the mapping_namespace argument. Furthermore parameter constrains can be specified. :param template: The encapsulated pulse template whose parameters, measurement names and channels are mapped