Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
128 changes: 90 additions & 38 deletions qupulse/hardware/dacs/alazar.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
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

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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions qupulse/hardware/dacs/dac_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
""""""
Expand Down
116 changes: 96 additions & 20 deletions tests/hardware/alazar_tests.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
6 changes: 5 additions & 1 deletion tests/hardware/dummy_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
3 changes: 3 additions & 0 deletions tests/hardware/dummy_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down