pyglaze 0.2.2__py3-none-any.whl → 0.4.0__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 +1 -1
- pyglaze/datamodels/pulse.py +178 -127
- pyglaze/datamodels/waveform.py +1 -1
- pyglaze/device/__init__.py +2 -3
- pyglaze/device/ampcom.py +25 -199
- pyglaze/device/configuration.py +7 -99
- pyglaze/devtools/mock_device.py +15 -150
- pyglaze/devtools/thz_pulse.py +4 -3
- pyglaze/helpers/utilities.py +6 -6
- pyglaze/interpolation/__init__.py +2 -2
- pyglaze/interpolation/interpolation.py +22 -2
- pyglaze/scanning/_asyncscanner.py +32 -5
- pyglaze/scanning/client.py +17 -1
- pyglaze/scanning/scanner.py +51 -92
- {pyglaze-0.2.2.dist-info → pyglaze-0.4.0.dist-info}/METADATA +8 -7
- pyglaze-0.4.0.dist-info/RECORD +26 -0
- {pyglaze-0.2.2.dist-info → pyglaze-0.4.0.dist-info}/WHEEL +1 -1
- pyglaze-0.2.2.dist-info/RECORD +0 -26
- /pyglaze/helpers/{types.py → _types.py} +0 -0
- {pyglaze-0.2.2.dist-info → pyglaze-0.4.0.dist-info/licenses}/LICENSE +0 -0
- {pyglaze-0.2.2.dist-info → pyglaze-0.4.0.dist-info}/top_level.txt +0 -0
pyglaze/device/ampcom.py
CHANGED
|
@@ -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, Callable, ClassVar
|
|
10
|
+
from typing import TYPE_CHECKING, Callable, ClassVar
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
import serial
|
|
@@ -16,7 +16,6 @@ from serial import serialutil
|
|
|
16
16
|
|
|
17
17
|
from pyglaze.device.configuration import (
|
|
18
18
|
DeviceConfiguration,
|
|
19
|
-
ForceDeviceConfiguration,
|
|
20
19
|
Interval,
|
|
21
20
|
LeDeviceConfiguration,
|
|
22
21
|
)
|
|
@@ -24,12 +23,8 @@ from pyglaze.devtools.mock_device import _mock_device_factory
|
|
|
24
23
|
from pyglaze.helpers.utilities import LOGGER_NAME, _BackoffRetry
|
|
25
24
|
|
|
26
25
|
if TYPE_CHECKING:
|
|
27
|
-
from pyglaze.devtools.mock_device import
|
|
28
|
-
|
|
29
|
-
LeMockDevice,
|
|
30
|
-
MockDevice,
|
|
31
|
-
)
|
|
32
|
-
from pyglaze.helpers.types import FloatArray
|
|
26
|
+
from pyglaze.devtools.mock_device import LeMockDevice
|
|
27
|
+
from pyglaze.helpers._types import FloatArray
|
|
33
28
|
|
|
34
29
|
|
|
35
30
|
class DeviceComError(Exception):
|
|
@@ -39,186 +34,6 @@ class DeviceComError(Exception):
|
|
|
39
34
|
super().__init__(message)
|
|
40
35
|
|
|
41
36
|
|
|
42
|
-
@dataclass
|
|
43
|
-
class _ForceAmpCom:
|
|
44
|
-
config: ForceDeviceConfiguration
|
|
45
|
-
CONT_SCAN_UPDATE_FREQ: float = 1 # seconds
|
|
46
|
-
__ser: ForceMockDevice | serial.Serial = field(init=False)
|
|
47
|
-
|
|
48
|
-
ENCODING: ClassVar[str] = "utf-8"
|
|
49
|
-
OK_RESPONSE: ClassVar[str] = "!A,OK"
|
|
50
|
-
N_POINTS: ClassVar[int] = 10000
|
|
51
|
-
DAC_BITWIDTH: ClassVar[int] = 65535 # bit-width of amp DAC
|
|
52
|
-
# DO NOT change - antennas will break.
|
|
53
|
-
MIN_ALLOWED_MOD_VOLTAGE: ClassVar[float] = -1.0
|
|
54
|
-
MAX_ALLOWED_MOD_VOLTAGE: ClassVar[float] = 0.5
|
|
55
|
-
|
|
56
|
-
@cached_property
|
|
57
|
-
def scanning_points(self: _ForceAmpCom) -> int:
|
|
58
|
-
time_pr_point = (
|
|
59
|
-
self.config.integration_periods / self.config.modulation_frequency
|
|
60
|
-
)
|
|
61
|
-
return int(self.config.sweep_length_ms * 1e-3 / time_pr_point)
|
|
62
|
-
|
|
63
|
-
@cached_property
|
|
64
|
-
def _squished_intervals(self: _ForceAmpCom) -> list[Interval]:
|
|
65
|
-
"""Intervals squished into effective DAC range."""
|
|
66
|
-
return _squish_intervals(
|
|
67
|
-
intervals=self.config.scan_intervals or [Interval(lower=0.0, upper=1.0)],
|
|
68
|
-
lower_bound=self.config.dac_lower_bound,
|
|
69
|
-
upper_bound=self.config.dac_upper_bound,
|
|
70
|
-
bitwidth=self.DAC_BITWIDTH,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
@cached_property
|
|
74
|
-
def times(self: _ForceAmpCom) -> FloatArray:
|
|
75
|
-
return _delay_from_intervals(
|
|
76
|
-
delayunit=lambda x: x,
|
|
77
|
-
intervals=self.config.scan_intervals,
|
|
78
|
-
points_per_interval=_points_per_interval(
|
|
79
|
-
self.scanning_points, self._squished_intervals
|
|
80
|
-
),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
@cached_property
|
|
84
|
-
def scanning_list(self: _ForceAmpCom) -> list[float]:
|
|
85
|
-
scanning_list: list[float] = []
|
|
86
|
-
for interval, n_points in zip(
|
|
87
|
-
self._squished_intervals,
|
|
88
|
-
_points_per_interval(self.N_POINTS, self._squished_intervals),
|
|
89
|
-
):
|
|
90
|
-
scanning_list.extend(
|
|
91
|
-
np.linspace(interval.lower, interval.upper, n_points, endpoint=False)
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
return scanning_list
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def datapoints_per_update(self: _ForceAmpCom) -> int:
|
|
98
|
-
return int(
|
|
99
|
-
self.CONT_SCAN_UPDATE_FREQ
|
|
100
|
-
/ (self.config.integration_periods / self.config.modulation_frequency)
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def __post_init__(self: _ForceAmpCom) -> None:
|
|
104
|
-
self.__ser = _serial_factory(self.config)
|
|
105
|
-
|
|
106
|
-
def __del__(self: _ForceAmpCom) -> None:
|
|
107
|
-
"""Closes connection when class instance goes out of scope."""
|
|
108
|
-
self.disconnect()
|
|
109
|
-
|
|
110
|
-
def write_all(self: _ForceAmpCom) -> list[str]:
|
|
111
|
-
responses = []
|
|
112
|
-
responses.append(self.write_period_and_frequency())
|
|
113
|
-
responses.append(self.write_sweep_length())
|
|
114
|
-
responses.append(self.write_waveform())
|
|
115
|
-
responses.append(self.write_modulation_voltage())
|
|
116
|
-
responses.extend(self.write_list())
|
|
117
|
-
return responses
|
|
118
|
-
|
|
119
|
-
def write_period_and_frequency(self: _ForceAmpCom) -> str:
|
|
120
|
-
s = f"!set timing,{self.config.integration_periods},{self.config.modulation_frequency}\r"
|
|
121
|
-
return self._encode_send_response(s)
|
|
122
|
-
|
|
123
|
-
def write_sweep_length(self: _ForceAmpCom) -> str:
|
|
124
|
-
s = f"!set sweep length,{self.config.sweep_length_ms}\r"
|
|
125
|
-
return self._encode_send_response(s)
|
|
126
|
-
|
|
127
|
-
def write_waveform(self: _ForceAmpCom) -> str:
|
|
128
|
-
s = f"!set wave,{self.config.modulation_waveform}\r"
|
|
129
|
-
return self._encode_send_response(s)
|
|
130
|
-
|
|
131
|
-
def write_modulation_voltage(self: _ForceAmpCom) -> str:
|
|
132
|
-
min_v = self.config.min_modulation_voltage
|
|
133
|
-
max_v = self.config.max_modulation_voltage
|
|
134
|
-
crit1 = self.MIN_ALLOWED_MOD_VOLTAGE <= min_v <= self.MAX_ALLOWED_MOD_VOLTAGE
|
|
135
|
-
crit2 = self.MIN_ALLOWED_MOD_VOLTAGE <= max_v <= self.MAX_ALLOWED_MOD_VOLTAGE
|
|
136
|
-
|
|
137
|
-
if crit1 and crit2:
|
|
138
|
-
s = f"!set generator,{min_v},{max_v}\r"
|
|
139
|
-
return self._encode_send_response(s)
|
|
140
|
-
|
|
141
|
-
msg = f"Modulation voltages min: {min_v:.1f}, max: {max_v:.1f} not allowed."
|
|
142
|
-
raise ValueError(msg)
|
|
143
|
-
|
|
144
|
-
def write_list(self: _ForceAmpCom) -> list[str]:
|
|
145
|
-
for iteration, entry in enumerate(self.scanning_list):
|
|
146
|
-
string = f"!lut,{iteration},{entry}\r"
|
|
147
|
-
self._encode_and_send(string)
|
|
148
|
-
return self._get_response().split("\r")
|
|
149
|
-
|
|
150
|
-
def start_scan(self: _ForceAmpCom) -> tuple[str, np.ndarray]:
|
|
151
|
-
start_command = "!s,\r"
|
|
152
|
-
self._encode_and_send(start_command)
|
|
153
|
-
responses = self._get_response().split("\r")
|
|
154
|
-
output_array = np.zeros((self.scanning_points, 3))
|
|
155
|
-
output_array[:, 0] = self.times
|
|
156
|
-
iteration = 0
|
|
157
|
-
for entry in responses:
|
|
158
|
-
if "!R" in entry:
|
|
159
|
-
radius, angle = self._format_output(entry)
|
|
160
|
-
output_array[iteration, 1] = radius
|
|
161
|
-
output_array[iteration, 2] = angle
|
|
162
|
-
iteration += 1
|
|
163
|
-
elif "!D" in entry:
|
|
164
|
-
break
|
|
165
|
-
return start_command, output_array
|
|
166
|
-
|
|
167
|
-
def start_continuous_scan(self: _ForceAmpCom) -> tuple[str, list[str]]:
|
|
168
|
-
start_command = "!dat,1\r"
|
|
169
|
-
self._encode_and_send(start_command)
|
|
170
|
-
# Call self._read_until() twice, because amp returns !A,OK twice for
|
|
171
|
-
# continuous output (for some unknown reason)
|
|
172
|
-
responses = [self._read_until(expected=b"\r") for _ in range(2)]
|
|
173
|
-
return start_command, responses
|
|
174
|
-
|
|
175
|
-
def stop_continuous_scan(self: _ForceAmpCom) -> tuple[str, str]:
|
|
176
|
-
start_command = "!dat,0\r"
|
|
177
|
-
self._encode_and_send(start_command)
|
|
178
|
-
response = self._read_until(expected=b"!A,OK\r")
|
|
179
|
-
return start_command, response
|
|
180
|
-
|
|
181
|
-
def read_continuous_data(self: _ForceAmpCom) -> FloatArray:
|
|
182
|
-
output_array = np.zeros((self.datapoints_per_update, 3))
|
|
183
|
-
output_array[:, 0] = np.linspace(0, 1, self.datapoints_per_update)
|
|
184
|
-
for iteration in range(self.datapoints_per_update):
|
|
185
|
-
amp_output = self._read_until(expected=b"\r")
|
|
186
|
-
radius, angle = self._format_output(amp_output)
|
|
187
|
-
output_array[iteration, 1] = radius
|
|
188
|
-
output_array[iteration, 2] = angle
|
|
189
|
-
return output_array
|
|
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
|
-
|
|
197
|
-
def _encode_send_response(self: _ForceAmpCom, command: str) -> str:
|
|
198
|
-
self._encode_and_send(command)
|
|
199
|
-
return self._get_response()
|
|
200
|
-
|
|
201
|
-
def _encode_and_send(self: _ForceAmpCom, command: str) -> None:
|
|
202
|
-
self.__ser.write(command.encode(self.ENCODING))
|
|
203
|
-
|
|
204
|
-
@_BackoffRetry(backoff_base=0.2, logger=logging.getLogger(LOGGER_NAME))
|
|
205
|
-
def _get_response(self: _ForceAmpCom) -> str:
|
|
206
|
-
r = self.__ser.readline().decode(self.ENCODING).strip()
|
|
207
|
-
if r[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
|
|
208
|
-
msg = f"Expected response '{self.OK_RESPONSE}', received: '{r}'"
|
|
209
|
-
raise serialutil.SerialException(msg)
|
|
210
|
-
|
|
211
|
-
return r
|
|
212
|
-
|
|
213
|
-
def _read_until(self: _ForceAmpCom, expected: bytes) -> str:
|
|
214
|
-
return self.__ser.read_until(expected=expected).decode(self.ENCODING).strip()
|
|
215
|
-
|
|
216
|
-
def _format_output(self: _ForceAmpCom, amp_output: str) -> tuple[float, float]:
|
|
217
|
-
"""Format output from Force LIA to radius and angle."""
|
|
218
|
-
response_list = amp_output.split(",")
|
|
219
|
-
return float(response_list[1]), float(response_list[2])
|
|
220
|
-
|
|
221
|
-
|
|
222
37
|
@dataclass
|
|
223
38
|
class _LeAmpCom:
|
|
224
39
|
config: LeDeviceConfiguration
|
|
@@ -233,6 +48,8 @@ class _LeAmpCom:
|
|
|
233
48
|
STATUS_COMMAND: ClassVar[str] = "H"
|
|
234
49
|
SEND_LIST_COMMAND: ClassVar[str] = "L"
|
|
235
50
|
SEND_SETTINGS_COMMAND: ClassVar[str] = "S"
|
|
51
|
+
SERIAL_NUMBER_COMMAND: ClassVar[str] = "s"
|
|
52
|
+
FIRMWARE_VERSION_COMMAND: ClassVar[str] = "v"
|
|
236
53
|
|
|
237
54
|
@cached_property
|
|
238
55
|
def scanning_points(self: _LeAmpCom) -> int:
|
|
@@ -263,6 +80,14 @@ class _LeAmpCom:
|
|
|
263
80
|
"""
|
|
264
81
|
return self.scanning_points * 12
|
|
265
82
|
|
|
83
|
+
@property
|
|
84
|
+
def serial_number_bytes(self: _LeAmpCom) -> int:
|
|
85
|
+
"""Number of bytes to receive for a serial number.
|
|
86
|
+
|
|
87
|
+
Serial number has the form "<CHARACTER>-<4_DIGITS>, hence expect 6 bytes."
|
|
88
|
+
"""
|
|
89
|
+
return 6
|
|
90
|
+
|
|
266
91
|
def __post_init__(self: _LeAmpCom) -> None:
|
|
267
92
|
self.__ser = _serial_factory(self.config)
|
|
268
93
|
|
|
@@ -302,6 +127,17 @@ class _LeAmpCom:
|
|
|
302
127
|
# If the serial device does not exist, self.__ser is never created - hence catch
|
|
303
128
|
self.__ser.close()
|
|
304
129
|
|
|
130
|
+
def get_serial_number(self: _LeAmpCom) -> str:
|
|
131
|
+
"""Get the serial number of the connected device."""
|
|
132
|
+
return "X-9999"
|
|
133
|
+
# self._encode_and_send(self.SERIAL_NUMBER_COMMAND) # noqa: ERA001
|
|
134
|
+
# return self.__ser.read(self.serial_number_bytes).decode(self.ENCODING) # noqa: ERA001
|
|
135
|
+
|
|
136
|
+
def get_firmware_version(self: _LeAmpCom) -> str:
|
|
137
|
+
"""Get the firmware version of the connected device."""
|
|
138
|
+
self._encode_and_send(self.FIRMWARE_VERSION_COMMAND)
|
|
139
|
+
return self.__ser.read_until().decode(self.ENCODING).strip()
|
|
140
|
+
|
|
305
141
|
@cached_property
|
|
306
142
|
def _intervals(self: _LeAmpCom) -> list[Interval]:
|
|
307
143
|
"""Intervals squished into effective DAC range."""
|
|
@@ -398,17 +234,7 @@ class _LeStatus(Enum):
|
|
|
398
234
|
IDLE = "ACK: Idle."
|
|
399
235
|
|
|
400
236
|
|
|
401
|
-
|
|
402
|
-
def _serial_factory(
|
|
403
|
-
config: ForceDeviceConfiguration,
|
|
404
|
-
) -> serial.Serial | ForceMockDevice: ...
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
@overload
|
|
408
|
-
def _serial_factory(config: LeDeviceConfiguration) -> serial.Serial | LeMockDevice: ...
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def _serial_factory(config: DeviceConfiguration) -> serial.Serial | MockDevice:
|
|
237
|
+
def _serial_factory(config: DeviceConfiguration) -> serial.Serial | LeMockDevice:
|
|
412
238
|
if "mock_device" in config.amp_port:
|
|
413
239
|
return _mock_device_factory(config)
|
|
414
240
|
|
pyglaze/device/configuration.py
CHANGED
|
@@ -74,91 +74,6 @@ class DeviceConfiguration(ABC):
|
|
|
74
74
|
"""Load a DeviceConfiguration from a file."""
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
@dataclass
|
|
78
|
-
class ForceDeviceConfiguration(DeviceConfiguration):
|
|
79
|
-
"""Represents a configuration that can be sent to the lock-in amp for scans.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
amp_port: The name of the serial port the amp is connected to.
|
|
83
|
-
sweep_length_ms: The length of the sweep in milliseconds.
|
|
84
|
-
scan_intervals: The intervals to scan.
|
|
85
|
-
integration_periods: The number of integration periods to use.
|
|
86
|
-
modulation_frequency: The frequency of the modulation in Hz.
|
|
87
|
-
dac_lower_bound: The lower bound of the modulation voltage in bits.
|
|
88
|
-
dac_upper_bound: The upper bound of the modulation voltage in bits.
|
|
89
|
-
min_modulation_voltage: The minimum modulation voltage in volts.
|
|
90
|
-
max_modulation_voltage: The maximum modulation voltage in volts.
|
|
91
|
-
modulation_waveform: The waveform to use for modulation.
|
|
92
|
-
amp_timeout_seconds: The timeout for the amp in seconds.
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
amp_port: str
|
|
96
|
-
sweep_length_ms: float
|
|
97
|
-
scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
|
|
98
|
-
integration_periods: int = 100
|
|
99
|
-
modulation_frequency: int = 10000 # Hz
|
|
100
|
-
dac_lower_bound: int = 6400
|
|
101
|
-
dac_upper_bound: int = 59300
|
|
102
|
-
min_modulation_voltage: float = -1.0 # V
|
|
103
|
-
max_modulation_voltage: float = 0.5 # V
|
|
104
|
-
modulation_waveform: str = "square"
|
|
105
|
-
amp_timeout_seconds: float = 0.05
|
|
106
|
-
|
|
107
|
-
amp_baudrate: ClassVar[int] = 1200000 # bit/s
|
|
108
|
-
|
|
109
|
-
@property
|
|
110
|
-
def _sweep_length_ms(self: ForceDeviceConfiguration) -> float:
|
|
111
|
-
return self.sweep_length_ms
|
|
112
|
-
|
|
113
|
-
def save(self: ForceDeviceConfiguration, path: Path) -> str:
|
|
114
|
-
"""Save a DeviceConfiguration to a file.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
path: The path to save the configuration to.
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
str: Final path component of the saved file, without the extension.
|
|
121
|
-
|
|
122
|
-
"""
|
|
123
|
-
with path.open("w") as f:
|
|
124
|
-
json.dump(asdict(self), f, indent=4, sort_keys=True)
|
|
125
|
-
|
|
126
|
-
return path.stem
|
|
127
|
-
|
|
128
|
-
@classmethod
|
|
129
|
-
def from_dict(
|
|
130
|
-
cls: type[ForceDeviceConfiguration], amp_config: dict
|
|
131
|
-
) -> ForceDeviceConfiguration:
|
|
132
|
-
"""Create a DeviceConfiguration from a dict.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
amp_config: An amp configuration in dict form.
|
|
136
|
-
|
|
137
|
-
Raises:
|
|
138
|
-
ValueError: If the dictionary is empty.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
DeviceConfiguration: A DeviceConfiguration object.
|
|
142
|
-
"""
|
|
143
|
-
return _config_w_intervals_from_dict(cls, amp_config)
|
|
144
|
-
|
|
145
|
-
@classmethod
|
|
146
|
-
def load(
|
|
147
|
-
cls: type[ForceDeviceConfiguration], file_path: Path
|
|
148
|
-
) -> ForceDeviceConfiguration:
|
|
149
|
-
"""Load a DeviceConfiguration from a file.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
file_path: The path to the file to load.
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
DeviceConfiguration: A DeviceConfiguration object.
|
|
156
|
-
"""
|
|
157
|
-
with file_path.open() as f:
|
|
158
|
-
configuration_dict = json.load(f)
|
|
159
|
-
return cls.from_dict(configuration_dict)
|
|
160
|
-
|
|
161
|
-
|
|
162
77
|
@dataclass
|
|
163
78
|
class LeDeviceConfiguration(DeviceConfiguration):
|
|
164
79
|
"""Represents a configuration that can be sent to a Le-type lock-in amp for scans.
|
|
@@ -220,7 +135,13 @@ class LeDeviceConfiguration(DeviceConfiguration):
|
|
|
220
135
|
Returns:
|
|
221
136
|
DeviceConfiguration: A DeviceConfiguration object.
|
|
222
137
|
"""
|
|
223
|
-
|
|
138
|
+
if not amp_config:
|
|
139
|
+
msg = "'amp_config' is empty."
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
|
|
142
|
+
config = cls(**amp_config)
|
|
143
|
+
config.scan_intervals = [Interval.from_dict(d) for d in config.scan_intervals] # type: ignore[arg-type]
|
|
144
|
+
return config
|
|
224
145
|
|
|
225
146
|
@classmethod
|
|
226
147
|
def load(
|
|
@@ -237,16 +158,3 @@ class LeDeviceConfiguration(DeviceConfiguration):
|
|
|
237
158
|
with file_path.open() as f:
|
|
238
159
|
configuration_dict = json.load(f)
|
|
239
160
|
return cls.from_dict(configuration_dict)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
C = TypeVar("C", LeDeviceConfiguration, ForceDeviceConfiguration)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _config_w_intervals_from_dict(cls: type[C], amp_config: dict) -> C:
|
|
246
|
-
if amp_config:
|
|
247
|
-
config = cls(**amp_config)
|
|
248
|
-
config.scan_intervals = [Interval.from_dict(d) for d in config.scan_intervals] # type: ignore[arg-type]
|
|
249
|
-
return config
|
|
250
|
-
|
|
251
|
-
msg = "'amp_config' is empty."
|
|
252
|
-
raise ValueError(msg)
|
pyglaze/devtools/mock_device.py
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import struct
|
|
5
4
|
import time
|
|
6
5
|
from abc import ABC, abstractmethod
|
|
7
6
|
from enum import Enum, auto
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from time import sleep
|
|
10
7
|
|
|
11
8
|
import numpy as np
|
|
12
|
-
from serial import serialutil
|
|
13
9
|
|
|
14
|
-
from pyglaze.device.configuration import
|
|
15
|
-
DeviceConfiguration,
|
|
16
|
-
ForceDeviceConfiguration,
|
|
17
|
-
LeDeviceConfiguration,
|
|
18
|
-
)
|
|
10
|
+
from pyglaze.device.configuration import DeviceConfiguration, LeDeviceConfiguration
|
|
19
11
|
|
|
20
12
|
|
|
21
13
|
class MockDevice(ABC):
|
|
@@ -33,143 +25,6 @@ class MockDevice(ABC):
|
|
|
33
25
|
pass
|
|
34
26
|
|
|
35
27
|
|
|
36
|
-
class ForceMockDevice(MockDevice):
|
|
37
|
-
"""Mock device for devices using a FORCE lockin for testing purposes."""
|
|
38
|
-
|
|
39
|
-
CONFIG_PATH = Path(__file__).parents[1] / "devtools" / "mockup_config.json"
|
|
40
|
-
ENCODING: str = "utf-8"
|
|
41
|
-
|
|
42
|
-
def __init__(
|
|
43
|
-
self: ForceMockDevice,
|
|
44
|
-
fail_after: float = np.inf,
|
|
45
|
-
n_fails: float = np.inf,
|
|
46
|
-
*,
|
|
47
|
-
instant_response: bool = False,
|
|
48
|
-
) -> None:
|
|
49
|
-
self.fail_after = fail_after
|
|
50
|
-
self.fails_wanted = n_fails
|
|
51
|
-
self.n_failures = 0
|
|
52
|
-
self.n_scans = 0
|
|
53
|
-
self.rng = np.random.default_rng()
|
|
54
|
-
self.valid_input = True
|
|
55
|
-
self.experiment_running = False
|
|
56
|
-
self.instant_response = instant_response
|
|
57
|
-
|
|
58
|
-
self._periods = None
|
|
59
|
-
self._frequency = None
|
|
60
|
-
self._sweep_length = None
|
|
61
|
-
if not self.CONFIG_PATH.exists():
|
|
62
|
-
conf = {"periods": None, "frequency": None, "sweep_length_ms": None}
|
|
63
|
-
self.__save(conf)
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def periods(self: ForceMockDevice) -> int:
|
|
67
|
-
"""Number of integration periods."""
|
|
68
|
-
return int(self.__get_val("periods"))
|
|
69
|
-
|
|
70
|
-
@periods.setter
|
|
71
|
-
def periods(self: ForceMockDevice, value: int) -> None:
|
|
72
|
-
self.__set_val("periods", value)
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def frequency(self: ForceMockDevice) -> int:
|
|
76
|
-
"""Modulation frequency in Hz."""
|
|
77
|
-
return int(self.__get_val("frequency"))
|
|
78
|
-
|
|
79
|
-
@frequency.setter
|
|
80
|
-
def frequency(self: ForceMockDevice, value: int) -> None:
|
|
81
|
-
self.__set_val("frequency", value)
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def sweep_length(self: ForceMockDevice) -> int:
|
|
85
|
-
"""Total sweep length for one pulse."""
|
|
86
|
-
return int(self.__get_val("sweep_length"))
|
|
87
|
-
|
|
88
|
-
@sweep_length.setter
|
|
89
|
-
def sweep_length(self: ForceMockDevice, value: int) -> None:
|
|
90
|
-
self.__set_val("sweep_length", value)
|
|
91
|
-
|
|
92
|
-
def close(self: ForceMockDevice) -> None:
|
|
93
|
-
"""Mock-close the serial connection."""
|
|
94
|
-
|
|
95
|
-
def write(self: ForceMockDevice, input_string: bytes) -> None:
|
|
96
|
-
"""Mock-write to the serial connection."""
|
|
97
|
-
decoded_input_string = input_string.decode("utf-8")
|
|
98
|
-
split_input_string = decoded_input_string.split(",")
|
|
99
|
-
cmd = split_input_string[0]
|
|
100
|
-
if cmd == "!set timing":
|
|
101
|
-
self.periods = int(split_input_string[1])
|
|
102
|
-
self.frequency = int(split_input_string[2])
|
|
103
|
-
elif cmd == "!set sweep length":
|
|
104
|
-
self.sweep_length = int(split_input_string[1])
|
|
105
|
-
elif cmd == "!s":
|
|
106
|
-
time_pr_point = self.periods / self.frequency
|
|
107
|
-
self.in_waiting = int(self.sweep_length * 1e-3 / time_pr_point)
|
|
108
|
-
self.experiment_running = True
|
|
109
|
-
elif cmd in ["!lut", "!set wave", "!set generator"]:
|
|
110
|
-
pass
|
|
111
|
-
elif cmd != "!dat":
|
|
112
|
-
self.valid_input = False
|
|
113
|
-
|
|
114
|
-
def readline(self: ForceMockDevice) -> bytes:
|
|
115
|
-
"""Mock-readline from the serial connection."""
|
|
116
|
-
if self.valid_input:
|
|
117
|
-
return self.__run_experiment() if self.experiment_running else b"!A,OK\r"
|
|
118
|
-
|
|
119
|
-
return b"A,FAULT\r"
|
|
120
|
-
|
|
121
|
-
def read_until(self: ForceMockDevice, expected: bytes = b"\r") -> bytes: # noqa: ARG002
|
|
122
|
-
"""Mock-read_until from the serial connection."""
|
|
123
|
-
random_datapoint = self.__create_random_datapoint
|
|
124
|
-
return random_datapoint.encode(self.ENCODING)
|
|
125
|
-
|
|
126
|
-
def __run_experiment(self: ForceMockDevice) -> bytes:
|
|
127
|
-
return_string = "!A,OK\\r"
|
|
128
|
-
return_string += (
|
|
129
|
-
f"!S,ip: {self.periods}, "
|
|
130
|
-
f"Freq: {self.frequency}, "
|
|
131
|
-
f"sl: {self.sweep_length}, "
|
|
132
|
-
f"from: 0.0000, "
|
|
133
|
-
f"to:1.0000\r"
|
|
134
|
-
)
|
|
135
|
-
for _ in range(self.in_waiting):
|
|
136
|
-
return_string += self.__create_random_datapoint
|
|
137
|
-
return_string += "!D,DONE\\r"
|
|
138
|
-
if not self.instant_response:
|
|
139
|
-
sleep(self.sweep_length * 1e-3)
|
|
140
|
-
self.n_scans += 1
|
|
141
|
-
if self.n_scans > self.fail_after and self.n_failures < self.fails_wanted:
|
|
142
|
-
self.n_failures += 1
|
|
143
|
-
msg = "MOCK_DEVICE: scan failed"
|
|
144
|
-
raise serialutil.SerialException(msg)
|
|
145
|
-
|
|
146
|
-
return return_string.encode("utf-8")
|
|
147
|
-
|
|
148
|
-
@property
|
|
149
|
-
def __create_random_datapoint(self: ForceMockDevice) -> str:
|
|
150
|
-
radius = self.rng.random() * 10
|
|
151
|
-
theta = self.rng.random() * 360
|
|
152
|
-
return f"!R,{radius},{theta}\r"
|
|
153
|
-
|
|
154
|
-
def __save(self: ForceMockDevice, conf: dict) -> None:
|
|
155
|
-
with self.CONFIG_PATH.open("w") as f:
|
|
156
|
-
json.dump(conf, f)
|
|
157
|
-
|
|
158
|
-
def __get_val(self: ForceMockDevice, key: str) -> int:
|
|
159
|
-
val: int = getattr(self, f"_{key}")
|
|
160
|
-
if not val:
|
|
161
|
-
with self.CONFIG_PATH.open() as f:
|
|
162
|
-
val = json.load(f)[key]
|
|
163
|
-
return val
|
|
164
|
-
|
|
165
|
-
def __set_val(self: ForceMockDevice, key: str, val: str | float) -> None:
|
|
166
|
-
with self.CONFIG_PATH.open() as f:
|
|
167
|
-
config = json.load(f)
|
|
168
|
-
config[key] = val
|
|
169
|
-
setattr(self, f"_{key}", val)
|
|
170
|
-
self.__save(config)
|
|
171
|
-
|
|
172
|
-
|
|
173
28
|
class _LeMockState(Enum):
|
|
174
29
|
IDLE = auto()
|
|
175
30
|
WAITING_FOR_SETTINGS = auto()
|
|
@@ -177,6 +32,8 @@ class _LeMockState(Enum):
|
|
|
177
32
|
RECEIVED_SETTINGS = auto()
|
|
178
33
|
RECEIVED_LIST = auto()
|
|
179
34
|
RECEIVED_STATUS_REQUEST = auto()
|
|
35
|
+
RECEIVED_SERIAL_NUMBER_REQUEST = auto()
|
|
36
|
+
RECEIVED_FIRMWARE_VERSION_REQUEST = auto()
|
|
180
37
|
STARTING_SCAN = auto()
|
|
181
38
|
SCANNING = auto()
|
|
182
39
|
|
|
@@ -235,6 +92,9 @@ class LeMockDevice(MockDevice):
|
|
|
235
92
|
return self._create_scan_bytes(n_bytes=0)
|
|
236
93
|
if self.state == _LeMockState.IDLE:
|
|
237
94
|
return self._create_scan_bytes(n_bytes=size)
|
|
95
|
+
if self.state == _LeMockState.RECEIVED_SERIAL_NUMBER_REQUEST:
|
|
96
|
+
self.state = _LeMockState.IDLE
|
|
97
|
+
return "M-9999".encode(self.ENCODING)
|
|
238
98
|
raise NotImplementedError
|
|
239
99
|
|
|
240
100
|
def read_until(self: LeMockDevice, _: bytes = b"\r") -> bytes: # noqa: PLR0911
|
|
@@ -255,6 +115,9 @@ class LeMockDevice(MockDevice):
|
|
|
255
115
|
self.state = _LeMockState.SCANNING
|
|
256
116
|
self.is_scanning = True
|
|
257
117
|
return "ACK: Scan started.".encode(self.ENCODING)
|
|
118
|
+
if self.state == _LeMockState.RECEIVED_FIRMWARE_VERSION_REQUEST:
|
|
119
|
+
self.state = _LeMockState.IDLE
|
|
120
|
+
return "v0.1.0".encode(self.ENCODING)
|
|
258
121
|
if self.state == _LeMockState.RECEIVED_STATUS_REQUEST:
|
|
259
122
|
if self._scan_has_finished():
|
|
260
123
|
self.state = _LeMockState.IDLE
|
|
@@ -291,6 +154,10 @@ class LeMockDevice(MockDevice):
|
|
|
291
154
|
self._scan_start_time = time.time()
|
|
292
155
|
elif msg == "R":
|
|
293
156
|
self._scan_has_finished()
|
|
157
|
+
elif msg == "s":
|
|
158
|
+
self.state = _LeMockState.RECEIVED_SERIAL_NUMBER_REQUEST
|
|
159
|
+
elif msg == "v":
|
|
160
|
+
self.state = _LeMockState.RECEIVED_FIRMWARE_VERSION_REQUEST
|
|
294
161
|
else:
|
|
295
162
|
msg = f"Unknown message: {msg}"
|
|
296
163
|
raise NotImplementedError(msg)
|
|
@@ -376,7 +243,7 @@ def list_mock_devices() -> list[str]:
|
|
|
376
243
|
]
|
|
377
244
|
|
|
378
245
|
|
|
379
|
-
def _mock_device_factory(config: DeviceConfiguration) ->
|
|
246
|
+
def _mock_device_factory(config: DeviceConfiguration) -> LeMockDevice:
|
|
380
247
|
mock_class = _get_mock_class(config)
|
|
381
248
|
if config.amp_port == "mock_device_scan_should_fail":
|
|
382
249
|
return mock_class(fail_after=0)
|
|
@@ -393,9 +260,7 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
|
|
|
393
260
|
raise ValueError(msg)
|
|
394
261
|
|
|
395
262
|
|
|
396
|
-
def _get_mock_class(config: DeviceConfiguration) -> type[
|
|
397
|
-
if isinstance(config, ForceDeviceConfiguration):
|
|
398
|
-
return ForceMockDevice
|
|
263
|
+
def _get_mock_class(config: DeviceConfiguration) -> type[LeMockDevice]:
|
|
399
264
|
if isinstance(config, LeDeviceConfiguration):
|
|
400
265
|
return LeMockDevice
|
|
401
266
|
|
pyglaze/devtools/thz_pulse.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import cast
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pyglaze.helpers._types import FloatArray
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def gaussian_derivative_pulse(
|
|
@@ -32,4 +33,4 @@ def gaussian_derivative_pulse(
|
|
|
32
33
|
scale=1.0 / signal_to_noise, size=len(signal)
|
|
33
34
|
)
|
|
34
35
|
)
|
|
35
|
-
return cast(FloatArray, signal / np.max(signal) + noise)
|
|
36
|
+
return cast("FloatArray", signal / np.max(signal) + noise)
|
pyglaze/helpers/utilities.py
CHANGED
|
@@ -8,11 +8,11 @@ from typing import TYPE_CHECKING, Callable, cast
|
|
|
8
8
|
import serial
|
|
9
9
|
import serial.tools.list_ports
|
|
10
10
|
|
|
11
|
-
from pyglaze.helpers.types import P, T
|
|
12
|
-
|
|
13
11
|
if TYPE_CHECKING:
|
|
14
12
|
import logging
|
|
15
13
|
|
|
14
|
+
from pyglaze.helpers._types import P, T
|
|
15
|
+
|
|
16
16
|
APP_NAME = "Glaze"
|
|
17
17
|
LOGGER_NAME = "glaze-logger"
|
|
18
18
|
|
|
@@ -59,17 +59,17 @@ class _BackoffRetry:
|
|
|
59
59
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
60
60
|
for tries in range(self.max_tries - 1):
|
|
61
61
|
try:
|
|
62
|
-
return cast(T, func(*args, **kwargs))
|
|
62
|
+
return cast("T", func(*args, **kwargs))
|
|
63
63
|
except (KeyboardInterrupt, SystemExit):
|
|
64
64
|
raise
|
|
65
65
|
except Exception as e: # noqa: BLE001
|
|
66
66
|
self._log(
|
|
67
|
-
f"{func.__name__} failed {tries+1} time(s) with: '{e}'. Trying again"
|
|
67
|
+
f"{func.__name__} failed {tries + 1} time(s) with: '{e}'. Trying again"
|
|
68
68
|
)
|
|
69
69
|
backoff = min(self.backoff_base * 2**tries, self.max_backoff)
|
|
70
70
|
time.sleep(backoff)
|
|
71
|
-
self._log(f"{func.__name__}: Last try ({tries+2}).")
|
|
72
|
-
return cast(T, func(*args, **kwargs))
|
|
71
|
+
self._log(f"{func.__name__}: Last try ({tries + 2}).")
|
|
72
|
+
return cast("T", func(*args, **kwargs))
|
|
73
73
|
|
|
74
74
|
return wrapper
|
|
75
75
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .interpolation import ws_interpolate
|
|
1
|
+
from .interpolation import cubic_spline_interpolate, ws_interpolate
|
|
2
2
|
|
|
3
|
-
__all__ = ["ws_interpolate"]
|
|
3
|
+
__all__ = ["cubic_spline_interpolate", "ws_interpolate"]
|