Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions changes.d/578.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a ``to_single_waveform`` keyword argument to ``SequencePT``, ``RepetitionPT``, ``ForLoopPT`` and ``MappingPT``.
When ``to_single_waveform='always'`` is passed the corresponding pulse template is translated into a single waveform on program creation.
10 changes: 6 additions & 4 deletions qupulse/pulses/loop_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from qupulse.expressions import ExpressionScalar, ExpressionVariableMissingException, Expression
from qupulse.utils import checked_int_cast, cached_property
from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstrainer, ParameterNotProvidedException
from qupulse.pulses.pulse_template import PulseTemplate, ChannelID, AtomicPulseTemplate
from qupulse.pulses.pulse_template import PulseTemplate, ChannelID, AtomicPulseTemplate, SingleWaveformStrategy
from qupulse.program.waveforms import SequenceWaveform as ForLoopWaveform
from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration
from qupulse.pulses.range import ParametrizedRange, RangeScope
Expand All @@ -34,8 +34,9 @@
class LoopPulseTemplate(PulseTemplate):
"""Base class for loop based pulse templates. This class is still abstract and cannot be instantiated."""
def __init__(self, body: PulseTemplate,
identifier: Optional[str]):
super().__init__(identifier=identifier)
identifier: Optional[str],
to_single_waveform: Optional[SingleWaveformStrategy] = None):
super().__init__(identifier=identifier, to_single_waveform=to_single_waveform)
self.__body = body

@property
Expand Down Expand Up @@ -68,6 +69,7 @@ def __init__(self,
*,
measurements: Optional[Sequence[MeasurementDeclaration]]=None,
parameter_constraints: Optional[Sequence]=None,
to_single_waveform: Optional[SingleWaveformStrategy] = None,
registry: PulseRegistryType=None) -> None:
"""
Args:
Expand All @@ -76,7 +78,7 @@ def __init__(self,
loop_range: Range to loop through
identifier: Used for serialization
"""
LoopPulseTemplate.__init__(self, body=body, identifier=identifier)
LoopPulseTemplate.__init__(self, body=body, identifier=identifier, to_single_waveform=to_single_waveform)
MeasurementDefiner.__init__(self, measurements=measurements)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)

Expand Down
5 changes: 3 additions & 2 deletions qupulse/pulses/mapping_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from qupulse.utils.types import ChannelID, FrozenDict, FrozenMapping
from qupulse.expressions import Expression, ExpressionScalar
from qupulse.parameter_scope import Scope, MappedScope
from qupulse.pulses.pulse_template import PulseTemplate, MappingTuple
from qupulse.pulses.pulse_template import PulseTemplate, MappingTuple, SingleWaveformStrategy
from qupulse.pulses.parameters import ParameterNotProvidedException, ParameterConstrainer
from qupulse.program.waveforms import Waveform
from qupulse.program import ProgramBuilder
Expand Down Expand Up @@ -38,6 +38,7 @@ def __init__(self, template: PulseTemplate, *,
channel_mapping: Optional[Dict[ChannelID, ChannelID]] = None,
parameter_constraints: Optional[List[str]]=None,
allow_partial_parameter_mapping: bool = None,
to_single_waveform: Optional[SingleWaveformStrategy]=None,
registry: PulseRegistryType=None) -> None:
"""Standard constructor for the MappingPulseTemplate.

Expand All @@ -56,7 +57,7 @@ def __init__(self, template: PulseTemplate, *,
:param parameter_constraints:
:param allow_partial_parameter_mapping: If None the value of the class variable ALLOW_PARTIAL_PARAMETER_MAPPING
"""
PulseTemplate.__init__(self, identifier=identifier)
PulseTemplate.__init__(self, identifier=identifier, to_single_waveform=to_single_waveform)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)

if allow_partial_parameter_mapping is None:
Expand Down
33 changes: 28 additions & 5 deletions qupulse/pulses/pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
directly translated into a waveform.
"""
import warnings
import typing
from abc import abstractmethod
from typing import Dict, Tuple, Set, Optional, Union, List, Callable, Any, Generic, TypeVar, Mapping
from typing import Dict, Tuple, Set, Optional, Union, List, Callable, Any, Generic, TypeVar, Mapping, Literal
import itertools
import collections
from numbers import Real, Number
Expand All @@ -34,7 +35,7 @@
from qupulse.program import ProgramBuilder, default_program_builder, Program

__all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple",
"UnknownVolatileParameter"]
"UnknownVolatileParameter", "SingleWaveformStrategy"]


MappingTuple = Union[Tuple['PulseTemplate'],
Expand All @@ -43,6 +44,9 @@
Tuple['PulseTemplate', Dict, Dict, Dict]]


SingleWaveformStrategy = Literal['always']


class PulseTemplate(Serializable):
"""A PulseTemplate represents the parametrized general structure of a pulse.

Expand All @@ -60,10 +64,20 @@ class PulseTemplate(Serializable):
_CAST_INT_TO_INT64 = True

def __init__(self, *,
identifier: Optional[str]) -> None:
identifier: Optional[str],
to_single_waveform: Optional[SingleWaveformStrategy] = None) -> None:
super().__init__(identifier=identifier)
if to_single_waveform is not None and to_single_waveform not in typing.get_args(SingleWaveformStrategy):
warnings.warn(f"Unknown to_single_waveform parameter: {to_single_waveform!r}")
self._to_single_waveform = to_single_waveform
self.__cached_hash_value = None

def get_serialization_data(self, serializer: Optional['Serializer'] = None) -> Dict[str, Any]:
data = super().get_serialization_data(serializer=serializer)
if self._to_single_waveform:
data['to_single_waveform'] = self._to_single_waveform
return data

@property
@abstractmethod
def parameter_names(self) -> Set[str]:
Expand Down Expand Up @@ -91,7 +105,16 @@ def num_channels(self) -> int:

def _is_atomic(self) -> bool:
"""This is (currently a private) a check if this pulse template always is translated into a single waveform."""
return False
return self._to_single_waveform == 'always'

@property
def to_single_waveform(self) -> Optional[SingleWaveformStrategy]:
"""This property describes whether this pulse template is translated into a single waveform.

'always': It is always translated into a single waveform.
None: It depends on the `create_program` arguments and the pulse template itself.
"""
return self._to_single_waveform

def __matmul__(self, other: Union['PulseTemplate', MappingTuple]) -> 'SequencePulseTemplate':
"""This method enables using the @-operator (intended for matrix multiplication) for
Expand Down Expand Up @@ -244,7 +267,7 @@ def _create_program(self, *,
program_builder: ProgramBuilder):
"""Generic part of create program. This method handles to_single_waveform and the configuration of the
transformer."""
if self.identifier in to_single_waveform or self in to_single_waveform:
if self._to_single_waveform == 'always' or self.identifier in to_single_waveform or self in to_single_waveform:
with program_builder.new_subprogram(global_transformation=global_transformation) as inner_program_builder:

if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names):
Expand Down
5 changes: 3 additions & 2 deletions qupulse/pulses/repetition_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from qupulse.utils.types import ChannelID
from qupulse.expressions import ExpressionScalar
from qupulse.utils import checked_int_cast
from qupulse.pulses.pulse_template import PulseTemplate
from qupulse.pulses.pulse_template import PulseTemplate, SingleWaveformStrategy
from qupulse.pulses.loop_pulse_template import LoopPulseTemplate
from qupulse.pulses.parameters import ParameterConstrainer
from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration
Expand All @@ -44,6 +44,7 @@ def __init__(self,
*args,
parameter_constraints: Optional[List]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
to_single_waveform: Optional[SingleWaveformStrategy] = None,
registry: PulseRegistryType=None
) -> None:
"""Create a new RepetitionPulseTemplate instance.
Expand All @@ -59,7 +60,7 @@ def __init__(self,
elif args:
TypeError('RepetitionPulseTemplate expects 3 positional arguments, got ' + str(3 + len(args)))

LoopPulseTemplate.__init__(self, identifier=identifier, body=body)
LoopPulseTemplate.__init__(self, identifier=identifier, body=body, to_single_waveform=to_single_waveform)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
MeasurementDefiner.__init__(self, measurements=measurements)

Expand Down
5 changes: 3 additions & 2 deletions qupulse/pulses/sequence_pulse_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from qupulse.parameter_scope import Scope
from qupulse.utils import cached_property
from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType
from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate
from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate, SingleWaveformStrategy
from qupulse.pulses.parameters import ConstraintLike, ParameterConstrainer
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple
from qupulse.program.waveforms import SequenceWaveform
Expand Down Expand Up @@ -44,6 +44,7 @@ def __init__(self,
identifier: Optional[str]=None,
parameter_constraints: Optional[Iterable[ConstraintLike]]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
to_single_waveform: Optional[SingleWaveformStrategy]=None,
registry: PulseRegistryType=None) -> None:
"""Create a new SequencePulseTemplate instance.

Expand All @@ -64,7 +65,7 @@ def __init__(self,
SequencePulseTemplate as tuples of the form (PulseTemplate, Dict(str -> str)).
identifier (str): A unique identifier for use in serialization. (optional)
"""
PulseTemplate.__init__(self, identifier=identifier)
PulseTemplate.__init__(self, identifier=identifier, to_single_waveform=to_single_waveform)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
MeasurementDefiner.__init__(self, measurements=measurements)

Expand Down
10 changes: 10 additions & 0 deletions tests/pulses/loop_pulse_template_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ParameterNotProvidedException, ParameterConstraint

from qupulse.program.loop import LoopBuilder, Loop
from qupulse.program.waveforms import SequenceWaveform

from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform
from tests.serialization_dummies import DummySerializer
Expand Down Expand Up @@ -426,6 +427,15 @@ def test_create_program_append(self) -> None:
# not ensure same result as from Sequencer here - we're testing appending to an already existing parent loop
# which is a use case that does not immediately arise from using Sequencer

def test_single_waveform(self):
inner_wf = DummyWaveform()
inner_pt = DummyPulseTemplate(waveform=inner_wf, parameter_names={'idx'})

flpt = ForLoopPulseTemplate(inner_pt, loop_index='idx', loop_range=3, to_single_waveform='always')
program = flpt.create_program()
expected = Loop(children=[Loop(repetition_count=1, waveform=SequenceWaveform.from_sequence([inner_wf] * 3))])
self.assertEqual(expected, program)


class ForLoopPulseTemplateSerializationTests(SerializableTests, unittest.TestCase):

Expand Down
9 changes: 9 additions & 0 deletions tests/pulses/mapping_pulse_template_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,15 @@ def test_same_channel_error(self):
with self.assertRaisesRegex(ValueError, 'multiple channels to the same target'):
MappingPulseTemplate(dpt, channel_mapping={'A': 'X', 'B': 'X'})

def test_single_waveform(self):
inner_wf = DummyWaveform()
inner_pt = DummyPulseTemplate(waveform=inner_wf)

mpt = MappingPulseTemplate(inner_pt, to_single_waveform='always')
program = mpt.create_program()
expected = Loop(children=[Loop(repetition_count=1, waveform=inner_wf)])
self.assertEqual(expected, program)


class PulseTemplateParameterMappingExceptionsTests(unittest.TestCase):

Expand Down
10 changes: 10 additions & 0 deletions tests/pulses/repetition_pulse_template_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest import mock

from qupulse.parameter_scope import Scope, DictScope
from qupulse.program.waveforms import RepetitionWaveform
from qupulse.utils.types import FrozenDict

from qupulse.program import default_program_builder
Expand Down Expand Up @@ -570,6 +571,15 @@ def test_create_program_none_subprogram_with_measurement(self) -> None:
program_builder=program_builder)
self.assertIsNone(program_builder.to_program())

def test_single_waveform(self):
inner_wf = DummyWaveform()
inner_pt = DummyPulseTemplate(waveform=inner_wf)

rpt = RepetitionPulseTemplate(inner_pt, repetition_count=42, to_single_waveform='always')
program = rpt.create_program()
expected = Loop(children=[Loop(repetition_count=1, waveform=RepetitionWaveform.from_repetition_count(inner_wf, 42))])
self.assertEqual(expected, program)


class RepetitionPulseTemplateSerializationTests(SerializableTests, unittest.TestCase):

Expand Down
9 changes: 9 additions & 0 deletions tests/pulses/sequence_pulse_template_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ def test_build_waveform(self):
for wfa, wfb in zip(wf.sequenced_waveforms, wfs):
self.assertIs(wfa, wfb)

def test_single_waveform(self):
wfs = [DummyWaveform(), DummyWaveform()]
pts = [DummyPulseTemplate(waveform=wf) for wf in wfs]

spt = SequencePulseTemplate(*pts, to_single_waveform='always')
program = spt.create_program()
expected = Loop(children=[Loop(repetition_count=1, waveform=SequenceWaveform.from_sequence(wfs))])
self.assertEqual(expected, program)

def test_identifier(self) -> None:
identifier = 'some name'
pulse = SequencePulseTemplate(DummyPulseTemplate(), identifier=identifier)
Expand Down
Loading