Skip to content

Commit 107b1a6

Browse files
author
Simon Humpohl
authored
Merge pull request #791 from qutech/feat/auto_mask_shrinking
Add automated mask shrinking
2 parents c9fc1e6 + 9c4507e commit 107b1a6

5 files changed

Lines changed: 131 additions & 7 deletions

File tree

changes.d/791.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Measurement windows can now automatically shrank in case of overlap to counteract small numeric errors.

qupulse/hardware/dacs/alazar.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from qupulse.utils.types import TimeType
1717
from qupulse.hardware.dacs.dac_base import DAC
1818
from qupulse.hardware.util import traced
19-
from qupulse.utils.performance import time_windows_to_samples
19+
from qupulse.utils.performance import time_windows_to_samples, shrink_overlapping_windows
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -283,8 +283,7 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask:
283283
if mask_type not in ('auto', 'cross_buffer', None):
284284
warnings.warn("Currently only CrossBufferMask is implemented.")
285285

286-
if np.any(begins[:-1]+lengths[:-1] > begins[1:]):
287-
raise ValueError('Found overlapping windows in begins')
286+
begins, lengths = shrink_overlapping_windows(begins, lengths)
288287

289288
mask = CrossBufferMask()
290289
mask.identifier = mask_id

qupulse/utils/performance.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import Tuple
23
import numpy as np
34

@@ -24,6 +25,76 @@ def _is_monotonic_numpy(arr: np.ndarray) -> bool:
2425
return np.all(arr[1:] >= arr[:-1])
2526

2627

28+
def _shrink_overlapping_windows_numpy(begins, lengths) -> bool:
29+
supported_dtypes = ('int64', 'uint64')
30+
if begins.dtype.name not in supported_dtypes or lengths.dtype.name not in supported_dtypes:
31+
raise NotImplementedError("This function only supports 64 bit integer types yet.")
32+
33+
ends = begins + lengths
34+
35+
overlaps = np.zeros_like(ends, dtype=np.int64)
36+
np.maximum(ends[:-1].view(np.int64) - begins[1:].view(np.int64), 0, out=overlaps[1:])
37+
38+
if np.any(overlaps >= lengths):
39+
raise ValueError("Overlap is bigger than measurement window")
40+
if np.any(overlaps > 0):
41+
begins += overlaps.view(begins.dtype)
42+
lengths -= overlaps.view(lengths.dtype)
43+
return True
44+
return False
45+
46+
47+
@njit
48+
def _shrink_overlapping_windows_numba(begins, lengths) -> bool:
49+
shrank = False
50+
for idx in range(len(begins) - 1):
51+
end = begins[idx] + lengths[idx]
52+
next_begin = begins[idx + 1]
53+
54+
if end > next_begin:
55+
overlap = end - next_begin
56+
shrank = True
57+
if lengths[idx + 1] > overlap:
58+
begins[idx + 1] += overlap
59+
lengths[idx + 1] -= overlap
60+
else:
61+
raise ValueError("Overlap is bigger than measurement window")
62+
return shrank
63+
64+
65+
class WindowOverlapWarning(RuntimeWarning):
66+
COMMENT = (" This warning is an error by default. "
67+
"Call 'warnings.simplefilter(WindowOverlapWarning, \"always\")' "
68+
"to demote it to a regular warning.")
69+
70+
def __str__(self):
71+
return super().__str__() + self.COMMENT
72+
73+
74+
warnings.simplefilter(category=WindowOverlapWarning, action='error')
75+
76+
77+
def shrink_overlapping_windows(begins, lengths, use_numba: bool = numba is not None) -> Tuple[np.array, np.array]:
78+
"""Shrink windows in place if they overlap. Emits WindowOverlapWarning if a window was shrunk.
79+
80+
Raises:
81+
ValueError: if the overlap is bigger than a window.
82+
83+
Warnings:
84+
WindowOverlapWarning
85+
"""
86+
if use_numba:
87+
backend = _shrink_overlapping_windows_numba
88+
else:
89+
backend = _shrink_overlapping_windows_numpy
90+
begins = begins.copy()
91+
lengths = lengths.copy()
92+
if backend(begins, lengths):
93+
warnings.warn("Found overlapping measurement windows which can be automatically shrunken if possible.",
94+
category=WindowOverlapWarning)
95+
return begins, lengths
96+
97+
2798
@njit
2899
def _time_windows_to_samples_sorted_numba(begins, lengths,
29100
sample_rate: float) -> Tuple[np.ndarray, np.ndarray]:

tests/hardware/alazar_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..hardware import *
77
from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram
88
from qupulse.utils.types import TimeType
9-
9+
from qupulse.utils.performance import WindowOverlapWarning
1010

1111
class AlazarProgramTest(unittest.TestCase):
1212
def setUp(self) -> None:
@@ -112,7 +112,7 @@ def test_make_mask(self):
112112
with self.assertRaises(KeyError):
113113
card._make_mask('N', begins, lengths)
114114

115-
with self.assertRaises(ValueError):
115+
with self.assertWarns(WindowOverlapWarning):
116116
card._make_mask('M', begins, lengths*3)
117117

118118
mask = card._make_mask('M', begins, lengths)

tests/utils/performance_tests.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import unittest
2+
import warnings
23

34
import numpy as np
45

5-
from qupulse.utils.performance import (_time_windows_to_samples_numba, _time_windows_to_samples_numpy,
6-
_average_windows_numba, _average_windows_numpy, average_windows)
6+
from qupulse.utils.performance import (
7+
_time_windows_to_samples_numba, _time_windows_to_samples_numpy,
8+
_average_windows_numba, _average_windows_numpy, average_windows,
9+
shrink_overlapping_windows, WindowOverlapWarning)
710

811

912
class TimeWindowsToSamplesTest(unittest.TestCase):
@@ -55,3 +58,53 @@ def test_single_channel(self):
5558

5659
def test_dual_channel(self):
5760
self.assert_implementations_equal(self.time, self.values, self.begins, self.ends)
61+
62+
63+
class TestOverlappingWindowReduction(unittest.TestCase):
64+
def setUp(self):
65+
self.shrank = np.array([1, 4, 8], dtype=np.uint64), np.array([3, 4, 4], dtype=np.uint64)
66+
self.to_shrink = np.array([1, 4, 7], dtype=np.uint64), np.array([3, 4, 5], dtype=np.uint64)
67+
68+
def assert_noop(self, shrink_fn):
69+
begins = np.array([1, 3, 5], dtype=np.uint64)
70+
lengths = np.array([2, 1, 6], dtype=np.uint64)
71+
result = shrink_fn(begins, lengths)
72+
np.testing.assert_equal((begins, lengths), result)
73+
74+
begins = (np.arange(100) * 176.5).astype(dtype=np.uint64)
75+
lengths = (np.ones(100) * 10 * np.pi).astype(dtype=np.uint64)
76+
result = shrink_fn(begins, lengths)
77+
np.testing.assert_equal((begins, lengths), result)
78+
79+
begins = np.arange(15, dtype=np.uint64)*16
80+
lengths = 1+np.arange(15, dtype=np.uint64)
81+
result = shrink_fn(begins, lengths)
82+
np.testing.assert_equal((begins, lengths), result)
83+
84+
def assert_shrinks(self, shrink_fn):
85+
with warnings.catch_warnings():
86+
warnings.simplefilter("always", WindowOverlapWarning)
87+
with self.assertWarns(WindowOverlapWarning):
88+
shrank = shrink_fn(*self.to_shrink)
89+
np.testing.assert_equal(self.shrank, shrank)
90+
91+
def assert_empty_window_error(self, shrink_fn):
92+
invalid = np.array([1, 2], dtype=np.uint64), np.array([5, 1], dtype=np.uint64)
93+
with self.assertRaisesRegex(ValueError, "Overlap is bigger than measurement window"):
94+
shrink_fn(*invalid)
95+
96+
def test_shrink_overlapping_windows_numba(self):
97+
def shrink_fn(begins, lengths):
98+
return shrink_overlapping_windows(begins, lengths, use_numba=True)
99+
100+
self.assert_noop(shrink_fn)
101+
self.assert_shrinks(shrink_fn)
102+
self.assert_empty_window_error(shrink_fn)
103+
104+
def test_shrink_overlapping_windows_numpy(self):
105+
def shrink_fn(begins, lengths):
106+
return shrink_overlapping_windows(begins, lengths, use_numba=False)
107+
108+
self.assert_noop(shrink_fn)
109+
self.assert_shrinks(shrink_fn)
110+
self.assert_empty_window_error(shrink_fn)

0 commit comments

Comments
 (0)