1
1
"""Runtime variable value implementations."""
2
2
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass , field
4
+ from functools import cached_property
4
5
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
6
8
7
- from qupulse .program .volatile import VolatileRepetitionCount
8
- from qupulse .utils .types import TimeType
9
+ import numpy as np
9
10
11
+ from qupulse .program .volatile import VolatileRepetitionCount
12
+ from qupulse .utils .types import TimeType , frozendict
10
13
from qupulse .expressions import sympy as sym_expr
11
14
from qupulse .utils .sympy import _lambdify_modules
12
15
13
16
14
17
NumVal = TypeVar ('NumVal' , bound = Real )
15
18
16
19
17
- @dataclass
20
+ @dataclass (
21
+ frozen = True ,
22
+ repr = False , # dont leak frozendict implementation detail in repr
23
+ )
18
24
class DynamicLinearValue (Generic [NumVal ]):
19
25
"""This is a potential runtime-evaluable expression of the form
20
26
@@ -34,8 +40,9 @@ class DynamicLinearValue(Generic[NumVal]):
34
40
factors : Mapping [str , NumVal ]
35
41
36
42
def __post_init__ (self ):
37
- assert isinstance (self .factors , Mapping )
38
-
43
+ immutable = frozendict (self .factors )
44
+ object .__setattr__ (self , 'factors' , immutable )
45
+
39
46
def value (self , scope : Mapping [str , NumVal ]) -> NumVal :
40
47
"""Numeric value of the expression with the given scope.
41
48
Args:
@@ -44,19 +51,34 @@ def value(self, scope: Mapping[str, NumVal]) -> NumVal:
44
51
The numeric value.
45
52
"""
46
53
value = self .base
47
- for name , factor in self .factors :
54
+ for name , factor in self .factors . items () :
48
55
value += scope [name ] * factor
49
56
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
+
51
73
def __add__ (self , other ):
52
74
if isinstance (other , (float , int , TimeType )):
53
75
return DynamicLinearValue (self .base + other , self .factors )
54
76
55
77
if type (other ) == type (self ):
56
- offsets = dict (self .factors )
78
+ factors = dict (self .factors )
57
79
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 )
60
82
61
83
# this defers evaluation when other is still a symbolic expression
62
84
return NotImplemented
@@ -86,7 +108,7 @@ def __rmul__(self, other):
86
108
def __truediv__ (self , other ):
87
109
inv = 1 / other
88
110
return self .__mul__ (inv )
89
-
111
+
90
112
@property
91
113
def free_symbols (self ):
92
114
"""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):
114
136
"""
115
137
return self
116
138
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
+
117
248
118
249
# TODO: hackedy, hackedy
119
250
sym_expr .ALLOWED_NUMERIC_SCALAR_TYPES = sym_expr .ALLOWED_NUMERIC_SCALAR_TYPES + (DynamicLinearValue ,)
120
251
121
252
# 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
+ })
123
257
124
258
RepetitionCount = Union [int , VolatileRepetitionCount , DynamicLinearValue [int ]]
125
259
HardwareTime = Union [TimeType , DynamicLinearValue [TimeType ]]
0 commit comments