pyglaze 0.2.0__tar.gz → 0.2.2__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 (33) hide show
  1. {pyglaze-0.2.0/src/pyglaze.egg-info → pyglaze-0.2.2}/PKG-INFO +1 -1
  2. {pyglaze-0.2.0 → pyglaze-0.2.2}/pyproject.toml +2 -2
  3. pyglaze-0.2.2/src/pyglaze/__init__.py +1 -0
  4. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/device/ampcom.py +37 -12
  5. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/devtools/mock_device.py +26 -2
  6. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/scanning/_asyncscanner.py +8 -5
  7. pyglaze-0.2.2/src/pyglaze/scanning/_exceptions.py +8 -0
  8. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/scanning/scanner.py +34 -2
  9. {pyglaze-0.2.0 → pyglaze-0.2.2/src/pyglaze.egg-info}/PKG-INFO +1 -1
  10. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze.egg-info/SOURCES.txt +1 -0
  11. pyglaze-0.2.0/src/pyglaze/__init__.py +0 -1
  12. {pyglaze-0.2.0 → pyglaze-0.2.2}/LICENSE +0 -0
  13. {pyglaze-0.2.0 → pyglaze-0.2.2}/MANIFEST.in +0 -0
  14. {pyglaze-0.2.0 → pyglaze-0.2.2}/README.md +0 -0
  15. {pyglaze-0.2.0 → pyglaze-0.2.2}/setup.cfg +0 -0
  16. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/datamodels/__init__.py +0 -0
  17. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/datamodels/pulse.py +0 -0
  18. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/datamodels/waveform.py +0 -0
  19. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/device/__init__.py +0 -0
  20. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/device/configuration.py +0 -0
  21. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/devtools/__init__.py +0 -0
  22. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/devtools/thz_pulse.py +0 -0
  23. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/helpers/__init__.py +0 -0
  24. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/helpers/types.py +0 -0
  25. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/helpers/utilities.py +0 -0
  26. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/interpolation/__init__.py +0 -0
  27. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/interpolation/interpolation.py +0 -0
  28. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/py.typed +0 -0
  29. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/scanning/__init__.py +0 -0
  30. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze/scanning/client.py +0 -0
  31. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze.egg-info/dependency_links.txt +0 -0
  32. {pyglaze-0.2.0 → pyglaze-0.2.2}/src/pyglaze.egg-info/requires.txt +0 -0
  33. {pyglaze-0.2.0 → pyglaze-0.2.2}/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.2.0
3
+ Version: 0.2.2
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.2.0"
3
+ version = "0.2.2"
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.2.0"
77
+ current_version = "0.2.2"
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.2"
@@ -32,6 +32,13 @@ if TYPE_CHECKING:
32
32
  from pyglaze.helpers.types import FloatArray
33
33
 
34
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
+
35
42
  @dataclass
36
43
  class _ForceAmpCom:
37
44
  config: ForceDeviceConfiguration
@@ -98,9 +105,7 @@ class _ForceAmpCom:
98
105
 
99
106
  def __del__(self: _ForceAmpCom) -> None:
100
107
  """Closes connection when class instance goes out of scope."""
101
- with contextlib.suppress(AttributeError):
102
- # If the serial device does not exist, self.__ser is never created - hence catch
103
- self.__ser.close()
108
+ self.disconnect()
104
109
 
105
110
  def write_all(self: _ForceAmpCom) -> list[str]:
106
111
  responses = []
@@ -183,6 +188,12 @@ class _ForceAmpCom:
183
188
  output_array[iteration, 2] = angle
184
189
  return output_array
185
190
 
191
+ def disconnect(self: _ForceAmpCom) -> None:
192
+ """Closes connection."""
193
+ with contextlib.suppress(AttributeError):
194
+ # If the serial device does not exist, self.__ser is never created - hence catch
195
+ self.__ser.close()
196
+
186
197
  def _encode_send_response(self: _ForceAmpCom, command: str) -> str:
187
198
  self._encode_and_send(command)
188
199
  return self._get_response()
@@ -257,9 +268,7 @@ class _LeAmpCom:
257
268
 
258
269
  def __del__(self: _LeAmpCom) -> None:
259
270
  """Closes connection when class instance goes out of scope."""
260
- with contextlib.suppress(AttributeError):
261
- # If the serial device does not exist, self.__ser is never created - hence catch
262
- self.__ser.close()
271
+ self.disconnect()
263
272
 
264
273
  def write_all(self: _LeAmpCom) -> list[str]:
265
274
  responses: list[str] = []
@@ -272,12 +281,12 @@ class _LeAmpCom:
272
281
  self._raw_byte_send_ints(
273
282
  [self.scanning_points, self.config.integration_periods, self.config.use_ema]
274
283
  )
275
- return self._get_response()
284
+ return self._get_response(self.SEND_SETTINGS_COMMAND)
276
285
 
277
286
  def write_list(self: _LeAmpCom) -> str:
278
287
  self._encode_send_response(self.SEND_LIST_COMMAND)
279
288
  self._raw_byte_send_floats(self.scanning_list)
280
- return self._get_response()
289
+ return self._get_response(self.SEND_LIST_COMMAND)
281
290
 
282
291
  def start_scan(self: _LeAmpCom) -> tuple[str, np.ndarray, np.ndarray, np.ndarray]:
283
292
  self._encode_send_response(self.START_COMMAND)
@@ -287,6 +296,12 @@ class _LeAmpCom:
287
296
  radii, angles = self._convert_to_r_angle(Xs, Ys)
288
297
  return self.START_COMMAND, np.array(times), np.array(radii), np.array(angles)
289
298
 
299
+ def disconnect(self: _LeAmpCom) -> None:
300
+ """Closes connection when class instance goes out of scope."""
301
+ with contextlib.suppress(AttributeError):
302
+ # If the serial device does not exist, self.__ser is never created - hence catch
303
+ self.__ser.close()
304
+
290
305
  @cached_property
291
306
  def _intervals(self: _LeAmpCom) -> list[Interval]:
292
307
  """Intervals squished into effective DAC range."""
@@ -301,7 +316,7 @@ class _LeAmpCom:
301
316
 
302
317
  def _encode_send_response(self: _LeAmpCom, command: str) -> str:
303
318
  self._encode_and_send(command)
304
- return self._get_response()
319
+ return self._get_response(command)
305
320
 
306
321
  def _encode_and_send(self: _LeAmpCom, command: str) -> None:
307
322
  self.__ser.write(command.encode(self.ENCODING))
@@ -326,9 +341,19 @@ class _LeAmpCom:
326
341
  time.sleep(self.config._sweep_length_ms * 1e-3 * 0.01) # noqa: SLF001, access to private attribute for backwards compatibility
327
342
  status = self._get_status()
328
343
 
329
- @_BackoffRetry(backoff_base=0.05, logger=logging.getLogger(LOGGER_NAME))
330
- def _get_response(self: _LeAmpCom) -> str:
331
- return self.__ser.read_until().decode(self.ENCODING).strip()
344
+ @_BackoffRetry(
345
+ backoff_base=1e-2, max_tries=3, logger=logging.getLogger(LOGGER_NAME)
346
+ )
347
+ def _get_response(self: _LeAmpCom, command: str) -> str:
348
+ response = self.__ser.read_until().decode(self.ENCODING).strip()
349
+
350
+ if len(response) == 0:
351
+ msg = f"Command: '{command}'. Empty response received"
352
+ raise serialutil.SerialException(msg)
353
+ if response[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
354
+ msg = f"Command: '{command}'. Expected response '{self.OK_RESPONSE}', received: '{response}'"
355
+ raise DeviceComError(msg)
356
+ return response
332
357
 
333
358
  @_BackoffRetry(
334
359
  backoff_base=1e-2, max_tries=5, logger=logging.getLogger(LOGGER_NAME)
@@ -26,6 +26,9 @@ 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,
31
+ instant_response: bool = False,
29
32
  ) -> None:
30
33
  pass
31
34
 
@@ -40,6 +43,8 @@ class ForceMockDevice(MockDevice):
40
43
  self: ForceMockDevice,
41
44
  fail_after: float = np.inf,
42
45
  n_fails: float = np.inf,
46
+ *,
47
+ instant_response: bool = False,
43
48
  ) -> None:
44
49
  self.fail_after = fail_after
45
50
  self.fails_wanted = n_fails
@@ -48,6 +53,7 @@ class ForceMockDevice(MockDevice):
48
53
  self.rng = np.random.default_rng()
49
54
  self.valid_input = True
50
55
  self.experiment_running = False
56
+ self.instant_response = instant_response
51
57
 
52
58
  self._periods = None
53
59
  self._frequency = None
@@ -129,7 +135,8 @@ class ForceMockDevice(MockDevice):
129
135
  for _ in range(self.in_waiting):
130
136
  return_string += self.__create_random_datapoint
131
137
  return_string += "!D,DONE\\r"
132
- sleep(self.sweep_length * 1e-3)
138
+ if not self.instant_response:
139
+ sleep(self.sweep_length * 1e-3)
133
140
  self.n_scans += 1
134
141
  if self.n_scans > self.fail_after and self.n_failures < self.fails_wanted:
135
142
  self.n_failures += 1
@@ -183,7 +190,12 @@ class LeMockDevice(MockDevice):
183
190
  DAC_BITWIDTH = 2**12
184
191
 
185
192
  def __init__(
186
- self: LeMockDevice, fail_after: float = np.inf, n_fails: float = np.inf
193
+ self: LeMockDevice,
194
+ fail_after: float = np.inf,
195
+ n_fails: float = np.inf,
196
+ *,
197
+ empty_responses: bool = False,
198
+ instant_response: bool = False,
187
199
  ) -> None:
188
200
  self.fail_after = fail_after
189
201
  self.fails_wanted = n_fails
@@ -197,6 +209,8 @@ class LeMockDevice(MockDevice):
197
209
  self.use_ema: bool | None = None
198
210
  self.scanning_list: list[float] | None = None
199
211
  self._scan_start_time: float | None = None
212
+ self.empty_responses = empty_responses
213
+ self.instant_response = instant_response
200
214
 
201
215
  def write(self: LeMockDevice, input_bytes: bytes) -> None:
202
216
  """Mock-write to the serial connection."""
@@ -217,12 +231,16 @@ class LeMockDevice(MockDevice):
217
231
 
218
232
  def read(self: LeMockDevice, size: int) -> bytes:
219
233
  """Mock-read from the serial connection."""
234
+ if self.empty_responses:
235
+ return self._create_scan_bytes(n_bytes=0)
220
236
  if self.state == _LeMockState.IDLE:
221
237
  return self._create_scan_bytes(n_bytes=size)
222
238
  raise NotImplementedError
223
239
 
224
240
  def read_until(self: LeMockDevice, _: bytes = b"\r") -> bytes: # noqa: PLR0911
225
241
  """Mock-read_until from the serial connection."""
242
+ if self.empty_responses:
243
+ return "".encode(self.ENCODING)
226
244
  if self.state == _LeMockState.WAITING_FOR_SETTINGS:
227
245
  return "ACK: Ready to receive settings.".encode(self.ENCODING)
228
246
  if self.state == _LeMockState.RECEIVED_SETTINGS:
@@ -353,6 +371,8 @@ def list_mock_devices() -> list[str]:
353
371
  "mock_device",
354
372
  "mock_device_scan_should_fail",
355
373
  "mock_device_fail_first_scan",
374
+ "mock_device_empty_responses",
375
+ "mock_device_instant",
356
376
  ]
357
377
 
358
378
 
@@ -362,8 +382,12 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
362
382
  return mock_class(fail_after=0)
363
383
  if config.amp_port == "mock_device":
364
384
  return mock_class()
385
+ if config.amp_port == "mock_device_instant":
386
+ return mock_class(instant_response=True)
365
387
  if config.amp_port == "mock_device_fail_first_scan":
366
388
  return mock_class(fail_after=0, n_fails=1)
389
+ if config.amp_port == "mock_device_empty_responses":
390
+ return mock_class(empty_responses=True)
367
391
 
368
392
  msg = f"Unknown mock device requested: {config.amp_port}. Valid options are: {list_mock_devices()}"
369
393
  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,10 +123,11 @@ 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
  )
130
+ scanner.disconnect()
128
131
  break
129
132
 
130
133
  try:
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ScanError(Exception):
5
+ """Exception raised when an error while scanning occurs."""
6
+
7
+ def __init__(self: ScanError, msg: str) -> None:
8
+ super().__init__(msg)
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from typing import TYPE_CHECKING, Generic, TypeVar
5
5
 
6
6
  import numpy as np
7
+ from serial import SerialException
7
8
 
8
9
  from pyglaze.datamodels import UnprocessedWaveform
9
10
  from pyglaze.device.ampcom import _ForceAmpCom, _LeAmpCom
@@ -12,6 +13,7 @@ from pyglaze.device.configuration import (
12
13
  ForceDeviceConfiguration,
13
14
  LeDeviceConfiguration,
14
15
  )
16
+ from pyglaze.scanning._exceptions import ScanError
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  from pyglaze.helpers.types import FloatArray
@@ -42,6 +44,10 @@ class _ScannerImplementation(ABC, Generic[TConfig]):
42
44
  def update_config(self: _ScannerImplementation, new_config: TConfig) -> None:
43
45
  pass
44
46
 
47
+ @abstractmethod
48
+ def disconnect(self: _ScannerImplementation) -> None:
49
+ pass
50
+
45
51
 
46
52
  class Scanner:
47
53
  """A synchronous scanner for Glaze terahertz devices."""
@@ -76,6 +82,10 @@ class Scanner:
76
82
  """
77
83
  self._scanner_impl.update_config(new_config)
78
84
 
85
+ def disconnect(self: Scanner) -> None:
86
+ """Close serial connection."""
87
+ self._scanner_impl.disconnect()
88
+
79
89
 
80
90
  class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
81
91
  """Perform synchronous terahertz scanning using a given DeviceConfiguration.
@@ -87,7 +97,7 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
87
97
 
88
98
  def __init__(self: ForceScanner, config: ForceDeviceConfiguration) -> None:
89
99
  self._config: ForceDeviceConfiguration
90
- self._ampcom: _ForceAmpCom
100
+ self._ampcom: _ForceAmpCom | None = None
91
101
  self.config = config
92
102
  self._phase_estimator = _LockinPhaseEstimator()
93
103
 
@@ -133,6 +143,9 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
133
143
  Returns:
134
144
  Unprocessed scan.
135
145
  """
146
+ if self._ampcom is None:
147
+ msg = "Scanner not configured"
148
+ raise ScanError(msg)
136
149
  _, responses = self._ampcom.start_scan()
137
150
 
138
151
  time = responses[:, 0]
@@ -152,6 +165,14 @@ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
152
165
  """
153
166
  self.config = new_config
154
167
 
168
+ def disconnect(self: ForceScanner) -> None:
169
+ """Close serial connection."""
170
+ if self._ampcom is None:
171
+ msg = "Scanner not connected"
172
+ raise SerialException(msg)
173
+ self._ampcom.disconnect()
174
+ self._ampcom = None
175
+
155
176
 
156
177
  class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
157
178
  """Perform synchronous terahertz scanning using a given DeviceConfiguration.
@@ -162,7 +183,7 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
162
183
 
163
184
  def __init__(self: LeScanner, config: LeDeviceConfiguration) -> None:
164
185
  self._config: LeDeviceConfiguration
165
- self._ampcom: _LeAmpCom
186
+ self._ampcom: _LeAmpCom | None = None
166
187
  self.config = config
167
188
  self._phase_estimator = _LockinPhaseEstimator()
168
189
 
@@ -198,6 +219,9 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
198
219
  Returns:
199
220
  Unprocessed scan.
200
221
  """
222
+ if self._ampcom is None:
223
+ msg = "Scanner not configured"
224
+ raise ScanError(msg)
201
225
  _, time, radius, theta = self._ampcom.start_scan()
202
226
  self._phase_estimator.update_estimate(radius=radius, theta=theta)
203
227
 
@@ -213,6 +237,14 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
213
237
  """
214
238
  self.config = new_config
215
239
 
240
+ def disconnect(self: LeScanner) -> None:
241
+ """Close serial connection."""
242
+ if self._ampcom is None:
243
+ msg = "Scanner not connected"
244
+ raise ScanError(msg)
245
+ self._ampcom.disconnect()
246
+ self._ampcom = None
247
+
216
248
 
217
249
  def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
218
250
  if isinstance(config, ForceDeviceConfiguration):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyglaze
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -25,5 +25,6 @@ src/pyglaze/interpolation/__init__.py
25
25
  src/pyglaze/interpolation/interpolation.py
26
26
  src/pyglaze/scanning/__init__.py
27
27
  src/pyglaze/scanning/_asyncscanner.py
28
+ src/pyglaze/scanning/_exceptions.py
28
29
  src/pyglaze/scanning/client.py
29
30
  src/pyglaze/scanning/scanner.py
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes
File without changes