pyglaze 0.2.1__py3-none-any.whl → 0.3.0__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.
- pyglaze/__init__.py +1 -1
- pyglaze/datamodels/pulse.py +173 -124
- pyglaze/datamodels/waveform.py +1 -1
- pyglaze/device/__init__.py +1 -1
- pyglaze/device/ampcom.py +15 -7
- pyglaze/devtools/mock_device.py +11 -1
- pyglaze/devtools/thz_pulse.py +1 -1
- pyglaze/helpers/utilities.py +3 -3
- pyglaze/interpolation/__init__.py +2 -2
- pyglaze/interpolation/interpolation.py +22 -2
- pyglaze/scanning/_asyncscanner.py +3 -4
- pyglaze/scanning/_exceptions.py +8 -0
- pyglaze/scanning/scanner.py +45 -6
- {pyglaze-0.2.1.dist-info → pyglaze-0.3.0.dist-info}/METADATA +7 -7
- pyglaze-0.3.0.dist-info/RECORD +26 -0
- {pyglaze-0.2.1.dist-info → pyglaze-0.3.0.dist-info}/WHEEL +1 -1
- pyglaze-0.2.1.dist-info/RECORD +0 -25
- /pyglaze/helpers/{types.py → _types.py} +0 -0
- {pyglaze-0.2.1.dist-info → pyglaze-0.3.0.dist-info}/LICENSE +0 -0
- {pyglaze-0.2.1.dist-info → pyglaze-0.3.0.dist-info}/top_level.txt +0 -0
pyglaze/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.3.0"
|
pyglaze/datamodels/pulse.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from
|
|
5
|
-
from typing import Literal, cast
|
|
4
|
+
from typing import Callable, Literal, cast
|
|
6
5
|
|
|
7
6
|
import numpy as np
|
|
7
|
+
from scipy import optimize as opt
|
|
8
8
|
from scipy import signal
|
|
9
|
+
from scipy.stats import linregress
|
|
9
10
|
|
|
10
|
-
from pyglaze.helpers.
|
|
11
|
+
from pyglaze.helpers._types import ComplexArray, FloatArray
|
|
11
12
|
from pyglaze.interpolation import ws_interpolate
|
|
12
13
|
|
|
13
14
|
__all__ = ["Pulse"]
|
|
@@ -20,12 +21,10 @@ class Pulse:
|
|
|
20
21
|
Args:
|
|
21
22
|
time: The time values recorded by the lock-in amp during the scan.
|
|
22
23
|
signal: The signal values recorded by the lock-in amp during the scan.
|
|
23
|
-
signal_err: Potential errors on signal
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
time: FloatArray
|
|
27
27
|
signal: FloatArray
|
|
28
|
-
signal_err: FloatArray | None = None
|
|
29
28
|
|
|
30
29
|
def __len__(self: Pulse) -> int: # noqa: D105
|
|
31
30
|
return len(self.time)
|
|
@@ -38,15 +37,14 @@ class Pulse:
|
|
|
38
37
|
return bool(
|
|
39
38
|
np.array_equal(self.time, obj.time)
|
|
40
39
|
and np.array_equal(self.signal, obj.signal)
|
|
41
|
-
and np.array_equal(self.signal_err, obj.signal_err) # type: ignore[arg-type]
|
|
42
40
|
)
|
|
43
41
|
|
|
44
|
-
@
|
|
42
|
+
@property
|
|
45
43
|
def fft(self: Pulse) -> ComplexArray:
|
|
46
44
|
"""Return the Fourier Transform of a signal."""
|
|
47
45
|
return np.fft.rfft(self.signal, norm="forward")
|
|
48
46
|
|
|
49
|
-
@
|
|
47
|
+
@property
|
|
50
48
|
def frequency(self: Pulse) -> FloatArray:
|
|
51
49
|
"""Return the Fourier Transform sample frequencies."""
|
|
52
50
|
return np.fft.rfftfreq(len(self.signal), d=self.time[1] - self.time[0])
|
|
@@ -91,6 +89,14 @@ class Pulse:
|
|
|
91
89
|
"""Time delay at the minimum value of the pulse."""
|
|
92
90
|
return float(self.time[np.argmin(self.signal)])
|
|
93
91
|
|
|
92
|
+
@property
|
|
93
|
+
def energy(self: Pulse) -> float:
|
|
94
|
+
"""Energy of the pulse.
|
|
95
|
+
|
|
96
|
+
Note that the energy is not the same as the physical energy of the pulse, but rather the integral of the square of the pulse.
|
|
97
|
+
"""
|
|
98
|
+
return cast(float, np.trapz(self.signal * self.signal, x=self.time)) # noqa: NPY201 - trapz removed in numpy 2.0
|
|
99
|
+
|
|
94
100
|
@classmethod
|
|
95
101
|
def from_dict(
|
|
96
102
|
cls: type[Pulse], d: dict[str, FloatArray | list[float] | None]
|
|
@@ -98,12 +104,9 @@ class Pulse:
|
|
|
98
104
|
"""Create a Pulse object from a dictionary.
|
|
99
105
|
|
|
100
106
|
Args:
|
|
101
|
-
d: A dictionary containing the keys 'time', 'signal'
|
|
107
|
+
d: A dictionary containing the keys 'time', 'signal'.
|
|
102
108
|
"""
|
|
103
|
-
|
|
104
|
-
return Pulse(
|
|
105
|
-
time=np.array(d["time"]), signal=np.array(d["signal"]), signal_err=err
|
|
106
|
-
)
|
|
109
|
+
return Pulse(time=np.array(d["time"]), signal=np.array(d["signal"]))
|
|
107
110
|
|
|
108
111
|
@classmethod
|
|
109
112
|
def from_fft(cls: type[Pulse], time: FloatArray, fft: ComplexArray) -> Pulse:
|
|
@@ -129,10 +132,7 @@ class Pulse:
|
|
|
129
132
|
return scans[0]
|
|
130
133
|
signals = np.array([scan.signal for scan in scans])
|
|
131
134
|
mean_signal = np.mean(signals, axis=0)
|
|
132
|
-
|
|
133
|
-
root_n_scans = np.sqrt(len(scans))
|
|
134
|
-
std_signal = np.std(signals, axis=0, ddof=1) / root_n_scans
|
|
135
|
-
return Pulse(scans[0].time, mean_signal, signal_err=std_signal)
|
|
135
|
+
return Pulse(scans[0].time, mean_signal)
|
|
136
136
|
|
|
137
137
|
@classmethod
|
|
138
138
|
def align(
|
|
@@ -142,11 +142,11 @@ class Pulse:
|
|
|
142
142
|
wrt_max: bool = True,
|
|
143
143
|
translate_to_zero: bool = True,
|
|
144
144
|
) -> list[Pulse]:
|
|
145
|
-
"""Aligns a list of
|
|
145
|
+
"""Aligns a list of pulses with respect to the zerocrossings of their main pulse.
|
|
146
146
|
|
|
147
147
|
Args:
|
|
148
148
|
scans: List of scans
|
|
149
|
-
wrt_max: Whether to
|
|
149
|
+
wrt_max: Whether to perform rough alignment with respect to their maximum (true) or minimum(false). Defaults to True.
|
|
150
150
|
translate_to_zero: Whether to translate all scans to t[0] = 0. Defaults to True.
|
|
151
151
|
|
|
152
152
|
Returns:
|
|
@@ -163,15 +163,17 @@ class Pulse:
|
|
|
163
163
|
if translate_to_zero:
|
|
164
164
|
for scan in roughly_aligned:
|
|
165
165
|
scan.time = scan.time - scan.time[0]
|
|
166
|
+
zerocrossings = [p.estimate_zero_crossing() for p in roughly_aligned]
|
|
167
|
+
mean_zerocrossing = cast(float, np.mean(zerocrossings))
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
return [
|
|
170
|
+
p.propagate(mean_zerocrossing - zc)
|
|
171
|
+
for p, zc in zip(roughly_aligned, zerocrossings)
|
|
172
|
+
]
|
|
170
173
|
|
|
171
174
|
@classmethod
|
|
172
175
|
def _from_slice(cls: type[Pulse], scan: Pulse, indices: slice) -> Pulse:
|
|
173
|
-
|
|
174
|
-
return cls(scan.time[indices], scan.signal[indices], err)
|
|
176
|
+
return cls(scan.time[indices], scan.signal[indices])
|
|
175
177
|
|
|
176
178
|
def cut(self: Pulse, from_time: float, to_time: float) -> Pulse:
|
|
177
179
|
"""Create a Pulse object by cutting out a specific section of the scan.
|
|
@@ -182,11 +184,18 @@ class Pulse:
|
|
|
182
184
|
"""
|
|
183
185
|
from_idx = int(np.searchsorted(self.time, from_time))
|
|
184
186
|
to_idx = int(np.searchsorted(self.time, to_time, side="right"))
|
|
185
|
-
return Pulse(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
return Pulse(self.time[from_idx:to_idx], self.signal[from_idx:to_idx])
|
|
188
|
+
|
|
189
|
+
def fft_at_f(self: Pulse, f: float) -> complex:
|
|
190
|
+
"""Returns the Fourier Transform at a specific frequency.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
f: Frequency in Hz
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
complex: Fourier Transform at the given frequency
|
|
197
|
+
"""
|
|
198
|
+
return cast(complex, self.fft[np.searchsorted(self.frequency, f)])
|
|
190
199
|
|
|
191
200
|
def timeshift(self: Pulse, scale: float, offset: float = 0) -> Pulse:
|
|
192
201
|
"""Rescales and offsets the time axis as.
|
|
@@ -200,11 +209,7 @@ class Pulse:
|
|
|
200
209
|
Returns:
|
|
201
210
|
Timeshifted pulse
|
|
202
211
|
"""
|
|
203
|
-
return Pulse(
|
|
204
|
-
time=scale * (self.time + offset),
|
|
205
|
-
signal=self.signal,
|
|
206
|
-
signal_err=self.signal_err,
|
|
207
|
-
)
|
|
212
|
+
return Pulse(time=scale * (self.time + offset), signal=self.signal)
|
|
208
213
|
|
|
209
214
|
def add_white_noise(
|
|
210
215
|
self: Pulse, noise_std: float, seed: int | None = None
|
|
@@ -224,7 +229,6 @@ class Pulse:
|
|
|
224
229
|
+ np.random.default_rng(seed).normal(
|
|
225
230
|
loc=0, scale=noise_std, size=len(self)
|
|
226
231
|
),
|
|
227
|
-
signal_err=np.ones(len(self)) * noise_std,
|
|
228
232
|
)
|
|
229
233
|
|
|
230
234
|
def zeropadded(self: Pulse, n_zeros: int) -> Pulse:
|
|
@@ -242,6 +246,28 @@ class Pulse:
|
|
|
242
246
|
)
|
|
243
247
|
return Pulse(time=zeropadded_time, signal=zeropadded_signal)
|
|
244
248
|
|
|
249
|
+
def signal_at_t(self: Pulse, t: float) -> float:
|
|
250
|
+
"""Returns the signal at a specific time using Whittaker Shannon interpolation.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
t: Time in seconds
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Signal at the given time
|
|
257
|
+
"""
|
|
258
|
+
return cast(float, ws_interpolate(self.time, self.signal, np.array([t]))[0])
|
|
259
|
+
|
|
260
|
+
def subtract_mean(self: Pulse, fraction: float = 0.99) -> Pulse:
|
|
261
|
+
"""Subtracts the mean of the pulse.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
fraction: Fraction of the mean to subtract. Defaults to 0.99.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Pulse with the mean subtracted
|
|
268
|
+
"""
|
|
269
|
+
return Pulse(self.time, self.signal - fraction * np.mean(self.signal))
|
|
270
|
+
|
|
245
271
|
def tukey(
|
|
246
272
|
self: Pulse,
|
|
247
273
|
taper_length: float,
|
|
@@ -338,53 +364,52 @@ class Pulse:
|
|
|
338
364
|
20 * np.log10((abs_spectrum + offset) / ref), dtype=np.float64
|
|
339
365
|
)
|
|
340
366
|
|
|
341
|
-
def estimate_bandwidth(self: Pulse,
|
|
367
|
+
def estimate_bandwidth(self: Pulse, linear_segments: int = 1) -> float:
|
|
342
368
|
"""Estimates the bandwidth of the pulse.
|
|
343
369
|
|
|
344
|
-
|
|
370
|
+
The bandwidth is estimated by modelling the log of the pulse's spectrum above the center frequency as a constant noisefloor and n linear segments of equal size. The bandwidth is then defined as the frequency at which the noisefloor is reached.
|
|
345
371
|
|
|
346
372
|
Args:
|
|
347
|
-
|
|
373
|
+
linear_segments: Number of linear segments to fit to the spectrum. Defaults to 1.
|
|
348
374
|
|
|
349
375
|
Returns:
|
|
350
376
|
float: Estimated bandwidth in Hz
|
|
351
377
|
"""
|
|
352
|
-
return self._estimate_pulse_properties(
|
|
378
|
+
return self._estimate_pulse_properties(linear_segments)[0]
|
|
353
379
|
|
|
354
|
-
def estimate_dynamic_range(self: Pulse,
|
|
380
|
+
def estimate_dynamic_range(self: Pulse, linear_segments: int = 1) -> float:
|
|
355
381
|
"""Estimates the dynamic range of the pulse.
|
|
356
382
|
|
|
357
|
-
|
|
383
|
+
The dynamic range is estimated by modelling the log of the pulse's spectrum above the center frequency as a constant noisefloor and n linear segments of equal size. The dynamic range is then calculated as the maximum of the spectrum minus the noisefloor.
|
|
358
384
|
|
|
359
385
|
Args:
|
|
360
|
-
|
|
386
|
+
linear_segments: Number of linear segments to fit to the spectrum. Defaults to 1.
|
|
361
387
|
|
|
362
388
|
Returns:
|
|
363
389
|
float: Estimated dynamic range in dB
|
|
364
390
|
"""
|
|
365
|
-
return self._estimate_pulse_properties(
|
|
391
|
+
return self._estimate_pulse_properties(linear_segments)[1]
|
|
366
392
|
|
|
367
|
-
def estimate_avg_noise_power(self: Pulse,
|
|
393
|
+
def estimate_avg_noise_power(self: Pulse, linear_segments: int = 1) -> float:
|
|
368
394
|
"""Estimates the noise power.
|
|
369
395
|
|
|
370
|
-
Noise power is
|
|
371
|
-
Uses the approach described in [Algorithm for Determination of Cutoff Frequency of Noise Floor Level for Terahertz Time-Domain Signals](https://doi.org/10.1007/s10762-022-00886-y).
|
|
396
|
+
The noise power is estimated by modelling the the log of pulse's spectrum above the center frequency as a constant noisefloor and n linear segments of equal size. Noise power is then calculated as the mean of the absolute square of the spectral bins above the frequency at which the noise floor is reached.
|
|
372
397
|
|
|
373
398
|
Args:
|
|
374
|
-
|
|
399
|
+
linear_segments: Number of linear segments to fit to the spectrum. Defaults to 1.
|
|
375
400
|
|
|
376
401
|
Returns:
|
|
377
402
|
float: Estimated noise power.
|
|
378
403
|
"""
|
|
379
|
-
return self._estimate_pulse_properties(
|
|
404
|
+
return self._estimate_pulse_properties(linear_segments)[2]
|
|
380
405
|
|
|
381
|
-
def estimate_SNR(self: Pulse,
|
|
406
|
+
def estimate_SNR(self: Pulse, linear_segments: int = 1) -> FloatArray:
|
|
382
407
|
"""Estimates the signal-to-noise ratio.
|
|
383
408
|
|
|
384
|
-
Estimates the SNR, assuming white noise.
|
|
409
|
+
Estimates the SNR, assuming white noise. The noisefloor is estimated by modelling the log of the pulse's spectrum above the center frequency as a constant noisefloor and n linear segments of equal size. Noise power is then calculated as the mean of the absolute square of the spectral bins above the frequency at which the noise floor is reached. The signal power is then extrapolated above the bandwidth by fitting a second order polynomial to the spectrum above the noisefloor.
|
|
385
410
|
|
|
386
411
|
Args:
|
|
387
|
-
|
|
412
|
+
linear_segments: Number of linear segments to fit to the spectrum. Defaults to 1.
|
|
388
413
|
|
|
389
414
|
Returns:
|
|
390
415
|
float: Estimated signal-to-noise ratio.
|
|
@@ -392,7 +417,7 @@ class Pulse:
|
|
|
392
417
|
# Get spectrum between maximum and noisefloor
|
|
393
418
|
_from = np.argmax(self.spectrum_dB())
|
|
394
419
|
_to = np.searchsorted(
|
|
395
|
-
self.frequency, self.estimate_bandwidth(
|
|
420
|
+
self.frequency, self.estimate_bandwidth(linear_segments=linear_segments)
|
|
396
421
|
)
|
|
397
422
|
x = self.frequency[_from:_to]
|
|
398
423
|
y = self.spectrum_dB()[_from:_to]
|
|
@@ -411,10 +436,16 @@ class Pulse:
|
|
|
411
436
|
),
|
|
412
437
|
)
|
|
413
438
|
signal_power = 10 ** (y_values / 10) * self.maximum_spectral_density**2
|
|
414
|
-
return signal_power / self.estimate_avg_noise_power(
|
|
439
|
+
return signal_power / self.estimate_avg_noise_power(
|
|
440
|
+
linear_segments=linear_segments
|
|
441
|
+
)
|
|
415
442
|
|
|
416
443
|
def estimate_peak_to_peak(
|
|
417
|
-
self: Pulse,
|
|
444
|
+
self: Pulse,
|
|
445
|
+
delay_tolerance: float | None = None,
|
|
446
|
+
strategy: Callable[
|
|
447
|
+
[FloatArray, FloatArray, FloatArray], FloatArray
|
|
448
|
+
] = ws_interpolate,
|
|
418
449
|
) -> float:
|
|
419
450
|
"""Estimates the peak-to-peak value of the pulse.
|
|
420
451
|
|
|
@@ -422,6 +453,7 @@ class Pulse:
|
|
|
422
453
|
|
|
423
454
|
Args:
|
|
424
455
|
delay_tolerance: Tolerance for peak detection. Defaults to None.
|
|
456
|
+
strategy: Interpolation strategy. Defaults to Whittaker-Shannon interpolation
|
|
425
457
|
|
|
426
458
|
Returns:
|
|
427
459
|
float: Estimated peak-to-peak value.
|
|
@@ -433,10 +465,10 @@ class Pulse:
|
|
|
433
465
|
msg = "Tolerance must be smaller than the time spacing of the pulse."
|
|
434
466
|
raise ValueError(msg)
|
|
435
467
|
|
|
436
|
-
max_estimate =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
468
|
+
max_estimate = strategy(
|
|
469
|
+
self.time,
|
|
470
|
+
self.signal,
|
|
471
|
+
np.linspace(
|
|
440
472
|
self.delay_at_max - self.dt,
|
|
441
473
|
self.delay_at_max + self.dt,
|
|
442
474
|
num=1 + int(self.dt / delay_tolerance),
|
|
@@ -444,10 +476,10 @@ class Pulse:
|
|
|
444
476
|
),
|
|
445
477
|
)
|
|
446
478
|
|
|
447
|
-
min_estimate =
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
479
|
+
min_estimate = strategy(
|
|
480
|
+
self.time,
|
|
481
|
+
self.signal,
|
|
482
|
+
np.linspace(
|
|
451
483
|
self.delay_at_min - self.dt,
|
|
452
484
|
self.delay_at_min + self.dt,
|
|
453
485
|
num=1 + int(self.dt / delay_tolerance),
|
|
@@ -475,85 +507,102 @@ class Pulse:
|
|
|
475
507
|
a = (self.signal[idx + 1] - self.signal[idx]) / self.dt
|
|
476
508
|
return cast(float, t1 - s1 / a)
|
|
477
509
|
|
|
510
|
+
def propagate(self: Pulse, time: float) -> Pulse:
|
|
511
|
+
"""Propagates the pulse in time by a given amount.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
time: Time in seconds to propagate the pulse by
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Pulse: Propagated pulse
|
|
518
|
+
"""
|
|
519
|
+
return Pulse.from_fft(
|
|
520
|
+
time=self.time,
|
|
521
|
+
fft=self.fft * np.exp(-1j * 2 * np.pi * self.frequency * time),
|
|
522
|
+
)
|
|
523
|
+
|
|
478
524
|
def to_native_dict(self: Pulse) -> dict[str, list[float] | None]:
|
|
479
525
|
"""Converts the Pulse object to a native dictionary.
|
|
480
526
|
|
|
481
527
|
Returns:
|
|
482
528
|
Native dictionary representation of the Pulse object.
|
|
483
529
|
"""
|
|
484
|
-
return {
|
|
485
|
-
"time": list(self.time),
|
|
486
|
-
"signal": list(self.signal),
|
|
487
|
-
"signal_err": None if self.signal_err is None else list(self.signal_err),
|
|
488
|
-
}
|
|
530
|
+
return {"time": list(self.time), "signal": list(self.signal)}
|
|
489
531
|
|
|
490
532
|
def _get_min_or_max_idx(self: Pulse, *, wrt_max: bool) -> int:
|
|
491
533
|
return int(np.argmax(self.signal)) if wrt_max else int(np.argmin(self.signal))
|
|
492
534
|
|
|
493
535
|
def _estimate_pulse_properties(
|
|
494
|
-
self: Pulse,
|
|
536
|
+
self: Pulse, linear_segments: int
|
|
495
537
|
) -> tuple[float, float, float]:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
avg_noise_power = np.mean(abs_spectrum[
|
|
538
|
+
mean_substracted = self.subtract_mean()
|
|
539
|
+
argmax = np.argmax(np.abs(mean_substracted.fft))
|
|
540
|
+
freqs = mean_substracted.frequency[argmax:]
|
|
541
|
+
abs_spectrum = np.abs(mean_substracted.fft[argmax:])
|
|
542
|
+
bw_idx_estimate = _estimate_bw_idx(freqs, abs_spectrum, linear_segments)
|
|
543
|
+
avg_noise_power = np.mean(abs_spectrum[bw_idx_estimate:] ** 2)
|
|
502
544
|
noisefloor = np.sqrt(avg_noise_power)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
# to an index in the original array
|
|
507
|
-
cutoff_idx = noisefloor_idx_estimate - np.searchsorted(
|
|
508
|
-
np.flip(abs_spectrum[: noisefloor_idx_estimate + 1]),
|
|
509
|
-
noisefloor,
|
|
510
|
-
side="right",
|
|
545
|
+
bandwidth = freqs[bw_idx_estimate]
|
|
546
|
+
dynamic_range_dB = 20 * np.log10(
|
|
547
|
+
mean_substracted.maximum_spectral_density / noisefloor
|
|
511
548
|
)
|
|
512
|
-
bandwidth = freqs[cutoff_idx]
|
|
513
|
-
dynamic_range_dB = 20 * np.log10(self.maximum_spectral_density / noisefloor)
|
|
514
549
|
return bandwidth, dynamic_range_dB, avg_noise_power
|
|
515
550
|
|
|
516
551
|
|
|
517
|
-
def
|
|
518
|
-
|
|
519
|
-
) -> list[Pulse]:
|
|
520
|
-
# corresponds to a template of length 8 - chosen as a compromise between speed and accuracy
|
|
521
|
-
window_size = 4
|
|
522
|
-
ref_slice = slice(extremum - window_size, extremum + window_size)
|
|
552
|
+
def _estimate_bw_idx(x: FloatArray, y: FloatArray, segments: int) -> int:
|
|
553
|
+
"""Estimate the noise floor of a spectrum.
|
|
523
554
|
|
|
524
|
-
|
|
525
|
-
|
|
555
|
+
Args:
|
|
556
|
+
x: Frequency values
|
|
557
|
+
y: Spectrum values
|
|
558
|
+
segments: Number of linear segments to fit
|
|
526
559
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
slice(extremum - window_size + i, extremum + window_size + i)
|
|
532
|
-
for i in cut_candidates
|
|
533
|
-
]
|
|
534
|
-
cuts[i_scan] = cut_candidates[
|
|
535
|
-
np.argmax(
|
|
536
|
-
[correlate(scan.signal[s], ref.signal[ref_slice]) for s in slices]
|
|
537
|
-
)
|
|
538
|
-
]
|
|
560
|
+
Returns:
|
|
561
|
+
float: Estimated noise floor
|
|
562
|
+
"""
|
|
563
|
+
target = np.log(y)
|
|
539
564
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
565
|
+
def L1(x: FloatArray, y: FloatArray) -> FloatArray:
|
|
566
|
+
return np.sum(np.abs(y - x)) # type: ignore[no-any-return]
|
|
567
|
+
|
|
568
|
+
def model(pars: list[float]) -> FloatArray:
|
|
569
|
+
idx = np.searchsorted(x, pars[0])
|
|
570
|
+
x_before = x[:idx]
|
|
571
|
+
x_after = x[idx:]
|
|
572
|
+
target_before = target[:idx]
|
|
573
|
+
target_after = target[idx:]
|
|
574
|
+
y_fit = _fit_linear_segments(x_before, target_before, segments)
|
|
575
|
+
noise = np.ones(len(x_after)) * y_fit[-1]
|
|
576
|
+
return L1(y_fit, target_before) + L1(noise, target_after)
|
|
577
|
+
|
|
578
|
+
BW_estimate = opt.minimize(
|
|
579
|
+
fun=model, x0=[x[len(x) // 2]], bounds=[(x[0], x[-1])], method="Nelder-Mead"
|
|
580
|
+
).x[0]
|
|
581
|
+
|
|
582
|
+
return cast(int, x.searchsorted(BW_estimate))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _fit_linear_segments(x: FloatArray, y: FloatArray, n_segments: int) -> FloatArray:
|
|
586
|
+
"""Fit a pulse with a piecewise linear function.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
x: Time values
|
|
590
|
+
y: Signal values
|
|
591
|
+
n_segments: Number of segments to fit
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
FloatArray: Fitted signal
|
|
595
|
+
"""
|
|
596
|
+
segment_indices = np.linspace(0, len(x), n_segments + 1, dtype=int)
|
|
597
|
+
y_fit = np.zeros_like(y)
|
|
598
|
+
|
|
599
|
+
for i in range(n_segments):
|
|
600
|
+
# Get the indices for this segment
|
|
601
|
+
start, end = segment_indices[i], segment_indices[i + 1]
|
|
602
|
+
x_segment = x[start:end]
|
|
603
|
+
y_segment = y[start:end]
|
|
604
|
+
|
|
605
|
+
# Fit a linear function to this segment
|
|
606
|
+
slope, intercept, _, _, _ = linregress(x_segment, y_segment)
|
|
607
|
+
y_fit[start:end] = slope * x_segment + intercept
|
|
608
|
+
return y_fit
|
pyglaze/datamodels/waveform.py
CHANGED
pyglaze/device/__init__.py
CHANGED
pyglaze/device/ampcom.py
CHANGED
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
LeMockDevice,
|
|
30
30
|
MockDevice,
|
|
31
31
|
)
|
|
32
|
-
from pyglaze.helpers.
|
|
32
|
+
from pyglaze.helpers._types import FloatArray
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class DeviceComError(Exception):
|
|
@@ -105,9 +105,7 @@ class _ForceAmpCom:
|
|
|
105
105
|
|
|
106
106
|
def __del__(self: _ForceAmpCom) -> None:
|
|
107
107
|
"""Closes connection when class instance goes out of scope."""
|
|
108
|
-
|
|
109
|
-
# If the serial device does not exist, self.__ser is never created - hence catch
|
|
110
|
-
self.__ser.close()
|
|
108
|
+
self.disconnect()
|
|
111
109
|
|
|
112
110
|
def write_all(self: _ForceAmpCom) -> list[str]:
|
|
113
111
|
responses = []
|
|
@@ -190,6 +188,12 @@ class _ForceAmpCom:
|
|
|
190
188
|
output_array[iteration, 2] = angle
|
|
191
189
|
return output_array
|
|
192
190
|
|
|
191
|
+
def disconnect(self: _ForceAmpCom) -> None:
|
|
192
|
+
"""Closes connection."""
|
|
193
|
+
with contextlib.suppress(AttributeError):
|
|
194
|
+
# If the serial device does not exist, self.__ser is never created - hence catch
|
|
195
|
+
self.__ser.close()
|
|
196
|
+
|
|
193
197
|
def _encode_send_response(self: _ForceAmpCom, command: str) -> str:
|
|
194
198
|
self._encode_and_send(command)
|
|
195
199
|
return self._get_response()
|
|
@@ -264,9 +268,7 @@ class _LeAmpCom:
|
|
|
264
268
|
|
|
265
269
|
def __del__(self: _LeAmpCom) -> None:
|
|
266
270
|
"""Closes connection when class instance goes out of scope."""
|
|
267
|
-
|
|
268
|
-
# If the serial device does not exist, self.__ser is never created - hence catch
|
|
269
|
-
self.__ser.close()
|
|
271
|
+
self.disconnect()
|
|
270
272
|
|
|
271
273
|
def write_all(self: _LeAmpCom) -> list[str]:
|
|
272
274
|
responses: list[str] = []
|
|
@@ -294,6 +296,12 @@ class _LeAmpCom:
|
|
|
294
296
|
radii, angles = self._convert_to_r_angle(Xs, Ys)
|
|
295
297
|
return self.START_COMMAND, np.array(times), np.array(radii), np.array(angles)
|
|
296
298
|
|
|
299
|
+
def disconnect(self: _LeAmpCom) -> None:
|
|
300
|
+
"""Closes connection when class instance goes out of scope."""
|
|
301
|
+
with contextlib.suppress(AttributeError):
|
|
302
|
+
# If the serial device does not exist, self.__ser is never created - hence catch
|
|
303
|
+
self.__ser.close()
|
|
304
|
+
|
|
297
305
|
@cached_property
|
|
298
306
|
def _intervals(self: _LeAmpCom) -> list[Interval]:
|
|
299
307
|
"""Intervals squished into effective DAC range."""
|
pyglaze/devtools/mock_device.py
CHANGED
|
@@ -28,6 +28,7 @@ class MockDevice(ABC):
|
|
|
28
28
|
n_fails: float = np.inf,
|
|
29
29
|
*,
|
|
30
30
|
empty_responses: bool = False,
|
|
31
|
+
instant_response: bool = False,
|
|
31
32
|
) -> None:
|
|
32
33
|
pass
|
|
33
34
|
|
|
@@ -42,6 +43,8 @@ class ForceMockDevice(MockDevice):
|
|
|
42
43
|
self: ForceMockDevice,
|
|
43
44
|
fail_after: float = np.inf,
|
|
44
45
|
n_fails: float = np.inf,
|
|
46
|
+
*,
|
|
47
|
+
instant_response: bool = False,
|
|
45
48
|
) -> None:
|
|
46
49
|
self.fail_after = fail_after
|
|
47
50
|
self.fails_wanted = n_fails
|
|
@@ -50,6 +53,7 @@ class ForceMockDevice(MockDevice):
|
|
|
50
53
|
self.rng = np.random.default_rng()
|
|
51
54
|
self.valid_input = True
|
|
52
55
|
self.experiment_running = False
|
|
56
|
+
self.instant_response = instant_response
|
|
53
57
|
|
|
54
58
|
self._periods = None
|
|
55
59
|
self._frequency = None
|
|
@@ -131,7 +135,8 @@ class ForceMockDevice(MockDevice):
|
|
|
131
135
|
for _ in range(self.in_waiting):
|
|
132
136
|
return_string += self.__create_random_datapoint
|
|
133
137
|
return_string += "!D,DONE\\r"
|
|
134
|
-
|
|
138
|
+
if not self.instant_response:
|
|
139
|
+
sleep(self.sweep_length * 1e-3)
|
|
135
140
|
self.n_scans += 1
|
|
136
141
|
if self.n_scans > self.fail_after and self.n_failures < self.fails_wanted:
|
|
137
142
|
self.n_failures += 1
|
|
@@ -190,6 +195,7 @@ class LeMockDevice(MockDevice):
|
|
|
190
195
|
n_fails: float = np.inf,
|
|
191
196
|
*,
|
|
192
197
|
empty_responses: bool = False,
|
|
198
|
+
instant_response: bool = False,
|
|
193
199
|
) -> None:
|
|
194
200
|
self.fail_after = fail_after
|
|
195
201
|
self.fails_wanted = n_fails
|
|
@@ -204,6 +210,7 @@ class LeMockDevice(MockDevice):
|
|
|
204
210
|
self.scanning_list: list[float] | None = None
|
|
205
211
|
self._scan_start_time: float | None = None
|
|
206
212
|
self.empty_responses = empty_responses
|
|
213
|
+
self.instant_response = instant_response
|
|
207
214
|
|
|
208
215
|
def write(self: LeMockDevice, input_bytes: bytes) -> None:
|
|
209
216
|
"""Mock-write to the serial connection."""
|
|
@@ -365,6 +372,7 @@ def list_mock_devices() -> list[str]:
|
|
|
365
372
|
"mock_device_scan_should_fail",
|
|
366
373
|
"mock_device_fail_first_scan",
|
|
367
374
|
"mock_device_empty_responses",
|
|
375
|
+
"mock_device_instant",
|
|
368
376
|
]
|
|
369
377
|
|
|
370
378
|
|
|
@@ -374,6 +382,8 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
|
|
|
374
382
|
return mock_class(fail_after=0)
|
|
375
383
|
if config.amp_port == "mock_device":
|
|
376
384
|
return mock_class()
|
|
385
|
+
if config.amp_port == "mock_device_instant":
|
|
386
|
+
return mock_class(instant_response=True)
|
|
377
387
|
if config.amp_port == "mock_device_fail_first_scan":
|
|
378
388
|
return mock_class(fail_after=0, n_fails=1)
|
|
379
389
|
if config.amp_port == "mock_device_empty_responses":
|
pyglaze/devtools/thz_pulse.py
CHANGED
pyglaze/helpers/utilities.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Callable, cast
|
|
|
8
8
|
import serial
|
|
9
9
|
import serial.tools.list_ports
|
|
10
10
|
|
|
11
|
-
from pyglaze.helpers.
|
|
11
|
+
from pyglaze.helpers._types import P, T
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
import logging
|
|
@@ -64,11 +64,11 @@ class _BackoffRetry:
|
|
|
64
64
|
raise
|
|
65
65
|
except Exception as e: # noqa: BLE001
|
|
66
66
|
self._log(
|
|
67
|
-
f"{func.__name__} failed {tries+1} time(s) with: '{e}'. Trying again"
|
|
67
|
+
f"{func.__name__} failed {tries + 1} time(s) with: '{e}'. Trying again"
|
|
68
68
|
)
|
|
69
69
|
backoff = min(self.backoff_base * 2**tries, self.max_backoff)
|
|
70
70
|
time.sleep(backoff)
|
|
71
|
-
self._log(f"{func.__name__}: Last try ({tries+2}).")
|
|
71
|
+
self._log(f"{func.__name__}: Last try ({tries + 2}).")
|
|
72
72
|
return cast(T, func(*args, **kwargs))
|
|
73
73
|
|
|
74
74
|
return wrapper
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .interpolation import ws_interpolate
|
|
1
|
+
from .interpolation import cubic_spline_interpolate, ws_interpolate
|
|
2
2
|
|
|
3
|
-
__all__ = ["ws_interpolate"]
|
|
3
|
+
__all__ = ["cubic_spline_interpolate", "ws_interpolate"]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
4
|
+
from scipy.interpolate import CubicSpline
|
|
2
5
|
|
|
3
|
-
from pyglaze.helpers.
|
|
6
|
+
from pyglaze.helpers._types import FloatArray
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def ws_interpolate(
|
|
@@ -21,4 +24,21 @@ def ws_interpolate(
|
|
|
21
24
|
# times must be zero-centered for formula to work
|
|
22
25
|
sinc = np.sinc((interp_times[:, np.newaxis] - times[0] - dt * _range) / dt)
|
|
23
26
|
|
|
24
|
-
return
|
|
27
|
+
return cast(FloatArray, np.sum(pulse * sinc, axis=1))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cubic_spline_interpolate(
|
|
31
|
+
times: FloatArray, pulse: FloatArray, interp_times: FloatArray
|
|
32
|
+
) -> FloatArray:
|
|
33
|
+
"""Performs cubic spline interpolation at the supplied times given a pulse.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
times: Sampling times
|
|
37
|
+
pulse: A sampled pulse satisfying the Nyquist criterion
|
|
38
|
+
interp_times: Array of times at which to interpolate
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
FloatArray: Interpolated values
|
|
42
|
+
"""
|
|
43
|
+
spline = CubicSpline(times, pulse, bc_type="natural")
|
|
44
|
+
return cast(FloatArray, spline(interp_times))
|
|
@@ -87,10 +87,8 @@ class _AsyncScanner:
|
|
|
87
87
|
|
|
88
88
|
return [self._get_scan().waveform for _ in range(n_pulses)]
|
|
89
89
|
|
|
90
|
-
def get_next(self: _AsyncScanner
|
|
91
|
-
return
|
|
92
|
-
[self._get_scan().waveform for _ in range(averaged_over_n)]
|
|
93
|
-
)
|
|
90
|
+
def get_next(self: _AsyncScanner) -> UnprocessedWaveform:
|
|
91
|
+
return self._get_scan().waveform
|
|
94
92
|
|
|
95
93
|
def _get_scan(self: _AsyncScanner) -> _TimestampedWaveform:
|
|
96
94
|
try:
|
|
@@ -127,6 +125,7 @@ class _AsyncScanner:
|
|
|
127
125
|
parent_conn.send(
|
|
128
126
|
_ScannerHealth(is_alive=False, is_healthy=False, error=e)
|
|
129
127
|
)
|
|
128
|
+
scanner.disconnect()
|
|
130
129
|
break
|
|
131
130
|
|
|
132
131
|
try:
|
pyglaze/scanning/scanner.py
CHANGED
|
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
|
+
from serial import SerialException
|
|
7
8
|
|
|
8
9
|
from pyglaze.datamodels import UnprocessedWaveform
|
|
9
10
|
from pyglaze.device.ampcom import _ForceAmpCom, _LeAmpCom
|
|
@@ -12,9 +13,10 @@ from pyglaze.device.configuration import (
|
|
|
12
13
|
ForceDeviceConfiguration,
|
|
13
14
|
LeDeviceConfiguration,
|
|
14
15
|
)
|
|
16
|
+
from pyglaze.scanning._exceptions import ScanError
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
17
|
-
from pyglaze.helpers.
|
|
19
|
+
from pyglaze.helpers._types import FloatArray
|
|
18
20
|
|
|
19
21
|
TConfig = TypeVar("TConfig", bound=DeviceConfiguration)
|
|
20
22
|
|
|
@@ -42,6 +44,10 @@ class _ScannerImplementation(ABC, Generic[TConfig]):
|
|
|
42
44
|
def update_config(self: _ScannerImplementation, new_config: TConfig) -> None:
|
|
43
45
|
pass
|
|
44
46
|
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def disconnect(self: _ScannerImplementation) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
45
51
|
|
|
46
52
|
class Scanner:
|
|
47
53
|
"""A synchronous scanner for Glaze terahertz devices."""
|
|
@@ -76,6 +82,10 @@ class Scanner:
|
|
|
76
82
|
"""
|
|
77
83
|
self._scanner_impl.update_config(new_config)
|
|
78
84
|
|
|
85
|
+
def disconnect(self: Scanner) -> None:
|
|
86
|
+
"""Close serial connection."""
|
|
87
|
+
self._scanner_impl.disconnect()
|
|
88
|
+
|
|
79
89
|
|
|
80
90
|
class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
|
|
81
91
|
"""Perform synchronous terahertz scanning using a given DeviceConfiguration.
|
|
@@ -87,7 +97,7 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
|
|
|
87
97
|
|
|
88
98
|
def __init__(self: ForceScanner, config: ForceDeviceConfiguration) -> None:
|
|
89
99
|
self._config: ForceDeviceConfiguration
|
|
90
|
-
self._ampcom: _ForceAmpCom
|
|
100
|
+
self._ampcom: _ForceAmpCom | None = None
|
|
91
101
|
self.config = config
|
|
92
102
|
self._phase_estimator = _LockinPhaseEstimator()
|
|
93
103
|
|
|
@@ -133,6 +143,9 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
|
|
|
133
143
|
Returns:
|
|
134
144
|
Unprocessed scan.
|
|
135
145
|
"""
|
|
146
|
+
if self._ampcom is None:
|
|
147
|
+
msg = "Scanner not configured"
|
|
148
|
+
raise ScanError(msg)
|
|
136
149
|
_, responses = self._ampcom.start_scan()
|
|
137
150
|
|
|
138
151
|
time = responses[:, 0]
|
|
@@ -152,6 +165,14 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
|
|
|
152
165
|
"""
|
|
153
166
|
self.config = new_config
|
|
154
167
|
|
|
168
|
+
def disconnect(self: ForceScanner) -> None:
|
|
169
|
+
"""Close serial connection."""
|
|
170
|
+
if self._ampcom is None:
|
|
171
|
+
msg = "Scanner not connected"
|
|
172
|
+
raise SerialException(msg)
|
|
173
|
+
self._ampcom.disconnect()
|
|
174
|
+
self._ampcom = None
|
|
175
|
+
|
|
155
176
|
|
|
156
177
|
class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
157
178
|
"""Perform synchronous terahertz scanning using a given DeviceConfiguration.
|
|
@@ -162,7 +183,7 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
162
183
|
|
|
163
184
|
def __init__(self: LeScanner, config: LeDeviceConfiguration) -> None:
|
|
164
185
|
self._config: LeDeviceConfiguration
|
|
165
|
-
self._ampcom: _LeAmpCom
|
|
186
|
+
self._ampcom: _LeAmpCom | None = None
|
|
166
187
|
self.config = config
|
|
167
188
|
self._phase_estimator = _LockinPhaseEstimator()
|
|
168
189
|
|
|
@@ -198,6 +219,9 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
198
219
|
Returns:
|
|
199
220
|
Unprocessed scan.
|
|
200
221
|
"""
|
|
222
|
+
if self._ampcom is None:
|
|
223
|
+
msg = "Scanner not configured"
|
|
224
|
+
raise ScanError(msg)
|
|
201
225
|
_, time, radius, theta = self._ampcom.start_scan()
|
|
202
226
|
self._phase_estimator.update_estimate(radius=radius, theta=theta)
|
|
203
227
|
|
|
@@ -213,6 +237,14 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
213
237
|
"""
|
|
214
238
|
self.config = new_config
|
|
215
239
|
|
|
240
|
+
def disconnect(self: LeScanner) -> None:
|
|
241
|
+
"""Close serial connection."""
|
|
242
|
+
if self._ampcom is None:
|
|
243
|
+
msg = "Scanner not connected"
|
|
244
|
+
raise ScanError(msg)
|
|
245
|
+
self._ampcom.disconnect()
|
|
246
|
+
self._ampcom = None
|
|
247
|
+
|
|
216
248
|
|
|
217
249
|
def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
|
|
218
250
|
if isinstance(config, ForceDeviceConfiguration):
|
|
@@ -226,10 +258,13 @@ def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
|
|
|
226
258
|
|
|
227
259
|
class _LockinPhaseEstimator:
|
|
228
260
|
def __init__(
|
|
229
|
-
self: _LockinPhaseEstimator,
|
|
261
|
+
self: _LockinPhaseEstimator,
|
|
262
|
+
r_threshold_for_update: float = 2.0,
|
|
263
|
+
theta_threshold_for_adjustment: float = 1.0,
|
|
230
264
|
) -> None:
|
|
231
|
-
self.phase_estimate: float | None = None
|
|
232
265
|
self.r_threshold_for_update = r_threshold_for_update
|
|
266
|
+
self.theta_threshold_for_adjustment = theta_threshold_for_adjustment
|
|
267
|
+
self.phase_estimate: float | None = None
|
|
233
268
|
self._radius_of_est: float | None = None
|
|
234
269
|
|
|
235
270
|
def update_estimate(
|
|
@@ -242,7 +277,11 @@ class _LockinPhaseEstimator:
|
|
|
242
277
|
self._set_estimates(theta_at_max, r_max)
|
|
243
278
|
return
|
|
244
279
|
|
|
245
|
-
if r_max > self.r_threshold_for_update * self._radius_of_est
|
|
280
|
+
if r_max > self.r_threshold_for_update * self._radius_of_est or (
|
|
281
|
+
r_max > self._radius_of_est
|
|
282
|
+
and abs(theta_at_max - self.phase_estimate)
|
|
283
|
+
< self.theta_threshold_for_adjustment
|
|
284
|
+
):
|
|
246
285
|
self._set_estimates(theta_at_max, r_max)
|
|
247
286
|
|
|
248
287
|
def _set_estimates(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pyglaze
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pyglaze is a library used to operate the devices of Glaze Technologies
|
|
5
5
|
Author: GLAZE Technologies ApS
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -38,11 +38,11 @@ Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
|
|
|
38
38
|
Requires-Python: <3.13,>=3.9
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist: numpy
|
|
42
|
-
Requires-Dist: pyserial
|
|
43
|
-
Requires-Dist: scipy
|
|
44
|
-
Requires-Dist: bitstring
|
|
45
|
-
Requires-Dist:
|
|
41
|
+
Requires-Dist: numpy<2.0.0,>=1.26.4
|
|
42
|
+
Requires-Dist: pyserial>=3.5
|
|
43
|
+
Requires-Dist: scipy>=1.7.3
|
|
44
|
+
Requires-Dist: bitstring>=4.1.2
|
|
45
|
+
Requires-Dist: typing_extensions>=4.12.2
|
|
46
46
|
|
|
47
47
|
# Pyglaze
|
|
48
48
|
Pyglaze is a python library used to operate the devices of [Glaze Technologies](https://www.glazetech.dk/).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
pyglaze/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
|
2
|
+
pyglaze/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pyglaze/datamodels/__init__.py,sha256=DJLByl2C7pC4RM4Uh6PW-McM5RIGBjcopzGywCKhSlI,111
|
|
4
|
+
pyglaze/datamodels/pulse.py,sha256=YnfwOkTyKUH-HFZAg2qpwe7kh8rb0B4HilZDFDdY36Q,22511
|
|
5
|
+
pyglaze/datamodels/waveform.py,sha256=T0wV7saJNPowwQs518VLypul-p1bg_1REPTflw8UNzM,5810
|
|
6
|
+
pyglaze/device/__init__.py,sha256=Y__HNJTPTAxLB_IpbLD4h2K9mDkI1_l4Ruc9mONH-3A,177
|
|
7
|
+
pyglaze/device/ampcom.py,sha256=FFrxAlLQbkslnlvNhIR5sd3xouMoUlMeodicZhYtgwg,17466
|
|
8
|
+
pyglaze/device/configuration.py,sha256=gh_eerX8TdXx3LnFxHieJqOpfDfE9cV6Xgm5WYVnvO0,7994
|
|
9
|
+
pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
|
|
10
|
+
pyglaze/devtools/mock_device.py,sha256=Fz0ZCRIslokBz6gFEztv1-V1UJhdTDqujl7-l8seX7U,14394
|
|
11
|
+
pyglaze/devtools/thz_pulse.py,sha256=C3O7s93rFc9xRC4KIqUwDWYsud8xZDP0FsE7-bNSWbo,924
|
|
12
|
+
pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
pyglaze/helpers/_types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
|
|
14
|
+
pyglaze/helpers/utilities.py,sha256=9JEaD2KzHXO1olFNCECMI6-sdX9bizkN7vvsc1dGlf8,2428
|
|
15
|
+
pyglaze/interpolation/__init__.py,sha256=BZU-mIAO7XxrPGG7tsUks-ghbylOaIANJGIYrtF5Y_Y,126
|
|
16
|
+
pyglaze/interpolation/interpolation.py,sha256=qxcK7zvQmEM2qiOoTs4b2Qs9U_QiGIEe16Z059eu6RA,1341
|
|
17
|
+
pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
|
|
18
|
+
pyglaze/scanning/_asyncscanner.py,sha256=pdE4w1QlIzpSxdl5GB7IB0x4FjuhQeqPSs9yIuVQF6E,5203
|
|
19
|
+
pyglaze/scanning/_exceptions.py,sha256=vS28Dijj76jVuF6cSDBKqM9SQIa9rbIyUaHF-RA3PyM,213
|
|
20
|
+
pyglaze/scanning/client.py,sha256=3qrQStkeLQzCeu4yMHJ_ENLGQ7E5GMc4CP9J55rk-ug,1817
|
|
21
|
+
pyglaze/scanning/scanner.py,sha256=0e8jXhzY27BckwT7ii1PdpYE8JK4tD1DZPj3YmQqM7E,9506
|
|
22
|
+
pyglaze-0.3.0.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
|
|
23
|
+
pyglaze-0.3.0.dist-info/METADATA,sha256=TbEOazvaA3HP1RE5nUQ5rNQxqVlt_-g7px2B_mItpV0,3493
|
|
24
|
+
pyglaze-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
25
|
+
pyglaze-0.3.0.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
|
|
26
|
+
pyglaze-0.3.0.dist-info/RECORD,,
|
pyglaze-0.2.1.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
pyglaze/__init__.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
|
|
2
|
-
pyglaze/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
pyglaze/datamodels/__init__.py,sha256=DJLByl2C7pC4RM4Uh6PW-McM5RIGBjcopzGywCKhSlI,111
|
|
4
|
-
pyglaze/datamodels/pulse.py,sha256=BoW_GDvkwEpn_UYUFPoUfi5E_oCnvv8cU4nZKG7Ph2U,20911
|
|
5
|
-
pyglaze/datamodels/waveform.py,sha256=n31DhJHFeBNZ3hHqQUiCGXssm5Dc8wV6tGPkhmFYB4Q,5809
|
|
6
|
-
pyglaze/device/__init__.py,sha256=5RjCHuFKMi9g2KLUkxixO9hNpAgkUBcOURNTuhAdoUk,177
|
|
7
|
-
pyglaze/device/ampcom.py,sha256=6JXl7PojYr4F1_pdVItO-XbGCp8mpy4wpwaaUn8Loog,17214
|
|
8
|
-
pyglaze/device/configuration.py,sha256=gh_eerX8TdXx3LnFxHieJqOpfDfE9cV6Xgm5WYVnvO0,7994
|
|
9
|
-
pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
|
|
10
|
-
pyglaze/devtools/mock_device.py,sha256=3RMa-JAehbbRCmOO74aZzGPdYp2qsP-XE8ulzLXDP6Q,13994
|
|
11
|
-
pyglaze/devtools/thz_pulse.py,sha256=xp-T9psdOrUMtSUFu8HEwQJVu_aMixJdZHtg_BCVu_k,923
|
|
12
|
-
pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
pyglaze/helpers/types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
|
|
14
|
-
pyglaze/helpers/utilities.py,sha256=n_x9Tqm305MUorS29O6CoJM8Mi4apo2bsN_odrRaVAw,2423
|
|
15
|
-
pyglaze/interpolation/__init__.py,sha256=WCxHPsiI7zvJykp-jfytoEbO4Tla-YIF6A7fjDfcDvU,72
|
|
16
|
-
pyglaze/interpolation/interpolation.py,sha256=rQWzPD7W8TXETps7VZI0gcfAOCWO8pGL1HhhBnyxaMw,735
|
|
17
|
-
pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
|
|
18
|
-
pyglaze/scanning/_asyncscanner.py,sha256=SldM7XCavfugsDstxMVI_WLL6GUJutLYN0iU__mfY5A,5277
|
|
19
|
-
pyglaze/scanning/client.py,sha256=3qrQStkeLQzCeu4yMHJ_ENLGQ7E5GMc4CP9J55rk-ug,1817
|
|
20
|
-
pyglaze/scanning/scanner.py,sha256=PSjXVpSpHpYIl-sW34pIThocbM9GSHJ_E4gGcsePeTw,8139
|
|
21
|
-
pyglaze-0.2.1.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
|
|
22
|
-
pyglaze-0.2.1.dist-info/METADATA,sha256=WETmnm6CF1_eyojd-ahfIFLJ_VCxV2jshcrbSoPLg3Y,3498
|
|
23
|
-
pyglaze-0.2.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
24
|
-
pyglaze-0.2.1.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
|
|
25
|
-
pyglaze-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|