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.
Files changed (40) hide show
  1. {pyglaze-0.1.2/src/pyglaze.egg-info → pyglaze-0.2.1}/PKG-INFO +1 -1
  2. {pyglaze-0.1.2 → pyglaze-0.2.1}/pyproject.toml +2 -2
  3. pyglaze-0.2.1/src/pyglaze/__init__.py +1 -0
  4. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/pulse.py +18 -0
  5. pyglaze-0.2.1/src/pyglaze/device/__init__.py +7 -0
  6. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/device/ampcom.py +77 -65
  7. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/device/configuration.py +1 -15
  8. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/mock_device.py +30 -4
  9. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/_asyncscanner.py +7 -5
  10. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/scanner.py +1 -5
  11. {pyglaze-0.1.2 → pyglaze-0.2.1/src/pyglaze.egg-info}/PKG-INFO +1 -1
  12. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/SOURCES.txt +0 -7
  13. pyglaze-0.1.2/src/pyglaze/__init__.py +0 -1
  14. pyglaze-0.1.2/src/pyglaze/device/__init__.py +0 -15
  15. pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/carmen-nonuniform-2023-10-20.pickle +0 -0
  16. pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g1-linearized-2023-04-04.pickle +0 -0
  17. pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g2-linearized-2023-04-04.pickle +0 -0
  18. pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/g2-nonuniform-2023-04-04.pickle +0 -0
  19. pyglaze-0.1.2/src/pyglaze/device/_delayunit_data/mock_delay.pickle +0 -0
  20. pyglaze-0.1.2/src/pyglaze/device/delayunit.py +0 -151
  21. pyglaze-0.1.2/src/pyglaze/device/identifiers.py +0 -41
  22. {pyglaze-0.1.2 → pyglaze-0.2.1}/LICENSE +0 -0
  23. {pyglaze-0.1.2 → pyglaze-0.2.1}/MANIFEST.in +0 -0
  24. {pyglaze-0.1.2 → pyglaze-0.2.1}/README.md +0 -0
  25. {pyglaze-0.1.2 → pyglaze-0.2.1}/setup.cfg +0 -0
  26. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/__init__.py +0 -0
  27. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/datamodels/waveform.py +0 -0
  28. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/__init__.py +0 -0
  29. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/devtools/thz_pulse.py +0 -0
  30. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/__init__.py +0 -0
  31. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/types.py +0 -0
  32. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/helpers/utilities.py +0 -0
  33. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/interpolation/__init__.py +0 -0
  34. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/interpolation/interpolation.py +0 -0
  35. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/py.typed +0 -0
  36. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/__init__.py +0 -0
  37. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze/scanning/client.py +0 -0
  38. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/dependency_links.txt +0 -0
  39. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/requires.txt +0 -0
  40. {pyglaze-0.1.2 → pyglaze-0.2.1}/src/pyglaze.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyglaze
3
- Version: 0.1.2
3
+ Version: 0.2.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyglaze"
3
- version = "0.1.2"
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.2"
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
 
@@ -0,0 +1,7 @@
1
+ from .configuration import ForceDeviceConfiguration, Interval, LeDeviceConfiguration
2
+
3
+ __all__ = [
4
+ "LeDeviceConfiguration",
5
+ "ForceDeviceConfiguration",
6
+ "Interval",
7
+ ]
@@ -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=load_delayunit(self.config.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 times(self: _LeAmpCom) -> FloatArray:
234
- return _delay_from_intervals(
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._squished_intervals,
247
- _points_per_interval(self.scanning_points, self._squished_intervals),
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
- denormalized[0], denormalized[1], n_points, endpoint=False
253
- ).astype(int),
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._raw_byte_send(
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._raw_byte_send(self.scanning_list)
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 _squished_intervals(self: _LeAmpCom) -> list[Interval]:
298
+ def _intervals(self: _LeAmpCom) -> list[Interval]:
300
299
  """Intervals squished into effective DAC range."""
301
- return _squish_intervals(
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 _raw_byte_send(self: _LeAmpCom, values: list[int]) -> None:
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(backoff_base=0.05, logger=logging.getLogger(LOGGER_NAME))
342
- def _get_response(self: _LeAmpCom) -> str:
343
- return self.__ser.read_until().decode(self.ENCODING).strip()
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
- bytes_to_receive = self.scanning_points * 4 + self.scanning_points * 4
352
- scan_bytes = self.__ser.read(bytes_to_receive)
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
- Xs = [
358
- BitArray(bytes=scan_bytes[d : d + 4]).floatle
359
- for d in range(0, self.scanning_points * 4, 4)
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(self.scanning_points * 4, self.scanning_points * 8, 4)
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: Delay, intervals: list[Interval], points_per_interval: list[int]
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, fail_after: float = np.inf, n_fails: float = np.inf
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[int] | None = None
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._decode_ints(input_bytes)
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 = self.rng.random(2 * len(self.scanning_list))
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 Empty as err:
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
- raise msg.error from err
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 (serialutil.SerialException, TimeoutError) as e:
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
- _, responses = self._ampcom.start_scan()
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyglaze
3
- Version: 0.1.2
3
+ Version: 0.2.1
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
@@ -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
- ]
@@ -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