pyglaze 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyglaze/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.1"
1
+ __version__ = "0.2.2"
pyglaze/device/ampcom.py CHANGED
@@ -105,9 +105,7 @@ class _ForceAmpCom:
105
105
 
106
106
  def __del__(self: _ForceAmpCom) -> None:
107
107
  """Closes connection when class instance goes out of scope."""
108
- with contextlib.suppress(AttributeError):
109
- # If the serial device does not exist, self.__ser is never created - hence catch
110
- self.__ser.close()
108
+ self.disconnect()
111
109
 
112
110
  def write_all(self: _ForceAmpCom) -> list[str]:
113
111
  responses = []
@@ -190,6 +188,12 @@ class _ForceAmpCom:
190
188
  output_array[iteration, 2] = angle
191
189
  return output_array
192
190
 
191
+ def disconnect(self: _ForceAmpCom) -> None:
192
+ """Closes connection."""
193
+ with contextlib.suppress(AttributeError):
194
+ # If the serial device does not exist, self.__ser is never created - hence catch
195
+ self.__ser.close()
196
+
193
197
  def _encode_send_response(self: _ForceAmpCom, command: str) -> str:
194
198
  self._encode_and_send(command)
195
199
  return self._get_response()
@@ -264,9 +268,7 @@ class _LeAmpCom:
264
268
 
265
269
  def __del__(self: _LeAmpCom) -> None:
266
270
  """Closes connection when class instance goes out of scope."""
267
- with contextlib.suppress(AttributeError):
268
- # If the serial device does not exist, self.__ser is never created - hence catch
269
- self.__ser.close()
271
+ self.disconnect()
270
272
 
271
273
  def write_all(self: _LeAmpCom) -> list[str]:
272
274
  responses: list[str] = []
@@ -294,6 +296,12 @@ class _LeAmpCom:
294
296
  radii, angles = self._convert_to_r_angle(Xs, Ys)
295
297
  return self.START_COMMAND, np.array(times), np.array(radii), np.array(angles)
296
298
 
299
+ def disconnect(self: _LeAmpCom) -> None:
300
+ """Closes connection when class instance goes out of scope."""
301
+ with contextlib.suppress(AttributeError):
302
+ # If the serial device does not exist, self.__ser is never created - hence catch
303
+ self.__ser.close()
304
+
297
305
  @cached_property
298
306
  def _intervals(self: _LeAmpCom) -> list[Interval]:
299
307
  """Intervals squished into effective DAC range."""
@@ -28,6 +28,7 @@ class MockDevice(ABC):
28
28
  n_fails: float = np.inf,
29
29
  *,
30
30
  empty_responses: bool = False,
31
+ instant_response: bool = False,
31
32
  ) -> None:
32
33
  pass
33
34
 
@@ -42,6 +43,8 @@ class ForceMockDevice(MockDevice):
42
43
  self: ForceMockDevice,
43
44
  fail_after: float = np.inf,
44
45
  n_fails: float = np.inf,
46
+ *,
47
+ instant_response: bool = False,
45
48
  ) -> None:
46
49
  self.fail_after = fail_after
47
50
  self.fails_wanted = n_fails
@@ -50,6 +53,7 @@ class ForceMockDevice(MockDevice):
50
53
  self.rng = np.random.default_rng()
51
54
  self.valid_input = True
52
55
  self.experiment_running = False
56
+ self.instant_response = instant_response
53
57
 
54
58
  self._periods = None
55
59
  self._frequency = None
@@ -131,7 +135,8 @@ class ForceMockDevice(MockDevice):
131
135
  for _ in range(self.in_waiting):
132
136
  return_string += self.__create_random_datapoint
133
137
  return_string += "!D,DONE\\r"
134
- sleep(self.sweep_length * 1e-3)
138
+ if not self.instant_response:
139
+ sleep(self.sweep_length * 1e-3)
135
140
  self.n_scans += 1
136
141
  if self.n_scans > self.fail_after and self.n_failures < self.fails_wanted:
137
142
  self.n_failures += 1
@@ -190,6 +195,7 @@ class LeMockDevice(MockDevice):
190
195
  n_fails: float = np.inf,
191
196
  *,
192
197
  empty_responses: bool = False,
198
+ instant_response: bool = False,
193
199
  ) -> None:
194
200
  self.fail_after = fail_after
195
201
  self.fails_wanted = n_fails
@@ -204,6 +210,7 @@ class LeMockDevice(MockDevice):
204
210
  self.scanning_list: list[float] | None = None
205
211
  self._scan_start_time: float | None = None
206
212
  self.empty_responses = empty_responses
213
+ self.instant_response = instant_response
207
214
 
208
215
  def write(self: LeMockDevice, input_bytes: bytes) -> None:
209
216
  """Mock-write to the serial connection."""
@@ -365,6 +372,7 @@ def list_mock_devices() -> list[str]:
365
372
  "mock_device_scan_should_fail",
366
373
  "mock_device_fail_first_scan",
367
374
  "mock_device_empty_responses",
375
+ "mock_device_instant",
368
376
  ]
369
377
 
370
378
 
@@ -374,6 +382,8 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
374
382
  return mock_class(fail_after=0)
375
383
  if config.amp_port == "mock_device":
376
384
  return mock_class()
385
+ if config.amp_port == "mock_device_instant":
386
+ return mock_class(instant_response=True)
377
387
  if config.amp_port == "mock_device_fail_first_scan":
378
388
  return mock_class(fail_after=0, n_fails=1)
379
389
  if config.amp_port == "mock_device_empty_responses":
@@ -127,6 +127,7 @@ class _AsyncScanner:
127
127
  parent_conn.send(
128
128
  _ScannerHealth(is_alive=False, is_healthy=False, error=e)
129
129
  )
130
+ scanner.disconnect()
130
131
  break
131
132
 
132
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.1
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,13 +1,13 @@
1
- pyglaze/__init__.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
1
+ pyglaze/__init__.py,sha256=m6kyaNpwBcP1XYcqrelX2oS3PJuOnElOcRdBa9pEb8c,22
2
2
  pyglaze/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  pyglaze/datamodels/__init__.py,sha256=DJLByl2C7pC4RM4Uh6PW-McM5RIGBjcopzGywCKhSlI,111
4
4
  pyglaze/datamodels/pulse.py,sha256=BoW_GDvkwEpn_UYUFPoUfi5E_oCnvv8cU4nZKG7Ph2U,20911
5
5
  pyglaze/datamodels/waveform.py,sha256=n31DhJHFeBNZ3hHqQUiCGXssm5Dc8wV6tGPkhmFYB4Q,5809
6
6
  pyglaze/device/__init__.py,sha256=5RjCHuFKMi9g2KLUkxixO9hNpAgkUBcOURNTuhAdoUk,177
7
- pyglaze/device/ampcom.py,sha256=6JXl7PojYr4F1_pdVItO-XbGCp8mpy4wpwaaUn8Loog,17214
7
+ pyglaze/device/ampcom.py,sha256=9smeYg5-3accfYWRg3K2wg1tGfirnWmPxPa4IYSvyRI,17465
8
8
  pyglaze/device/configuration.py,sha256=gh_eerX8TdXx3LnFxHieJqOpfDfE9cV6Xgm5WYVnvO0,7994
9
9
  pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
10
- pyglaze/devtools/mock_device.py,sha256=3RMa-JAehbbRCmOO74aZzGPdYp2qsP-XE8ulzLXDP6Q,13994
10
+ pyglaze/devtools/mock_device.py,sha256=Fz0ZCRIslokBz6gFEztv1-V1UJhdTDqujl7-l8seX7U,14394
11
11
  pyglaze/devtools/thz_pulse.py,sha256=xp-T9psdOrUMtSUFu8HEwQJVu_aMixJdZHtg_BCVu_k,923
12
12
  pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  pyglaze/helpers/types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
@@ -15,11 +15,12 @@ pyglaze/helpers/utilities.py,sha256=n_x9Tqm305MUorS29O6CoJM8Mi4apo2bsN_odrRaVAw,
15
15
  pyglaze/interpolation/__init__.py,sha256=WCxHPsiI7zvJykp-jfytoEbO4Tla-YIF6A7fjDfcDvU,72
16
16
  pyglaze/interpolation/interpolation.py,sha256=rQWzPD7W8TXETps7VZI0gcfAOCWO8pGL1HhhBnyxaMw,735
17
17
  pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
18
- pyglaze/scanning/_asyncscanner.py,sha256=SldM7XCavfugsDstxMVI_WLL6GUJutLYN0iU__mfY5A,5277
18
+ pyglaze/scanning/_asyncscanner.py,sha256=blnpdKBieSrUyTqmm1wZtBqp7X66WE-sFae_PpimFNU,5314
19
+ pyglaze/scanning/_exceptions.py,sha256=vS28Dijj76jVuF6cSDBKqM9SQIa9rbIyUaHF-RA3PyM,213
19
20
  pyglaze/scanning/client.py,sha256=3qrQStkeLQzCeu4yMHJ_ENLGQ7E5GMc4CP9J55rk-ug,1817
20
- pyglaze/scanning/scanner.py,sha256=PSjXVpSpHpYIl-sW34pIThocbM9GSHJ_E4gGcsePeTw,8139
21
- pyglaze-0.2.1.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
22
- pyglaze-0.2.1.dist-info/METADATA,sha256=WETmnm6CF1_eyojd-ahfIFLJ_VCxV2jshcrbSoPLg3Y,3498
23
- pyglaze-0.2.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
24
- pyglaze-0.2.1.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
25
- pyglaze-0.2.1.dist-info/RECORD,,
21
+ pyglaze/scanning/scanner.py,sha256=5KYVUboK4f8DWp-qRHWbvdtQ0x46AIdnCkquAO4vQOI,9205
22
+ pyglaze-0.2.2.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
23
+ pyglaze-0.2.2.dist-info/METADATA,sha256=fZcMCJSLAWa1ZrZDV6CPQ0jdGjhQLt5jBJ9srEgYjQM,3498
24
+ pyglaze-0.2.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
25
+ pyglaze-0.2.2.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
26
+ pyglaze-0.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5