diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 545d91783..057122f97 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -2,6 +2,7 @@ - Hardware: - Add a `measure_program` method to the DAC interface. This method is used by the QCoDeS integration. + - Add a `set_measurement_mask` to DAC interface. This method is used by the QCoDeS integration. - Pulse Templates: - `MappingPulseTemplate`: diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py index d95c0a4ba..906e22d88 100644 --- a/qupulse/hardware/dacs/alazar.py +++ b/qupulse/hardware/dacs/alazar.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, Optional, Tuple, List, Iterable +from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable from collections import defaultdict import numpy as np @@ -6,16 +6,70 @@ from atsaverage.config import ScanlineConfiguration from atsaverage.masks import CrossBufferMask, Mask +from qupulse.utils.types import TimeType from qupulse.hardware.dacs.dac_base import DAC class AlazarProgram: - def __init__(self, masks=list(), operations=list(), total_length=None): - self.masks = masks - self.operations = operations - self.total_length = total_length - def __iter__(self): - yield self.masks + def __init__(self): + self._sample_factor = None + self._masks = {} + self.operations = [] + self._total_length = None + + def masks(self, mask_maker: Callable[[str, np.ndarray, np.ndarray], Mask]) -> List[Mask]: + return [mask_maker(mask_name, *data) for mask_name, data in self._masks.items()] + + @property + def total_length(self) -> int: + if not self._total_length: + total_length = 0 + for begins, lengths in self._masks.values(): + total_length = max(begins[-1] + lengths[-1], total_length) + + return total_length + else: + return self._total_length + + @total_length.setter + def total_length(self, val: int): + self._total_length = val + + def clear_masks(self): + self._masks.clear() + + @property + def sample_factor(self) -> Optional[TimeType]: + return self._sample_factor + + def set_measurement_mask(self, mask_name: str, sample_factor: TimeType, + begins: np.ndarray, lengths: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Raise error if sample factor has changed""" + if self._sample_factor is None: + self._sample_factor = sample_factor + + elif sample_factor != self._sample_factor: + raise RuntimeError('class AlazarProgram has already masks with differing sample factor') + + assert begins.dtype == np.float and lengths.dtype == np.float + + # optimization potential here (hash input?) + begins = np.rint(begins * float(sample_factor)).astype(dtype=np.uint64) + lengths = np.floor_divide(lengths * float(sample_factor.numerator), float(sample_factor.denominator)).astype(dtype=np.uint64) + + sorting_indices = np.argsort(begins) + begins = begins[sorting_indices] + lengths = lengths[sorting_indices] + + begins.flags.writeable = False + lengths.flags.writeable = False + + self._masks[mask_name] = begins, lengths + + return begins, lengths + + def iter(self, mask_maker): + yield self.masks(mask_maker) yield self.operations yield self.total_length @@ -54,32 +108,20 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask: mask.channel = hardware_channel return mask + def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple[np.ndarray, np.ndarray]: + sample_factor = TimeType(int(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model)), 10**9) + return self._registered_programs[program_name].set_measurement_mask(mask_name, sample_factor, begins, lengths) + def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple[np.ndarray, np.ndarray]]) -> None: - if not windows: - self._registered_programs[program_name].masks = [] - total_length = 0 - for mask_id, (begins, lengths) in windows.items(): - - sample_factor = self.config.captureClockConfiguration.numeric_sample_rate(self.__card.model) / 10**9 - - begins = np.rint(begins*sample_factor).astype(dtype=np.uint64) - lengths = np.floor(lengths*sample_factor).astype(dtype=np.uint64) - - sorting_indices = np.argsort(begins) - begins = begins[sorting_indices] - lengths = lengths[sorting_indices] + program = self._registered_programs[program_name] + sample_factor = TimeType(int(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model)), + 10 ** 9) + program.clear_masks() - windows[mask_id] = (begins, lengths) - total_length = max(total_length, begins[-1]+lengths[-1]) - - total_length = np.ceil(total_length/self.__card.minimum_record_size) * self.__card.minimum_record_size - - self._registered_programs[program_name].masks = [ - self._make_mask(mask_id, *window_begin_length) - for mask_id, window_begin_length in windows.items()] - self._registered_programs[program_name].total_length = total_length + for mask_name, (begins, lengths) in windows.items(): + program.set_measurement_mask(mask_name, sample_factor, begins, lengths) def register_operations(self, program_name: str, operations) -> None: self._registered_programs[program_name].operations = operations @@ -88,16 +130,26 @@ def arm_program(self, program_name: str) -> None: to_arm = self._registered_programs[program_name] if self.update_settings or self.__armed_program is not to_arm: config = self.config - config.masks, config.operations, total_record_size = self._registered_programs[program_name] + config.masks, config.operations, total_record_size = self._registered_programs[program_name].iter( + self._make_mask) + + sample_factor = TimeType(self.config.captureClockConfiguration.numeric_sample_rate(self.card.model), 10 ** 9) + + if not config.operations: + raise RuntimeError("No operations: Arming program without operations is an error as there will " + "be no result: %r" % program_name) + + elif not config.masks: + raise RuntimeError("No masks although there are operations in program: %r" % program_name) + + elif self._registered_programs[program_name].sample_factor != sample_factor: + raise RuntimeError("Masks were registered with a different sample rate {}!={}".format( + self._registered_programs[program_name].sample_factor, sample_factor)) - if len(config.operations) == 0: - raise RuntimeError('No operations configured for program {}'.format(program_name)) + assert total_record_size > 0 - if not config.masks: - if config.operations: - raise RuntimeError('Invalid configuration. Operations have no masks to work with') - else: - return + minimum_record_size = self.__card.minimum_record_size + total_record_size = (((total_record_size - 1) // minimum_record_size) + 1) * minimum_record_size if config.totalRecordSize == 0: config.totalRecordSize = total_record_size @@ -111,7 +163,7 @@ def arm_program(self, program_name: str) -> None: if config.totalRecordSize < 5*config.aimedBufferSize: config.aimedBufferSize = config.totalRecordSize // 5 - config.apply(self.__card, True) + self.__card.applyConfiguration(config, True) # "Hide" work around from the user config.aimedBufferSize = old_aimed_buffer_size diff --git a/qupulse/hardware/dacs/dac_base.py b/qupulse/hardware/dacs/dac_base.py index 2e4da6df1..a2c3c8d3c 100644 --- a/qupulse/hardware/dacs/dac_base.py +++ b/qupulse/hardware/dacs/dac_base.py @@ -12,6 +12,10 @@ def register_measurement_windows(self, program_name: str, windows: Dict[str, Tup 'numpy.ndarray']]) -> None: """""" + @abstractmethod + def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']: + """returns length of windows in samples""" + @abstractmethod def register_operations(self, program_name: str, operations) -> None: """""" diff --git a/tests/hardware/alazar_tests.py b/tests/hardware/alazar_tests.py index ae9f57ee1..87cf34190 100644 --- a/tests/hardware/alazar_tests.py +++ b/tests/hardware/alazar_tests.py @@ -1,18 +1,91 @@ import unittest +from unittest import mock import numpy as np from ..hardware import * from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram +from qupulse.utils.types import TimeType class AlazarProgramTest(unittest.TestCase): + def setUp(self) -> None: + # we currently allow overlapping masks in AlazarProgram (It will throw an error on upload) + # This probably will change in the future + self.masks = { + 'unsorted': (np.array([1., 100, 13]), np.array([10., 999, 81])), + 'sorted': (np.array([30., 100, 1300]), np.array([10., 990, 811])), + 'overlapping': (np.array([30., 100, 300]), np.array([20., 900, 100])) + } + self.sample_factor = TimeType(10**8, 10**9) + self.expected = { + 'unsorted': (np.array([0, 1, 10]).astype(np.uint64), np.array([1, 8, 99]).astype(np.uint64)), + 'sorted': (np.array([3, 10, 130]).astype(np.uint64), np.array([1, 99, 81]).astype(np.uint64)), + 'overlapping': (np.array([3, 10, 30]).astype(np.uint64), np.array([2, 90, 10]).astype(np.uint64)) + } + + def test_length_computation(self): + program = AlazarProgram() + for name, data in self.masks.items(): + program.set_measurement_mask(name, self.sample_factor, *data) + + self.assertEqual(program.total_length, 130 + 81) + self.assertIsNone(program._total_length) + program.total_length = 17 + self.assertEqual(program.total_length, 17) + + def test_masks(self): + program = AlazarProgram() + for name, data in self.masks.items(): + program.set_measurement_mask(name, self.sample_factor, *data) + + names = [] + def make_mask(name, *data): + np.testing.assert_equal(data, self.expected[name]) + assert name not in names + names.append(name) + return name + + result = program.masks(make_mask) + + self.assertEqual(names, result) + + def test_set_measurement_mask(self): + program = AlazarProgram() + + begins, lengths = self.masks['sorted'] + with self.assertRaises(AssertionError): + program.set_measurement_mask('foo', self.sample_factor, begins.astype(int), lengths) + + expected_unsorted = np.array([0, 1, 10]).astype(np.uint64), np.array([1, 8, 99]).astype(np.uint64) + result = program.set_measurement_mask('unsorted', self.sample_factor, *self.masks['unsorted']) + + np.testing.assert_equal(program._masks, {'unsorted': expected_unsorted}) + np.testing.assert_equal(result, expected_unsorted) + self.assertFalse(result[0].flags.writeable) + self.assertFalse(result[1].flags.writeable) + + with self.assertRaisesRegex(RuntimeError, 'differing sample factor'): + program.set_measurement_mask('sorted', self.sample_factor*5/4, *self.masks['sorted']) + + result = program.set_measurement_mask('sorted', self.sample_factor, *self.masks['sorted']) + np.testing.assert_equal(result, self.expected['sorted']) + def test_iter(self): - args = ([], [], 0) + program = AlazarProgram() + program._masks = [4, 5, 6] + program.operations = [1, 2, 3] + program.total_length = 13 + program.masks = mock.MagicMock(return_value=342) + + mask_maker = mock.MagicMock() - program = AlazarProgram(*args) - for x, y in zip(program, args): - self.assertIs(x, y) + a, b, c = program.iter(mask_maker) + + self.assertEqual(a, 342) + self.assertEqual(b, [1, 2, 3]) + self.assertEqual(c, 13) + program.masks.assert_called_once_with(mask_maker) class AlazarTest(unittest.TestCase): @@ -66,17 +139,14 @@ def test_register_measurement_windows(self): card.register_measurement_windows('otto', dict(A=(begins, lengths))) self.assertEqual(set(card._registered_programs.keys()), {'empty', 'otto'}) - self.assertEqual(card._registered_programs['empty'].masks, []) + self.assertEqual(card._registered_programs['empty'].masks(lambda x: x), []) + [(result_begins, result_lengths)] = card._registered_programs['otto'].masks(lambda _, b, l: (b, l)) expected_begins = np.rint(begins / 10).astype(dtype=np.uint64) - np.testing.assert_equal(card._registered_programs['otto'].masks[0].begin, expected_begins) + np.testing.assert_equal(result_begins, expected_begins) # pi ist genau 3 - length = card._registered_programs['otto'].masks[0].length - np.testing.assert_equal(length if isinstance(length, np.ndarray) else length.as_ndarray(), 3) - - self.assertEqual(card._registered_programs['otto'].masks[0].channel, 3) - self.assertEqual(card._registered_programs['otto'].masks[0].identifier, 'A') + np.testing.assert_equal(result_lengths if isinstance(result_lengths, np.ndarray) else result_lengths.as_ndarray(), 3) def test_register_operations(self): card = AlazarCard(None) @@ -101,12 +171,12 @@ def test_arm_operation(self): card.config = dummy_modules.dummy_atsaverage.config.ScanlineConfiguration() - with self.assertRaises(RuntimeError): + with self.assertRaisesRegex(RuntimeError, 'No operations'): card.arm_program('otto') card.register_operations('otto', ['asd']) - with self.assertRaises(RuntimeError): + with self.assertRaisesRegex(RuntimeError, "No masks"): card.arm_program('otto') begins = np.arange(100) * 176.5 @@ -115,15 +185,21 @@ def test_arm_operation(self): card.config.totalRecordSize = 17 - with self.assertRaises(ValueError): + with self.assertRaisesRegex(ValueError, "total record size is smaller than needed"): card.arm_program('otto') card.config.totalRecordSize = 0 - card.arm_program('otto') - self.assertEqual(card.config._apply_calls, [(raw_card, True)]) - self.assertEqual(card.card._startAcquisition_calls, [1]) + with mock.patch.object(card.card, 'applyConfiguration') as mock_applyConfiguration: + with mock.patch.object(card.card, 'startAcquisition') as mock_startAcquisition: + card.arm_program('otto') + + mock_applyConfiguration.assert_called_once_with(card.config, True) + mock_startAcquisition.assert_called_once_with(1) + + mock_applyConfiguration.reset_mock() + mock_startAcquisition.reset_mock() + card.arm_program('otto') - card.arm_program('otto') - self.assertEqual(card.config._apply_calls, [(raw_card, True)]) - self.assertEqual(card.card._startAcquisition_calls, [1, 1]) + mock_applyConfiguration.assert_not_called() + mock_startAcquisition.assert_called_once_with(1) diff --git a/tests/hardware/dummy_devices.py b/tests/hardware/dummy_devices.py index 6a4330478..01f281933 100644 --- a/tests/hardware/dummy_devices.py +++ b/tests/hardware/dummy_devices.py @@ -10,7 +10,7 @@ def __init__(self): self._measurement_windows = dict() self._operations = dict() self.measured_data = deque([]) - + self._meas_masks = {} self._armed_program = None @property @@ -41,6 +41,10 @@ def clear(self) -> None: def measure_program(self, channels): return self.measured_data.pop() + def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']: + self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths) + return begins, lengths + class DummyAWG(AWG): """Dummy AWG for debugging purposes.""" diff --git a/tests/hardware/dummy_modules.py b/tests/hardware/dummy_modules.py index 1baebe0eb..c84fda31c 100644 --- a/tests/hardware/dummy_modules.py +++ b/tests/hardware/dummy_modules.py @@ -184,8 +184,11 @@ class AlazarCard: minimum_record_size = 256 def __init__(self): self._startAcquisition_calls = [] + self._applyConfiguration_calls = [] def startAcquisition(self, x: int): self._startAcquisition_calls.append(x) + def applyConfiguration(self, config): + self._applyConfiguration_calls.append(config) class config(dummy_package): class CaptureClockConfig: def numeric_sample_rate(self, card):