Skip to content
11 changes: 9 additions & 2 deletions qupulse/_program/waveforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ def get_sampled(self,

if np.any(sample_times[:-1] >= sample_times[1:]):
raise ValueError('The sample times are not monotonously increasing')
if sample_times[0] < 0 or sample_times[-1] > self.duration:
raise ValueError('The sample times are not in the range [0, duration]')
if sample_times[0] < 0 or sample_times[-1] > float(self.duration):
raise ValueError(f'The sample times [{sample_times[0]}, ..., {sample_times[-1]}] are not in the range'
f' [0, duration={float(self.duration)}]')
if channel not in self.defined_channels:
raise KeyError('Channel not defined in this waveform: {}'.format(channel))

Expand Down Expand Up @@ -145,6 +146,9 @@ def __init__(self, t: float, v: float, interp: InterpolationStrategy):
if not callable(interp):
raise TypeError('{} is neither callable nor of type InterpolationStrategy'.format(interp))

def __repr__(self):
return f'{type(self).__name__}(t={self.t}, v={self.v}, interp="{self.interp}")'


class TableWaveform(Waveform):
EntryInInit = Union[TableWaveformEntry, Tuple[float, float, InterpolationStrategy]]
Expand Down Expand Up @@ -234,6 +238,9 @@ def defined_channels(self) -> Set[ChannelID]:
def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
return self

def __repr__(self):
return f'{type(self).__name__}(channel={self._channel_id}, waveform_table={self._table})'


class FunctionWaveform(Waveform):
"""Waveform obtained from instantiating a FunctionPulseTemplate."""
Expand Down
10 changes: 10 additions & 0 deletions qupulse/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,16 @@ 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:
"""Overwrite super class method because we do not want to return a scalar numpy.ndarray"""
parsed = super()._parse_evaluate_numeric_result(result, call_arguments)
if isinstance(parsed, numpy.ndarray):
return parsed[()]
else:
return parsed

def evaluate_with_exact_rationals(self, scope: Mapping) -> Number:
parsed_kwargs = self._parse_evaluate_numeric_arguments(scope)
result, self._exact_rational_lambdified = evaluate_lamdified_exact_rational(self.sympified_expression,
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
6 changes: 6 additions & 0 deletions qupulse/pulses/multi_channel_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]:
expressions.update(subtemplate.integral)
return expressions

def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
expressions = dict()
for subtemplate in self._subtemplates:
expressions.update(subtemplate._as_expression())
return expressions


class ParallelConstantChannelPulseTemplate(PulseTemplate):
def __init__(self,
Expand Down
56 changes: 37 additions & 19 deletions qupulse/pulses/point_pulse_template.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
from typing import Optional, List, Union, Set, Dict, Sequence, Any
from typing import Optional, List, Union, Set, Dict, Sequence, Any, Tuple
from numbers import Real
import itertools
import numbers

import sympy
import numpy as np

from qupulse.utils.sympy import Broadcast
from qupulse.utils.sympy import IndexedBroadcast
from qupulse.utils.types import ChannelID
from qupulse.expressions import Expression, ExpressionScalar
from qupulse._program.waveforms import TableWaveform, TableWaveformEntry
from qupulse.pulses.parameters import Parameter, ParameterNotProvidedException, ParameterConstraint,\
ParameterConstrainer
from qupulse.pulses.parameters import ParameterConstraint, ParameterConstrainer
from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration
from qupulse.pulses.table_pulse_template import TableEntry, EntryInInit
from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform
Expand Down Expand Up @@ -64,7 +63,8 @@ def defined_channels(self) -> Set[ChannelID]:

def build_waveform(self,
parameters: Dict[str, Real],
channel_mapping: Dict[ChannelID, Optional[ChannelID]]) -> Optional[TableWaveform]:
channel_mapping: Dict[ChannelID, Optional[ChannelID]]) -> Optional[Union[TableWaveform,
MultiChannelWaveform]]:
self.validate_parameter_constraints(parameters=parameters, volatile=set())

if all(channel_mapping[channel] is None
Expand Down Expand Up @@ -136,21 +136,39 @@ def parameter_names(self) -> Set[str]:

@property
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': first_entry.t.sympified_expression,
't1': second_entry.t.sympified_expression}

v0 = sympy.IndexedBase(Broadcast(first_entry.v.underlying_expression, (len(self.defined_channels),)))
v1 = sympy.IndexedBase(Broadcast(second_entry.v.underlying_expression, (len(self.defined_channels),)))

for i, channel in enumerate(self._channels):
substitutions['v0'] = v0[i]
substitutions['v1'] = v1[i]

expressions[channel] += first_entry.interp.integral.sympified_expression.subs(substitutions)
expressions = {}
shape = (len(self.defined_channels),)

for i, channel in enumerate(self._channels):
def value_trafo(v):
try:
return v.underlying_expression[i]
except TypeError:
return IndexedBroadcast(v.underlying_expression, shape, i)
pre_entry = TableEntry(0, self._entries[0].v, None)
entries = [pre_entry] + self._entries
expressions[channel] = TableEntry._sequence_integral(entries, expression_extractor=value_trafo)
return expressions

expressions = {c: ExpressionScalar(expressions[c]) for c in expressions}
def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
t = self._AS_EXPRESSION_TIME
shape = (len(self.defined_channels),)
expressions = {}

for i, channel in enumerate(self._channels):
def value_trafo(v):
try:
return v.underlying_expression[i]
except TypeError:
return IndexedBroadcast(v.underlying_expression, shape, i)
pre_value = value_trafo(self._entries[0].v)
post_value = value_trafo(self._entries[-1].v)
pw = TableEntry._sequence_as_expression(self._entries,
expression_extractor=value_trafo,
t=t,
post_value=post_value,
pre_value=pre_value)
expressions[channel] = pw
return expressions


Expand Down
9 changes: 9 additions & 0 deletions qupulse/pulses/pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import collections
from numbers import Real, Number

import sympy

from qupulse.utils.types import ChannelID, DocStringABCMeta, FrozenDict
from qupulse.serialization import Serializable
from qupulse.expressions import ExpressionScalar, Expression, ExpressionLike
Expand Down Expand Up @@ -290,6 +292,8 @@ class AtomicPulseTemplate(PulseTemplate, MeasurementDefiner):

Implies that no AtomicPulseTemplate object is interruptable.
"""
_AS_EXPRESSION_TIME = sympy.Dummy('_t', positive=True)

def __init__(self, *,
identifier: Optional[str],
measurements: Optional[List[MeasurementDeclaration]]):
Expand Down Expand Up @@ -345,6 +349,11 @@ def build_waveform(self,
does not represent a valid waveform of finite length.
"""

@abstractmethod
def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]:
"""Helper function to allow integral calculation in case of truncation. AtomicPulseTemplate._AS_EXPRESSION_TIME
is by convention the time variable."""


class DoubleParameterNameException(Exception):

Expand Down
Loading