From 233662fbcea8cf12789c426e8dc7707dd94dde44 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 14 Feb 2025 13:54:48 +0100 Subject: [PATCH 1/5] Add to_single_waveform property to PulseTemplate --- qupulse/pulses/pulse_template.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 68d4adac9..f3578d8b6 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -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 @@ -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. @@ -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]: @@ -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 @@ -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): From 724c53e64e2f251b4dc8edf9aa91039904632a86 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 14 Feb 2025 14:32:41 +0100 Subject: [PATCH 2/5] Add to to_single_waveform to the most important non-atomic pulse templates --- qupulse/pulses/loop_pulse_template.py | 10 ++++++---- qupulse/pulses/pulse_template.py | 2 +- qupulse/pulses/repetition_pulse_template.py | 5 +++-- qupulse/pulses/sequence_pulse_template.py | 5 +++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index cd7aefaaa..39f7b9d47 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -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 @@ -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 @@ -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: @@ -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) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index f3578d8b6..0ff0850dc 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -35,7 +35,7 @@ from qupulse.program import ProgramBuilder, default_program_builder, Program __all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple", - "UnknownVolatileParameter"] + "UnknownVolatileParameter", "SingleWaveformStrategy"] MappingTuple = Union[Tuple['PulseTemplate'], diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py index a88485ed0..a0979ed5e 100644 --- a/qupulse/pulses/repetition_pulse_template.py +++ b/qupulse/pulses/repetition_pulse_template.py @@ -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 @@ -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. @@ -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) diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py index 19e80ed04..6cefb7737 100644 --- a/qupulse/pulses/sequence_pulse_template.py +++ b/qupulse/pulses/sequence_pulse_template.py @@ -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 @@ -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. @@ -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) From 65c3d9c0c0d30d72f4b81323349c6333fb2803e2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 18 Feb 2025 15:38:54 +0100 Subject: [PATCH 3/5] Add MappingPT to single waveform kwarg --- 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 af5119b66..6a9fecbda 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -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 @@ -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. @@ -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: From 6f5bb7e9f5a8c9729339a3ea8839fed54f3298fb Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 18 Feb 2025 15:39:18 +0100 Subject: [PATCH 4/5] Add tests for to_single_waveform kwarg --- tests/pulses/loop_pulse_template_tests.py | 10 ++++++++++ tests/pulses/mapping_pulse_template_tests.py | 9 +++++++++ tests/pulses/repetition_pulse_template_tests.py | 10 ++++++++++ tests/pulses/sequence_pulse_template_tests.py | 9 +++++++++ 4 files changed, 38 insertions(+) diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index c6a658bf8..9eb4ee373 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -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 @@ -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): diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index c740db0ef..ffca6703e 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -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): diff --git a/tests/pulses/repetition_pulse_template_tests.py b/tests/pulses/repetition_pulse_template_tests.py index ff399682d..a1af2a665 100644 --- a/tests/pulses/repetition_pulse_template_tests.py +++ b/tests/pulses/repetition_pulse_template_tests.py @@ -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 @@ -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): diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py index ff14a0ed5..0435b00f7 100644 --- a/tests/pulses/sequence_pulse_template_tests.py +++ b/tests/pulses/sequence_pulse_template_tests.py @@ -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) From 3fea47d9f108396270a657b68ed45aa28f5b2a3c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 18 Feb 2025 15:42:40 +0100 Subject: [PATCH 5/5] Add newspiece --- changes.d/578.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes.d/578.feature diff --git a/changes.d/578.feature b/changes.d/578.feature new file mode 100644 index 000000000..381f32031 --- /dev/null +++ b/changes.d/578.feature @@ -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.