pyglaze 0.1.2__tar.gz → 0.2.1__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.1.2/src/pyglaze.egg-info → pyglaze-0.2.1}/PKG-INFO +1 -1
- {pyglaze-0.1.2 → pyglaze-0.2.1}/pyproject.toml +2 -2
- pyglaze-0.2.1/src/pyglaze/__init__.py +1 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/pulse.py +18 -0
- pyglaze-0.2.1/src/pyglaze/device/__init__.py +7 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/device/ampcom.py +77 -65
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/device/configuration.py +1 -15
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/mock_device.py +30 -4
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/_asyncscanner.py +7 -5
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/scanner.py +1 -5
- {pyglaze-0.1.2 → pyglaze-0.2.1/src/pyglaze.egg-info}/PKG-INFO +1 -1
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/SOURCES.txt +0 -7
- pyglaze-0.1.2/src/pyglaze/__init__.py +0 -1
- pyglaze-0.1.2/src/pyglaze/device/__init__.py +0 -15
- pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/carmen-nonuniform-2023-10-20.pickle +0 -0
- pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g1-linearized-2023-04-04.pickle +0 -0
- pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g2-linearized-2023-04-04.pickle +0 -0
- pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g2-nonuniform-2023-04-04.pickle +0 -0
- pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/mock_delay.pickle +0 -0
- pyglaze-0.1.2/src/pyglaze/device/delayunit.py +0 -151
- pyglaze-0.1.2/src/pyglaze/device/identifiers.py +0 -41
- {pyglaze-0.1.2 → pyglaze-0.2.1}/LICENSE +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/MANIFEST.in +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/README.md +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/setup.cfg +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/__init__.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/waveform.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/__init__.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/thz_pulse.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/__init__.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/types.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/utilities.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/interpolation/__init__.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/interpolation/interpolation.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/py.typed +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/__init__.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/client.py +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/dependency_links.txt +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/requires.txt +0 -0
- {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyglaze"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
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.1
|
|
77
|
+
current_version = "0.2.1"
|
|
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}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -457,6 +457,24 @@ class Pulse:
|
|
|
457
457
|
|
|
458
458
|
return cast(float, np.max(max_estimate) - np.min(min_estimate))
|
|
459
459
|
|
|
460
|
+
def estimate_zero_crossing(self: Pulse) -> float:
|
|
461
|
+
"""Estimates the zero crossing of the pulse between the maximum and minimum value.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
float: Estimated zero crossing.
|
|
465
|
+
"""
|
|
466
|
+
argmax = np.argmax(self.signal)
|
|
467
|
+
argmin = np.argmin(self.signal)
|
|
468
|
+
if argmax < argmin:
|
|
469
|
+
idx = np.searchsorted(-self.signal[argmax:argmin], 0) + argmax - 1
|
|
470
|
+
else:
|
|
471
|
+
idx = np.searchsorted(self.signal[argmin:argmax], 0) + argmin - 1
|
|
472
|
+
|
|
473
|
+
# To find the zero crossing, solve 0 = s1 + a * (t - t1) for t: t = t1 - s1 / a
|
|
474
|
+
t1, s1 = self.time[idx], self.signal[idx]
|
|
475
|
+
a = (self.signal[idx + 1] - self.signal[idx]) / self.dt
|
|
476
|
+
return cast(float, t1 - s1 / a)
|
|
477
|
+
|
|
460
478
|
def to_native_dict(self: Pulse) -> dict[str, list[float] | None]:
|
|
461
479
|
"""Converts the Pulse object to a native dictionary.
|
|
462
480
|
|
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from math import modf
|
|
10
|
-
from typing import TYPE_CHECKING, ClassVar, overload
|
|
10
|
+
from typing import TYPE_CHECKING, Callable, ClassVar, overload
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
import serial
|
|
@@ -20,7 +20,6 @@ from pyglaze.device.configuration import (
|
|
|
20
20
|
Interval,
|
|
21
21
|
LeDeviceConfiguration,
|
|
22
22
|
)
|
|
23
|
-
from pyglaze.device.delayunit import Delay, load_delayunit
|
|
24
23
|
from pyglaze.devtools.mock_device import _mock_device_factory
|
|
25
24
|
from pyglaze.helpers.utilities import LOGGER_NAME, _BackoffRetry
|
|
26
25
|
|
|
@@ -33,6 +32,13 @@ if TYPE_CHECKING:
|
|
|
33
32
|
from pyglaze.helpers.types import FloatArray
|
|
34
33
|
|
|
35
34
|
|
|
35
|
+
class DeviceComError(Exception):
|
|
36
|
+
"""Raised when an error occurs in the communication with the device."""
|
|
37
|
+
|
|
38
|
+
def __init__(self: DeviceComError, message: str) -> None:
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
|
|
41
|
+
|
|
36
42
|
@dataclass
|
|
37
43
|
class _ForceAmpCom:
|
|
38
44
|
config: ForceDeviceConfiguration
|
|
@@ -67,7 +73,7 @@ class _ForceAmpCom:
|
|
|
67
73
|
@cached_property
|
|
68
74
|
def times(self: _ForceAmpCom) -> FloatArray:
|
|
69
75
|
return _delay_from_intervals(
|
|
70
|
-
delayunit=
|
|
76
|
+
delayunit=lambda x: x,
|
|
71
77
|
intervals=self.config.scan_intervals,
|
|
72
78
|
points_per_interval=_points_per_interval(
|
|
73
79
|
self.scanning_points, self._squished_intervals
|
|
@@ -216,7 +222,6 @@ class _LeAmpCom:
|
|
|
216
222
|
__ser: serial.Serial | LeMockDevice = field(init=False)
|
|
217
223
|
|
|
218
224
|
ENCODING: ClassVar[str] = "utf-8"
|
|
219
|
-
DAC_BITWIDTH: ClassVar[int] = 4096 # 12-bit DAC
|
|
220
225
|
|
|
221
226
|
OK_RESPONSE: ClassVar[str] = "ACK"
|
|
222
227
|
START_COMMAND: ClassVar[str] = "G"
|
|
@@ -230,30 +235,30 @@ class _LeAmpCom:
|
|
|
230
235
|
return self.config.n_points
|
|
231
236
|
|
|
232
237
|
@cached_property
|
|
233
|
-
def
|
|
234
|
-
|
|
235
|
-
delayunit=load_delayunit(self.config.delayunit),
|
|
236
|
-
intervals=self.config.scan_intervals,
|
|
237
|
-
points_per_interval=_points_per_interval(
|
|
238
|
-
self.scanning_points, self._squished_intervals
|
|
239
|
-
),
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
@cached_property
|
|
243
|
-
def scanning_list(self: _LeAmpCom) -> list[int]:
|
|
244
|
-
scanning_list: list[int] = []
|
|
238
|
+
def scanning_list(self: _LeAmpCom) -> list[float]:
|
|
239
|
+
scanning_list: list[float] = []
|
|
245
240
|
for interval, n_points in zip(
|
|
246
|
-
self.
|
|
247
|
-
_points_per_interval(self.scanning_points, self.
|
|
241
|
+
self._intervals,
|
|
242
|
+
_points_per_interval(self.scanning_points, self._intervals),
|
|
248
243
|
):
|
|
249
|
-
denormalized = self._denormalize_interval(interval)
|
|
250
244
|
scanning_list.extend(
|
|
251
245
|
np.linspace(
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
interval.lower,
|
|
247
|
+
interval.upper,
|
|
248
|
+
n_points,
|
|
249
|
+
endpoint=len(self._intervals) == 1,
|
|
250
|
+
),
|
|
254
251
|
)
|
|
255
252
|
return scanning_list
|
|
256
253
|
|
|
254
|
+
@cached_property
|
|
255
|
+
def bytes_to_receive(self: _LeAmpCom) -> int:
|
|
256
|
+
"""Number of bytes to receive for a single scan.
|
|
257
|
+
|
|
258
|
+
We expect to receive 3 arrays of floats (delays, X and Y), each with self.scanning_points elements.
|
|
259
|
+
"""
|
|
260
|
+
return self.scanning_points * 12
|
|
261
|
+
|
|
257
262
|
def __post_init__(self: _LeAmpCom) -> None:
|
|
258
263
|
self.__ser = _serial_factory(self.config)
|
|
259
264
|
|
|
@@ -271,39 +276,28 @@ class _LeAmpCom:
|
|
|
271
276
|
|
|
272
277
|
def write_list_length_and_integration_periods_and_use_ema(self: _LeAmpCom) -> str:
|
|
273
278
|
self._encode_send_response(self.SEND_SETTINGS_COMMAND)
|
|
274
|
-
self.
|
|
279
|
+
self._raw_byte_send_ints(
|
|
275
280
|
[self.scanning_points, self.config.integration_periods, self.config.use_ema]
|
|
276
281
|
)
|
|
277
|
-
return self._get_response()
|
|
282
|
+
return self._get_response(self.SEND_SETTINGS_COMMAND)
|
|
278
283
|
|
|
279
284
|
def write_list(self: _LeAmpCom) -> str:
|
|
280
285
|
self._encode_send_response(self.SEND_LIST_COMMAND)
|
|
281
|
-
self.
|
|
282
|
-
return self._get_response()
|
|
286
|
+
self._raw_byte_send_floats(self.scanning_list)
|
|
287
|
+
return self._get_response(self.SEND_LIST_COMMAND)
|
|
283
288
|
|
|
284
|
-
def start_scan(self: _LeAmpCom) -> tuple[str, np.ndarray]:
|
|
289
|
+
def start_scan(self: _LeAmpCom) -> tuple[str, np.ndarray, np.ndarray, np.ndarray]:
|
|
285
290
|
self._encode_send_response(self.START_COMMAND)
|
|
286
291
|
self._await_scan_finished()
|
|
287
|
-
Xs, Ys = self._read_scan()
|
|
292
|
+
times, Xs, Ys = self._read_scan()
|
|
288
293
|
|
|
289
294
|
radii, angles = self._convert_to_r_angle(Xs, Ys)
|
|
290
|
-
|
|
291
|
-
output_array = np.zeros((self.scanning_points, 3))
|
|
292
|
-
output_array[:, 0] = self.times
|
|
293
|
-
output_array[:, 1] = radii
|
|
294
|
-
output_array[:, 2] = angles
|
|
295
|
-
|
|
296
|
-
return self.START_COMMAND, output_array
|
|
295
|
+
return self.START_COMMAND, np.array(times), np.array(radii), np.array(angles)
|
|
297
296
|
|
|
298
297
|
@cached_property
|
|
299
|
-
def
|
|
298
|
+
def _intervals(self: _LeAmpCom) -> list[Interval]:
|
|
300
299
|
"""Intervals squished into effective DAC range."""
|
|
301
|
-
return
|
|
302
|
-
intervals=self.config.scan_intervals or [Interval(lower=0.0, upper=1.0)],
|
|
303
|
-
lower_bound=self.config.fs_dac_lower_bound,
|
|
304
|
-
upper_bound=self.config.fs_dac_upper_bound,
|
|
305
|
-
bitwidth=self.DAC_BITWIDTH,
|
|
306
|
-
)
|
|
300
|
+
return self.config.scan_intervals or [Interval(lower=0.0, upper=1.0)]
|
|
307
301
|
|
|
308
302
|
def _convert_to_r_angle(
|
|
309
303
|
self: _LeAmpCom, Xs: list, Ys: list
|
|
@@ -312,24 +306,25 @@ class _LeAmpCom:
|
|
|
312
306
|
angle = np.arctan2(np.array(Ys), np.array(Xs))
|
|
313
307
|
return r, np.rad2deg(angle)
|
|
314
308
|
|
|
315
|
-
def _denormalize_interval(self: _LeAmpCom, interval: Interval) -> list[int]:
|
|
316
|
-
lower = int(interval.lower * self.DAC_BITWIDTH)
|
|
317
|
-
upper = int(interval.upper * self.DAC_BITWIDTH)
|
|
318
|
-
return [lower, upper]
|
|
319
|
-
|
|
320
309
|
def _encode_send_response(self: _LeAmpCom, command: str) -> str:
|
|
321
310
|
self._encode_and_send(command)
|
|
322
|
-
return self._get_response()
|
|
311
|
+
return self._get_response(command)
|
|
323
312
|
|
|
324
313
|
def _encode_and_send(self: _LeAmpCom, command: str) -> None:
|
|
325
314
|
self.__ser.write(command.encode(self.ENCODING))
|
|
326
315
|
|
|
327
|
-
def
|
|
316
|
+
def _raw_byte_send_ints(self: _LeAmpCom, values: list[int]) -> None:
|
|
328
317
|
c = BitArray()
|
|
329
318
|
for value in values:
|
|
330
319
|
c.append(BitArray(uintle=value, length=16))
|
|
331
320
|
self.__ser.write(c.tobytes())
|
|
332
321
|
|
|
322
|
+
def _raw_byte_send_floats(self: _LeAmpCom, values: list[float]) -> None:
|
|
323
|
+
c = BitArray()
|
|
324
|
+
for value in values:
|
|
325
|
+
c.append(BitArray(floatle=value, length=32))
|
|
326
|
+
self.__ser.write(c.tobytes())
|
|
327
|
+
|
|
333
328
|
def _await_scan_finished(self: _LeAmpCom) -> None:
|
|
334
329
|
time.sleep(self.config._sweep_length_ms * 1.0e-3) # noqa: SLF001, access to private attribute for backwards compatibility
|
|
335
330
|
status = self._get_status()
|
|
@@ -338,33 +333,48 @@ class _LeAmpCom:
|
|
|
338
333
|
time.sleep(self.config._sweep_length_ms * 1e-3 * 0.01) # noqa: SLF001, access to private attribute for backwards compatibility
|
|
339
334
|
status = self._get_status()
|
|
340
335
|
|
|
341
|
-
@_BackoffRetry(
|
|
342
|
-
|
|
343
|
-
|
|
336
|
+
@_BackoffRetry(
|
|
337
|
+
backoff_base=1e-2, max_tries=3, logger=logging.getLogger(LOGGER_NAME)
|
|
338
|
+
)
|
|
339
|
+
def _get_response(self: _LeAmpCom, command: str) -> str:
|
|
340
|
+
response = self.__ser.read_until().decode(self.ENCODING).strip()
|
|
341
|
+
|
|
342
|
+
if len(response) == 0:
|
|
343
|
+
msg = f"Command: '{command}'. Empty response received"
|
|
344
|
+
raise serialutil.SerialException(msg)
|
|
345
|
+
if response[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
|
|
346
|
+
msg = f"Command: '{command}'. Expected response '{self.OK_RESPONSE}', received: '{response}'"
|
|
347
|
+
raise DeviceComError(msg)
|
|
348
|
+
return response
|
|
344
349
|
|
|
345
350
|
@_BackoffRetry(
|
|
346
351
|
backoff_base=1e-2, max_tries=5, logger=logging.getLogger(LOGGER_NAME)
|
|
347
352
|
)
|
|
348
|
-
def _read_scan(self: _LeAmpCom) -> tuple[list[float], list[float]]:
|
|
353
|
+
def _read_scan(self: _LeAmpCom) -> tuple[list[float], list[float], list[float]]:
|
|
349
354
|
self._encode_and_send(self.FETCH_COMMAND)
|
|
355
|
+
scan_bytes = self.__ser.read(self.bytes_to_receive)
|
|
350
356
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if len(scan_bytes) != bytes_to_receive:
|
|
354
|
-
msg = f"received {len(scan_bytes)} bytes, expected {bytes_to_receive}"
|
|
357
|
+
if len(scan_bytes) != self.bytes_to_receive:
|
|
358
|
+
msg = f"received {len(scan_bytes)} bytes, expected {self.bytes_to_receive}"
|
|
355
359
|
raise serialutil.SerialException(msg)
|
|
356
360
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
Ys =
|
|
361
|
+
times = self._bytes_to_floats(scan_bytes, 0, self.scanning_points * 4)
|
|
362
|
+
Xs = self._bytes_to_floats(
|
|
363
|
+
scan_bytes, self.scanning_points * 4, self.scanning_points * 8
|
|
364
|
+
)
|
|
365
|
+
Ys = self._bytes_to_floats(
|
|
366
|
+
scan_bytes, self.scanning_points * 8, self.scanning_points * 12
|
|
367
|
+
)
|
|
368
|
+
return times, Xs, Ys
|
|
369
|
+
|
|
370
|
+
def _bytes_to_floats(
|
|
371
|
+
self: _LeAmpCom, scan_bytes: bytes, from_idx: int, to_idx: int
|
|
372
|
+
) -> list[float]:
|
|
373
|
+
return [
|
|
362
374
|
BitArray(bytes=scan_bytes[d : d + 4]).floatle
|
|
363
|
-
for d in range(
|
|
375
|
+
for d in range(from_idx, to_idx, 4)
|
|
364
376
|
]
|
|
365
377
|
|
|
366
|
-
return Xs, Ys
|
|
367
|
-
|
|
368
378
|
def _get_status(self: _LeAmpCom) -> _LeStatus:
|
|
369
379
|
msg = self._encode_send_response(self.STATUS_COMMAND)
|
|
370
380
|
if msg == _LeStatus.SCANNING.value:
|
|
@@ -434,7 +444,9 @@ def _squish_intervals(
|
|
|
434
444
|
|
|
435
445
|
|
|
436
446
|
def _delay_from_intervals(
|
|
437
|
-
delayunit:
|
|
447
|
+
delayunit: Callable[[FloatArray], FloatArray],
|
|
448
|
+
intervals: list[Interval],
|
|
449
|
+
points_per_interval: list[int],
|
|
438
450
|
) -> FloatArray:
|
|
439
451
|
"""Convert a list of intervals to a list of delay times."""
|
|
440
452
|
times: list[float] = []
|
|
@@ -5,8 +5,6 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from dataclasses import asdict, dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, ClassVar, TypeVar
|
|
7
7
|
|
|
8
|
-
from .delayunit import validate_delayunit
|
|
9
|
-
|
|
10
8
|
if TYPE_CHECKING:
|
|
11
9
|
from pathlib import Path
|
|
12
10
|
|
|
@@ -83,7 +81,6 @@ class ForceDeviceConfiguration(DeviceConfiguration):
|
|
|
83
81
|
Args:
|
|
84
82
|
amp_port: The name of the serial port the amp is connected to.
|
|
85
83
|
sweep_length_ms: The length of the sweep in milliseconds.
|
|
86
|
-
delayunit: Name of the delay calculator.
|
|
87
84
|
scan_intervals: The intervals to scan.
|
|
88
85
|
integration_periods: The number of integration periods to use.
|
|
89
86
|
modulation_frequency: The frequency of the modulation in Hz.
|
|
@@ -97,7 +94,6 @@ class ForceDeviceConfiguration(DeviceConfiguration):
|
|
|
97
94
|
|
|
98
95
|
amp_port: str
|
|
99
96
|
sweep_length_ms: float
|
|
100
|
-
delayunit: str
|
|
101
97
|
scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
|
|
102
98
|
integration_periods: int = 100
|
|
103
99
|
modulation_frequency: int = 10000 # Hz
|
|
@@ -114,9 +110,6 @@ class ForceDeviceConfiguration(DeviceConfiguration):
|
|
|
114
110
|
def _sweep_length_ms(self: ForceDeviceConfiguration) -> float:
|
|
115
111
|
return self.sweep_length_ms
|
|
116
112
|
|
|
117
|
-
def __post_init__(self: ForceDeviceConfiguration) -> None: # noqa: D105
|
|
118
|
-
validate_delayunit(self.delayunit)
|
|
119
|
-
|
|
120
113
|
def save(self: ForceDeviceConfiguration, path: Path) -> str:
|
|
121
114
|
"""Save a DeviceConfiguration to a file.
|
|
122
115
|
|
|
@@ -172,7 +165,6 @@ class LeDeviceConfiguration(DeviceConfiguration):
|
|
|
172
165
|
|
|
173
166
|
Args:
|
|
174
167
|
amp_port: The name of the serial port the amp is connected to.
|
|
175
|
-
delayunit: Name of the delay calculator.
|
|
176
168
|
use_ema: Whether to use en exponentially moving average filter during lockin detection.
|
|
177
169
|
n_points: The number of points to scan.
|
|
178
170
|
scan_intervals: The intervals to scan.
|
|
@@ -181,21 +173,15 @@ class LeDeviceConfiguration(DeviceConfiguration):
|
|
|
181
173
|
"""
|
|
182
174
|
|
|
183
175
|
amp_port: str
|
|
184
|
-
delayunit: str
|
|
185
176
|
use_ema: bool = True
|
|
186
177
|
n_points: int = 1000
|
|
187
178
|
scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
|
|
188
179
|
integration_periods: int = 10
|
|
189
180
|
amp_timeout_seconds: float = 0.2
|
|
181
|
+
modulation_frequency: int = 10000 # Hz
|
|
190
182
|
|
|
191
|
-
modulation_frequency: ClassVar[int] = 10000 # Hz
|
|
192
|
-
fs_dac_lower_bound: ClassVar[int] = 300
|
|
193
|
-
fs_dac_upper_bound: ClassVar[int] = 3700
|
|
194
183
|
amp_baudrate: ClassVar[int] = 1000000 # bit/s
|
|
195
184
|
|
|
196
|
-
def __post_init__(self: LeDeviceConfiguration) -> None: # noqa: D105
|
|
197
|
-
validate_delayunit(self.delayunit)
|
|
198
|
-
|
|
199
185
|
@property
|
|
200
186
|
def _sweep_length_ms(self: LeDeviceConfiguration) -> float:
|
|
201
187
|
return self.n_points * self._time_constant_ms
|
|
@@ -26,6 +26,8 @@ class MockDevice(ABC):
|
|
|
26
26
|
self: MockDevice,
|
|
27
27
|
fail_after: float = np.inf,
|
|
28
28
|
n_fails: float = np.inf,
|
|
29
|
+
*,
|
|
30
|
+
empty_responses: bool = False,
|
|
29
31
|
) -> None:
|
|
30
32
|
pass
|
|
31
33
|
|
|
@@ -183,7 +185,11 @@ class LeMockDevice(MockDevice):
|
|
|
183
185
|
DAC_BITWIDTH = 2**12
|
|
184
186
|
|
|
185
187
|
def __init__(
|
|
186
|
-
self: LeMockDevice,
|
|
188
|
+
self: LeMockDevice,
|
|
189
|
+
fail_after: float = np.inf,
|
|
190
|
+
n_fails: float = np.inf,
|
|
191
|
+
*,
|
|
192
|
+
empty_responses: bool = False,
|
|
187
193
|
) -> None:
|
|
188
194
|
self.fail_after = fail_after
|
|
189
195
|
self.fails_wanted = n_fails
|
|
@@ -195,8 +201,9 @@ class LeMockDevice(MockDevice):
|
|
|
195
201
|
self.n_scanning_points: int | None = None
|
|
196
202
|
self.integration_periods: int | None = None
|
|
197
203
|
self.use_ema: bool | None = None
|
|
198
|
-
self.scanning_list: list[
|
|
204
|
+
self.scanning_list: list[float] | None = None
|
|
199
205
|
self._scan_start_time: float | None = None
|
|
206
|
+
self.empty_responses = empty_responses
|
|
200
207
|
|
|
201
208
|
def write(self: LeMockDevice, input_bytes: bytes) -> None:
|
|
202
209
|
"""Mock-write to the serial connection."""
|
|
@@ -217,12 +224,16 @@ class LeMockDevice(MockDevice):
|
|
|
217
224
|
|
|
218
225
|
def read(self: LeMockDevice, size: int) -> bytes:
|
|
219
226
|
"""Mock-read from the serial connection."""
|
|
227
|
+
if self.empty_responses:
|
|
228
|
+
return self._create_scan_bytes(n_bytes=0)
|
|
220
229
|
if self.state == _LeMockState.IDLE:
|
|
221
230
|
return self._create_scan_bytes(n_bytes=size)
|
|
222
231
|
raise NotImplementedError
|
|
223
232
|
|
|
224
233
|
def read_until(self: LeMockDevice, _: bytes = b"\r") -> bytes: # noqa: PLR0911
|
|
225
234
|
"""Mock-read_until from the serial connection."""
|
|
235
|
+
if self.empty_responses:
|
|
236
|
+
return "".encode(self.ENCODING)
|
|
226
237
|
if self.state == _LeMockState.WAITING_FOR_SETTINGS:
|
|
227
238
|
return "ACK: Ready to receive settings.".encode(self.ENCODING)
|
|
228
239
|
if self.state == _LeMockState.RECEIVED_SETTINGS:
|
|
@@ -297,7 +308,7 @@ class LeMockDevice(MockDevice):
|
|
|
297
308
|
self.state = _LeMockState.RECEIVED_SETTINGS
|
|
298
309
|
|
|
299
310
|
def _handle_waiting_for_list(self: LeMockDevice, input_bytes: bytes) -> None:
|
|
300
|
-
self.scanning_list = self.
|
|
311
|
+
self.scanning_list = self._decode_floats(input_bytes)
|
|
301
312
|
self.state = _LeMockState.RECEIVED_LIST
|
|
302
313
|
|
|
303
314
|
def _decode_ints(self: LeMockDevice, input_bytes: bytes) -> list[int]:
|
|
@@ -307,6 +318,13 @@ class LeMockDevice(MockDevice):
|
|
|
307
318
|
for i in range(0, len(input_bytes), 2)
|
|
308
319
|
]
|
|
309
320
|
|
|
321
|
+
def _decode_floats(self: LeMockDevice, input_bytes: bytes) -> list[float]:
|
|
322
|
+
# Convert every four bytes to a 32-bit float (assuming little-endian format)
|
|
323
|
+
return [
|
|
324
|
+
struct.unpack("<f", input_bytes[i : i + 4])[0]
|
|
325
|
+
for i in range(0, len(input_bytes), 4)
|
|
326
|
+
]
|
|
327
|
+
|
|
310
328
|
def _scan_has_finished(self: LeMockDevice) -> bool:
|
|
311
329
|
if not self.is_scanning:
|
|
312
330
|
return True
|
|
@@ -329,7 +347,12 @@ class LeMockDevice(MockDevice):
|
|
|
329
347
|
self.n_failures += 1
|
|
330
348
|
numbers = np.array([])
|
|
331
349
|
else:
|
|
332
|
-
numbers =
|
|
350
|
+
numbers = np.concatenate(
|
|
351
|
+
(
|
|
352
|
+
np.array(self.scanning_list) * 100e-12, # mock time values
|
|
353
|
+
self.rng.random(2 * len(self.scanning_list)),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
333
356
|
|
|
334
357
|
# Each scanning point will generate an X and a Y value (lockin detection)
|
|
335
358
|
return struct.pack("<" + "f" * len(numbers), *numbers)
|
|
@@ -341,6 +364,7 @@ def list_mock_devices() -> list[str]:
|
|
|
341
364
|
"mock_device",
|
|
342
365
|
"mock_device_scan_should_fail",
|
|
343
366
|
"mock_device_fail_first_scan",
|
|
367
|
+
"mock_device_empty_responses",
|
|
344
368
|
]
|
|
345
369
|
|
|
346
370
|
|
|
@@ -352,6 +376,8 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
|
|
|
352
376
|
return mock_class()
|
|
353
377
|
if config.amp_port == "mock_device_fail_first_scan":
|
|
354
378
|
return mock_class(fail_after=0, n_fails=1)
|
|
379
|
+
if config.amp_port == "mock_device_empty_responses":
|
|
380
|
+
return mock_class(empty_responses=True)
|
|
355
381
|
|
|
356
382
|
msg = f"Unknown mock device requested: {config.amp_port}. Valid options are: {list_mock_devices()}"
|
|
357
383
|
raise ValueError(msg)
|
|
@@ -95,13 +95,15 @@ class _AsyncScanner:
|
|
|
95
95
|
def _get_scan(self: _AsyncScanner) -> _TimestampedWaveform:
|
|
96
96
|
try:
|
|
97
97
|
return self._shared_mem.get(timeout=self._SCAN_TIMEOUT)
|
|
98
|
-
except
|
|
98
|
+
except Exception as err:
|
|
99
|
+
scanner_err: Exception | None = None
|
|
99
100
|
if self._scanner_conn.poll(timeout=self.startup_timeout):
|
|
100
101
|
msg: _ScannerHealth = self._scanner_conn.recv()
|
|
101
|
-
if not msg.is_alive:
|
|
102
|
-
self.is_scanning = False
|
|
103
102
|
if msg.error:
|
|
104
|
-
|
|
103
|
+
scanner_err = msg.error
|
|
104
|
+
self.stop_scan()
|
|
105
|
+
if scanner_err:
|
|
106
|
+
raise scanner_err from err
|
|
105
107
|
raise
|
|
106
108
|
|
|
107
109
|
@staticmethod
|
|
@@ -121,7 +123,7 @@ class _AsyncScanner:
|
|
|
121
123
|
while not stop_signal.is_set():
|
|
122
124
|
try:
|
|
123
125
|
waveform = _TimestampedWaveform(datetime.now(), scanner.scan()) # noqa: DTZ005
|
|
124
|
-
except
|
|
126
|
+
except Exception as e: # noqa: BLE001
|
|
125
127
|
parent_conn.send(
|
|
126
128
|
_ScannerHealth(is_alive=False, is_healthy=False, error=e)
|
|
127
129
|
)
|
|
@@ -198,11 +198,7 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
198
198
|
Returns:
|
|
199
199
|
Unprocessed scan.
|
|
200
200
|
"""
|
|
201
|
-
_,
|
|
202
|
-
|
|
203
|
-
time = responses[:, 0]
|
|
204
|
-
radius = responses[:, 1]
|
|
205
|
-
theta = responses[:, 2]
|
|
201
|
+
_, time, radius, theta = self._ampcom.start_scan()
|
|
206
202
|
self._phase_estimator.update_estimate(radius=radius, theta=theta)
|
|
207
203
|
|
|
208
204
|
return UnprocessedWaveform.from_polar_coords(
|
|
@@ -15,13 +15,6 @@ src/pyglaze/datamodels/waveform.py
|
|
|
15
15
|
src/pyglaze/device/__init__.py
|
|
16
16
|
src/pyglaze/device/ampcom.py
|
|
17
17
|
src/pyglaze/device/configuration.py
|
|
18
|
-
src/pyglaze/device/delayunit.py
|
|
19
|
-
src/pyglaze/device/identifiers.py
|
|
20
|
-
src/pyglaze/device/_delayunit_data/carmen-nonuniform-2023-10-20.pickle
|
|
21
|
-
src/pyglaze/device/_delayunit_data/g1-linearized-2023-04-04.pickle
|
|
22
|
-
src/pyglaze/device/_delayunit_data/g2-linearized-2023-04-04.pickle
|
|
23
|
-
src/pyglaze/device/_delayunit_data/g2-nonuniform-2023-04-04.pickle
|
|
24
|
-
src/pyglaze/device/_delayunit_data/mock_delay.pickle
|
|
25
18
|
src/pyglaze/devtools/__init__.py
|
|
26
19
|
src/pyglaze/devtools/mock_device.py
|
|
27
20
|
src/pyglaze/devtools/thz_pulse.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from .configuration import ForceDeviceConfiguration, Interval, LeDeviceConfiguration
|
|
2
|
-
from .delayunit import NonuniformDelay, UniformDelay, list_delayunits, load_delayunit
|
|
3
|
-
from .identifiers import get_device_id, list_devices
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"LeDeviceConfiguration",
|
|
7
|
-
"ForceDeviceConfiguration",
|
|
8
|
-
"Interval",
|
|
9
|
-
"NonuniformDelay",
|
|
10
|
-
"UniformDelay",
|
|
11
|
-
"list_delayunits",
|
|
12
|
-
"load_delayunit",
|
|
13
|
-
"get_device_id",
|
|
14
|
-
"list_devices",
|
|
15
|
-
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import pickle
|
|
4
|
-
from abc import ABC, abstractmethod
|
|
5
|
-
from dataclasses import asdict, dataclass
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Callable, cast
|
|
9
|
-
from uuid import UUID, uuid4
|
|
10
|
-
|
|
11
|
-
import numpy as np
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from pyglaze.helpers.types import FloatArray
|
|
15
|
-
|
|
16
|
-
__all__ = ["UniformDelay", "NonuniformDelay", "list_delayunits", "load_delayunit"]
|
|
17
|
-
|
|
18
|
-
_DELAYUNITS_PATH = Path(__file__).parent / "_delayunit_data"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def validate_delayunit(name: str) -> None:
|
|
22
|
-
delayunits = list_delayunits()
|
|
23
|
-
if name not in delayunits:
|
|
24
|
-
msg = f"Unknown delayunit '{name}'. Valid options are: {', '.join(delayunits)}."
|
|
25
|
-
raise ValueError(msg)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def list_delayunits() -> list[str]:
|
|
29
|
-
"""List all available delayunits.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
A list of all available delayunits.
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
return [delayunit.stem for delayunit in _DELAYUNITS_PATH.iterdir()]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def load_delayunit(name: str) -> Delay:
|
|
39
|
-
"""Load a delayunit by name.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
name: The name of the delayunit to load.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
The loaded delayunit.
|
|
46
|
-
"""
|
|
47
|
-
try:
|
|
48
|
-
return _load_delayunit_from_path(_DELAYUNITS_PATH / f"{name}.pickle")
|
|
49
|
-
except FileNotFoundError as e:
|
|
50
|
-
msg = f"Unknown delayunit requested ('{name}'). Known units are: {list_delayunits()}"
|
|
51
|
-
raise NameError(msg) from e
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _load_delayunit_from_path(path: Path) -> Delay:
|
|
55
|
-
with Path(path).open("rb") as f:
|
|
56
|
-
_dict: dict = pickle.load(f)
|
|
57
|
-
delay_type = _dict.pop("type")
|
|
58
|
-
return cast(Delay, globals()[delay_type](**_dict))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@dataclass(frozen=True)
|
|
62
|
-
class Delay(ABC):
|
|
63
|
-
friendly_name: str
|
|
64
|
-
unique_id: UUID
|
|
65
|
-
creation_time: datetime
|
|
66
|
-
|
|
67
|
-
def __call__(self: Delay, x: FloatArray) -> FloatArray:
|
|
68
|
-
if np.max(x) > 1.0 or np.min(x) < 0.0:
|
|
69
|
-
msg = "All values of 'x' must be between 0 and 1."
|
|
70
|
-
raise ValueError(msg)
|
|
71
|
-
|
|
72
|
-
return self._call(x)
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def filename(self: Delay) -> str:
|
|
76
|
-
return f"{self.friendly_name}-{self.creation_time.strftime('%Y-%m-%d')}.pickle"
|
|
77
|
-
|
|
78
|
-
@abstractmethod
|
|
79
|
-
def _call(self: Delay, x: FloatArray) -> FloatArray: ...
|
|
80
|
-
|
|
81
|
-
def save(self: Delay, path: Path) -> None:
|
|
82
|
-
with Path(path).open("wb") as f:
|
|
83
|
-
pickle.dump({"type": self.__class__.__name__, **asdict(self)}, f)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@dataclass(frozen=True)
|
|
87
|
-
class UniformDelay(Delay):
|
|
88
|
-
"""A delay calculator that calculates equidisant delays."""
|
|
89
|
-
|
|
90
|
-
time_window: float
|
|
91
|
-
|
|
92
|
-
def _call(self: UniformDelay, x: FloatArray) -> FloatArray:
|
|
93
|
-
return x * self.time_window
|
|
94
|
-
|
|
95
|
-
@classmethod
|
|
96
|
-
def new(cls: type[UniformDelay], time_window: float, friendly_name: str) -> Delay:
|
|
97
|
-
"""Create a new Delay object.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
time_window: The time window for the delay.
|
|
101
|
-
friendly_name: The friendly name of the delay.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
Delay: The newly created Delay object.
|
|
105
|
-
"""
|
|
106
|
-
return cls(
|
|
107
|
-
friendly_name=friendly_name,
|
|
108
|
-
unique_id=uuid4(),
|
|
109
|
-
creation_time=datetime.now(), # noqa: DTZ005
|
|
110
|
-
time_window=time_window,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@dataclass(frozen=True)
|
|
115
|
-
class NonuniformDelay(Delay):
|
|
116
|
-
"""A delay calculator that calculates non-equidistant delays."""
|
|
117
|
-
|
|
118
|
-
time_window: float
|
|
119
|
-
residual_interpolator: Callable[[FloatArray], FloatArray]
|
|
120
|
-
|
|
121
|
-
def _call(self: NonuniformDelay, x: FloatArray) -> FloatArray:
|
|
122
|
-
return (
|
|
123
|
-
x * self.time_window
|
|
124
|
-
+ self.residual_interpolator(x)
|
|
125
|
-
- self.residual_interpolator(np.asarray(0.0))
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
@classmethod
|
|
129
|
-
def new(
|
|
130
|
-
cls: type[NonuniformDelay],
|
|
131
|
-
friendly_name: str,
|
|
132
|
-
time_window: float,
|
|
133
|
-
residual_interpolator: Callable[[FloatArray], FloatArray],
|
|
134
|
-
) -> Delay:
|
|
135
|
-
"""Create a new NonuniformDelay object.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
friendly_name: The friendly name of the NonuniformDelay object.
|
|
139
|
-
time_window: The time window of the pulse.
|
|
140
|
-
residual_interpolator: a residual interpolator for calculating the nonuniform part of the delay.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Delay: The newly created Delay object.
|
|
144
|
-
"""
|
|
145
|
-
return cls(
|
|
146
|
-
friendly_name=friendly_name,
|
|
147
|
-
unique_id=uuid4(),
|
|
148
|
-
creation_time=datetime.now(), # noqa: DTZ005
|
|
149
|
-
time_window=time_window,
|
|
150
|
-
residual_interpolator=residual_interpolator,
|
|
151
|
-
)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from typing import Literal, get_args
|
|
2
|
-
from uuid import UUID
|
|
3
|
-
|
|
4
|
-
from typing_extensions import TypeAlias
|
|
5
|
-
|
|
6
|
-
DeviceName: TypeAlias = Literal["glaze1", "glaze2", "carmen"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _device_ids() -> dict[DeviceName, UUID]:
|
|
10
|
-
return {
|
|
11
|
-
"glaze1": UUID("5042dbda-e9bc-4216-a614-ac56d0a32023"),
|
|
12
|
-
"glaze2": UUID("66fa482a-1ef4-4076-a883-72d7bf43e151"),
|
|
13
|
-
"carmen": UUID("6a54db26-fa88-4146-b04f-b84b945bfea8"),
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def list_devices() -> list[DeviceName]:
|
|
18
|
-
"""List all available devices.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
A list of all available devices.
|
|
22
|
-
"""
|
|
23
|
-
return list(_device_ids().keys())
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_device_id(name: DeviceName) -> UUID:
|
|
27
|
-
"""Get the UUID of a device by its name.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
name: The name of the device.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
Unique identifier of a device.
|
|
34
|
-
"""
|
|
35
|
-
try:
|
|
36
|
-
return _device_ids()[name]
|
|
37
|
-
except KeyError as e:
|
|
38
|
-
msg = (
|
|
39
|
-
f"Device {name} does not exist. Possible values are {get_args(DeviceName)}"
|
|
40
|
-
)
|
|
41
|
-
raise ValueError(msg) from e
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|