Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4488d13
MappingPT now always accepts partial parameter mappings; deprecated t…
lumip Aug 16, 2018
09d62e7
MappingPT now optionally places unmapped parameters into namespace.
lumip Aug 16, 2018
0a45063
Merge branch 'master' into issues/344_parameter_namespaces
lumip Sep 28, 2018
15a4514
Some comments to clarify structure of utils/sympy.py.
lumip Oct 2, 2018
832b477
Attempt to enable namespace dot-notation (e.g. "foo.bar") in sympy ex…
lumip Oct 2, 2018
c2016de
Custom sympy token transform now replaces "." with "___".
lumip Oct 2, 2018
2a1c129
Evaluation methods transparently rename parameters from '.' to '___' …
lumip Oct 2, 2018
49c628a
Fixes to custom sympy token transformation and affected code.
lumip Oct 2, 2018
da9f161
Changed MappingPulseTemplate to dot namespace notation.
lumip Oct 2, 2018
1560afa
Increased test coverage for namespaced parameters.
lumip Oct 2, 2018
a840a70
Merge branch 'master' into issues/344_parameter_namespaces
lumip Oct 29, 2018
564f117
Ensuring nice behavior of substitutions in expressions in integral() …
lumip Oct 29, 2018
3ff811c
Added release notes for changes made in this branch.
lumip Oct 29, 2018
19941d6
Reverted sympy_tests.SubstitutionTests to behavior closer to current …
lumip Oct 29, 2018
32bbad2
Merge branch 'master' into issues/344_parameter_namespaces
lumip Nov 13, 2018
ca0ea79
custom_auto_symbol_transformation that doesn't fail in sympy tests su…
lumip Nov 19, 2018
5744f4a
fix parameter namespaces and indexing
lumip Nov 28, 2018
0ed85a9
Merge branch 'master' into issues/344_parameter_namespaces
lumip Dec 4, 2018
39c2f4f
More concise information for namespace mapping in MappingPT.
lumip Dec 4, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@
- `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.
- Plotting:
- Add `time_slice` keyword argument to render() and plot()

- 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

- Parameters:
- `ConstantParameter` now accepts a `Expression` without free variables as value (given as `Expression` or string)
Expand Down
17 changes: 15 additions & 2 deletions qupulse/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -316,6 +319,16 @@ 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]],
**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), **kwargs)


class ExpressionVariableMissingException(Exception):
"""An exception indicating that a variable value was not provided during expression evaluation.
Expand Down
4 changes: 3 additions & 1 deletion qupulse/pulses/loop_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 27 additions & 47 deletions qupulse/pulses/mapping_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Optional, Set, Dict, Union, List, Any, Tuple
import itertools
import numbers
import warnings

from qupulse.utils.types import ChannelID
from qupulse.expressions import Expression, ExpressionScalar
Expand Down Expand Up @@ -31,41 +32,44 @@ 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,
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. 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.
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 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
:param parameter_mapping: if not none, mappings for all parameters must be specified
: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 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:
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)

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:
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 = 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
Expand Down Expand Up @@ -311,18 +315,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]
Expand All @@ -331,20 +325,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."""
Expand Down
11 changes: 5 additions & 6 deletions qupulse/pulses/point_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
8 changes: 4 additions & 4 deletions qupulse/pulses/table_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading