From 65195ab328e83d3e2395d534964466404699f6da Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 4 Jan 2025 15:30:40 +0100 Subject: [PATCH 1/7] First version of a measurement builder # Conflicts: # qupulse/pulses/measurement.py --- qupulse/program/measurement.py | 256 ++++++++++++++++++++++++++++++ qupulse/pulses/measurement.py | 8 +- tests/program/measurement_test.py | 39 +++++ 3 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 qupulse/program/measurement.py create mode 100644 tests/program/measurement_test.py diff --git a/qupulse/program/measurement.py b/qupulse/program/measurement.py new file mode 100644 index 000000000..90ff26835 --- /dev/null +++ b/qupulse/program/measurement.py @@ -0,0 +1,256 @@ +from typing import Sequence, Mapping, Iterable, Optional, Union, ContextManager +from dataclasses import dataclass + +import numpy +from rich.measure import Measurement + +from qupulse.utils.types import TimeType +from qupulse.program import (ProgramBuilder, Program, HardwareVoltage, HardwareTime, + MeasurementWindow, Waveform, RepetitionCount, SimpleExpression) +from qupulse.parameter_scope import Scope + + +@dataclass +class MeasurementNode: + windows: Sequence[MeasurementWindow] + duration: HardwareTime + + +@dataclass +class MeasurementRepetition(MeasurementNode): + body: MeasurementNode + count: RepetitionCount + +@dataclass +class MeasurementSequence(MeasurementNode): + nodes: Sequence[tuple[HardwareTime, MeasurementNode]] + + +@dataclass +class MeasurementFrame: + commands: list['Command'] + has_duration: bool + +MeasurementID = str | int + + +class MeasurementBuilder(ProgramBuilder): + def __init__(self): + super().__init__() + + self._frames = [MeasurementFrame([], False)] + self._ranges: list[tuple[str, range]] = [] + self._repetitions = [] + self._measurements = [] + self._label_counter = 0 + + def _with_new_frame(self, measurements): + self._frames.append(MeasurementFrame([], False)) + yield self + frame = self._frames.pop() + if not frame.has_duration: + return + parent = self._frames[-1] + parent.has_duration = True + if measurements: + parent.commands.extend(map(Measure, measurements)) + return frame.commands + + def inner_scope(self, scope: Scope) -> Scope: + """This function is necessary to inject program builder specific parameter implementations into the build + process.""" + if self._ranges: + name, rng = self._ranges[-1] + return scope.overwrite({name: SimpleExpression(base=rng.start, offsets={name: rng.step})}) + else: + return scope + + def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): + """Supports dynamic i.e. for loop generated offsets and duration""" + self._frames[-1].commands.append(Wait(duration)) + self._frames[-1].has_duration = True + + def play_arbitrary_waveform(self, waveform: Waveform): + """""" + self._frames[-1].commands.append(Wait(waveform.duration)) + self._frames[-1].has_duration = True + + def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): + """Unconditionally add given measurements relative to the current position.""" + if measurements: + commands = self._frames[-1].commands + commands.extend(Measure(*meas) for meas in measurements) + self._frames[-1].has_duration = True + + def with_repetition(self, repetition_count: RepetitionCount, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" + new_commands = yield from self._with_new_frame(measurements) + if new_commands is None: + return + parent = self._frames[-1] + + self._label_counter += 1 + label_idx = self._label_counter + parent.commands.append(LoopLabel(idx=label_idx, runtime_name=None, count=RepetitionCount)) + parent.commands.extend(new_commands) + parent.commands.append(LoopJmp(idx=label_idx)) + + def with_sequence(self, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: + """ + + Measurements that are added in to the returned program builder are discarded if the sequence is empty on exit. + + Args: + measurements: Measurements to attach to the potential child. + Returns: + """ + new_commands = yield from self._with_new_frame(measurements) + if new_commands is None: + return + parent = self._frames[-1] + parent.commands.extend(new_commands) + + def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: + """Create a context managed program builder whose contents are translated into a single waveform upon exit if + it is not empty.""" + yield self + + def with_iteration(self, index_name: str, rng: range, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + self._ranges.append((index_name, rng)) + new_commands = yield from self._with_new_frame(measurements) + self._ranges.pop() + if new_commands is None: + return + parent = self._frames[-1] + + self._label_counter += 1 + label_idx = self._label_counter + parent.commands.append(LoopLabel(idx=label_idx, runtime_name=index_name, count=len(rng))) + parent.commands.extend(new_commands) + parent.commands.append(LoopJmp(idx=label_idx)) + + def time_reversed(self) -> ContextManager['ProgramBuilder']: + self._frames.append(MeasurementFrame([], False)) + yield self + frame = self._frames.pop() + if not frame.has_duration: + return + + self._frames[-1].has_duration = True + self._frames[-1].commands.extend(_reversed_commands(frame.commands)) + + def to_program(self) -> Optional[Program]: + """Further addition of new elements might fail after finalizing the program.""" + if self._frames[0].has_duration: + return self._frames[0].commands + + +@dataclass +class LoopLabel: + idx: int + runtime_name: str | None + count: RepetitionCount + + +@dataclass +class Measure: + meas_id: MeasurementID + delay: HardwareTime + length: HardwareTime + + +@dataclass +class Wait: + duration: HardwareTime + + +@dataclass +class LoopJmp: + idx: int + + +Command = Union[LoopLabel, LoopJmp, Wait, Measure] + + +def _reversed_commands(cmds: Sequence[Command]) -> Sequence[Command]: + reversed_cmds = [] + jumps = {} + for cmd in reversed(cmds): + if isinstance(cmd, LoopJmp): + jumps[cmd.idx] = len(reversed_cmds) + reversed_cmds.append(cmd) + elif isinstance(cmd, LoopLabel): + jump_idx = jumps[cmd.idx] + jump = reversed_cmds[jump_idx] + reversed_cmds[jump_idx] = cmd + reversed_cmds.append(jump) + + elif isinstance(cmd, Measure): + if isinstance(cmd.delay, SimpleExpression) or isinstance(cmd.delay, SimpleExpression): + raise NotImplementedError("TODO") + reversed_cmds.append(Measure(meas_id=cmd.meas_id, + delay=-(cmd.delay + cmd.length), + length=cmd.length,)) + elif isinstance(cmd, Wait): + reversed_cmds.append(cmd) + else: + raise ValueError("Not a command", cmd) + + return reversed_cmds + + +def to_table(commands: Sequence[Command]) -> dict[str, numpy.ndarray]: + time = TimeType(0) + + memory = {} + counts = [None] + + tables = {} + + def eval_hardware_time(t: HardwareTime): + if isinstance(t, SimpleExpression): + value = t.base + for (factor_name, factor_val) in t.offsets.items(): + count = counts[memory[factor_name]] + value += factor_val * count + return value + else: + return t + + def execute(sequence: Sequence[Command]) -> int: + nonlocal time + nonlocal tables + nonlocal memory + nonlocal counts + + skip = 0 + for idx, cmd in enumerate(sequence): + if idx < skip: + continue + if isinstance(cmd, LoopJmp): + return idx + elif isinstance(cmd, LoopLabel): + if cmd.runtime_name: + memory[cmd.runtime_name] = cmd.idx + if cmd.idx == len(counts): + counts.append(0) + assert cmd.idx < len(counts) + + for iter_val in range(cmd.count): + counts[cmd.idx] = iter_val + pos = execute(sequence[idx + 1:]) + skip = idx + pos + 2 + elif isinstance(cmd, Measure): + meas_time = float(eval_hardware_time(cmd.delay) + time) + meas_len = float(eval_hardware_time(cmd.length)) + tables.setdefault(cmd.meas_id, []).append((meas_time, meas_len)) + elif isinstance(cmd, Wait): + time += eval_hardware_time(cmd.duration) + + execute(commands) + return { + name: numpy.array(values) for name, values in tables.items() + } diff --git a/qupulse/pulses/measurement.py b/qupulse/pulses/measurement.py index 76145e35e..76998c246 100644 --- a/qupulse/pulses/measurement.py +++ b/qupulse/pulses/measurement.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import warnings from typing import Optional, List, Tuple, Union, Dict, Set, Mapping, AbstractSet from numbers import Real import itertools @@ -62,8 +63,11 @@ def get_measurement_windows(self, begin_val = begin.evaluate_in_scope(parameters) length_val = length.evaluate_in_scope(parameters) - if begin_val < 0 or length_val < 0: - raise ValueError('Measurement window with negative begin or length: {}, {}'.format(begin, length)) + try: + if begin_val < 0 or length_val < 0: + warnings.warn('Measurement window with negative begin or length: {}, {}'.format(begin, length)) + except TypeError: + pass resulting_windows.append( (name, diff --git a/tests/program/measurement_test.py b/tests/program/measurement_test.py new file mode 100644 index 000000000..7a86f3655 --- /dev/null +++ b/tests/program/measurement_test.py @@ -0,0 +1,39 @@ +import copy +import unittest +from unittest import TestCase + +import numpy as np + +from qupulse.pulses import * +from qupulse.program.measurement import * + + +class SingleRampTest(TestCase): + def setUp(self): + hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}, measurements=[('A', 10, 100), ('B', '1 + idx * 2', 200)]) + self.pulse_template = hold.with_iteration('idx', 200) + + self.commands = [ + LoopLabel(1, 'idx', 200), + Measure('A', 10, 100), + Measure('B', SimpleExpression(base=1, offsets={'idx': 2}), 200), + Wait(TimeType(10 ** 6)), + LoopJmp(1) + ] + + self.table_a = np.array([(10 + 10**6 * idx, 100) for idx in range(200)]) + self.table_b = np.array([(1 + idx * 2 + 10**6 * idx, 200) for idx in range(200)]) + + def test_commands(self): + builder = MeasurementBuilder() + commands = self.pulse_template.create_program(program_builder=builder) + self.assertEqual(self.commands, commands) + + def test_table(self): + table = to_table(self.commands) + tab_a = table['A'] + tab_b = table['B'] + np.testing.assert_array_equal(self.table_a, tab_a) + np.testing.assert_array_equal(self.table_b, tab_b) + + From ba983a3c71cc048868f2c401bc3e794f9835108d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 4 Jan 2025 16:53:54 +0100 Subject: [PATCH 2/7] Remove dead code, improve readability and create dedicated vm. --- qupulse/program/measurement.py | 162 ++++++++++++++++-------------- tests/program/measurement_test.py | 25 ++++- 2 files changed, 108 insertions(+), 79 deletions(-) diff --git a/qupulse/program/measurement.py b/qupulse/program/measurement.py index 90ff26835..bf4d43a6e 100644 --- a/qupulse/program/measurement.py +++ b/qupulse/program/measurement.py @@ -1,8 +1,9 @@ -from typing import Sequence, Mapping, Iterable, Optional, Union, ContextManager +import contextlib +from typing import Sequence, Mapping, Iterable, Optional, Union, ContextManager, Callable from dataclasses import dataclass +from functools import cached_property import numpy -from rich.measure import Measurement from qupulse.utils.types import TimeType from qupulse.program import (ProgramBuilder, Program, HardwareVoltage, HardwareTime, @@ -10,28 +11,58 @@ from qupulse.parameter_scope import Scope +MeasurementID = str | int + + +@dataclass +class LoopLabel: + idx: int + runtime_name: str | None + count: RepetitionCount + + @dataclass -class MeasurementNode: - windows: Sequence[MeasurementWindow] +class Measure: + meas_id: MeasurementID + delay: HardwareTime + length: HardwareTime + + +@dataclass +class Wait: duration: HardwareTime @dataclass -class MeasurementRepetition(MeasurementNode): - body: MeasurementNode - count: RepetitionCount +class LoopJmp: + idx: int + + +Command = Union[LoopLabel, LoopJmp, Wait, Measure] + @dataclass -class MeasurementSequence(MeasurementNode): - nodes: Sequence[tuple[HardwareTime, MeasurementNode]] +class MeasurementInstructions(Program): + commands: Sequence[Command] + + @cached_property + def duration(self) -> float: + latest = 0. + + def process(_, begin, length): + nonlocal latest + end = begin + length + latest = max(latest, end) + + vm = MeasurementVM(process) + vm.execute(commands=self.commands) + return latest @dataclass class MeasurementFrame: commands: list['Command'] - has_duration: bool - -MeasurementID = str | int + keep: bool class MeasurementBuilder(ProgramBuilder): @@ -48,12 +79,11 @@ def _with_new_frame(self, measurements): self._frames.append(MeasurementFrame([], False)) yield self frame = self._frames.pop() - if not frame.has_duration: + if not frame.keep: return - parent = self._frames[-1] - parent.has_duration = True - if measurements: - parent.commands.extend(map(Measure, measurements)) + self.measure(measurements) + # measure does not keep if there are no measurements + self._frames[-1].keep = True return frame.commands def inner_scope(self, scope: Scope) -> Scope: @@ -68,19 +98,19 @@ def inner_scope(self, scope: Scope) -> Scope: def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): """Supports dynamic i.e. for loop generated offsets and duration""" self._frames[-1].commands.append(Wait(duration)) - self._frames[-1].has_duration = True + self._frames[-1].keep = True def play_arbitrary_waveform(self, waveform: Waveform): """""" self._frames[-1].commands.append(Wait(waveform.duration)) - self._frames[-1].has_duration = True + self._frames[-1].keep = True def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): """Unconditionally add given measurements relative to the current position.""" if measurements: commands = self._frames[-1].commands commands.extend(Measure(*meas) for meas in measurements) - self._frames[-1].has_duration = True + self._frames[-1].keep = True def with_repetition(self, repetition_count: RepetitionCount, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: @@ -92,10 +122,11 @@ def with_repetition(self, repetition_count: RepetitionCount, self._label_counter += 1 label_idx = self._label_counter - parent.commands.append(LoopLabel(idx=label_idx, runtime_name=None, count=RepetitionCount)) + parent.commands.append(LoopLabel(idx=label_idx, runtime_name=None, count=repetition_count)) parent.commands.extend(new_commands) parent.commands.append(LoopJmp(idx=label_idx)) + @contextlib.contextmanager def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: """ @@ -112,6 +143,7 @@ def with_sequence(self, parent = self._frames[-1] parent.commands.extend(new_commands) + @contextlib.contextmanager def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if it is not empty.""" @@ -136,43 +168,16 @@ def time_reversed(self) -> ContextManager['ProgramBuilder']: self._frames.append(MeasurementFrame([], False)) yield self frame = self._frames.pop() - if not frame.has_duration: + if not frame.keep: return - self._frames[-1].has_duration = True + self._frames[-1].keep = True self._frames[-1].commands.extend(_reversed_commands(frame.commands)) def to_program(self) -> Optional[Program]: """Further addition of new elements might fail after finalizing the program.""" - if self._frames[0].has_duration: - return self._frames[0].commands - - -@dataclass -class LoopLabel: - idx: int - runtime_name: str | None - count: RepetitionCount - - -@dataclass -class Measure: - meas_id: MeasurementID - delay: HardwareTime - length: HardwareTime - - -@dataclass -class Wait: - duration: HardwareTime - - -@dataclass -class LoopJmp: - idx: int - - -Command = Union[LoopLabel, LoopJmp, Wait, Measure] + if self._frames[0].keep: + return MeasurementInstructions(self._frames[0].commands) def _reversed_commands(cmds: Sequence[Command]) -> Sequence[Command]: @@ -202,30 +207,26 @@ def _reversed_commands(cmds: Sequence[Command]) -> Sequence[Command]: return reversed_cmds -def to_table(commands: Sequence[Command]) -> dict[str, numpy.ndarray]: - time = TimeType(0) - - memory = {} - counts = [None] +class MeasurementVM: + """A VM that is capable of executing the measurement commands""" - tables = {} + def __init__(self, callback: Callable[[str, float, float], None]): + self._time = TimeType(0) + self._memory = {} + self._counts = {} + self._callback = callback - def eval_hardware_time(t: HardwareTime): + def _eval_hardware_time(self, t: HardwareTime): if isinstance(t, SimpleExpression): value = t.base for (factor_name, factor_val) in t.offsets.items(): - count = counts[memory[factor_name]] + count = self._counts[self._memory[factor_name]] value += factor_val * count return value else: return t - def execute(sequence: Sequence[Command]) -> int: - nonlocal time - nonlocal tables - nonlocal memory - nonlocal counts - + def _execute_after_label(self, sequence: Sequence[Command]) -> int: skip = 0 for idx, cmd in enumerate(sequence): if idx < skip: @@ -234,23 +235,30 @@ def execute(sequence: Sequence[Command]) -> int: return idx elif isinstance(cmd, LoopLabel): if cmd.runtime_name: - memory[cmd.runtime_name] = cmd.idx - if cmd.idx == len(counts): - counts.append(0) - assert cmd.idx < len(counts) + self._memory[cmd.runtime_name] = cmd.idx for iter_val in range(cmd.count): - counts[cmd.idx] = iter_val - pos = execute(sequence[idx + 1:]) + self._counts[cmd.idx] = iter_val + pos = self._execute_after_label(sequence[idx + 1:]) skip = idx + pos + 2 + elif isinstance(cmd, Measure): - meas_time = float(eval_hardware_time(cmd.delay) + time) - meas_len = float(eval_hardware_time(cmd.length)) - tables.setdefault(cmd.meas_id, []).append((meas_time, meas_len)) + meas_time = float(self._eval_hardware_time(cmd.delay) + self._time) + meas_len = float(self._eval_hardware_time(cmd.length)) + self._callback(cmd.meas_id, meas_time, meas_len) + elif isinstance(cmd, Wait): - time += eval_hardware_time(cmd.duration) + self._time += self._eval_hardware_time(cmd.duration) + + def execute(self, commands: Sequence[Command]): + self._execute_after_label(commands) + + +def to_table(commands: Sequence[Command]) -> dict[str, numpy.ndarray]: + tables = {} - execute(commands) + vm = MeasurementVM(lambda name, begin, length: tables.setdefault(name, []).append((begin, length))) + vm.execute(commands) return { name: numpy.array(values) for name, values in tables.items() } diff --git a/tests/program/measurement_test.py b/tests/program/measurement_test.py index 7a86f3655..92606c1e6 100644 --- a/tests/program/measurement_test.py +++ b/tests/program/measurement_test.py @@ -26,8 +26,8 @@ def setUp(self): def test_commands(self): builder = MeasurementBuilder() - commands = self.pulse_template.create_program(program_builder=builder) - self.assertEqual(self.commands, commands) + instructions = self.pulse_template.create_program(program_builder=builder) + self.assertEqual(self.commands, instructions.commands) def test_table(self): table = to_table(self.commands) @@ -37,3 +37,24 @@ def test_table(self): np.testing.assert_array_equal(self.table_b, tab_b) +class ComplexPulse(TestCase): + def setUp(self): + hold = ConstantPT(10 ** 6, {'a': 1}, measurements=[('A', 10, 100), ('B', '1 + ii * 2 + jj', '3 + ii + jj')]) + dyn_hold = ConstantPT('10 ** 6 - 4 * ii', {'a': 1}, measurements=[('A', 10, 100), ('B', '1 + ii * 2 + jj', '3 + ii + jj')]) + + self.pulse_template = SequencePT( + hold.with_repetition(2).with_iteration('ii', 100).with_repetition(2).with_iteration('jj', 200), + measurements=[('A', 1, 100)] + ).with_repetition(2) + + self.commands = [] + + def test_commands(self): + builder = MeasurementBuilder() + commands = self.pulse_template.create_program(program_builder=builder) + to_table(commands.commands) + raise NotImplementedError("TODO") + + def test_table(self): + + raise NotImplementedError("TODO") \ No newline at end of file From 2f271d8fbbfde1f759a87c5d9d2d623886081ab2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 23 Jan 2025 16:19:59 +0100 Subject: [PATCH 3/7] Remove code duplication --- qupulse/program/measurement.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/qupulse/program/measurement.py b/qupulse/program/measurement.py index bf4d43a6e..989afc378 100644 --- a/qupulse/program/measurement.py +++ b/qupulse/program/measurement.py @@ -86,6 +86,18 @@ def _with_new_frame(self, measurements): self._frames[-1].keep = True return frame.commands + def _with_loop_scope(self, measurements, loop_name, loop_count): + new_commands = yield from self._with_new_frame(measurements) + if new_commands is None: + return + parent = self._frames[-1] + + self._label_counter += 1 + label_idx = self._label_counter + parent.commands.append(LoopLabel(idx=label_idx, runtime_name=loop_name, count=loop_count)) + parent.commands.extend(new_commands) + parent.commands.append(LoopJmp(idx=label_idx)) + def inner_scope(self, scope: Scope) -> Scope: """This function is necessary to inject program builder specific parameter implementations into the build process.""" @@ -115,16 +127,7 @@ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): def with_repetition(self, repetition_count: RepetitionCount, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" - new_commands = yield from self._with_new_frame(measurements) - if new_commands is None: - return - parent = self._frames[-1] - - self._label_counter += 1 - label_idx = self._label_counter - parent.commands.append(LoopLabel(idx=label_idx, runtime_name=None, count=repetition_count)) - parent.commands.extend(new_commands) - parent.commands.append(LoopJmp(idx=label_idx)) + yield from self._with_loop_scope(measurements, loop_name=None, loop_count=repetition_count) @contextlib.contextmanager def with_sequence(self, @@ -152,17 +155,8 @@ def new_subprogram(self, global_transformation: 'Transformation' = None) -> Cont def with_iteration(self, index_name: str, rng: range, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: self._ranges.append((index_name, rng)) - new_commands = yield from self._with_new_frame(measurements) + yield from self._with_loop_scope(measurements, loop_name=index_name, loop_count=len(rng)) self._ranges.pop() - if new_commands is None: - return - parent = self._frames[-1] - - self._label_counter += 1 - label_idx = self._label_counter - parent.commands.append(LoopLabel(idx=label_idx, runtime_name=index_name, count=len(rng))) - parent.commands.extend(new_commands) - parent.commands.append(LoopJmp(idx=label_idx)) def time_reversed(self) -> ContextManager['ProgramBuilder']: self._frames.append(MeasurementFrame([], False)) From d3141550c3083ba78321aff9520434b7843c717a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 1 Jul 2025 19:39:11 +0200 Subject: [PATCH 4/7] Test warning instead of exception --- tests/pulses/measurement_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pulses/measurement_tests.py b/tests/pulses/measurement_tests.py index f010e4092..8dd40e213 100644 --- a/tests/pulses/measurement_tests.py +++ b/tests/pulses/measurement_tests.py @@ -47,10 +47,10 @@ def test_measurement_windows_invalid(self) -> None: pulse = self.to_test_constructor(measurements=[('mw', 'a', 'd')]) measurement_mapping = {'mw': 'mw'} - with self.assertRaises(ValueError): + with self.assertWarnsRegex(UserWarning, "negative begin or length"): pulse.get_measurement_windows(measurement_mapping=measurement_mapping, parameters=dict(length=10, a=-1, d=3)) - with self.assertRaises(ValueError): + with self.assertWarnsRegex(UserWarning, "negative begin or length"): pulse.get_measurement_windows(measurement_mapping=measurement_mapping, parameters=dict(length=10, a=3, d=-1)) From 1a1d74edb0dab3af17f8034a9dffc2152490d4ef Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 1 Jul 2025 19:40:51 +0200 Subject: [PATCH 5/7] Fix test name --- tests/program/{measurement_test.py => measurement_tests.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/program/{measurement_test.py => measurement_tests.py} (100%) diff --git a/tests/program/measurement_test.py b/tests/program/measurement_tests.py similarity index 100% rename from tests/program/measurement_test.py rename to tests/program/measurement_tests.py From b21289472c34282d7ee0724e50a26879cc004fe6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 4 Jul 2025 13:26:58 +0200 Subject: [PATCH 6/7] Fix imports --- qupulse/program/measurement.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/qupulse/program/measurement.py b/qupulse/program/measurement.py index 989afc378..d68adb3f0 100644 --- a/qupulse/program/measurement.py +++ b/qupulse/program/measurement.py @@ -5,9 +5,10 @@ import numpy -from qupulse.utils.types import TimeType -from qupulse.program import (ProgramBuilder, Program, HardwareVoltage, HardwareTime, - MeasurementWindow, Waveform, RepetitionCount, SimpleExpression) +from qupulse.utils.types import TimeType, MeasurementWindow +from qupulse.program.protocol import ProgramBuilder, Program +from qupulse.program.values import RepetitionCount, HardwareTime, HardwareVoltage, DynamicLinearValue, TimeType +from qupulse.program.waveforms import Waveform from qupulse.parameter_scope import Scope @@ -103,7 +104,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, rng = self._ranges[-1] - return scope.overwrite({name: SimpleExpression(base=rng.start, offsets={name: rng.step})}) + return scope.overwrite({name: DynamicLinearValue(base=rng.start, offsets={name: rng.step})}) else: return scope @@ -188,7 +189,7 @@ def _reversed_commands(cmds: Sequence[Command]) -> Sequence[Command]: reversed_cmds.append(jump) elif isinstance(cmd, Measure): - if isinstance(cmd.delay, SimpleExpression) or isinstance(cmd.delay, SimpleExpression): + if isinstance(cmd.delay, DynamicLinearValue) or isinstance(cmd.delay, DynamicLinearValue): raise NotImplementedError("TODO") reversed_cmds.append(Measure(meas_id=cmd.meas_id, delay=-(cmd.delay + cmd.length), @@ -211,7 +212,7 @@ def __init__(self, callback: Callable[[str, float, float], None]): self._callback = callback def _eval_hardware_time(self, t: HardwareTime): - if isinstance(t, SimpleExpression): + if isinstance(t, DynamicLinearValue): value = t.base for (factor_name, factor_val) in t.offsets.items(): count = self._counts[self._memory[factor_name]] From a0c90298aceb07883d1f7f96f8f7091536d1b9de Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Aug 2025 16:19:57 +0200 Subject: [PATCH 7/7] Fix errors due to DLV changes --- qupulse/program/measurement.py | 6 +++--- tests/program/measurement_tests.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qupulse/program/measurement.py b/qupulse/program/measurement.py index d68adb3f0..4de94b012 100644 --- a/qupulse/program/measurement.py +++ b/qupulse/program/measurement.py @@ -104,7 +104,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, rng = self._ranges[-1] - return scope.overwrite({name: DynamicLinearValue(base=rng.start, offsets={name: rng.step})}) + return scope.overwrite({name: DynamicLinearValue(base=rng.start, factors={name: rng.step})}) else: return scope @@ -169,7 +169,7 @@ def time_reversed(self) -> ContextManager['ProgramBuilder']: self._frames[-1].keep = True self._frames[-1].commands.extend(_reversed_commands(frame.commands)) - def to_program(self) -> Optional[Program]: + def to_program(self, channels = None) -> Optional[Program]: """Further addition of new elements might fail after finalizing the program.""" if self._frames[0].keep: return MeasurementInstructions(self._frames[0].commands) @@ -214,7 +214,7 @@ def __init__(self, callback: Callable[[str, float, float], None]): def _eval_hardware_time(self, t: HardwareTime): if isinstance(t, DynamicLinearValue): value = t.base - for (factor_name, factor_val) in t.offsets.items(): + for (factor_name, factor_val) in t.factors.items(): count = self._counts[self._memory[factor_name]] value += factor_val * count return value diff --git a/tests/program/measurement_tests.py b/tests/program/measurement_tests.py index 92606c1e6..c0aacf600 100644 --- a/tests/program/measurement_tests.py +++ b/tests/program/measurement_tests.py @@ -6,6 +6,7 @@ from qupulse.pulses import * from qupulse.program.measurement import * +from qupulse.program import DynamicLinearValue class SingleRampTest(TestCase): @@ -16,7 +17,7 @@ def setUp(self): self.commands = [ LoopLabel(1, 'idx', 200), Measure('A', 10, 100), - Measure('B', SimpleExpression(base=1, offsets={'idx': 2}), 200), + Measure('B', DynamicLinearValue(base=1, factors={'idx': 2}), 200), Wait(TimeType(10 ** 6)), LoopJmp(1) ]