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 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 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 diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index b92c76f1..235cc945 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 @@ -429,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]]: @@ -463,19 +464,22 @@ 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) + ((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 = [] - for i, waveform in enumerate(self._parsed_program.waveforms): - t = time_array[:segment_lengths[i]] + segments: Dict[TaborSegment, int] = {} + segment_lengths = [] + + waveform_to_segment = [] + 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) @@ -487,8 +491,12 @@ 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 + previous_segment_count = len(segments) + segment_idx = segments.setdefault(segment, previous_segment_count) + waveform_to_segment.append(segment_idx) + 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 @@ -555,10 +563,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 [[(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] - 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 @@ -645,12 +656,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: 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__()) diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index c6ec80f2..68643db7 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 diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 246639b3..5080a92e 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]) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 97c64fea..171ca17f 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 @@ -52,6 +53,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) @@ -98,6 +101,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]: @@ -171,8 +178,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) @@ -356,6 +370,47 @@ def with_appended(self, *appended: 'PulseTemplate'): else: return self + 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) + + # 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: + 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 + 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/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 diff --git a/setup.cfg b/setup.cfg index 2101b343..8b33e961 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,8 @@ 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 diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 75644fdf..12522ac5 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -1,25 +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._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: @@ -27,6 +25,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: + 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" @@ -63,6 +70,7 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) + @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) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 919147ec..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 @@ -232,8 +233,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 +248,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 +267,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 +286,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), @@ -380,13 +381,31 @@ 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=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), + 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 * 2, 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), 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 +438,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), 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') diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py index 2cb6bbaf..a9b619cc 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) 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', diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index a846ae74..ad82dba9 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 @@ -26,12 +28,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) @@ -39,6 +43,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) @@ -92,7 +97,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(), @@ -363,6 +371,74 @@ def test_create_program_volatile(self): template.create_program(parameters=parameters, volatile={'abc', 'dfg'}) _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) + def test_pad_to(self): + 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) + @mock.patch('qupulse.pulses.pulse_template.default_program_builder') def test_create_program_none(self, pb_mock) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) @@ -399,6 +475,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) diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index f101d879..21c3c7e6 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -54,7 +54,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) 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()