rtint 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rtint/_version.py +24 -0
- rtint/rtint_acquisition.py +327 -0
- rtint/rtint_counter.py +202 -0
- rtint/rtint_ethercat.py +127 -0
- rtint/rtint_filter.py +872 -0
- rtint/rtint_generator.py +577 -0
- rtint/rtint_hardware.py +336 -0
- rtint/rtint_lut.py +143 -0
- rtint/rtint_motor.py +191 -0
- rtint/rtint_parameter.py +105 -0
- rtint/rtint_regul.py +310 -0
- rtint/rtint_signal.py +42 -0
- rtint/rtint_trigger.py +107 -0
- rtint/rtint_utils.py +648 -0
- rtint-0.0.3.dist-info/METADATA +74 -0
- rtint-0.0.3.dist-info/RECORD +19 -0
- rtint-0.0.3.dist-info/WHEEL +5 -0
- rtint-0.0.3.dist-info/licenses/LICENSE +238 -0
- rtint-0.0.3.dist-info/top_level.txt +1 -0
rtint/rtint_generator.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# This file is part of the mechatronic project
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) Beamline Control Unit, ESRF
|
|
4
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
REAL-TIME TARGET Signal Generators
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import enum
|
|
11
|
+
import math
|
|
12
|
+
import numpy as np
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from scipy.signal import chirp
|
|
15
|
+
from tabulate import tabulate
|
|
16
|
+
|
|
17
|
+
RED = "\033[31m"
|
|
18
|
+
GREEN = "\033[32m"
|
|
19
|
+
RESET = "\033[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _force_fft_symmetry(X: np.ndarray) -> np.ndarray:
|
|
23
|
+
"""Force conjugate symmetry so IFFT yields a real signal."""
|
|
24
|
+
Y = X.copy()
|
|
25
|
+
N = len(X)
|
|
26
|
+
half_floor = N // 2
|
|
27
|
+
half_ceil = math.ceil(N / 2)
|
|
28
|
+
Y[half_ceil + 1 :] = np.conj(X[1:half_floor][::-1])
|
|
29
|
+
return Y
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _schroeder_phases(Ns: int, mag: np.ndarray) -> np.ndarray:
|
|
33
|
+
"""Schroeder phases for minimum crest factor (Schroeder 1970)."""
|
|
34
|
+
rel_mag = mag / mag.sum()
|
|
35
|
+
phase = np.zeros(Ns)
|
|
36
|
+
for n in range(1, Ns // 2 + 1):
|
|
37
|
+
ll = np.arange(n)
|
|
38
|
+
phase[n] = -2 * np.pi * np.sum((n - ll) * rel_mag[ll])
|
|
39
|
+
return phase
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _generate_multisine(
|
|
43
|
+
Fs: float,
|
|
44
|
+
Ns: int,
|
|
45
|
+
asd: np.ndarray = None,
|
|
46
|
+
phase_type: str = "schroeder",
|
|
47
|
+
) -> np.ndarray:
|
|
48
|
+
"""Generate a multi-sine excitation signal.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
Fs: Sampling frequency [Hz].
|
|
52
|
+
Ns: Number of samples in the signal.
|
|
53
|
+
asd: Amplitude spectral density [unit/sqrt(Hz)], length Ns.
|
|
54
|
+
If None, a flat unitary ASD is used.
|
|
55
|
+
phase_type: 'schroeder' (deterministic, low crest factor) or 'normal' (random).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Real-valued signal of length Ns, phase-shifted to start at the
|
|
59
|
+
minimum-gradient zero crossing.
|
|
60
|
+
"""
|
|
61
|
+
# --- magnitude ---
|
|
62
|
+
if asd is None:
|
|
63
|
+
mag = (np.ones(Ns) * 2 * np.sqrt(Fs) / np.sqrt(Ns)) ** 2
|
|
64
|
+
mag[Ns // 2 :] = 0
|
|
65
|
+
else:
|
|
66
|
+
asd = np.asarray(asd, dtype=float)
|
|
67
|
+
if len(asd) != Ns:
|
|
68
|
+
raise ValueError(f"asd must have length Ns={Ns}, got {len(asd)}")
|
|
69
|
+
mag = (asd * 2 * np.sqrt(Fs) / np.sqrt(Ns)) ** 2
|
|
70
|
+
|
|
71
|
+
if np.any(mag[Ns // 2 :]):
|
|
72
|
+
print(
|
|
73
|
+
f"{RED}WARNING: Non-zero magnitude values present outside of frequency limits.{RESET}"
|
|
74
|
+
)
|
|
75
|
+
mag[Ns // 2 :] = 0
|
|
76
|
+
|
|
77
|
+
# --- phase ---
|
|
78
|
+
if phase_type == "schroeder":
|
|
79
|
+
if mag.sum() == 0:
|
|
80
|
+
raise ValueError("All magnitude bins are zero — frequency band is empty.")
|
|
81
|
+
phase = _schroeder_phases(Ns, mag)
|
|
82
|
+
elif phase_type == "normal":
|
|
83
|
+
phase = np.random.randn(Ns)
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError("phase_type must be 'schroeder' or 'normal'")
|
|
86
|
+
|
|
87
|
+
# --- frequency → time domain ---
|
|
88
|
+
Y = np.sqrt(mag / 2) * np.exp(1j * phase)
|
|
89
|
+
y = np.fft.ifft(_force_fft_symmetry(Y)).real * (Ns / 2)
|
|
90
|
+
|
|
91
|
+
# --- align to minimum-gradient zero crossing ---
|
|
92
|
+
y_sign = y > 0
|
|
93
|
+
zero_inds = np.where(y_sign[1:] != y_sign[:-1])[0]
|
|
94
|
+
zero_grad = np.abs(y[zero_inds] - y[zero_inds + 1])
|
|
95
|
+
start = zero_inds[np.argmin(zero_grad)]
|
|
96
|
+
return np.concatenate([y[start:], y[:start]])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RtintHdwGeneratorController:
|
|
100
|
+
def __init__(self, tg):
|
|
101
|
+
self._tg = tg
|
|
102
|
+
self._generators: dict[str, RtintHdwGenerator] | None = None
|
|
103
|
+
self._load()
|
|
104
|
+
|
|
105
|
+
def __info__(self, debug=False):
|
|
106
|
+
if self._generators is None:
|
|
107
|
+
return "\n No Generator in the model"
|
|
108
|
+
|
|
109
|
+
if debug:
|
|
110
|
+
lines = [
|
|
111
|
+
[
|
|
112
|
+
"Name",
|
|
113
|
+
"State",
|
|
114
|
+
"Duration [s]",
|
|
115
|
+
"Repeat",
|
|
116
|
+
"Unique Name",
|
|
117
|
+
"Max Duration [s]",
|
|
118
|
+
"Period [s]",
|
|
119
|
+
]
|
|
120
|
+
]
|
|
121
|
+
else:
|
|
122
|
+
lines = [["Name", "State", "Duration [s]", "Repeat"]]
|
|
123
|
+
for gen in self._generators.values():
|
|
124
|
+
repeat_str = f"True ({gen.period:.3f} s)" if gen.repeat else "False"
|
|
125
|
+
row = [
|
|
126
|
+
gen._name,
|
|
127
|
+
gen.state.name,
|
|
128
|
+
f"{gen.duration:.3f}",
|
|
129
|
+
repeat_str,
|
|
130
|
+
]
|
|
131
|
+
if debug:
|
|
132
|
+
row += [
|
|
133
|
+
gen._unique_name,
|
|
134
|
+
f"{gen.max_duration:.3f}",
|
|
135
|
+
f"{gen.period:.3f}",
|
|
136
|
+
]
|
|
137
|
+
lines.append(row)
|
|
138
|
+
return "\n" + tabulate(lines, headers="firstrow", tablefmt="grid", stralign="left")
|
|
139
|
+
|
|
140
|
+
def _load(self):
|
|
141
|
+
generators = self._tg._get_all_objects_from_key("bliss_signalgen")
|
|
142
|
+
if len(generators) > 0:
|
|
143
|
+
self._generators = {}
|
|
144
|
+
for generator in generators:
|
|
145
|
+
tg_generator = RtintHdwGenerator(self._tg, generator)
|
|
146
|
+
if hasattr(self, tg_generator._name):
|
|
147
|
+
print(
|
|
148
|
+
f"{RED}WARNING: Generator '{tg_generator._name}' already exists{RESET}"
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
else:
|
|
152
|
+
setattr(self, tg_generator._name, tg_generator)
|
|
153
|
+
self._generators[tg_generator._name] = tg_generator
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class GeneratorState(enum.IntEnum):
|
|
157
|
+
Idle = 0
|
|
158
|
+
Moving = 1
|
|
159
|
+
Stopped = 2
|
|
160
|
+
Fault = 3
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class RtintHdwGenerator:
|
|
164
|
+
def __init__(self, tg, unique_name):
|
|
165
|
+
self._tg = tg
|
|
166
|
+
self._unique_name = unique_name
|
|
167
|
+
self._max_samples: int = self._tg._program.tree.params[
|
|
168
|
+
f"{self._tg._program.name}/{self._unique_name}/samples/Value"
|
|
169
|
+
].shape[0]
|
|
170
|
+
|
|
171
|
+
def __info__(self):
|
|
172
|
+
Ts = self._tg._Ts
|
|
173
|
+
n_active = round(self.duration / Ts)
|
|
174
|
+
lines = [
|
|
175
|
+
["Name", self._name],
|
|
176
|
+
["Unique name", self._unique_name],
|
|
177
|
+
["", ""],
|
|
178
|
+
["State", self.state.name],
|
|
179
|
+
["", ""],
|
|
180
|
+
["Duration", f"{self.duration:.3f} s ({n_active} samples)"],
|
|
181
|
+
[
|
|
182
|
+
"Max duration",
|
|
183
|
+
f"{self.max_duration:.3f} s ({self._max_samples} samples)",
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
"Repeat",
|
|
187
|
+
(
|
|
188
|
+
f"True ({self.period:.3f} s, {self._period_samples} samples)"
|
|
189
|
+
if self.repeat
|
|
190
|
+
else "False"
|
|
191
|
+
),
|
|
192
|
+
],
|
|
193
|
+
]
|
|
194
|
+
return tabulate(lines, tablefmt="plain", stralign="right")
|
|
195
|
+
|
|
196
|
+
def _tree(self):
|
|
197
|
+
print("Parameters:")
|
|
198
|
+
self._tg.parameter._tree.subtree(
|
|
199
|
+
self._tg._program.name + "/" + self._unique_name
|
|
200
|
+
).show()
|
|
201
|
+
print("Signals:")
|
|
202
|
+
self._tg.signal._tree.subtree(
|
|
203
|
+
self._tg._program.name + "/" + self._unique_name
|
|
204
|
+
).show()
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def _name(self):
|
|
208
|
+
return self._tg.parameter.get(
|
|
209
|
+
f"{self._unique_name}/bliss_signalgen/String"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# --- read-only ---
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def max_duration(self) -> float:
|
|
216
|
+
"""Maximum signal duration [s], fixed at model compile time."""
|
|
217
|
+
return self._max_samples * self._tg._Ts
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def state(self) -> GeneratorState:
|
|
221
|
+
return GeneratorState(
|
|
222
|
+
int(self._tg.signal.get(f"{self._unique_name}/state"))
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def output(self) -> float:
|
|
227
|
+
return self._tg.signal.get(f"{self._unique_name}/gen_signal")
|
|
228
|
+
|
|
229
|
+
# --- samples ---
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def samples(self) -> np.ndarray:
|
|
233
|
+
return self._tg.parameter.get(f"{self._unique_name}/samples/Value")
|
|
234
|
+
|
|
235
|
+
@samples.setter
|
|
236
|
+
def samples(self, value):
|
|
237
|
+
signal = np.asarray(value, dtype=float).flatten()
|
|
238
|
+
if len(signal) > self._max_samples:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Signal length ({len(signal)} samples = {len(signal) * self._tg._Ts:.3f} s) "
|
|
241
|
+
f"exceeds maximum ({self._max_samples} samples = {self.max_duration:.3f} s)"
|
|
242
|
+
)
|
|
243
|
+
# Add zeros at the end of the vector if necessary
|
|
244
|
+
if len(signal) < self._max_samples:
|
|
245
|
+
signal = np.pad(signal, (0, self._max_samples - len(signal)))
|
|
246
|
+
self._tg.parameter.set(f"{self._unique_name}/samples/Value", signal)
|
|
247
|
+
|
|
248
|
+
# --- parameters ---
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def duration(self) -> float:
|
|
252
|
+
"""Total playback duration [s]. Output is zero after this time."""
|
|
253
|
+
return self._tg.parameter.get(f"{self._unique_name}/duration/Value")
|
|
254
|
+
|
|
255
|
+
@duration.setter
|
|
256
|
+
def duration(self, value: float):
|
|
257
|
+
self._tg.parameter.set(f"{self._unique_name}/duration/Value", value)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def repeat(self) -> bool:
|
|
261
|
+
"""If True, the signal pattern cycles over period [s]."""
|
|
262
|
+
return bool(self._tg.parameter.get(f"{self._unique_name}/repeat/Value"))
|
|
263
|
+
|
|
264
|
+
@repeat.setter
|
|
265
|
+
def repeat(self, value: bool):
|
|
266
|
+
self._tg.parameter.set(f"{self._unique_name}/repeat/Value", int(value))
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def _period_samples(self) -> int:
|
|
270
|
+
return int(self._tg.parameter.get(f"{self._unique_name}/period/Value"))
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def period(self) -> float:
|
|
274
|
+
"""Signal pattern period [s]. Stored as samples on the real-time target."""
|
|
275
|
+
return self._period_samples * self._tg._Ts
|
|
276
|
+
|
|
277
|
+
@period.setter
|
|
278
|
+
def period(self, value: float):
|
|
279
|
+
"""Set signal pattern period [s]. Converted to samples internally."""
|
|
280
|
+
self._tg.parameter.set(
|
|
281
|
+
f"{self._unique_name}/period/Value", round(value / self._tg._Ts)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# --- start / stop ---
|
|
285
|
+
|
|
286
|
+
def start(self):
|
|
287
|
+
start_trigger = int(
|
|
288
|
+
self._tg.parameter.get(f"{self._unique_name}/start_trigger/Bias")
|
|
289
|
+
)
|
|
290
|
+
self._tg.parameter.set(
|
|
291
|
+
f"{self._unique_name}/start_trigger/Bias", start_trigger + 1
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def stop(self):
|
|
295
|
+
stop_trigger = int(
|
|
296
|
+
self._tg.parameter.get(f"{self._unique_name}/stop_trigger/Bias")
|
|
297
|
+
)
|
|
298
|
+
self._tg.parameter.set(
|
|
299
|
+
f"{self._unique_name}/stop_trigger/Bias", stop_trigger + 1
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# --- internal ---
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _prepend_zeros(signal: np.ndarray, start_time: float, Ts: float) -> np.ndarray:
|
|
306
|
+
if start_time > 0:
|
|
307
|
+
return np.concatenate([np.zeros(int(start_time / Ts)), signal])
|
|
308
|
+
return signal
|
|
309
|
+
|
|
310
|
+
def _upload(self, signal: np.ndarray, duration: float = None):
|
|
311
|
+
"""Upload signal pattern. Sets samples, period (= pattern length), and duration."""
|
|
312
|
+
self.samples = signal
|
|
313
|
+
pattern_duration = len(signal) * self._tg._Ts
|
|
314
|
+
self.period = pattern_duration
|
|
315
|
+
self.duration = duration if duration is not None else pattern_duration
|
|
316
|
+
|
|
317
|
+
# --- convenience methods ---
|
|
318
|
+
# Periodic signals (sinus, triangle, noise): upload one natural period, repeat=True by default.
|
|
319
|
+
# Pass duration= to set total playback time in the same call.
|
|
320
|
+
# Example: gen.sinus(1e-6, 1.0, duration=100); gen.start()
|
|
321
|
+
# One-shot signals (step, ramp, sweep, impulse): repeat=False by default.
|
|
322
|
+
|
|
323
|
+
def sinus(
|
|
324
|
+
self,
|
|
325
|
+
amplitude: float,
|
|
326
|
+
frequency: float,
|
|
327
|
+
offset: float = 0,
|
|
328
|
+
duration: float = None,
|
|
329
|
+
repeat: bool = True,
|
|
330
|
+
):
|
|
331
|
+
"""Upload one cycle of a sinus at frequency [Hz]. Repeats by default."""
|
|
332
|
+
Ts = self._tg._Ts
|
|
333
|
+
t = np.arange(0, 1.0 / frequency, Ts)
|
|
334
|
+
self._upload(offset + amplitude * np.sin(2 * np.pi * frequency * t), duration)
|
|
335
|
+
self.repeat = repeat
|
|
336
|
+
|
|
337
|
+
def triangle(
|
|
338
|
+
self,
|
|
339
|
+
amplitude: float,
|
|
340
|
+
frequency: float,
|
|
341
|
+
offset: float = 0,
|
|
342
|
+
duration: float = None,
|
|
343
|
+
repeat: bool = True,
|
|
344
|
+
):
|
|
345
|
+
"""Upload one cycle of a triangle wave at frequency [Hz]. Repeats by default."""
|
|
346
|
+
Ts = self._tg._Ts
|
|
347
|
+
period = 1.0 / frequency
|
|
348
|
+
t = np.arange(0, period, Ts)
|
|
349
|
+
slope = 4 * amplitude * frequency
|
|
350
|
+
t_mod = np.mod(t, period)
|
|
351
|
+
signal = np.where(
|
|
352
|
+
t_mod <= period / 4,
|
|
353
|
+
slope * t_mod,
|
|
354
|
+
np.where(
|
|
355
|
+
t_mod <= 3 * period / 4,
|
|
356
|
+
amplitude - slope * (t_mod - period / 4),
|
|
357
|
+
-amplitude + slope * (t_mod - 3 * period / 4),
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
self._upload(offset + signal, duration)
|
|
361
|
+
self.repeat = repeat
|
|
362
|
+
|
|
363
|
+
def uniform_white_noise(
|
|
364
|
+
self,
|
|
365
|
+
amplitude: float,
|
|
366
|
+
pattern_duration: float,
|
|
367
|
+
offset: float = 0,
|
|
368
|
+
duration: float = None,
|
|
369
|
+
repeat: bool = True,
|
|
370
|
+
):
|
|
371
|
+
"""Upload a uniform noise pattern of pattern_duration [s]. Repeats by default."""
|
|
372
|
+
Ts = self._tg._Ts
|
|
373
|
+
n = int(pattern_duration / Ts)
|
|
374
|
+
self._upload(
|
|
375
|
+
offset + 2 * amplitude * (np.random.uniform(size=n) - 0.5), duration
|
|
376
|
+
)
|
|
377
|
+
self.repeat = repeat
|
|
378
|
+
|
|
379
|
+
def normal_white_noise(
|
|
380
|
+
self,
|
|
381
|
+
amplitude: float,
|
|
382
|
+
pattern_duration: float,
|
|
383
|
+
offset: float = 0,
|
|
384
|
+
duration: float = None,
|
|
385
|
+
repeat: bool = True,
|
|
386
|
+
):
|
|
387
|
+
"""Upload a normal noise pattern of pattern_duration [s]. Repeats by default."""
|
|
388
|
+
Ts = self._tg._Ts
|
|
389
|
+
n = int(pattern_duration / Ts)
|
|
390
|
+
self._upload(offset + 2 * amplitude * np.random.randn(n), duration)
|
|
391
|
+
self.repeat = repeat
|
|
392
|
+
|
|
393
|
+
def multisine(
|
|
394
|
+
self,
|
|
395
|
+
amplitude: float,
|
|
396
|
+
pattern_duration: float,
|
|
397
|
+
f_min: float = None,
|
|
398
|
+
f_max: float = None,
|
|
399
|
+
offset: float = 0,
|
|
400
|
+
duration: float = None,
|
|
401
|
+
asd: np.ndarray = None,
|
|
402
|
+
phase_type: str = "schroeder",
|
|
403
|
+
repeat: bool = True,
|
|
404
|
+
):
|
|
405
|
+
"""Upload a multi-sine excitation signal.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
amplitude: Peak amplitude of the generated signal [unit].
|
|
409
|
+
The signal is scaled so max(|signal|) == amplitude.
|
|
410
|
+
pattern_duration: Length of one signal period [s].
|
|
411
|
+
f_min: Lower frequency bound [Hz]. Bins below are zeroed.
|
|
412
|
+
f_max: Upper frequency bound [Hz]. Must not exceed Fs/2.
|
|
413
|
+
offset: DC offset added to the signal.
|
|
414
|
+
duration: Total playback duration [s]. Defaults to pattern_duration.
|
|
415
|
+
asd: Optional shaped ASD array (relative weights), length Ns=int(pattern_duration/Ts).
|
|
416
|
+
When provided, shapes the frequency content; amplitude still sets the peak.
|
|
417
|
+
phase_type: 'schroeder' or 'normal'.
|
|
418
|
+
repeat: If True, the pattern cycles. Default True.
|
|
419
|
+
"""
|
|
420
|
+
Ts = self._tg._Ts
|
|
421
|
+
Fs = 1.0 / Ts
|
|
422
|
+
Ns = int(pattern_duration / Ts)
|
|
423
|
+
|
|
424
|
+
if f_max is not None and f_max > Fs / 2:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"f_max ({f_max} Hz) exceeds Nyquist frequency ({Fs / 2} Hz)"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if asd is None:
|
|
430
|
+
asd = np.ones(Ns, dtype=float)
|
|
431
|
+
if f_min is not None:
|
|
432
|
+
asd[: round(Ns * f_min / Fs)] = 0
|
|
433
|
+
if f_max is not None:
|
|
434
|
+
asd[round(Ns * f_max / Fs) + 1 :] = 0
|
|
435
|
+
|
|
436
|
+
signal = _generate_multisine(Fs, Ns, asd=asd, phase_type=phase_type)
|
|
437
|
+
signal *= amplitude / np.max(np.abs(signal))
|
|
438
|
+
self._upload(offset + signal, duration)
|
|
439
|
+
self.repeat = repeat
|
|
440
|
+
|
|
441
|
+
def step(
|
|
442
|
+
self,
|
|
443
|
+
amplitude: float,
|
|
444
|
+
duration: float,
|
|
445
|
+
offset: float = 0,
|
|
446
|
+
start_time: float = 0,
|
|
447
|
+
repeat: bool = False,
|
|
448
|
+
):
|
|
449
|
+
Ts = self._tg._Ts
|
|
450
|
+
t = np.arange(0, duration, Ts)
|
|
451
|
+
self._upload(
|
|
452
|
+
self._prepend_zeros(offset + np.full_like(t, amplitude), start_time, Ts),
|
|
453
|
+
start_time + duration,
|
|
454
|
+
)
|
|
455
|
+
self.repeat = repeat
|
|
456
|
+
|
|
457
|
+
def impulse(
|
|
458
|
+
self,
|
|
459
|
+
amplitude: float,
|
|
460
|
+
start_time: float = 0,
|
|
461
|
+
offset: float = 0,
|
|
462
|
+
repeat: bool = False,
|
|
463
|
+
):
|
|
464
|
+
Ts = self._tg._Ts
|
|
465
|
+
start_idx = int(start_time / Ts)
|
|
466
|
+
signal = np.zeros(start_idx + 1)
|
|
467
|
+
signal[start_idx] = amplitude
|
|
468
|
+
self._upload(offset + signal, start_time + Ts)
|
|
469
|
+
self.repeat = repeat
|
|
470
|
+
|
|
471
|
+
def ramp(
|
|
472
|
+
self,
|
|
473
|
+
amplitude: float,
|
|
474
|
+
duration: float,
|
|
475
|
+
offset: float = 0,
|
|
476
|
+
start_time: float = 0,
|
|
477
|
+
repeat: bool = False,
|
|
478
|
+
):
|
|
479
|
+
Ts = self._tg._Ts
|
|
480
|
+
t = np.arange(0, duration, Ts)
|
|
481
|
+
self._upload(
|
|
482
|
+
self._prepend_zeros(offset + amplitude * (t / duration), start_time, Ts),
|
|
483
|
+
start_time + duration,
|
|
484
|
+
)
|
|
485
|
+
self.repeat = repeat
|
|
486
|
+
|
|
487
|
+
def linear_sweep(
|
|
488
|
+
self,
|
|
489
|
+
amplitude: float,
|
|
490
|
+
duration: float,
|
|
491
|
+
start_freq: float,
|
|
492
|
+
end_freq: float,
|
|
493
|
+
offset: float = 0,
|
|
494
|
+
start_time: float = 0,
|
|
495
|
+
repeat: bool = False,
|
|
496
|
+
):
|
|
497
|
+
Ts = self._tg._Ts
|
|
498
|
+
t = np.arange(0, duration, Ts)
|
|
499
|
+
signal = amplitude * chirp(
|
|
500
|
+
t, f0=start_freq, f1=end_freq, t1=duration, method="linear"
|
|
501
|
+
)
|
|
502
|
+
self._upload(
|
|
503
|
+
self._prepend_zeros(offset + signal, start_time, Ts),
|
|
504
|
+
start_time + duration,
|
|
505
|
+
)
|
|
506
|
+
self.repeat = repeat
|
|
507
|
+
|
|
508
|
+
def log_sweep(
|
|
509
|
+
self,
|
|
510
|
+
amplitude: float,
|
|
511
|
+
duration: float,
|
|
512
|
+
start_freq: float,
|
|
513
|
+
end_freq: float,
|
|
514
|
+
offset: float = 0,
|
|
515
|
+
start_time: float = 0,
|
|
516
|
+
repeat: bool = False,
|
|
517
|
+
):
|
|
518
|
+
Ts = self._tg._Ts
|
|
519
|
+
t = np.arange(0, duration, Ts)
|
|
520
|
+
signal = amplitude * chirp(
|
|
521
|
+
t, f0=start_freq, f1=end_freq, t1=duration, method="logarithmic"
|
|
522
|
+
)
|
|
523
|
+
self._upload(
|
|
524
|
+
self._prepend_zeros(offset + signal, start_time, Ts),
|
|
525
|
+
start_time + duration,
|
|
526
|
+
)
|
|
527
|
+
self.repeat = repeat
|
|
528
|
+
|
|
529
|
+
# --- plot ---
|
|
530
|
+
|
|
531
|
+
def plot(self, directory=None, file_name=None):
|
|
532
|
+
|
|
533
|
+
import matplotlib.pyplot as plt
|
|
534
|
+
|
|
535
|
+
Ts = self._tg._Ts
|
|
536
|
+
n_active = round(self.duration / Ts)
|
|
537
|
+
|
|
538
|
+
if self.repeat:
|
|
539
|
+
n_period = self._period_samples
|
|
540
|
+
period_signal = self.samples[:n_period]
|
|
541
|
+
n_repeats = int(np.ceil(n_active / n_period))
|
|
542
|
+
signal_plot = np.tile(period_signal, n_repeats)[:n_active]
|
|
543
|
+
x = np.arange(len(signal_plot)) * Ts
|
|
544
|
+
fig, ax = plt.subplots(figsize=(10, 3))
|
|
545
|
+
ax.plot(x, signal_plot)
|
|
546
|
+
for k in range(1, n_repeats):
|
|
547
|
+
ax.axvline(
|
|
548
|
+
k * n_period * Ts,
|
|
549
|
+
color="r",
|
|
550
|
+
linestyle="--",
|
|
551
|
+
alpha=0.6,
|
|
552
|
+
label="Period boundary" if k == 1 else "",
|
|
553
|
+
)
|
|
554
|
+
if n_repeats > 1:
|
|
555
|
+
ax.legend()
|
|
556
|
+
ax.set_title(
|
|
557
|
+
f"Signal (repeat, period={self.period:.3f} s, duration={self.duration:.3f} s)"
|
|
558
|
+
)
|
|
559
|
+
else:
|
|
560
|
+
signal_plot = self.samples[:n_active]
|
|
561
|
+
x = np.arange(len(signal_plot)) * Ts
|
|
562
|
+
fig, ax = plt.subplots(figsize=(10, 3))
|
|
563
|
+
ax.plot(x, signal_plot)
|
|
564
|
+
ax.set_title(f"Signal (duration={self.duration:.3f} s)")
|
|
565
|
+
|
|
566
|
+
ax.set_xlabel("Time [s]")
|
|
567
|
+
ax.set_ylabel("Amplitude")
|
|
568
|
+
ax.grid(True)
|
|
569
|
+
plt.tight_layout()
|
|
570
|
+
|
|
571
|
+
if directory is not None and file_name is not None:
|
|
572
|
+
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
573
|
+
fig.savefig(f"{directory}/{now}_{file_name}.pdf")
|
|
574
|
+
fig.savefig(f"{directory}/{now}_{file_name}.png")
|
|
575
|
+
plt.close(fig)
|
|
576
|
+
else:
|
|
577
|
+
plt.show()
|