Skip to content

Commit 21003ab

Browse files
authored
Merge pull request #917 from qutech/experiment_followup-DLV
Experiment followup dlv
2 parents 1848ea0 + 3229266 commit 21003ab

File tree

3 files changed

+287
-14
lines changed

3 files changed

+287
-14
lines changed

qupulse/program/values.py

Lines changed: 148 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
"""Runtime variable value implementations."""
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
4+
from functools import cached_property
45
from numbers import Real
5-
from typing import TypeVar, Generic, Mapping, Union
6+
from typing import TypeVar, Generic, Mapping, Union, Tuple, Optional
7+
from types import MappingProxyType
68

7-
from qupulse.program.volatile import VolatileRepetitionCount
8-
from qupulse.utils.types import TimeType
9+
import numpy as np
910

11+
from qupulse.program.volatile import VolatileRepetitionCount
12+
from qupulse.utils.types import TimeType, frozendict
1013
from qupulse.expressions import sympy as sym_expr
1114
from qupulse.utils.sympy import _lambdify_modules
1215

1316

1417
NumVal = TypeVar('NumVal', bound=Real)
1518

1619

17-
@dataclass
20+
@dataclass(
21+
frozen=True,
22+
repr=False, # dont leak frozendict implementation detail in repr
23+
)
1824
class DynamicLinearValue(Generic[NumVal]):
1925
"""This is a potential runtime-evaluable expression of the form
2026
@@ -34,8 +40,9 @@ class DynamicLinearValue(Generic[NumVal]):
3440
factors: Mapping[str, NumVal]
3541

3642
def __post_init__(self):
37-
assert isinstance(self.factors, Mapping)
38-
43+
immutable = frozendict(self.factors)
44+
object.__setattr__(self, 'factors', immutable)
45+
3946
def value(self, scope: Mapping[str, NumVal]) -> NumVal:
4047
"""Numeric value of the expression with the given scope.
4148
Args:
@@ -44,19 +51,34 @@ def value(self, scope: Mapping[str, NumVal]) -> NumVal:
4451
The numeric value.
4552
"""
4653
value = self.base
47-
for name, factor in self.factors:
54+
for name, factor in self.factors.items():
4855
value += scope[name] * factor
4956
return value
50-
57+
58+
def __abs__(self):
59+
# The deifnition of an absolute value is ambiguous, but there is a case
60+
# to define it as sum_i abs(f_i) + abs(base) for certain conveniences.
61+
# return abs(self.base)+sum([abs(o) for o in self.factors.values()])
62+
raise NotImplementedError(f'abs({self.__class__.__name__}) is ambiguous')
63+
64+
def __eq__(self, other):
65+
if isinstance(other, type(self)):
66+
return self.base == other.base and self.factors == other.factors
67+
68+
if (base_eq := self.base.__eq__(other)) is NotImplemented:
69+
return NotImplemented
70+
71+
return base_eq and not self.factors
72+
5173
def __add__(self, other):
5274
if isinstance(other, (float, int, TimeType)):
5375
return DynamicLinearValue(self.base + other, self.factors)
5476

5577
if type(other) == type(self):
56-
offsets = dict(self.factors)
78+
factors = dict(self.factors)
5779
for name, value in other.factors.items():
58-
offsets[name] = value + offsets.get(name, 0)
59-
return DynamicLinearValue(self.base + other.base, offsets)
80+
factors[name] = value + factors.get(name, 0)
81+
return DynamicLinearValue(self.base + other.base, factors)
6082

6183
# this defers evaluation when other is still a symbolic expression
6284
return NotImplemented
@@ -86,7 +108,7 @@ def __rmul__(self, other):
86108
def __truediv__(self, other):
87109
inv = 1 / other
88110
return self.__mul__(inv)
89-
111+
90112
@property
91113
def free_symbols(self):
92114
"""This is required for the :py:class:`sympy.expr.Expr` interface compliance. Since the keys of
@@ -114,12 +136,124 @@ def replace(self, r, s):
114136
"""
115137
return self
116138

139+
def __repr__(self):
140+
return f"{type(self).__name__}(base={self.base!r}, factors={dict(self.factors)!r})"
141+
142+
143+
# is there any way to cast the numpy cumprod to int?
144+
int_type = Union[np.int64,np.int32,int]
145+
146+
147+
def _to_resolution(x, resolution):
148+
"""Function used by :py:class:`.ResolutionDependentValue` for rounding to resolution multiples."""
149+
# to avoid conflicts between positive and negative vals from casting half to even, we only round positive numbers
150+
if x < 0:
151+
return -round(-x / resolution) * resolution
152+
else:
153+
return round(x / resolution) * resolution
154+
155+
156+
@dataclass(frozen=True)
157+
class ResolutionDependentValue(Generic[NumVal]):
158+
"""This is a potential runtime-evaluable expression of the form
159+
160+
o + sum_i b_i*m_i
161+
162+
with (potential) float o, b_i and integers m_i. o and b_i are rounded(gridded)
163+
to a resolution given upon __call__.
164+
165+
The main use case is the correct rounding of increments used in command-based
166+
voltage scans on some hardware devices, where an imprecise numeric value is
167+
looped over m_i times and could, if not rounded, accumulate a proportional
168+
error leading to unintended drift in output voltages when jump-back commands
169+
afterwards do not account for the deviations.
170+
Rounding the value preemptively and supplying corrected values to jump-back
171+
commands prevents this.
172+
"""
173+
174+
bases: Tuple[NumVal, ...]
175+
multiplicities: Tuple[int, ...]
176+
offset: NumVal
177+
178+
@cached_property
179+
def _is_time_or_int(self):
180+
return all(isinstance(b,(TimeType,int_type)) for b in self.bases) and isinstance(self.offset,(TimeType,int_type))
181+
182+
def with_resolution(self, resolution: Optional[NumVal]) -> NumVal:
183+
"""Get the numeric value rounding to the given resolution.
184+
185+
Args:
186+
resolution: Resolution the bases and offset are rounded to. If none all values must be integers.
187+
188+
Returns:
189+
The rounded numeric value.
190+
"""
191+
if resolution is None:
192+
assert self._is_time_or_int
193+
return sum(b * m for b, m in zip(self.bases, self.multiplicities)) + self.offset
194+
195+
offset = _to_resolution(self.offset, resolution)
196+
base_sum = sum(_to_resolution(base, resolution) * multiplicity
197+
for base, multiplicity in zip(self.bases, self.multiplicities))
198+
return base_sum + offset
199+
200+
def __call__(self, resolution: Optional[float]) -> Union[NumVal,TimeType]:
201+
"""Backward compatible alias of :py:meth:`~ResolutionDependentValue.with_resolution`."""
202+
return self.with_resolution(resolution)
203+
204+
def __bool__(self):
205+
#if any value is not zero - this helps for some checks
206+
return any(bool(b) for b in self.bases) or bool(self.offset)
207+
208+
def __add__(self, other):
209+
# this should happen in the context of an offset being added to it, not the bases being modified.
210+
if isinstance(other, (float, int, TimeType)):
211+
return ResolutionDependentValue(self.bases, self.multiplicities, self.offset+other)
212+
return NotImplemented
213+
214+
def __radd__(self, other):
215+
return self.__add__(other)
216+
217+
def __sub__(self, other):
218+
return self.__add__(-other)
219+
220+
def __mul__(self, other):
221+
# this should happen when the amplitude is being scaled
222+
# multiplicities are not affected
223+
if isinstance(other, (float, int, TimeType)):
224+
return ResolutionDependentValue(tuple(b*other for b in self.bases),self.multiplicities,self.offset*other)
225+
return NotImplemented
226+
227+
def __rmul__(self,other):
228+
return self.__mul__(other)
229+
230+
def __truediv__(self,other):
231+
return self.__mul__(1/other)
232+
233+
def __float__(self):
234+
return float(self.with_resolution(resolution=None))
235+
236+
def __str__(self):
237+
return f"RDP of {sum(b*m for b,m in zip(self.bases,self.multiplicities)) + self.offset}"
238+
239+
240+
241+
#This is a simple dervide class to allow better isinstance checks in the HDAWG driver
242+
@dataclass(frozen=True)
243+
class DynamicLinearValueStepped(DynamicLinearValue):
244+
step_nesting_level: int
245+
rng: range
246+
reverse: int|bool
247+
117248

118249
# TODO: hackedy, hackedy
119250
sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES = sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES + (DynamicLinearValue,)
120251

121252
# this keeps the simple expression in lambdified results
122-
_lambdify_modules.append({'DynamicLinearValue': DynamicLinearValue})
253+
_lambdify_modules.append({
254+
'DynamicLinearValue': DynamicLinearValue,
255+
'DynamicLinearValueStepped': DynamicLinearValueStepped,
256+
})
123257

124258
RepetitionCount = Union[int, VolatileRepetitionCount, DynamicLinearValue[int]]
125259
HardwareTime = Union[TimeType, DynamicLinearValue[TimeType]]

tests/program/__init__.py

Whitespace-only changes.

tests/program/values_tests.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import copy
2+
import unittest
3+
from unittest import TestCase
4+
5+
import numpy as np
6+
7+
from qupulse.pulses import *
8+
from qupulse.program.linspace import *
9+
from qupulse.program.transformation import *
10+
from qupulse.pulses.function_pulse_template import FunctionPulseTemplate
11+
from qupulse.program.values import DynamicLinearValue, ResolutionDependentValue, DynamicLinearValueStepped
12+
from qupulse.utils.types import TimeType
13+
14+
15+
class DynamicLinearValueTests(TestCase):
16+
def setUp(self):
17+
self.d = DynamicLinearValue(-100,{'a':np.pi,'b':np.e})
18+
self.d3 = DynamicLinearValue(-300,{'a':np.pi,'b':np.e})
19+
20+
def test_value(self):
21+
dval = self.d.value({'a':12,'b':34})
22+
np.testing.assert_allclose(dval, 12*np.pi+34*np.e-100)
23+
24+
# def test_abs(self):
25+
# np.testing.assert_allclose(abs(self.d),100+np.pi+np.e)
26+
27+
def test_add_sub_neg(self):
28+
29+
self.assertEqual(self.d + 3,
30+
DynamicLinearValue(-100+3,{'a':np.pi,'b':np.e}))
31+
self.assertEqual(self.d + np.pi,
32+
DynamicLinearValue(-100+np.pi,{'a':np.pi,'b':np.e}))
33+
self.assertEqual(self.d + TimeType(12/5),
34+
DynamicLinearValue(-100+TimeType(12/5),{'a':np.pi,'b':np.e}))
35+
#sub
36+
self.assertEqual(self.d - TimeType(12/5),
37+
DynamicLinearValue(-100-TimeType(12/5),{'a':np.pi,'b':np.e}))
38+
39+
#this would raise because of TimeType conversion
40+
# self.assertEqual(TimeType(12/5)-self.d,
41+
# DynamicLinearValue(100+TimeType(12/5),{'a':-np.pi,'b':-np.e}))
42+
#same type
43+
self.assertEqual(self.d+DynamicLinearValue(0.1,{'b':1,'c':2}),
44+
DynamicLinearValue(-99.9,{'a':np.pi,'b':np.e+1,'c':2}))
45+
46+
def test_mul(self):
47+
self.assertEqual(self.d*3,
48+
DynamicLinearValue(-3*100,{'a':3*np.pi,'b':3*np.e}))
49+
self.assertEqual(3*self.d,
50+
DynamicLinearValue(-3*100,{'a':3*np.pi,'b':3*np.e}))
51+
#div
52+
self.assertEqual(self.d3/3,
53+
DynamicLinearValue(-100,{'a':np.pi/3,'b':np.e/3}))
54+
#raise
55+
self.assertRaises(TypeError,lambda: 3/self.d,)
56+
57+
def test_eq(self):
58+
59+
self.assertEqual(self.d==1,False)
60+
self.assertEqual(self.d==1+1j,False)
61+
# self.assertEqual(self.d>-101,True) #if one wants to allow these comparisons
62+
# self.assertEqual(self.d<TimeType(24/5),True) #if one wants to allow these comparisons
63+
64+
self.assertEqual(self.d==self.d,True)
65+
self.assertEqual(self.d+1==self.d,False)
66+
67+
# inoperative features.
68+
# self.assertEqual(self.d+1>self.d,False)
69+
# self.assertEqual(self.d+1<self.d,False)
70+
# self.assertEqual(self.d+1>=self.d,True)
71+
# self.assertEqual(self.d+1<=self.d,False)
72+
73+
# self.assertEqual(self.d>self.d/2-51,True)
74+
# self.assertEqual(self.d<self.d*2+101,True)
75+
76+
def test_sympy(self):
77+
self.assertEqual(self.d._sympy_(), self.d)
78+
self.assertEqual(self.d.replace(1,1), self.d)
79+
80+
def test_hash(self):
81+
self.assertEqual(hash(self.d), hash(self.d))
82+
self.assertNotEqual(hash(self.d), hash(self.d3))
83+
84+
85+
86+
class ResolutionDependentValueTests(TestCase):
87+
def setUp(self):
88+
self.d = ResolutionDependentValue((np.pi*1e-5,np.e*1e-5),(10,20),0.1)
89+
self.d2 = ResolutionDependentValue((np.e*1e-5,np.pi*1e-5),(10,20),-0.1)
90+
self.dtt = ResolutionDependentValue((TimeType(12,5),TimeType(14,5)),(10,20),TimeType(12,5))
91+
self.dint = ResolutionDependentValue((1,2),(10,20),1)
92+
93+
self._default_res = 2**-16
94+
95+
def test_call(self):
96+
val = self.d(self._default_res)
97+
#expected to round to 2*res each, so 60*2**16, offset also rounded
98+
expected_val = 2**-16 * 60 + 0.100006103515625
99+
self.assertAlmostEqual(val,expected_val,places=12)
100+
self.assertEqual((val/self._default_res)%1,0)
101+
102+
#if no resolution must be tt or int
103+
self.assertEqual(self.dtt(None),TimeType(412, 5))
104+
self.assertEqual(self.dint(None),51)
105+
106+
def test_repr_round_trip(self):
107+
eval_str = repr(self.dint)
108+
evaluated = eval(eval_str)
109+
self.assertEqual(self.dint, evaluated)
110+
evaluated_repr = repr(evaluated)
111+
self.assertEqual(eval_str, evaluated_repr)
112+
113+
def test_dunder(self):
114+
self.assertEqual(bool(self.d), True)
115+
self.assertRaises(TypeError, lambda: self.d+self.d2)
116+
self.assertEqual(self.d-0.1,
117+
ResolutionDependentValue((np.pi*1e-5,np.e*1e-5),(10,20),0.0))
118+
self.assertEqual(self.d*2,
119+
ResolutionDependentValue((np.pi*1e-5*2,np.e*1e-5*2),(10,20),0.2))
120+
self.assertEqual(self.d/2,
121+
ResolutionDependentValue((np.pi*1e-5/2,np.e*1e-5/2),(10,20),0.1/2))
122+
123+
self.assertRaises(AssertionError, lambda: float(self.d))
124+
125+
try: str(self.d)
126+
except: self.fail()
127+
128+
self.assertEqual(hash(self.d), hash(self.d))
129+
self.assertNotEqual(hash(self.d), hash(self.d2))
130+
131+
self.assertEqual(self.d==ResolutionDependentValue((np.pi*1e-5,np.e*1e-5),(10,20),0.1),True)
132+
133+
134+
class DynamicLinearValueSteppedTests(TestCase):
135+
def test_properties(self):
136+
d = DynamicLinearValueStepped(0.,{'a':1},1,range(3),False)
137+
self.assertEqual(d.rng, range(3))
138+
self.assertEqual(d.step_nesting_level, 1)
139+
self.assertEqual(d.reverse, False)

0 commit comments

Comments
 (0)