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.
Files changed (34) hide show
  1. {pyglaze-0.4.7/src/pyglaze.egg-info → pyglaze-0.5.0}/PKG-INFO +1 -1
  2. {pyglaze-0.4.7 → pyglaze-0.5.0}/pyproject.toml +2 -2
  3. pyglaze-0.5.0/src/pyglaze/__init__.py +1 -0
  4. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/waveform.py +2 -0
  5. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/mock_device.py +1 -1
  6. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/_lockin.py +12 -1
  7. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/_asyncscanner.py +51 -8
  8. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/client.py +22 -1
  9. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/scanner.py +47 -7
  10. {pyglaze-0.4.7 → pyglaze-0.5.0/src/pyglaze.egg-info}/PKG-INFO +1 -1
  11. pyglaze-0.4.7/src/pyglaze/__init__.py +0 -1
  12. {pyglaze-0.4.7 → pyglaze-0.5.0}/LICENSE +0 -0
  13. {pyglaze-0.4.7 → pyglaze-0.5.0}/MANIFEST.in +0 -0
  14. {pyglaze-0.4.7 → pyglaze-0.5.0}/README.md +0 -0
  15. {pyglaze-0.4.7 → pyglaze-0.5.0}/setup.cfg +0 -0
  16. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/__init__.py +0 -0
  17. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/datamodels/pulse.py +0 -0
  18. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/__init__.py +0 -0
  19. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/ampcom.py +0 -0
  20. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/device/configuration.py +0 -0
  21. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/__init__.py +0 -0
  22. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/devtools/thz_pulse.py +0 -0
  23. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/__init__.py +0 -0
  24. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/_types.py +0 -0
  25. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/helpers/utilities.py +0 -0
  26. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/interpolation/__init__.py +0 -0
  27. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/interpolation/interpolation.py +0 -0
  28. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/py.typed +0 -0
  29. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/__init__.py +0 -0
  30. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze/scanning/_exceptions.py +0 -0
  31. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/SOURCES.txt +0 -0
  32. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/dependency_links.txt +0 -0
  33. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/requires.txt +0 -0
  34. {pyglaze-0.4.7 → pyglaze-0.5.0}/src/pyglaze.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglaze
3
- Version: 0.4.7
3
+ Version: 0.5.0
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.4.7"
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.4.7"
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
@@ -226,7 +226,7 @@ class LeMockDevice(MockDevice):
226
226
  numbers = np.concatenate(
227
227
  (
228
228
  np.array(self.scanning_list) * 100e-12, # mock time values
229
- self.rng.random(2 * len(self.scanning_list)),
229
+ self.rng.random(2 * len(self.scanning_list)) + 1,
230
230
  )
231
231
  )
232
232
 
@@ -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
- self.phase_estimate: float | None = None
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(self: _AsyncScanner, config: DeviceConfiguration) -> None:
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 configurtaion
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=[config, self._shared_mem, self._stop_signal, child_conn],
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() # noqa: DTZ005
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
- return self._shared_mem.get(timeout=self._SCAN_TIMEOUT)
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(config=config)
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
- waveform = _TimestampedWaveform(datetime.now(), scanner.scan()) # noqa: DTZ005
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__(self: Scanner, config: TConfig) -> None:
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__(self: LeScanner, config: LeDeviceConfiguration) -> None:
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(config: DeviceConfiguration) -> _ScannerImplementation:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglaze
3
- Version: 0.4.7
3
+ Version: 0.5.0
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 +0,0 @@
1
- __version__ = "0.4.7"
File without changes
File without changes
File without changes
File without changes
File without changes