pyglaze 0.4.7__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyglaze-0.4.7/src/pyglaze.egg-info → pyglaze-0.5.0}/PKG-INFO +1 -1
- {pyglaze-0.4.7 → pyglaze-0.5.0}/pyproject.toml +2 -2
- pyglaze-0.5.0/src/pyglaze/__init__.py +1 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/waveform.py +2 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/mock_device.py +1 -1
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/_lockin.py +12 -1
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/_asyncscanner.py +51 -8
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/client.py +22 -1
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/scanner.py +47 -7
- {pyglaze-0.4.7 → pyglaze-0.5.0/src/pyglaze.egg-info}/PKG-INFO +1 -1
- pyglaze-0.4.7/src/pyglaze/__init__.py +0 -1
- {pyglaze-0.4.7 → pyglaze-0.5.0}/LICENSE +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/MANIFEST.in +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/README.md +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/setup.cfg +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/pulse.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/ampcom.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/configuration.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/thz_pulse.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/_types.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/utilities.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/interpolation/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/interpolation/interpolation.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/py.typed +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/__init__.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/_exceptions.py +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/SOURCES.txt +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/dependency_links.txt +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/requires.txt +0 -0
- {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyglaze"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
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" }
|
|
@@ -103,7 +103,7 @@ convention = "google"
|
|
|
103
103
|
]
|
|
104
104
|
|
|
105
105
|
[tool.bumpver]
|
|
106
|
-
current_version = "0.
|
|
106
|
+
current_version = "0.5.0"
|
|
107
107
|
version_pattern = "MAJOR.MINOR.PATCH[-TAG]"
|
|
108
108
|
commit_message = "BUMP VERSION {old_version} -> {new_version}"
|
|
109
109
|
tag_message = "v{new_version}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -182,7 +182,9 @@ class _TimestampedWaveform:
|
|
|
182
182
|
Args:
|
|
183
183
|
timestamp: The timestamp of the pulse given by the Toptica server.
|
|
184
184
|
waveform: The terahertz pulse received from the Toptica server.
|
|
185
|
+
phase_estimate: The phase estimate from the lock-in phase estimator at the time of this scan.
|
|
185
186
|
"""
|
|
186
187
|
|
|
187
188
|
timestamp: datetime
|
|
188
189
|
waveform: UnprocessedWaveform
|
|
190
|
+
phase_estimate: float | None = None
|
|
@@ -115,9 +115,20 @@ class _LockinPhaseEstimator:
|
|
|
115
115
|
def __init__(
|
|
116
116
|
self: _LockinPhaseEstimator,
|
|
117
117
|
confidence_threshold: float = 0.8,
|
|
118
|
+
initial_phase_estimate: float | None = None,
|
|
118
119
|
) -> None:
|
|
120
|
+
"""Initialize the lock-in phase estimator.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
confidence_threshold: Minimum confidence (0-1) required to update phase estimate.
|
|
124
|
+
initial_phase_estimate: Optional initial phase in radians. If provided, sets the
|
|
125
|
+
initial phase estimate to this value (wrapped to [-π, π]).
|
|
126
|
+
"""
|
|
119
127
|
self.confidence_threshold = confidence_threshold
|
|
120
|
-
|
|
128
|
+
if initial_phase_estimate is not None:
|
|
129
|
+
self.phase_estimate = _wrap_to_pi(float(initial_phase_estimate))
|
|
130
|
+
else:
|
|
131
|
+
self.phase_estimate: float | None = None # Phase estimate in radians
|
|
121
132
|
|
|
122
133
|
def update_estimate(
|
|
123
134
|
self: _LockinPhaseEstimator, Xs: FloatArray, Ys: FloatArray
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from multiprocessing import Event, Pipe, Process, Queue, synchronize
|
|
6
6
|
from queue import Empty, Full
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
@@ -45,20 +45,38 @@ class _AsyncScanner:
|
|
|
45
45
|
_SCAN_TIMEOUT: float = field(init=False)
|
|
46
46
|
_stop_signal: synchronize.Event = field(init=False)
|
|
47
47
|
_scanner_conn: Connection = field(init=False)
|
|
48
|
+
_initial_phase_estimate: float | None = field(init=False, default=None)
|
|
49
|
+
_cached_phase_estimate: float | None = field(init=False, default=None)
|
|
48
50
|
|
|
49
|
-
def start_scan(
|
|
51
|
+
def start_scan(
|
|
52
|
+
self: _AsyncScanner,
|
|
53
|
+
config: DeviceConfiguration,
|
|
54
|
+
initial_phase_estimate: float | None = None,
|
|
55
|
+
) -> None:
|
|
50
56
|
"""Starts continuously scanning in new process.
|
|
51
57
|
|
|
52
58
|
Args:
|
|
53
|
-
config: Device
|
|
59
|
+
config: Device configuration
|
|
60
|
+
initial_phase_estimate: Optional initial phase estimate in radians for lock-in detection.
|
|
61
|
+
Use this to maintain consistent polarity across scanner instances.
|
|
54
62
|
"""
|
|
63
|
+
self._initial_phase_estimate = initial_phase_estimate
|
|
64
|
+
self._cached_phase_estimate = (
|
|
65
|
+
initial_phase_estimate # Initialize cache with initial value
|
|
66
|
+
)
|
|
55
67
|
self._SCAN_TIMEOUT = config._sweep_length_ms * 2e-3 + 1 # noqa: SLF001, access to private attribute for backwards compatibility
|
|
56
68
|
self._shared_mem = Queue(maxsize=self.queue_maxsize)
|
|
57
69
|
self._stop_signal = Event()
|
|
58
70
|
self._scanner_conn, child_conn = Pipe()
|
|
59
71
|
self._child_process = Process(
|
|
60
72
|
target=_AsyncScanner._run_scanner,
|
|
61
|
-
args=[
|
|
73
|
+
args=[
|
|
74
|
+
config,
|
|
75
|
+
self._shared_mem,
|
|
76
|
+
self._stop_signal,
|
|
77
|
+
child_conn,
|
|
78
|
+
initial_phase_estimate,
|
|
79
|
+
],
|
|
62
80
|
)
|
|
63
81
|
self._child_process.start()
|
|
64
82
|
|
|
@@ -90,7 +108,7 @@ class _AsyncScanner:
|
|
|
90
108
|
self.is_scanning = False
|
|
91
109
|
|
|
92
110
|
def get_scans(self: _AsyncScanner, n_pulses: int) -> list[UnprocessedWaveform]:
|
|
93
|
-
call_time = datetime.now()
|
|
111
|
+
call_time = datetime.now(tz=timezone.utc)
|
|
94
112
|
stamped_pulse = self._get_scan()
|
|
95
113
|
|
|
96
114
|
while stamped_pulse.timestamp < call_time:
|
|
@@ -114,9 +132,23 @@ class _AsyncScanner:
|
|
|
114
132
|
|
|
115
133
|
return self._metadata.firmware_version
|
|
116
134
|
|
|
135
|
+
def get_phase_estimate(self: _AsyncScanner) -> float | None:
|
|
136
|
+
"""Get the current phase estimate from the scanner.
|
|
137
|
+
|
|
138
|
+
Returns the cached phase estimate from the most recently received waveform.
|
|
139
|
+
This method returns instantly without blocking and can be called even after
|
|
140
|
+
the scanner has stopped, allowing phase estimates to be extracted and reused.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
float | None: The current phase estimate in radians, or None if not yet estimated.
|
|
144
|
+
"""
|
|
145
|
+
return self._cached_phase_estimate
|
|
146
|
+
|
|
117
147
|
def _get_scan(self: _AsyncScanner) -> _TimestampedWaveform:
|
|
118
148
|
try:
|
|
119
|
-
|
|
149
|
+
waveform = self._shared_mem.get(timeout=self._SCAN_TIMEOUT)
|
|
150
|
+
# Cache the phase estimate from this waveform
|
|
151
|
+
self._cached_phase_estimate = waveform.phase_estimate
|
|
120
152
|
except Exception as err:
|
|
121
153
|
scanner_err: Exception | None = None
|
|
122
154
|
if self._scanner_conn.poll(timeout=self.startup_timeout):
|
|
@@ -127,6 +159,8 @@ class _AsyncScanner:
|
|
|
127
159
|
if scanner_err:
|
|
128
160
|
raise scanner_err from err
|
|
129
161
|
raise
|
|
162
|
+
else:
|
|
163
|
+
return waveform
|
|
130
164
|
|
|
131
165
|
@staticmethod
|
|
132
166
|
def _run_scanner(
|
|
@@ -134,9 +168,12 @@ class _AsyncScanner:
|
|
|
134
168
|
shared_mem: Queue[_TimestampedWaveform],
|
|
135
169
|
stop_signal: synchronize.Event,
|
|
136
170
|
parent_conn: Connection,
|
|
171
|
+
initial_phase_estimate: float | None = None,
|
|
137
172
|
) -> None:
|
|
138
173
|
try:
|
|
139
|
-
scanner = Scanner(
|
|
174
|
+
scanner = Scanner(
|
|
175
|
+
config=config, initial_phase_estimate=initial_phase_estimate
|
|
176
|
+
)
|
|
140
177
|
device_metadata = _ScannerMetadata(
|
|
141
178
|
serial_number=scanner.get_serial_number(),
|
|
142
179
|
firmware_version=scanner.get_firmware_version(),
|
|
@@ -149,7 +186,13 @@ class _AsyncScanner:
|
|
|
149
186
|
|
|
150
187
|
while not stop_signal.is_set():
|
|
151
188
|
try:
|
|
152
|
-
|
|
189
|
+
scanned_waveform = scanner.scan()
|
|
190
|
+
phase = scanner.get_phase_estimate()
|
|
191
|
+
waveform = _TimestampedWaveform(
|
|
192
|
+
datetime.now(tz=timezone.utc),
|
|
193
|
+
scanned_waveform,
|
|
194
|
+
phase,
|
|
195
|
+
)
|
|
153
196
|
except Exception as e: # noqa: BLE001
|
|
154
197
|
parent_conn.send(
|
|
155
198
|
_ScannerHealth(is_alive=False, is_healthy=False, error=e)
|
|
@@ -28,16 +28,19 @@ class GlazeClient:
|
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
30
|
config: Configuration to use for scans
|
|
31
|
+
initial_phase_estimate: Optional initial phase estimate in radians for lock-in detection.
|
|
32
|
+
Use this to maintain consistent polarity across scanning sessions.
|
|
31
33
|
"""
|
|
32
34
|
|
|
33
35
|
config: DeviceConfiguration
|
|
36
|
+
initial_phase_estimate: float | None = None
|
|
34
37
|
_scanner: _AsyncScanner = field(init=False)
|
|
35
38
|
|
|
36
39
|
def __enter__(self: Self) -> Self:
|
|
37
40
|
"""Start the scanner and return the client."""
|
|
38
41
|
self._scanner = _AsyncScanner()
|
|
39
42
|
try:
|
|
40
|
-
self._scanner.start_scan(self.config)
|
|
43
|
+
self._scanner.start_scan(self.config, self.initial_phase_estimate)
|
|
41
44
|
except (TimeoutError, serialutil.SerialException) as e:
|
|
42
45
|
self.__exit__(e)
|
|
43
46
|
return self
|
|
@@ -73,3 +76,21 @@ class GlazeClient:
|
|
|
73
76
|
except AttributeError as e:
|
|
74
77
|
msg = "No connection to device."
|
|
75
78
|
raise SerialException(msg) from e
|
|
79
|
+
|
|
80
|
+
def get_phase_estimate(self: GlazeClient) -> float | None:
|
|
81
|
+
"""Get the current phase estimate from the lock-in phase estimator.
|
|
82
|
+
|
|
83
|
+
Can be called even after the client has been stopped, allowing phase estimates
|
|
84
|
+
to be extracted and reused for maintaining consistent polarity across sessions.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
float | None: The current phase estimate in radians, or None if not yet estimated.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
SerialException: If the scanner was never started.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
return self._scanner.get_phase_estimate()
|
|
94
|
+
except AttributeError as e:
|
|
95
|
+
msg = "No connection to device."
|
|
96
|
+
raise SerialException(msg) from e
|
|
@@ -47,13 +47,27 @@ class _ScannerImplementation(ABC, Generic[TConfig]):
|
|
|
47
47
|
def get_firmware_version(self: _ScannerImplementation) -> str:
|
|
48
48
|
pass
|
|
49
49
|
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_phase_estimate(self: _ScannerImplementation) -> float | None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
50
54
|
|
|
51
55
|
class Scanner:
|
|
52
|
-
"""A synchronous scanner for Glaze terahertz devices.
|
|
56
|
+
"""A synchronous scanner for Glaze terahertz devices.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: Device configuration for the scanner.
|
|
60
|
+
initial_phase_estimate: Optional initial phase estimate in radians for lock-in detection.
|
|
61
|
+
Use this to maintain consistent polarity across scanner instances.
|
|
62
|
+
"""
|
|
53
63
|
|
|
54
|
-
def __init__(
|
|
64
|
+
def __init__(
|
|
65
|
+
self: Scanner,
|
|
66
|
+
config: TConfig,
|
|
67
|
+
initial_phase_estimate: float | None = None,
|
|
68
|
+
) -> None:
|
|
55
69
|
self._scanner_impl: _ScannerImplementation[DeviceConfiguration] = (
|
|
56
|
-
_scanner_factory(config)
|
|
70
|
+
_scanner_factory(config, initial_phase_estimate)
|
|
57
71
|
)
|
|
58
72
|
|
|
59
73
|
@property
|
|
@@ -101,19 +115,35 @@ class Scanner:
|
|
|
101
115
|
"""
|
|
102
116
|
return self._scanner_impl.get_firmware_version()
|
|
103
117
|
|
|
118
|
+
def get_phase_estimate(self: Scanner) -> float | None:
|
|
119
|
+
"""Get the current phase estimate from the lock-in phase estimator.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
float | None: The current phase estimate in radians, or None if not yet estimated.
|
|
123
|
+
"""
|
|
124
|
+
return self._scanner_impl.get_phase_estimate()
|
|
125
|
+
|
|
104
126
|
|
|
105
127
|
class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
106
128
|
"""Perform synchronous terahertz scanning using a given DeviceConfiguration.
|
|
107
129
|
|
|
108
130
|
Args:
|
|
109
131
|
config: A DeviceConfiguration to use for the scan.
|
|
132
|
+
initial_phase_estimate: Optional initial phase estimate in radians for lock-in detection.
|
|
133
|
+
Use this to maintain consistent polarity across scanner instances.
|
|
110
134
|
"""
|
|
111
135
|
|
|
112
|
-
def __init__(
|
|
136
|
+
def __init__(
|
|
137
|
+
self: LeScanner,
|
|
138
|
+
config: LeDeviceConfiguration,
|
|
139
|
+
initial_phase_estimate: float | None = None,
|
|
140
|
+
) -> None:
|
|
113
141
|
self._config: LeDeviceConfiguration
|
|
114
142
|
self._ampcom: _LeAmpCom | None = None
|
|
115
143
|
self.config = config
|
|
116
|
-
self._phase_estimator = _LockinPhaseEstimator(
|
|
144
|
+
self._phase_estimator = _LockinPhaseEstimator(
|
|
145
|
+
initial_phase_estimate=initial_phase_estimate
|
|
146
|
+
)
|
|
117
147
|
|
|
118
148
|
@property
|
|
119
149
|
def config(self: LeScanner) -> LeDeviceConfiguration:
|
|
@@ -194,10 +224,20 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
194
224
|
raise ScanError(msg)
|
|
195
225
|
return self._ampcom.get_firmware_version()
|
|
196
226
|
|
|
227
|
+
def get_phase_estimate(self: LeScanner) -> float | None:
|
|
228
|
+
"""Get the current phase estimate from the lock-in phase estimator.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
float | None: The current phase estimate in radians, or None if not yet estimated.
|
|
232
|
+
"""
|
|
233
|
+
return self._phase_estimator.phase_estimate
|
|
234
|
+
|
|
197
235
|
|
|
198
|
-
def _scanner_factory(
|
|
236
|
+
def _scanner_factory(
|
|
237
|
+
config: DeviceConfiguration, initial_phase_estimate: float | None = None
|
|
238
|
+
) -> _ScannerImplementation:
|
|
199
239
|
if isinstance(config, LeDeviceConfiguration):
|
|
200
|
-
return LeScanner(config)
|
|
240
|
+
return LeScanner(config, initial_phase_estimate)
|
|
201
241
|
|
|
202
242
|
msg = f"Unsupported configuration type: {type(config).__name__}"
|
|
203
243
|
raise TypeError(msg)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|