From b07d801dbb54e4a4e24fc1952bab00a06474179e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 4 Oct 2023 10:26:44 +0200 Subject: [PATCH 1/4] Add PulseTemplate.pad_to --- qupulse/pulses/pulse_template.py | 37 ++++++++++++++++++++++++++++ tests/pulses/pulse_template_tests.py | 2 ++ 2 files changed, 39 insertions(+) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 5da9a2b5..a97b2f4b 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -383,6 +383,43 @@ def with_appended(self, *appended: 'PulseTemplate'): else: return self + def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], Expression]], + pt_kwargs: Mapping[str, Any] = None) -> 'PulseTemplate': + """ + Examples: + # pad to a fixed duration + padded_1 = my_pt.pad_to(1000) + + # pad to a fixed sample coun + padded_2 = my_pt.pad_to('sample_rate * 1000') + + # pad to the next muliple of 16 samples with a symbolic sample rate + padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) + + # pad to the next muliple of 16 samples with a fixed sample rate of 1 GHz + padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) + Args: + to_new_duration: Duration or callable that maps the current duration to the new duration + pt_kwargs: Keyword arguments for the newly created sequence pulse template. + + Returns: + + """ + from qupulse.pulses import ConstantPT, SequencePT + current_duration = self.duration + if callable(to_new_duration): + new_duration = to_new_duration(current_duration) + else: + new_duration = ExpressionScalar(to_new_duration) + pad_duration = new_duration - current_duration + if not pt_kwargs and pad_duration == 0: + return self + pad_pt = ConstantPT(pad_duration, self.final_values) + if pt_kwargs: + return SequencePT(self, pad_pt, **pt_kwargs) + else: + return self @ pad_pt + def __format__(self, format_spec: str): if format_spec == '': format_spec = self._DEFAULT_FORMAT_SPEC diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 8d4a5871..145f726e 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -343,6 +343,8 @@ def test_create_program_volatile(self): _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + def test_pad_to(self): + raise NotImplementedError("TODO") def test_create_program_none(self) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) From d1477e4c538130c4e1067236140b5bce4183eef1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:09:32 +0200 Subject: [PATCH 2/4] Add pad_to tests --- tests/pulses/pulse_template_tests.py | 79 ++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 145f726e..876c47cf 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -3,6 +3,8 @@ from unittest import mock from typing import Optional, Dict, Set, Any, Union + +import frozendict import sympy from qupulse.parameter_scope import Scope, DictScope @@ -23,12 +25,14 @@ class PulseTemplateStub(PulseTemplate): """All abstract methods are stubs that raise NotImplementedError to catch unexpected calls. If a method is needed in - a test one should use mock.patch or mock.patch.object""" + a test one should use mock.patch or mock.patch.object. + Properties can be passed as init argument because mocking them is a pita.""" def __init__(self, identifier=None, defined_channels=None, duration=None, parameter_names=None, measurement_names=None, + final_values=None, registry=None): super().__init__(identifier=identifier) @@ -36,6 +40,7 @@ def __init__(self, identifier=None, self._duration = duration self._parameter_names = parameter_names self._measurement_names = set() if measurement_names is None else measurement_names + self._final_values = final_values self.internal_create_program_args = [] self._register(registry=registry) @@ -89,7 +94,10 @@ def initial_values(self) -> Dict[ChannelID, ExpressionScalar]: @property def final_values(self) -> Dict[ChannelID, ExpressionScalar]: - raise NotImplementedError() + if self._final_values is None: + raise NotImplementedError() + else: + return self._final_values def get_appending_internal_create_program(waveform=DummyWaveform(), @@ -344,7 +352,72 @@ def test_create_program_volatile(self): _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) def test_pad_to(self): - raise NotImplementedError("TODO") + from qupulse.pulses import SequencePT + + def to_multiple_of_192(x: Expression) -> Expression: + return (x + 191) // 192 * 192 + + final_values = frozendict.frozendict({'A': ExpressionScalar(0.1), 'B': ExpressionScalar('a')}) + measurements = [('M', 0, 'y')] + + pt = PulseTemplateStub(duration=ExpressionScalar(10)) + padded = pt.pad_to(10) + self.assertIs(pt, padded) + + pt = PulseTemplateStub(duration=ExpressionScalar('duration')) + padded = pt.pad_to('duration') + self.assertIs(pt, padded) + + # padding with numeric durations + + pt = PulseTemplateStub(duration=ExpressionScalar(10), + final_values=final_values, + defined_channels=final_values.keys()) + padded = pt.pad_to(20) + self.assertEqual(padded.duration, 20) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + padded = pt.pad_to(20, pt_kwargs=dict(measurements=measurements)) + self.assertEqual(padded.duration, 20) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + self.assertEqual(measurements, padded.measurement_declarations) + + padded = pt.pad_to(10, pt_kwargs=dict(measurements=measurements)) + self.assertEqual(padded.duration, 10) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + self.assertEqual(measurements, padded.measurement_declarations) + + # padding with numeric duation and callable + padded = pt.pad_to(to_multiple_of_192) + self.assertEqual(padded.duration, 192) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + # padding with symbolic durations + + pt = PulseTemplateStub(duration=ExpressionScalar('duration'), + final_values=final_values, + defined_channels=final_values.keys()) + padded = pt.pad_to('new_duration') + self.assertEqual(padded.duration, 'new_duration') + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + # padding symbolic durations with callable + + padded = pt.pad_to(to_multiple_of_192) + self.assertEqual(padded.duration, '(duration + 191) // 192 * 192') + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) def test_create_program_none(self) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) From 72d6af8532e736bb75d5328fb30fc62a4383aec2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:53:25 +0200 Subject: [PATCH 3/4] Better docstring --- qupulse/pulses/pulse_template.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index a97b2f4b..91f0f39a 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -383,27 +383,31 @@ def with_appended(self, *appended: 'PulseTemplate'): else: return self - def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], Expression]], + def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], ExpressionLike]], pt_kwargs: Mapping[str, Any] = None) -> 'PulseTemplate': - """ + """Pad this pulse template to the given duration. + The target duration can be numeric, symbolic or a callable that returns a new duration from the current + duration. + Examples: # pad to a fixed duration - padded_1 = my_pt.pad_to(1000) + >>> padded_1 = my_pt.pad_to(1000) # pad to a fixed sample coun - padded_2 = my_pt.pad_to('sample_rate * 1000') + >>> padded_2 = my_pt.pad_to('sample_rate * 1000') # pad to the next muliple of 16 samples with a symbolic sample rate - padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) + >>> padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) # pad to the next muliple of 16 samples with a fixed sample rate of 1 GHz - padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) + >>> padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) Args: to_new_duration: Duration or callable that maps the current duration to the new duration pt_kwargs: Keyword arguments for the newly created sequence pulse template. Returns: - + A pulse template that has the duration given by ``to_new_duration``. It can be ``self`` if the duration is + already as required. It is never ``self`` if ``pt_kwargs`` is non-empty. """ from qupulse.pulses import ConstantPT, SequencePT current_duration = self.duration From f06cd64c77ddce223e63c027777ef5928bfbae78 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:56:41 +0200 Subject: [PATCH 4/4] Newspiece --- changes.d/801.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes.d/801.feature diff --git a/changes.d/801.feature b/changes.d/801.feature new file mode 100644 index 00000000..fa703198 --- /dev/null +++ b/changes.d/801.feature @@ -0,0 +1,2 @@ +Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations. + \ No newline at end of file