Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Padding in MultiChannelPT and a central function for duration comparability test #535

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
7 changes: 7 additions & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

- General:
- Unify `TimeType.from_float` between fractions and gmpy2 backend behaviour (fixes issue 529).
- Add central allocation function for sampled data `_program.waveforms.alloc_for_sample` that initializes with nan
per default

- Pulse Templates:
- AtomicMultiChannelPulseTemplate:
- Remove deprecated `external_parameters` keyword argument.
- Add padding and truncation functionality with `pad_values`

## 0.5 ##

Expand Down
253 changes: 166 additions & 87 deletions qupulse/_program/waveforms.py

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions qupulse/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ def get_serialization_data(self) -> Union[str, float, int]:
def is_nan(self) -> bool:
return sympy.sympify('nan') == self._sympified_expression

def _parse_evaluate_numeric_result(self,
result: Union[Number, numpy.ndarray],
call_arguments: Any) -> Number:
parsed = super()._parse_evaluate_numeric_result(result, call_arguments)
if isinstance(parsed, numpy.ndarray):
return parsed[()]
else:
return parsed


class ExpressionVariableMissingException(Exception):
"""An exception indicating that a variable value was not provided during expression evaluation.
Expand Down
27 changes: 17 additions & 10 deletions qupulse/pulses/arithmetic_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@
IdentityTransformation


def _apply_operation_to_channel_dict(operator: str,
lhs: Mapping[ChannelID, Any],
rhs: Mapping[ChannelID, Any]) -> Dict[ChannelID, Any]:
result = dict(lhs)
for channel, rhs_value in rhs.items():
if channel in result:
result[channel] = ArithmeticWaveform.operator_map[operator](result[channel], rhs_value)
else:
result[channel] = ArithmeticWaveform.rhs_only_map[operator](rhs_value)
return result


class ArithmeticAtomicPulseTemplate(AtomicPulseTemplate):
def __init__(self,
lhs: AtomicPulseTemplate,
Expand Down Expand Up @@ -96,17 +108,12 @@ def duration(self) -> ExpressionScalar:

@property
def integral(self) -> Dict[ChannelID, ExpressionScalar]:
lhs = self.lhs.integral
rhs = self.rhs.integral
return _apply_operation_to_channel_dict(self._arithmetic_operator, self.lhs.integral, self.rhs.integral)

result = lhs.copy()

for channel, rhs_value in rhs.items():
if channel in result:
result[channel] = ArithmeticWaveform.operator_map[self._arithmetic_operator](result[channel], rhs_value)
else:
result[channel] = ArithmeticWaveform.rhs_only_map[self._arithmetic_operator](rhs_value)
return result
def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
return _apply_operation_to_channel_dict(self._arithmetic_operator,
self.lhs._as_expression(),
self.rhs._as_expression())

def build_waveform(self,
parameters: Dict[str, Real],
Expand Down
5 changes: 4 additions & 1 deletion qupulse/pulses/function_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any, Dict, List, Set, Optional, Union
import numbers

import numpy as np
import sympy

from qupulse.expressions import ExpressionScalar
Expand Down Expand Up @@ -148,4 +147,8 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]:
sympy.integrate(self.__expression.sympified_expression, ('t', 0, self.duration.sympified_expression))
)}

def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
expr = ExpressionScalar.make(self.__expression.underlying_expression.subs({'t': self._AS_EXPRESSION_TIME}))
return {self.__channel: expr}


14 changes: 12 additions & 2 deletions qupulse/pulses/mapping_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,28 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]:
# todo: make Expressions compatible with sympy.subs()
parameter_mapping = {parameter_name: expression.underlying_expression
for parameter_name, expression in self.__parameter_mapping.items()}

for channel, ch_integral in internal_integral.items():
channel_out = self.__channel_mapping.get(channel, channel)
if channel_out is None:
continue

expressions[channel_out] = ExpressionScalar(
ch_integral.sympified_expression.subs(parameter_mapping)
ch_integral.sympified_expression.subs(parameter_mapping, simultaneous=True)
)

return expressions

def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
parameter_mapping = {parameter_name: expression.underlying_expression
for parameter_name, expression in self.__parameter_mapping.items()}
inner = self.__template._as_expression()
return {
self.__channel_mapping.get(ch, ch): ExpressionScalar(ch_expr.sympified_expression.subs(parameter_mapping,
simultaneous=True))
for ch, ch_expr in inner.items()
if self.__channel_mapping.get(ch, ch) is not None
}


class MissingMappingException(Exception):
"""Indicates that no mapping was specified for some parameter declaration of a
Expand Down
182 changes: 131 additions & 51 deletions qupulse/pulses/multi_channel_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,46 @@
import numbers
import warnings

import sympy

from qupulse.serialization import Serializer, PulseRegistryType
from qupulse.parameter_scope import Scope

from qupulse.utils import isclose
from qupulse.utils.sympy import almost_equal, Sympifyable
from qupulse.utils.types import ChannelID, TimeType
from qupulse.utils.types import ChannelID, FrozenDict, TimeType
from qupulse.utils.numeric import are_durations_compatible
from qupulse._program.waveforms import MultiChannelWaveform, Waveform, TransformingWaveform
from qupulse._program.transformation import ParallelConstantChannelTransformation, Transformation, chain_transformations
from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple
from qupulse.pulses.parameters import Parameter, ParameterConstrainer
from qupulse.pulses.measurement import MeasurementDeclaration, MeasurementWindow
from qupulse.expressions import Expression, ExpressionScalar
from qupulse.expressions import Expression, ExpressionScalar, ExpressionLike

__all__ = ["AtomicMultiChannelPulseTemplate", "ParallelConstantChannelPulseTemplate"]


class AtomicMultiChannelPulseTemplate(AtomicPulseTemplate, ParameterConstrainer):
"""Combines multiple PulseTemplates that are defined on different channels into an AtomicPulseTemplate."""
def __init__(self,
*subtemplates: Union[AtomicPulseTemplate, MappingTuple, MappingPulseTemplate],
external_parameters: Optional[Set[str]]=None,
identifier: Optional[str]=None,
parameter_constraints: Optional[List]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
registry: PulseRegistryType=None,
duration: Union[str, Expression, bool]=False) -> None:
"""Parallels multiple AtomicPulseTemplates of the same duration. The duration equality check is performed on
duration: Optional[ExpressionLike] = None,
pad_values: Mapping[ChannelID, ExpressionLike] = None) -> None:
"""Parallels multiple AtomicPulseTemplates that are defined on different channels. The `duration` and
`pad_values` arguments can be used to determine how differences in the sub-templates' durations are handled.

`duration` is True:
There are no compatibility checks performed during the initialization of this object.
`duration` is None (default):
The durations may not be incompatible if it can be determined



equality check is performed on
construction by default. If the duration keyword argument is given the check is performed on instantiation
(when build_waveform is called). duration can be a Expression to enforce a certain duration or True for an
unspecified duration.
Expand All @@ -55,23 +67,52 @@ def __init__(self,
AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)

if duration in (False, True):
warnings.warn('Boolean duration is deprecated since qupulse 0.6', DeprecationWarning)
duration = None

self._subtemplates = [st if isinstance(st, PulseTemplate) else MappingPulseTemplate.from_tuple(st) for st in
subtemplates]

for subtemplate in self._subtemplates:
if isinstance(subtemplate, AtomicPulseTemplate):
continue
elif isinstance(subtemplate, MappingPulseTemplate):
if isinstance(subtemplate.template, AtomicPulseTemplate):
continue
else:
raise TypeError('Non atomic subtemplate of MappingPulseTemplate: {}'.format(subtemplate.template))
else:
raise TypeError('Non atomic subtemplate: {}'.format(subtemplate))
if duration is None:
self._duration = None
else:
self._duration = ExpressionScalar(duration)

if pad_values is None:
self._pad_values = FrozenDict()
else:
self._pad_values = FrozenDict((ch, None if value is None else ExpressionScalar(value))
for ch, value in pad_values.items())

if not self._subtemplates:
raise ValueError('Cannot create empty MultiChannelPulseTemplate')

if self._pad_values.keys() - self.defined_channels:
raise ValueError('Padding value for channels not defined in subtemplates',
self._pad_values.keys() - self.defined_channels)

# factored out for easier readability
# important that asserts happen before register
self._assert_atomic_sub_templates()
self._assert_disjoint_channels()
self._assert_compatible_durations()

self._register(registry=registry)

def _assert_atomic_sub_templates(self):
for sub_template in self._subtemplates:
template = sub_template
while isinstance(template, MappingPulseTemplate):
template = template.template

if not isinstance(template, AtomicPulseTemplate):
if template is sub_template:
raise TypeError('Non atomic subtemplate: {}'.format(template))
else:
raise TypeError('Non atomic subtemplate of MappingPulseTemplate: {}'.format(template))

def _assert_disjoint_channels(self):
defined_channels = [st.defined_channels for st in self._subtemplates]

# check there are no intersections between channels
Expand All @@ -82,44 +123,40 @@ def __init__(self,
'subtemplate {}'.format(i + 2 + j),
(channels_i & channels_j).pop())

if external_parameters is not None:
warnings.warn("external_parameters is an obsolete argument and will be removed in the future.",
category=DeprecationWarning)

if not duration:
duration = self._subtemplates[0].duration
for subtemplate in self._subtemplates[1:]:
if almost_equal(duration.sympified_expression, subtemplate.duration.sympified_expression):
continue
else:
raise ValueError('Could not assert duration equality of {} and {}'.format(duration,
subtemplate.duration))
self._duration = None
elif duration is True:
self._duration = None
else:
self._duration = ExpressionScalar(duration)

self._register(registry=registry)
def _assert_compatible_durations(self):
"""Check if we can prove that durations of unpadded waveforms are incompatible."""
unpadded_durations = [sub_template.duration
for sub_template in self._subtemplates
if sub_template.defined_channels - self._pad_values.keys()]
are_compatible = are_durations_compatible(self.duration, *unpadded_durations)
if are_compatible is False:
# durations definitely not compatible
raise ValueError('Durations are definitely not compatible: {}'.format(unpadded_durations),
unpadded_durations)

@property
def duration(self) -> ExpressionScalar:
if self._duration:
return self._duration
else:
return self._subtemplates[0].duration
return ExpressionScalar(sympy.Max(*(subtemplate.duration for subtemplate in self._subtemplates)))

@property
def parameter_names(self) -> Set[str]:
return set.union(self.measurement_parameters,
self.constrained_parameters,
*(st.parameter_names for st in self._subtemplates),
self._duration.variables if self._duration else ())
self._duration.variables if self._duration else (),
*(value.variables for value in self._pad_values.values() if value is not None))

@property
def subtemplates(self) -> Sequence[Union[AtomicPulseTemplate, MappingPulseTemplate]]:
return self._subtemplates

@property
def pad_values(self) -> Mapping[ChannelID, Optional[Expression]]:
return self._pad_values

@property
def defined_channels(self) -> Set[ChannelID]:
return set.union(*(st.defined_channels for st in self._subtemplates))
Expand All @@ -139,21 +176,29 @@ def build_waveform(self, parameters: Dict[str, numbers.Real],
if sub_waveform is not None:
sub_waveforms.append(sub_waveform)

pad_values = {}
for ch, pad_expression in self._pad_values.items():
ch = channel_mapping[ch]
if ch is None:
continue
elif pad_expression is None:
pad_values[ch] = None
else:
pad_values[ch] = pad_expression.evaluate_in_scope(parameters)

if len(sub_waveforms) == 0:
return None

if len(sub_waveforms) == 1:
waveform = sub_waveforms[0]
if self._duration is None:
duration = None
else:
waveform = MultiChannelWaveform(sub_waveforms)

if self._duration:
expected_duration = self._duration.evaluate_numeric(**parameters)
duration = TimeType.from_float(self._duration.evaluate_numeric(**parameters))

if not isclose(expected_duration, waveform.duration):
raise ValueError('The duration does not '
'equal the expected duration',
expected_duration, waveform.duration)
if len(sub_waveforms) == 1 and (duration in (None, sub_waveforms[0].duration)):
# No padding
waveform = sub_waveforms[0]
else:
waveform = MultiChannelWaveform.from_iterable(sub_waveforms, pad_values, duration=duration)

return waveform

Expand All @@ -179,6 +224,10 @@ def get_serialization_data(self, serializer: Optional[Serializer]=None) -> Dict[
data['parameter_constraints'] = [str(constraint) for constraint in self.parameter_constraints]
if self.measurement_declarations:
data['measurements'] = self.measurement_declarations
if self._pad_values:
data['pad_values'] = self._pad_values
if self._duration is not None:
data['duration'] = self._duration

return data

Expand All @@ -194,10 +243,41 @@ def deserialize(cls, serializer: Optional[Serializer]=None, **kwargs) -> 'Atomic

@property
def integral(self) -> Dict[ChannelID, ExpressionScalar]:
expressions = dict()
for subtemplate in self._subtemplates:
expressions.update(subtemplate.integral)
return expressions
t = self._AS_EXPRESSION_TIME
self_duration = self.duration.underlying_expression
as_expression = self._as_expression()
integral = {}
for sub_template in self._subtemplates:
if sub_template.duration == self.duration:
# we use this shortcut if there is no truncation/padding to get nicer expressions
integral.update(sub_template.integral)
else:
for ch in sub_template.defined_channels:
expr = as_expression[ch]
ch_integral = sympy.integrate(expr.underlying_expression, (t, 0, self_duration))
integral[ch] = ExpressionScalar(ch_integral)
return integral

def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
t = self._AS_EXPRESSION_TIME
as_expression = {}
for sub_template in self.subtemplates:
sub_duration = sub_template.duration.sympified_expression
sub_as_expression = sub_template._as_expression()

if sub_duration == self.duration:
# we use this shortcut if there is no truncation/padding to get nicer expressions
as_expression.update(sub_as_expression)
else:
padding = t > sub_duration

for ch, ch_expr in sub_as_expression.items():
pad_value = self._pad_values.get(ch, None)
if pad_value is None:
pad_value = ch_expr.underlying_expression.subs({t: sub_duration})
as_expression[ch] = ExpressionScalar(sympy.Piecewise((pad_value, padding),
(ch_expr.underlying_expression, True)))
return as_expression


class ParallelConstantChannelPulseTemplate(PulseTemplate):
Expand Down
Loading