pyglaze 0.4.0__tar.gz → 0.4.4__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.4.0/src/pyglaze.egg-info → pyglaze-0.4.4}/PKG-INFO +3 -3
  2. {pyglaze-0.4.0 → pyglaze-0.4.4}/pyproject.toml +4 -4
  3. pyglaze-0.4.4/src/pyglaze/__init__.py +1 -0
  4. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/datamodels/pulse.py +24 -4
  5. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/device/ampcom.py +17 -12
  6. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/device/configuration.py +22 -2
  7. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/devtools/mock_device.py +2 -0
  8. {pyglaze-0.4.0 → pyglaze-0.4.4/src/pyglaze.egg-info}/PKG-INFO +3 -3
  9. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze.egg-info/requires.txt +1 -1
  10. pyglaze-0.4.0/src/pyglaze/__init__.py +0 -1
  11. {pyglaze-0.4.0 → pyglaze-0.4.4}/LICENSE +0 -0
  12. {pyglaze-0.4.0 → pyglaze-0.4.4}/MANIFEST.in +0 -0
  13. {pyglaze-0.4.0 → pyglaze-0.4.4}/README.md +0 -0
  14. {pyglaze-0.4.0 → pyglaze-0.4.4}/setup.cfg +0 -0
  15. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/datamodels/__init__.py +0 -0
  16. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/datamodels/waveform.py +0 -0
  17. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/device/__init__.py +0 -0
  18. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/devtools/__init__.py +0 -0
  19. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/devtools/thz_pulse.py +0 -0
  20. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/helpers/__init__.py +0 -0
  21. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/helpers/_types.py +0 -0
  22. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/helpers/utilities.py +0 -0
  23. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/interpolation/__init__.py +0 -0
  24. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/interpolation/interpolation.py +0 -0
  25. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/py.typed +0 -0
  26. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/scanning/__init__.py +0 -0
  27. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/scanning/_asyncscanner.py +0 -0
  28. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/scanning/_exceptions.py +0 -0
  29. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/scanning/client.py +0 -0
  30. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze/scanning/scanner.py +0 -0
  31. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze.egg-info/SOURCES.txt +0 -0
  32. {pyglaze-0.4.0 → pyglaze-0.4.4}/src/pyglaze.egg-info/dependency_links.txt +0 -0
  33. {pyglaze-0.4.0 → pyglaze-0.4.4}/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.0
3
+ Version: 0.4.4
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
@@ -35,10 +35,10 @@ Project-URL: Homepage, https://www.glazetech.dk/
35
35
  Project-URL: Documentation, https://glazetech.github.io/pyglaze/latest
36
36
  Project-URL: Repository, https://github.com/GlazeTech/pyglaze
37
37
  Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
38
- Requires-Python: <3.13,>=3.9
38
+ Requires-Python: <3.14,>=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
- Requires-Dist: numpy<2.0.0,>=1.26.4
41
+ Requires-Dist: numpy>=1.26.4
42
42
  Requires-Dist: pyserial>=3.5
43
43
  Requires-Dist: scipy>=1.7.3
44
44
  Requires-Dist: bitstring>=4.1.2
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "pyglaze"
3
- version = "0.4.0"
3
+ version = "0.4.4"
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" }
7
7
  authors = [
8
8
  {name = "GLAZE Technologies ApS"},
9
9
  ]
10
- requires-python = ">=3.9,<3.13"
10
+ requires-python = ">=3.9,<3.14"
11
11
 
12
12
  dependencies = [
13
- "numpy>=1.26.4,<2.0.0",
13
+ "numpy>=1.26.4",
14
14
  "pyserial>=3.5",
15
15
  "scipy>=1.7.3",
16
16
  "bitstring>=4.1.2",
@@ -74,7 +74,7 @@ convention = "google"
74
74
  ]
75
75
 
76
76
  [tool.bumpver]
77
- current_version = "0.4.0"
77
+ current_version = "0.4.4"
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.4.4"
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
  __all__ = ["Pulse"]
17
17
 
18
18
 
19
- @dataclass
19
+ @dataclass(frozen=True)
20
20
  class Pulse:
21
21
  """Data class for a THz pulse. The pulse is expected to be preprocessed such that times are uniformly spaced.
22
22
 
@@ -41,6 +41,24 @@ class Pulse:
41
41
  and np.array_equal(self.signal, obj.signal)
42
42
  )
43
43
 
44
+ def __hash__(self: Pulse) -> int:
45
+ """Return a hash based on the contents of ``time`` and ``signal``.
46
+
47
+ The hash combines shape, dtype and raw bytes of both arrays, ensuring that
48
+ two :class:`Pulse` instances that compare equal also have identical hashes.
49
+
50
+ """
51
+ return hash(
52
+ (
53
+ self.time.shape,
54
+ self.time.dtype.str,
55
+ self.time.tobytes(),
56
+ self.signal.shape,
57
+ self.signal.dtype.str,
58
+ self.signal.tobytes(),
59
+ )
60
+ )
61
+
44
62
  @property
45
63
  def fft(self: Pulse) -> ComplexArray:
46
64
  """Return the Fourier Transform of a signal."""
@@ -97,7 +115,7 @@ class Pulse:
97
115
 
98
116
  Note that the energy is not the same as the physical energy of the pulse, but rather the integral of the square of the pulse.
99
117
  """
100
- return cast("float", np.trapz(self.signal * self.signal, x=self.time)) # noqa: NPY201 - trapz removed in numpy 2.0
118
+ return cast("float", np.trapezoid(self.signal * self.signal, x=self.time)) # type: ignore[attr-defined, unused-ignore]
101
119
 
102
120
  @classmethod
103
121
  def from_dict(
@@ -163,8 +181,10 @@ class Pulse:
163
181
  ]
164
182
 
165
183
  if translate_to_zero:
166
- for scan in roughly_aligned:
167
- scan.time = scan.time - scan.time[0]
184
+ roughly_aligned = [
185
+ s.timeshift(scale=1.0, offset=-s.time[0]) for s in roughly_aligned
186
+ ]
187
+
168
188
  zerocrossings = [p.estimate_zero_crossing() for p in roughly_aligned]
169
189
  mean_zerocrossing = cast("float", np.mean(zerocrossings))
170
190
 
@@ -15,6 +15,8 @@ from bitstring import BitArray
15
15
  from serial import serialutil
16
16
 
17
17
  from pyglaze.device.configuration import (
18
+ BYTES_PER_CHANNEL,
19
+ N_CHANNELS,
18
20
  DeviceConfiguration,
19
21
  Interval,
20
22
  LeDeviceConfiguration,
@@ -78,7 +80,7 @@ class _LeAmpCom:
78
80
 
79
81
  We expect to receive 3 arrays of floats (delays, X and Y), each with self.scanning_points elements.
80
82
  """
81
- return self.scanning_points * 12
83
+ return self.scanning_points * N_CHANNELS * BYTES_PER_CHANNEL
82
84
 
83
85
  @property
84
86
  def serial_number_bytes(self: _LeAmpCom) -> int:
@@ -150,9 +152,11 @@ class _LeAmpCom:
150
152
  angle = np.arctan2(np.array(Ys), np.array(Xs))
151
153
  return r, np.rad2deg(angle)
152
154
 
153
- def _encode_send_response(self: _LeAmpCom, command: str) -> str:
155
+ def _encode_send_response(
156
+ self: _LeAmpCom, command: str, *, check_ack: bool = True
157
+ ) -> str:
154
158
  self._encode_and_send(command)
155
- return self._get_response(command)
159
+ return self._get_response(command, check_ack=check_ack)
156
160
 
157
161
  def _encode_and_send(self: _LeAmpCom, command: str) -> None:
158
162
  self.__ser.write(command.encode(self.ENCODING))
@@ -180,13 +184,13 @@ class _LeAmpCom:
180
184
  @_BackoffRetry(
181
185
  backoff_base=1e-2, max_tries=3, logger=logging.getLogger(LOGGER_NAME)
182
186
  )
183
- def _get_response(self: _LeAmpCom, command: str) -> str:
187
+ def _get_response(self: _LeAmpCom, command: str, *, check_ack: bool = True) -> str:
184
188
  response = self.__ser.read_until().decode(self.ENCODING).strip()
185
189
 
186
190
  if len(response) == 0:
187
191
  msg = f"Command: '{command}'. Empty response received"
188
192
  raise serialutil.SerialException(msg)
189
- if response[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
193
+ if check_ack and response[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
190
194
  msg = f"Command: '{command}'. Expected response '{self.OK_RESPONSE}', received: '{response}'"
191
195
  raise DeviceComError(msg)
192
196
  return response
@@ -220,13 +224,14 @@ class _LeAmpCom:
220
224
  ]
221
225
 
222
226
  def _get_status(self: _LeAmpCom) -> _LeStatus:
223
- msg = self._encode_send_response(self.STATUS_COMMAND)
224
- if msg == _LeStatus.SCANNING.value:
227
+ response = self._encode_send_response(self.STATUS_COMMAND, check_ack=False)
228
+
229
+ if response == _LeStatus.SCANNING.value:
225
230
  return _LeStatus.SCANNING
226
- if msg == _LeStatus.IDLE.value:
231
+ if response == _LeStatus.IDLE.value:
227
232
  return _LeStatus.IDLE
228
- msg = f"Unknown status: {msg}"
229
- raise ValueError(msg)
233
+ msg = f"Unknown status: {response}"
234
+ raise DeviceComError(msg)
230
235
 
231
236
 
232
237
  class _LeStatus(Enum):
@@ -238,8 +243,8 @@ def _serial_factory(config: DeviceConfiguration) -> serial.Serial | LeMockDevice
238
243
  if "mock_device" in config.amp_port:
239
244
  return _mock_device_factory(config)
240
245
 
241
- return serial.Serial(
242
- port=config.amp_port,
246
+ return serial.serial_for_url(
247
+ url=config.amp_port,
243
248
  baudrate=config.amp_baudrate,
244
249
  timeout=config.amp_timeout_seconds,
245
250
  )
@@ -11,6 +11,14 @@ if TYPE_CHECKING:
11
11
  T = TypeVar("T", bound="DeviceConfiguration")
12
12
 
13
13
 
14
+ # Serial protocol constants for timeout calculation
15
+ SERIAL_BITS_PER_BYTE = 10 # 8 data bits + start + stop bits
16
+ N_CHANNELS = 3 # delays, X, Y arrays transmitted
17
+ BYTES_PER_CHANNEL = 4 # 32-bit float = 4 bytes
18
+ TIMEOUT_SAFETY_FACTOR = 2.5 # Safety multiplier for network/processing delays
19
+ TIMEOUT_BASELINE_S = 0.05 # Fixed additive latency
20
+
21
+
14
22
  @dataclass
15
23
  class Interval:
16
24
  """An interval with a lower and upper bounds between 0 and 1 to scan."""
@@ -50,9 +58,10 @@ class Interval:
50
58
  class DeviceConfiguration(ABC):
51
59
  """Base class for device configurations."""
52
60
 
53
- amp_timeout_seconds: float
61
+ amp_timeout_seconds: float | None
54
62
  amp_port: str
55
63
  amp_baudrate: ClassVar[int]
64
+ n_points: int
56
65
 
57
66
  @property
58
67
  @abstractmethod
@@ -92,11 +101,22 @@ class LeDeviceConfiguration(DeviceConfiguration):
92
101
  n_points: int = 1000
93
102
  scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
94
103
  integration_periods: int = 10
95
- amp_timeout_seconds: float = 0.2
104
+ amp_timeout_seconds: float | None = None
96
105
  modulation_frequency: int = 10000 # Hz
97
106
 
98
107
  amp_baudrate: ClassVar[int] = 1000000 # bit/s
99
108
 
109
+ def __post_init__(self: LeDeviceConfiguration) -> None:
110
+ """Calculate dynamic timeout if not explicitly set."""
111
+ if self.amp_timeout_seconds is None:
112
+ # Calculate timeout based on data transfer requirements
113
+ bytes_to_receive = self.n_points * N_CHANNELS * BYTES_PER_CHANNEL
114
+ bits_to_transfer = bytes_to_receive * SERIAL_BITS_PER_BYTE
115
+ transfer_time = bits_to_transfer / self.amp_baudrate
116
+ self.amp_timeout_seconds = (
117
+ transfer_time + TIMEOUT_BASELINE_S
118
+ ) * TIMEOUT_SAFETY_FACTOR
119
+
100
120
  @property
101
121
  def _sweep_length_ms(self: LeDeviceConfiguration) -> float:
102
122
  return self.n_points * self._time_constant_ms
@@ -154,6 +154,8 @@ class LeMockDevice(MockDevice):
154
154
  self._scan_start_time = time.time()
155
155
  elif msg == "R":
156
156
  self._scan_has_finished()
157
+ elif msg == "H":
158
+ self.state = _LeMockState.RECEIVED_STATUS_REQUEST
157
159
  elif msg == "s":
158
160
  self.state = _LeMockState.RECEIVED_SERIAL_NUMBER_REQUEST
159
161
  elif msg == "v":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglaze
3
- Version: 0.4.0
3
+ Version: 0.4.4
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
@@ -35,10 +35,10 @@ Project-URL: Homepage, https://www.glazetech.dk/
35
35
  Project-URL: Documentation, https://glazetech.github.io/pyglaze/latest
36
36
  Project-URL: Repository, https://github.com/GlazeTech/pyglaze
37
37
  Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
38
- Requires-Python: <3.13,>=3.9
38
+ Requires-Python: <3.14,>=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
- Requires-Dist: numpy<2.0.0,>=1.26.4
41
+ Requires-Dist: numpy>=1.26.4
42
42
  Requires-Dist: pyserial>=3.5
43
43
  Requires-Dist: scipy>=1.7.3
44
44
  Requires-Dist: bitstring>=4.1.2
@@ -1,4 +1,4 @@
1
- numpy<2.0.0,>=1.26.4
1
+ numpy>=1.26.4
2
2
  pyserial>=3.5
3
3
  scipy>=1.7.3
4
4
  bitstring>=4.1.2
@@ -1 +0,0 @@
1
- __version__ = "0.4.0"
File without changes
File without changes
File without changes
File without changes
File without changes