From d393532109d2b9a7f1c6900775c81821bb34400b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 12:52:26 +0200 Subject: [PATCH 01/32] Add a simple implementation of the ELFManager that is used by default --- qupulse/hardware/awgs/zihdawg.py | 96 +++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 6d6972a9..9bcf1cf2 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -10,7 +10,7 @@ import hashlib import argparse import re -from abc import abstractmethod +from abc import abstractmethod, ABC try: # zhinst fires a DeprecationWarning from its own code in some versions... @@ -394,7 +394,7 @@ def _initialize_awg_module(self): self._awg_module.set('awgModule/device', self.master_device.serial) self._awg_module.set('awgModule/index', self.awg_group_index) self._awg_module.execute() - self._elf_manager = ELFManager(self._awg_module) + self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module) self._upload_generator = () @property @@ -501,6 +501,7 @@ def upload(self, name: str, def _start_compile_and_upload(self): self._uploaded_seqc_source = None self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source) + logger.debug(f"_start_compile_and_upload: %r", next(self._upload_generator, "Finished")) def _wait_for_compile_and_upload(self): for state in self._upload_generator: @@ -560,6 +561,8 @@ def arm(self, name: Optional[str]) -> None: if self._required_seqc_source != self._uploaded_seqc_source: self._wait_for_compile_and_upload() + assert self._required_seqc_source == self._uploaded_seqc_source, "_wait_for_compile_and_upload did not work " \ + "as expected." self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) @@ -581,10 +584,8 @@ def arm(self, name: Optional[str]) -> None: self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2)) - # this was a workaround for problems in the past and I totally forgot why it was here - # for ch_pair in self.master.channel_tuples: - # ch_pair._wait_for_compile_and_upload() - self.enable(True) + if name is not None: + self.enable(True) def run_current_program(self) -> None: """Run armed program.""" @@ -802,7 +803,9 @@ def offsets(self) -> Tuple[float, ...]: return tuple(map(self.master_device.offset, self._channels())) -class ELFManager: +class ELFManager(ABC): + DEFAULT_CLS = None + class AWGModule: def __init__(self, awg_module: zhinst_core.AwgModule): """Provide an easily mockable interface to the zhinst AwgModule object""" @@ -838,6 +841,14 @@ def compiler_source_file(self) -> str: def compiler_source_file(self, source_file: str): self._module.set('compiler/sourcefile', source_file) + @property + def compiler_source_string(self) -> str: + return self._module.getString('compiler/sourcestring') + + @compiler_source_string.setter + def compiler_source_string(self, source_string: str): + self._module.set('compiler/sourcestring', source_string) + @property def compiler_upload(self) -> bool: """auto upload after compiling""" @@ -912,6 +923,74 @@ def _source_hash(source_string: str) -> str: # use utf-16 because str is UTF16 on most relevant machines (Windows) return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest() + @abstractmethod + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + """The function returns a generator that yields the current state of the progress. The generator is empty iff + the upload is complete. An exception is raised if there is an error. + + To abort send 'abort' to the generator. (not implemented :P) + + Example: + >>> my_source = 'playWave("my_wave");' + >>> for state in elf_manager.compile_and_upload(my_source): + ... print('Current state:', state) + ... time.sleep(1) + + Args: + source_string: Source code to compile + + Returns: + Generator object that needs to be consumed + """ + + +class SimpleELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """This implementation does not attempt to do something clever like caching.""" + super().__init__(awg_module) + + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + self.awg_module.compiler_upload = True + self.awg_module.compiler_source_string = source_string + + while True: + status, msg = self.awg_module.compiler_status + if status == - 1: + yield 'compiling' + elif status == 0: + break + elif status == 1: + raise HDAWGCompilationException(msg) + elif status == 2: + logger.warning("Compiler warings: %s", msg) + break + else: + raise RuntimeError("Unexpected status", status, msg) + + while True: + status_int, progress = self.awg_module.elf_status + if progress == 1.0: + break + elif status_int == 1: + HDAWGUploadException(self.awg_module.compiler_status) + else: + yield 'uploading @ %d%%' % (100*progress) + + +ELFManager.DEFAULT_CLS = SimpleELFManager + + +class CachingELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON""" + super().__init__(awg_module) + + # automatically upload after successful compilation + self.awg_module.compiler_upload = True + + self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]] + self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]] + def _update_compile_job_status(self): """Store current compile status in self._compile_job.""" compiler_start = self.awg_module.compiler_start @@ -920,8 +999,7 @@ def _update_compile_job_status(self): elif isinstance(self._compile_job, str): if compiler_start: - # compilation is running - pass + logger.debug("Compiler is running.") else: compiler_status, status_string = self.awg_module.compiler_status From 4bd685512ed4628a9be3b57050729aefe7bd6eb6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:59:28 +0200 Subject: [PATCH 02/32] specify zhinst max version for python<3.9 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8c68a1ad..fbc526a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ docs = plotting = matplotlib tabor-instruments = tabor_control>=0.1.1 -zurich-instruments = zhinst +zurich-instruments = zhinst<=20.7.2701;python_version<'3.9' Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging From e2d838ef37d2f630010091953a55224db7786b73 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 16:32:50 +0200 Subject: [PATCH 03/32] Fix requirement marker --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fbc526a2..6ca9c3e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,8 @@ docs = plotting = matplotlib tabor-instruments = tabor_control>=0.1.1 -zurich-instruments = zhinst<=20.7.2701;python_version<'3.9' +zurich-instruments = + zhinst<=20.7.2701;python_version<'3.9' Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging From 0b311abaa99880d9e803127f10513452464b03f7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 16:38:49 +0200 Subject: [PATCH 04/32] Make Test use default class --- tests/hardware/zihdawg_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py index 9fe675ea..cc86ad15 100644 --- a/tests/hardware/zihdawg_tests.py +++ b/tests/hardware/zihdawg_tests.py @@ -199,7 +199,7 @@ def test_upload(self): @mock.patch('qupulse.hardware.awgs.zihdawg.ELFManager.AWGModule.compiler_upload', new_callable=mock.PropertyMock) class ELFManagerTests(unittest.TestCase): def test_init(self, compiler_upload): - manager = ELFManager(None) + manager = ELFManager.DEFAULT_CLS(None) compiler_upload.assert_called_once_with(True) self.assertIsNone(manager._compile_job) self.assertIsNone(manager._upload_job) From 58a3471085de95753aeaf215beb5f232c08f5eeb Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 23 Aug 2023 21:51:44 +0200 Subject: [PATCH 05/32] optimize unsafe_sample --- qupulse/program/waveforms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 9cdcbf4f..e08e410f 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -369,8 +369,8 @@ def unsafe_sample(self, entries = self._table for entry1, entry2 in pairwise(entries): - indices = slice(np.searchsorted(sample_times, entry1.t, 'left'), - np.searchsorted(sample_times, entry2.t, 'right')) + indices = slice(sample_times.searchsorted(entry1.t, 'left'), + sample_times.searchsorted(entry2.t, 'right')) output_array[indices] = \ entry2.interp((float(entry1.t), entry1.v), (float(entry2.t), entry2.v), @@ -626,7 +626,7 @@ def unsafe_sample(self, # indexing in numpy and their copy/reference behaviour end = time + subwaveform.duration - indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left')) + indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left')) subwaveform.unsafe_sample(channel=channel, sample_times=sample_times[indices]-np.float64(time), output_array=output_array[indices]) @@ -843,7 +843,7 @@ def unsafe_sample(self, time = 0 for _ in range(self._repetition_count): end = time + body_duration - indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left')) + indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left')) self._body.unsafe_sample(channel=channel, sample_times=sample_times[indices] - float(time), output_array=output_array[indices]) From 935dd30e95de44cabedb1b8529c13d9c19812030 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 12:12:51 +0200 Subject: [PATCH 06/32] Add __pow__ convenience repetition operation. --- qupulse/pulses/pulse_template.py | 4 ++++ tests/pulses/pulse_template_tests.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 57f756e2..f685bd08 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -96,6 +96,10 @@ def __rmatmul__(self, other: MappingTuple) -> 'SequencePulseTemplate': return SequencePulseTemplate.concatenate(other, self) + def __pow__(self, power: ExpressionLike): + """This is a convenience wrapper for :func:`.with_repetition`.""" + return self.with_repetition(power) + @property @abstractmethod def integral(self) -> Dict[ChannelID, ExpressionScalar]: diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index f8c0e241..8d4a5871 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -376,6 +376,11 @@ def test_matmul(self): self.assertEqual(a @ b, 'concat') mock_concatenate.assert_called_once_with(a, b) + def test_pow(self): + pt = PulseTemplateStub() + pow_pt = pt ** 5 + self.assertEqual(pow_pt, pt.with_repetition(5)) + def test_rmatmul(self): a = PulseTemplateStub() b = (1, 2, 3) From 1b0c991a286a32ff992a88dc7ec0e72f12d51f44 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 12:17:45 +0200 Subject: [PATCH 07/32] Add newspiece --- changes.d/692.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/692.feature diff --git a/changes.d/692.feature b/changes.d/692.feature new file mode 100644 index 00000000..7cbdb46c --- /dev/null +++ b/changes.d/692.feature @@ -0,0 +1 @@ +Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`. \ No newline at end of file From 8f101897fb0083867b9bbac8c86466d26a72d0ef Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 14:08:34 +0200 Subject: [PATCH 08/32] Give an exception with more context if deserialization fails --- qupulse/serialization.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qupulse/serialization.py b/qupulse/serialization.py index 3825057e..38974866 100644 --- a/qupulse/serialization.py +++ b/qupulse/serialization.py @@ -1024,7 +1024,11 @@ def filter_serializables(self, obj_dict) -> Any: if get_default_pulse_registry() is self.storage: registry = dict() - return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + try: + return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + except Exception as err: + raise ValueError(f"Unable to deserialize {type_identifier} from {obj_dict}", + type_identifier, obj_dict) from err return obj_dict From 6d1e3f93034f2d8acdad72fc45301f33ec079096 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 15:05:49 +0200 Subject: [PATCH 09/32] Do an exception cause check --- tests/serialization_tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/serialization_tests.py b/tests/serialization_tests.py index f230bd6d..1ebd93b8 100644 --- a/tests/serialization_tests.py +++ b/tests/serialization_tests.py @@ -1051,9 +1051,12 @@ def test_deserialize_storage_is_not_default_registry_id_occupied(self) -> None: del pulse_storage pulse_storage = PulseStorage(backend) - with self.assertRaisesRegex(RuntimeError, "Pulse with name already exists"): + with self.assertRaises(ValueError) as cm: pulse_storage['peter'] + # this is shitty + self.assertIsInstance(cm.exception.__cause__, RuntimeError) + def test_deserialize_twice_same_object_storage_is_default_registry(self) -> None: backend = DummyStorageBackend() From 6072f8aa659b1ab21d8daea216ff23fbf5c07c98 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 28 Aug 2023 21:50:06 +0200 Subject: [PATCH 10/32] Fix overflow due to pyint to int32 conversion --- qupulse/pulses/pulse_template.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 57f756e2..cdc3b42e 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -13,6 +13,7 @@ import collections from numbers import Real, Number +import numpy import sympy from qupulse.utils.types import ChannelID, DocStringABCMeta, FrozenDict @@ -50,6 +51,8 @@ class PulseTemplate(Serializable): """This is not stable""" _DEFAULT_FORMAT_SPEC = 'identifier' + _CAST_INT_TO_INT64 = True + def __init__(self, *, identifier: Optional[str]) -> None: super().__init__(identifier=identifier) @@ -166,8 +169,15 @@ def create_program(self, *, scope = parameters else: parameters = dict(parameters) + to_int = numpy.int64 if self._CAST_INT_TO_INT64 else lambda x: x for parameter_name, value in parameters.items(): - if not isinstance(value, Number): + if type(value) is int: + # numpy casts ints to int32 per default on windows + # this can easily lead to overflows when times of the order of seconds + # are represented with integers + parameters[parameter_name] = to_int(value) + + elif not isinstance(value, Number): parameters[parameter_name] = Expression(value).evaluate_numeric() scope = DictScope(values=FrozenDict(parameters), volatile=volatile) From 03deed3ab5a0eb60ea6566172af71903591c0e26 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:37:03 +0200 Subject: [PATCH 11/32] Skip seqc marker data test if prerequisites not installed --- tests/_program/seqc_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 75644fdf..16c5b8be 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -27,6 +27,10 @@ except ImportError: zhinst = None +try: + import numba +except ImportError: + numba = None def take(n, iterable): "Return first n items of the iterable as a list" @@ -63,6 +67,7 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) + @unittest.skipIf(zhinst is None and numba is None, "BinaryWaveform.from_sampled backend missing") def test_marker_data(self): channel_1_data = np.linspace(-0.3, 0.4, num=192) channel_2_data = np.linspace(-0.1, 0.1, num=192) From d65a99ce21287262ced2cd8ead0f29ae57c9510b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:44:30 +0200 Subject: [PATCH 12/32] Make test skip dependent on exception instead of hard coding dependencies --- tests/_program/seqc_tests.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 16c5b8be..8761d496 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -14,6 +14,7 @@ from qupulse.parameter_scope import DictScope from qupulse.program.loop import Loop +from qupulse.hardware.util import zhinst_voltage_to_uint16 from qupulse._program.waveforms import ConstantWaveform from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\ to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\ @@ -27,10 +28,15 @@ except ImportError: zhinst = None +# This block checks if zhinst_voltage_to_uint16 works. A failing implementation (due to missing dependencies) +# skips tests further down try: - import numba -except ImportError: - numba = None + zhinst_voltage_to_uint16(np.zeros(16), np.zeros(16), + (np.zeros(16), np.zeros(16), np.zeros(16), np.zeros(16))) +except AttributeError: + # prerequisites not installed + zhinst_voltage_to_uint16 = None + def take(n, iterable): "Return first n items of the iterable as a list" @@ -67,7 +73,7 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) - @unittest.skipIf(zhinst is None and numba is None, "BinaryWaveform.from_sampled backend missing") + @unittest.skipIf(zhinst_voltage_to_uint16 is None, "BinaryWaveform.from_sampled backend missing") def test_marker_data(self): channel_1_data = np.linspace(-0.3, 0.4, num=192) channel_2_data = np.linspace(-0.1, 0.1, num=192) From 56ed176c70b4b31a29c8e9e079c1cc4bb5fbcc74 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:45:52 +0200 Subject: [PATCH 13/32] Optimize imports --- tests/_program/seqc_tests.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 8761d496..12522ac5 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -1,26 +1,23 @@ -import unittest -from unittest import TestCase, mock -import time -from itertools import zip_longest, islice +import hashlib +import pathlib import sys import tempfile -import pathlib -import hashlib -import random +import time +import unittest +from itertools import zip_longest, islice +from unittest import TestCase, mock import numpy as np +from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope, \ + to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager, \ + UserRegister, WaveformFileSystem from qupulse.expressions import ExpressionScalar +from qupulse.hardware.util import zhinst_voltage_to_uint16 from qupulse.parameter_scope import DictScope - from qupulse.program.loop import Loop -from qupulse.hardware.util import zhinst_voltage_to_uint16 -from qupulse._program.waveforms import ConstantWaveform -from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\ - to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\ - UserRegister, WaveformFileSystem -from qupulse._program.volatile import VolatileRepetitionCount - +from qupulse.program.volatile import VolatileRepetitionCount +from qupulse.program.waveforms import ConstantWaveform from tests.pulses.sequencing_dummies import DummyWaveform try: From 67f77ba7bf8fc15fcb54f382bd9e88d7a65e4595 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 12:59:46 +0200 Subject: [PATCH 14/32] Replace ParsedProgram with a dataclass for better type checking --- qupulse/_program/tabor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index b92c76f1..bb0b4a98 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -1,3 +1,4 @@ +import dataclasses import sys from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\ Hashable @@ -645,12 +646,12 @@ def prepare_program_for_advanced_sequence_mode(program: Loop, min_seq_len: int, i += 1 -ParsedProgram = NamedTuple('ParsedProgram', [('advanced_sequencer_table', Sequence[TableEntry]), - ('sequencer_tables', Sequence[Sequence[ - Tuple[TableDescription, Optional[VolatileProperty]]]]), - ('waveforms', Tuple[Waveform, ...]), - ('volatile_parameter_positions', Dict[Union[int, Tuple[int, int]], - VolatileRepetitionCount])]) +@dataclasses.dataclass +class ParsedProgram: + advanced_sequencer_table: Sequence[TableEntry] + sequencer_tables: Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]] + waveforms: Tuple[Waveform, ...] + volatile_parameter_positions: Dict[Union[int, Tuple[int, int]], VolatileRepetitionCount] def parse_aseq_program(program: Loop, used_channels: FrozenSet[ChannelID]) -> ParsedProgram: From 1675f67350523a27611c7267031e377bcc7fb171 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 13:00:58 +0200 Subject: [PATCH 15/32] Add segment deduplication to sampling code --- qupulse/_program/tabor.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index bb0b4a98..5b96ac7f 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -430,7 +430,7 @@ def __init__(self, else: self.setup_advanced_sequence_mode() - self._sampled_segments = self._calc_sampled_segments() + self._sampled_segments, self._waveform_to_segment = self._calc_sampled_segments() @property def markers(self) -> Tuple[Optional[ChannelID], Optional[ChannelID]]: @@ -464,7 +464,7 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int): marker = self._markers[marker_idx] return waveform.get_sampled(channel=marker, sample_times=time) != 0 - def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]: + def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]: """ Returns: (segments, segment_lengths) @@ -474,7 +474,8 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]] if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192): raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16') - segments = [] + segments: Dict[TaborSegment, int] = {} + waveform_to_segment = [] for i, waveform in enumerate(self._parsed_program.waveforms): t = time_array[:segment_lengths[i]] marker_time = t[::2] @@ -488,8 +489,9 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]] ch_b=segment_b, marker_a=marker_a, marker_b=marker_b) - segments.append(segment) - return segments, segment_lengths + segment_idx = segments.setdefault(segment, len(segments)) + waveform_to_segment.append(segment_idx) + return (list(segments.keys()), segment_lengths), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 @@ -556,10 +558,13 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) - return modifications - def get_sequencer_tables(self): # -> List[List[TableDescription, Optional[MappedParameter]]]: - return self._parsed_program.sequencer_tables + def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]: + wf_to_seq = self._waveform_to_segment + return [[((rep_count, wf_to_seq[elem_idx], jump), volatile) + for (rep_count, elem_idx, jump), volatile in sequencer_table] + for sequencer_table in self._parsed_program.sequencer_tables] - def get_advanced_sequencer_table(self) -> List[TableEntry]: + def get_advanced_sequencer_table(self) -> Sequence[TableEntry]: """Advanced sequencer table that can be used via the download_adv_seq_table pytabor command""" return self._parsed_program.advanced_sequencer_table From def4627f7d4190c7827b94f64a26eaccd091f5ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 13:12:20 +0200 Subject: [PATCH 16/32] Fix auto close on backend switch deprecation warning --- tests/pulses/plotting_tests.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py index fdceac21..ea3765c8 100644 --- a/tests/pulses/plotting_tests.py +++ b/tests/pulses/plotting_tests.py @@ -14,6 +14,12 @@ from tests.pulses.sequencing_dummies import DummyWaveform, DummyPulseTemplate +def use_svg_backend(): + import matplotlib.pyplot + matplotlib.pyplot.close('all') + matplotlib.use('svg') + + class PlotterTests(unittest.TestCase): def test_render_loop_sliced(self) -> None: wf = DummyWaveform(duration=19) @@ -87,17 +93,18 @@ def integrated_test_with_sequencer_and_pulse_templates(self) -> None: self.assertEqual(expected_voltages, voltages) def test_plot_empty_pulse(self) -> None: - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis + # use non-interactive backend so that test does not fail on travis + use_svg_backend() pt = DummyPulseTemplate() with self.assertWarnsRegex(UserWarning, "empty", msg="plot() did not issue a warning for an empty pulse"): plot(pt, dict(), show=False) def test_plot_pulse_automatic_sample_rate(self) -> None: - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis - pt=ConstantPT(100, {'a': 1}) + # use non-interactive backend so that test does not fail on travis + use_svg_backend() + + pt = ConstantPT(100, {'a': 1}) plot(pt, sample_rate=None) def test_bug_447(self): @@ -132,8 +139,8 @@ def test(self) -> None: class PlottingIsinstanceTests(unittest.TestCase): @unittest.skip("Breaks other tests") def test_bug_422(self): - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis + # use non-interactive backend so that test does not fail on travis + use_svg_backend() to_reload = ['qupulse._program._loop', 'qupulse.pulses.pulse_template', From af83e326e31686a3b143609fd825ca8315bf465e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:20:33 +0200 Subject: [PATCH 17/32] Fix DummyWaveform hash for float outputs --- tests/pulses/sequencing_dummies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 18ca487c..b94fa1f7 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -47,7 +47,8 @@ def compare_key(self) -> Any: except AttributeError: pass return hash( - tuple(sorted((channel, output.tobytes()) for channel, output in self.sample_output.items())) + tuple(sorted((channel, getattr(output, 'tobytes', lambda: output)()) + for channel, output in self.sample_output.items())) ) else: return id(self) From 10ed70f369b359d05c66bb8a976b19b891e9b929 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:21:12 +0200 Subject: [PATCH 18/32] Fix get_sequener_table entry type --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 5b96ac7f..e4e54b72 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -560,7 +560,7 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) - def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]: wf_to_seq = self._waveform_to_segment - return [[((rep_count, wf_to_seq[elem_idx], jump), volatile) + return [[(TableDescription(rep_count, wf_to_seq[elem_idx], jump), volatile) for (rep_count, elem_idx, jump), volatile in sequencer_table] for sequencer_table in self._parsed_program.sequencer_tables] From 2a9a83beec6a2f1c9245deb78a4a5e23ce710bbc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:22:01 +0200 Subject: [PATCH 19/32] Make differing waveforms produce differing outputs in tabor tests --- tests/_program/tabor_tests.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 919147ec..c90b48aa 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -232,8 +232,8 @@ def test_depth_1_single_waveform(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)]) def test_depth_1_single_sequence(self): - program = Loop(children=[Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=3), - Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=4)], + program = Loop(children=[Loop(waveform=DummyWaveform(sample_output={'A': 0.1}, duration=1), repetition_count=3), + Loop(waveform=DummyWaveform(sample_output={'A': 0.2}, duration=1), repetition_count=4)], repetition_count=1) t_program = TaborProgram(program, channels=(None, 'A'), markers=(None, None), @@ -247,8 +247,8 @@ def test_depth_1_single_sequence(self): def test_depth_1_single_sequence_2(self): """Use the same wf twice""" - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4), @@ -266,8 +266,8 @@ def test_depth_1_single_sequence_2(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)]) def test_depth_1_advanced_sequence_unroll(self): - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4)], @@ -285,8 +285,8 @@ def test_depth_1_advanced_sequence_unroll(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableEntry(5, 1, 0)]) def test_depth_1_advanced_sequence(self): - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4), @@ -385,8 +385,8 @@ def test_update_volatile_parameters_with_depth1(self): s = VolatileRepetitionCount(expression=ExpressionScalar('s'), scope=DictScope(values=FrozenDict(s=3), volatile=set('s'))) - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=s), Loop(waveform=wf_2, repetition_count=4), @@ -419,8 +419,8 @@ def test_update_volatile_parameters_with_depth2(self): a = VolatileRepetitionCount(expression=ExpressionScalar('a'), scope=DictScope(values=FrozenDict(a=5), volatile=set('a'))) - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(children=[Loop(waveform=wf_1, repetition_count=s), Loop(waveform=wf_2, repetition_count=4), From 11b072600629b4f6e38f47c595a2e4f35463240f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 15:55:45 +0200 Subject: [PATCH 20/32] Remove gmpy build dependencies --- .github/workflows/pythontest.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index 6f7f0b74..64a7c27c 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -29,10 +29,9 @@ jobs: steps: - - name: Prepare gmpy2 build dependencies + - name: Add gmpy extra feature if: ${{ matrix.time-type }} == 'gmpy2' run: | - sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev echo "INSTALL_EXTRAS=${{ env.INSTALL_EXTRAS }},Faster-fractions" >> $GITHUB_ENV - name: Checkout repository From 2db12a37c0392e9a6db3842815da98718e64127d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 08:01:29 +0200 Subject: [PATCH 21/32] Trim segement lengths to correct number of elements --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index e4e54b72..00492c28 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -491,7 +491,7 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence marker_b=marker_b) segment_idx = segments.setdefault(segment, len(segments)) waveform_to_segment.append(segment_idx) - return (list(segments.keys()), segment_lengths), waveform_to_segment + return (list(segments.keys()), segment_lengths[:len(segments)]), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 From 5b4d9112620db852700523303f0aafee1f3e34f2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 13:47:51 +0200 Subject: [PATCH 22/32] Add deduplication test --- tests/_program/tabor_tests.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index c90b48aa..cd171e37 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -380,6 +380,23 @@ def my_gen(gen): np.testing.assert_equal(sampled_seg.ch_a, data[0]) np.testing.assert_equal(sampled_seg.ch_b, data[1]) + def test_calc_sampled_segments_deduplication(self): + wf1 = ConstantWaveform(duration=1, amplitude=0.1, channel='A') + wf2 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') + wf3 = SubsetWaveform( + ConstantWaveform.from_mapping(duration=1, constant_values={'A': 0.1, 'B': 0.2}), + {'A'} + ) + loop = Loop(children=[ + Loop(waveform=wf1), + Loop(waveform=wf2), + Loop(waveform=wf3), + ]) + prog = TaborProgram(loop, self.instr_props, ('A', None), (None, None), **self.program_entry_kwargs) + sampled, sampled_length = prog.get_sampled_segments() + self.assertEqual(len(sampled), 2) + self.assertEqual([192, 192], list(sampled_length)) + def test_update_volatile_parameters_with_depth1(self): parameters = {'s': 10, 'not': 13} s = VolatileRepetitionCount(expression=ExpressionScalar('s'), scope=DictScope(values=FrozenDict(s=3), From 8b13e0044aece9b35f0dfa7369ee4b8cb62607b8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 13:48:02 +0200 Subject: [PATCH 23/32] Adjust docstring --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 00492c28..7ded9197 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -467,7 +467,7 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int): def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]: """ Returns: - (segments, segment_lengths) + ((segments, segment_lengths), waveform_to_segment) """ time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate) From 691052eace21617dcb3ded1afbe53a2134b7b16a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 15:48:12 +0200 Subject: [PATCH 24/32] Test that the correct segment lengths are selected --- tests/_program/tabor_tests.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index cd171e37..5a5b7b5a 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -381,12 +381,13 @@ def my_gen(gen): np.testing.assert_equal(sampled_seg.ch_b, data[1]) def test_calc_sampled_segments_deduplication(self): - wf1 = ConstantWaveform(duration=1, amplitude=0.1, channel='A') - wf2 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') - wf3 = SubsetWaveform( - ConstantWaveform.from_mapping(duration=1, constant_values={'A': 0.1, 'B': 0.2}), + wf1 = ConstantWaveform(duration=2, amplitude=0.1, channel='A') + wf2 = SubsetWaveform( + ConstantWaveform.from_mapping(duration=2, constant_values={'A': 0.1, 'B': 0.2}), {'A'} ) + wf3 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') + loop = Loop(children=[ Loop(waveform=wf1), Loop(waveform=wf2), @@ -395,7 +396,7 @@ def test_calc_sampled_segments_deduplication(self): prog = TaborProgram(loop, self.instr_props, ('A', None), (None, None), **self.program_entry_kwargs) sampled, sampled_length = prog.get_sampled_segments() self.assertEqual(len(sampled), 2) - self.assertEqual([192, 192], list(sampled_length)) + self.assertEqual([192 * 2, 192], list(sampled_length)) def test_update_volatile_parameters_with_depth1(self): parameters = {'s': 10, 'not': 13} From d9a48c4803d055ef28c3c6f76a910c9e33db135c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 16:09:04 +0200 Subject: [PATCH 25/32] Correct segment length tracking for deduplicated segments --- qupulse/_program/tabor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 7ded9197..235cc945 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -469,15 +469,17 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence Returns: ((segments, segment_lengths), waveform_to_segment) """ - time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate) + time_array, waveform_samples = get_sample_times(self._parsed_program.waveforms, self._sample_rate) - if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192): + if np.any(waveform_samples % 16 > 0) or np.any(waveform_samples < 192): raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16') segments: Dict[TaborSegment, int] = {} + segment_lengths = [] + waveform_to_segment = [] - for i, waveform in enumerate(self._parsed_program.waveforms): - t = time_array[:segment_lengths[i]] + for waveform, n_samples in zip(self._parsed_program.waveforms, waveform_samples): + t = time_array[:n_samples] marker_time = t[::2] segment_a = self._channel_data(waveform, t, 0) segment_b = self._channel_data(waveform, t, 1) @@ -489,9 +491,12 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence ch_b=segment_b, marker_a=marker_a, marker_b=marker_b) - segment_idx = segments.setdefault(segment, len(segments)) + previous_segment_count = len(segments) + segment_idx = segments.setdefault(segment, previous_segment_count) waveform_to_segment.append(segment_idx) - return (list(segments.keys()), segment_lengths[:len(segments)]), waveform_to_segment + if segment_idx == previous_segment_count: + segment_lengths.append(n_samples) + return (list(segments.keys()), np.array(segment_lengths, dtype=np.uint64)), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 From c8ef798e541df92c36219520d6fdc1da5b8bfa65 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 30 Sep 2023 11:33:38 +0200 Subject: [PATCH 26/32] Missing imports in tests --- tests/_program/tabor_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 5a5b7b5a..bb1b7386 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -19,6 +19,7 @@ from qupulse._program.tabor import TaborException, TaborProgram, find_place_for_segments_in_memory,\ TaborSegment, TaborSequencing, PlottableProgram, TableDescription, make_combined_wave, TableEntry from qupulse.program.loop import Loop +from qupulse.program.waveforms import ConstantWaveform, SubsetWaveform from qupulse._program.volatile import VolatileRepetitionCount from qupulse.hardware.util import voltage_to_uint16 from qupulse.utils.types import TimeType From b07d801dbb54e4a4e24fc1952bab00a06474179e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 4 Oct 2023 10:26:44 +0200 Subject: [PATCH 27/32] 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 28/32] 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 4290e25b93144a7c41bc6191af151df52c5fb0cc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:10:45 +0200 Subject: [PATCH 29/32] Add floordiv implementation to sympy expression wrapper --- qupulse/expressions/sympy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qupulse/expressions/sympy.py b/qupulse/expressions/sympy.py index eeb64ee9..150a1b6a 100644 --- a/qupulse/expressions/sympy.py +++ b/qupulse/expressions/sympy.py @@ -401,6 +401,12 @@ def __truediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> ' def __rtruediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': return self.make(self._sympified_expression.__rtruediv__(self._extract_sympified(other))) + def __floordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': + return self.make(self._sympified_expression.__floordiv__(self._extract_sympified(other))) + + def __rfloordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': + return self.make(self._sympified_expression.__rfloordiv__(self._extract_sympified(other))) + def __neg__(self) -> 'ExpressionScalar': return self.make(self._sympified_expression.__neg__()) From 319fd18361676d8dbb18c073b3de45af32b2bf22 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:24:16 +0200 Subject: [PATCH 30/32] Add floordiv tests --- tests/expressions/expression_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/expressions/expression_tests.py b/tests/expressions/expression_tests.py index 19443493..e7fd5110 100644 --- a/tests/expressions/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -391,6 +391,9 @@ def test_number_math(self): self.assertExpressionEqual(a - b, -(b - a)) self.assertExpressionEqual(a * b, b * a) self.assertExpressionEqual(a / b, 1 / (b / a)) + self.assertExpressionEqual(a // 3, ExpressionScalar('floor(a / 3)')) + self.assertExpressionEqual(a // 3, ExpressionScalar('a // 3')) + self.assertExpressionEqual(3 // a, ExpressionScalar('floor(3 / a)')) def test_symbolic_math(self): a = ExpressionScalar('a') @@ -400,6 +403,7 @@ def test_symbolic_math(self): self.assertExpressionEqual(a - b, -(b - a)) self.assertExpressionEqual(a * b, b * a) self.assertExpressionEqual(a / b, 1 / (b / a)) + self.assertExpressionEqual(a // b, ExpressionScalar('floor(a / b)')) def test_sympy_math(self): a = ExpressionScalar('a') From 72d6af8532e736bb75d5328fb30fc62a4383aec2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:53:25 +0200 Subject: [PATCH 31/32] 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 32/32] 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