pyglaze 0.2.2__tar.gz → 0.3.0__tar.gz
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-0.2.2/src/pyglaze.egg-info → pyglaze-0.3.0}/PKG-INFO +2 -2
- {pyglaze-0.2.2 → pyglaze-0.3.0}/pyproject.toml +4 -4
- pyglaze-0.3.0/src/pyglaze/__init__.py +1 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/datamodels/pulse.py +173 -124
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/datamodels/waveform.py +1 -1
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/device/ampcom.py +1 -1
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/devtools/thz_pulse.py +1 -1
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/helpers/utilities.py +3 -3
- pyglaze-0.3.0/src/pyglaze/interpolation/__init__.py +3 -0
- pyglaze-0.3.0/src/pyglaze/interpolation/interpolation.py +44 -0
- pyglaze-0.3.0/src/pyglaze/py.typed +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/scanning/_asyncscanner.py +2 -4
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/scanning/scanner.py +11 -4
- {pyglaze-0.2.2 → pyglaze-0.3.0/src/pyglaze.egg-info}/PKG-INFO +2 -2
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze.egg-info/SOURCES.txt +1 -1
- pyglaze-0.2.2/MANIFEST.in +0 -1
- pyglaze-0.2.2/src/pyglaze/__init__.py +0 -1
- pyglaze-0.2.2/src/pyglaze/interpolation/__init__.py +0 -3
- pyglaze-0.2.2/src/pyglaze/interpolation/interpolation.py +0 -24
- {pyglaze-0.2.2 → pyglaze-0.3.0}/LICENSE +0 -0
- pyglaze-0.2.2/src/pyglaze/helpers/__init__.py → pyglaze-0.3.0/MANIFEST.in +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/README.md +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/setup.cfg +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/datamodels/__init__.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/device/__init__.py +1 -1
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/device/configuration.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/devtools/__init__.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/devtools/mock_device.py +0 -0
- /pyglaze-0.2.2/src/pyglaze/py.typed → /pyglaze-0.3.0/src/pyglaze/helpers/__init__.py +0 -0
- /pyglaze-0.2.2/src/pyglaze/helpers/types.py → /pyglaze-0.3.0/src/pyglaze/helpers/_types.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/scanning/__init__.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/scanning/_exceptions.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze/scanning/client.py +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze.egg-info/dependency_links.txt +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze.egg-info/requires.txt +0 -0
- {pyglaze-0.2.2 → pyglaze-0.3.0}/src/pyglaze.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyglaze"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Pyglaze is a library used to operate the devices of Glaze Technologies"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -74,7 +74,7 @@ convention = "google"
|
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
[tool.bumpver]
|
|
77
|
-
current_version = "0.
|
|
77
|
+
current_version = "0.3.0"
|
|
78
78
|
version_pattern = "MAJOR.MINOR.PATCH[-TAG]"
|
|
79
79
|
commit_message = "BUMP VERSION {old_version} -> {new_version}"
|
|
80
80
|
tag_message = "v{new_version}"
|
|
@@ -82,8 +82,8 @@ tag_scope = "default"
|
|
|
82
82
|
pre_commit_hook = ""
|
|
83
83
|
post_commit_hook = ""
|
|
84
84
|
commit = true
|
|
85
|
-
tag =
|
|
86
|
-
push =
|
|
85
|
+
tag = false
|
|
86
|
+
push = false
|
|
87
87
|
|
|
88
88
|
[tool.bumpver.file_patterns]
|
|
89
89
|
"pyproject.toml" = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.interpolate import CubicSpline
|
|
5
|
+
|
|
6
|
+
from pyglaze.helpers._types import FloatArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ws_interpolate(
|
|
10
|
+
times: FloatArray, pulse: FloatArray, interp_times: FloatArray
|
|
11
|
+
) -> FloatArray:
|
|
12
|
+
"""Performs Whittaker-Shannon interpolation at the supplied times given a pulse.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
times: Sampling times
|
|
16
|
+
pulse: A sampled pulse satisfying the Nyquist criterion
|
|
17
|
+
interp_times: Array of times at which to interpolate
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
FloatArray: Interpolated values
|
|
21
|
+
"""
|
|
22
|
+
dt = times[1] - times[0]
|
|
23
|
+
_range = np.arange(len(pulse))
|
|
24
|
+
# times must be zero-centered for formula to work
|
|
25
|
+
sinc = np.sinc((interp_times[:, np.newaxis] - times[0] - dt * _range) / dt)
|
|
26
|
+
|
|
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))
|
|
File without changes
|
|
@@ -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:
|
|
@@ -16,7 +16,7 @@ from pyglaze.device.configuration import (
|
|
|
16
16
|
from pyglaze.scanning._exceptions import ScanError
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
from pyglaze.helpers.
|
|
19
|
+
from pyglaze.helpers._types import FloatArray
|
|
20
20
|
|
|
21
21
|
TConfig = TypeVar("TConfig", bound=DeviceConfiguration)
|
|
22
22
|
|
|
@@ -258,10 +258,13 @@ def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
|
|
|
258
258
|
|
|
259
259
|
class _LockinPhaseEstimator:
|
|
260
260
|
def __init__(
|
|
261
|
-
self: _LockinPhaseEstimator,
|
|
261
|
+
self: _LockinPhaseEstimator,
|
|
262
|
+
r_threshold_for_update: float = 2.0,
|
|
263
|
+
theta_threshold_for_adjustment: float = 1.0,
|
|
262
264
|
) -> None:
|
|
263
|
-
self.phase_estimate: float | None = None
|
|
264
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
|
|
265
268
|
self._radius_of_est: float | None = None
|
|
266
269
|
|
|
267
270
|
def update_estimate(
|
|
@@ -274,7 +277,11 @@ class _LockinPhaseEstimator:
|
|
|
274
277
|
self._set_estimates(theta_at_max, r_max)
|
|
275
278
|
return
|
|
276
279
|
|
|
277
|
-
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
|
+
):
|
|
278
285
|
self._set_estimates(theta_at_max, r_max)
|
|
279
286
|
|
|
280
287
|
def _set_estimates(
|
|
@@ -19,7 +19,7 @@ src/pyglaze/devtools/__init__.py
|
|
|
19
19
|
src/pyglaze/devtools/mock_device.py
|
|
20
20
|
src/pyglaze/devtools/thz_pulse.py
|
|
21
21
|
src/pyglaze/helpers/__init__.py
|
|
22
|
-
src/pyglaze/helpers/
|
|
22
|
+
src/pyglaze/helpers/_types.py
|
|
23
23
|
src/pyglaze/helpers/utilities.py
|
|
24
24
|
src/pyglaze/interpolation/__init__.py
|
|
25
25
|
src/pyglaze/interpolation/interpolation.py
|
pyglaze-0.2.2/MANIFEST.in
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
include src/pyglaze/device/_delayunit_data/*
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
from pyglaze.helpers.types import FloatArray
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def ws_interpolate(
|
|
7
|
-
times: FloatArray, pulse: FloatArray, interp_times: FloatArray
|
|
8
|
-
) -> FloatArray:
|
|
9
|
-
"""Performs Whittaker-Shannon interpolation at the supplied times given a pulse.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
times: Sampling times
|
|
13
|
-
pulse: A sampled pulse satisfying the Nyquist criterion
|
|
14
|
-
interp_times: Array of times at which to interpolate
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
FloatArray: Interpolated values
|
|
18
|
-
"""
|
|
19
|
-
dt = times[1] - times[0]
|
|
20
|
-
_range = np.arange(len(pulse))
|
|
21
|
-
# times must be zero-centered for formula to work
|
|
22
|
-
sinc = np.sinc((interp_times[:, np.newaxis] - times[0] - dt * _range) / dt)
|
|
23
|
-
|
|
24
|
-
return np.asarray(np.sum(pulse * sinc, axis=1))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|