pyglaze 0.4.6__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.6/src/pyglaze.egg-info → pyglaze-0.5.0}/PKG-INFO +25 -2
- {pyglaze-0.4.6 → pyglaze-0.5.0}/pyproject.toml +38 -3
- pyglaze-0.5.0/src/pyglaze/__init__.py +1 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/datamodels/pulse.py +2 -2
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/datamodels/waveform.py +32 -7
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/device/ampcom.py +1 -10
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/devtools/mock_device.py +1 -1
- pyglaze-0.5.0/src/pyglaze/helpers/_lockin.py +161 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/helpers/utilities.py +6 -5
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/scanning/_asyncscanner.py +51 -8
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/scanning/client.py +22 -1
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/scanning/scanner.py +53 -53
- {pyglaze-0.4.6 → pyglaze-0.5.0/src/pyglaze.egg-info}/PKG-INFO +25 -2
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze.egg-info/SOURCES.txt +1 -0
- pyglaze-0.5.0/src/pyglaze.egg-info/requires.txt +32 -0
- pyglaze-0.4.6/src/pyglaze/__init__.py +0 -1
- pyglaze-0.4.6/src/pyglaze.egg-info/requires.txt +0 -5
- {pyglaze-0.4.6 → pyglaze-0.5.0}/LICENSE +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/MANIFEST.in +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/README.md +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/setup.cfg +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/datamodels/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/device/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/device/configuration.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/devtools/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/devtools/thz_pulse.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/helpers/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/helpers/_types.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/interpolation/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/interpolation/interpolation.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/py.typed +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/scanning/__init__.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze/scanning/_exceptions.py +0 -0
- {pyglaze-0.4.6 → pyglaze-0.5.0}/src/pyglaze.egg-info/dependency_links.txt +0 -0
- {pyglaze-0.4.6 → 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.
|
|
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
|
|
@@ -38,11 +38,34 @@ Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
|
|
|
38
38
|
Requires-Python: <3.14,>=3.9
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist: numpy
|
|
41
|
+
Requires-Dist: numpy<2.0.0,>=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
|
|
45
45
|
Requires-Dist: typing_extensions>=4.12.2
|
|
46
|
+
Provides-Extra: build
|
|
47
|
+
Requires-Dist: build>=1.2.1; extra == "build"
|
|
48
|
+
Requires-Dist: twine>=5.1.1; extra == "build"
|
|
49
|
+
Requires-Dist: bumpver>=2023.1129; extra == "build"
|
|
50
|
+
Provides-Extra: test
|
|
51
|
+
Requires-Dist: pytest>=7.0.1; extra == "test"
|
|
52
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: mkdocs>=1.2.3; extra == "docs"
|
|
55
|
+
Requires-Dist: mkdocs-material>=8.2.5; extra == "docs"
|
|
56
|
+
Requires-Dist: mkdocstrings[python]>=0.18.1; extra == "docs"
|
|
57
|
+
Requires-Dist: mike>=1.1.2; extra == "docs"
|
|
58
|
+
Requires-Dist: Pygments>=2.12.0; extra == "docs"
|
|
59
|
+
Requires-Dist: matplotlib>=3.7.0; extra == "docs"
|
|
60
|
+
Requires-Dist: bumpver>=2023.1129; extra == "docs"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: ruff>=0.14.10; extra == "dev"
|
|
63
|
+
Requires-Dist: ty>=0.0.13; extra == "dev"
|
|
64
|
+
Requires-Dist: pytest>=7.0.1; extra == "dev"
|
|
65
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
66
|
+
Requires-Dist: semver>=2.13.0; extra == "dev"
|
|
67
|
+
Requires-Dist: types-pyserial>=3.5.0.8; extra == "dev"
|
|
68
|
+
Requires-Dist: bumpver>=2023.1129; extra == "dev"
|
|
46
69
|
Dynamic: license-file
|
|
47
70
|
|
|
48
71
|
# Pyglaze
|
|
@@ -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" }
|
|
@@ -10,13 +10,42 @@ authors = [
|
|
|
10
10
|
requires-python = ">=3.9,<3.14"
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
|
-
"numpy>=1.26.4",
|
|
13
|
+
"numpy>=1.26.4,<2.0.0",
|
|
14
14
|
"pyserial>=3.5",
|
|
15
15
|
"scipy>=1.7.3",
|
|
16
16
|
"bitstring>=4.1.2",
|
|
17
17
|
"typing_extensions>=4.12.2"
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
build = [
|
|
22
|
+
"build>=1.2.1",
|
|
23
|
+
"twine>=5.1.1",
|
|
24
|
+
"bumpver>=2023.1129",
|
|
25
|
+
]
|
|
26
|
+
test = [
|
|
27
|
+
"pytest>=7.0.1",
|
|
28
|
+
"pytest-cov>=4.0.0",
|
|
29
|
+
]
|
|
30
|
+
docs = [
|
|
31
|
+
"mkdocs>=1.2.3",
|
|
32
|
+
"mkdocs-material>=8.2.5",
|
|
33
|
+
"mkdocstrings[python]>=0.18.1",
|
|
34
|
+
"mike>=1.1.2",
|
|
35
|
+
"Pygments>=2.12.0",
|
|
36
|
+
"matplotlib>=3.7.0",
|
|
37
|
+
"bumpver>=2023.1129",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"ruff>=0.14.10",
|
|
41
|
+
"ty>=0.0.13",
|
|
42
|
+
"pytest>=7.0.1",
|
|
43
|
+
"pytest-cov>=4.0.0",
|
|
44
|
+
"semver>=2.13.0",
|
|
45
|
+
"types-pyserial>=3.5.0.8",
|
|
46
|
+
"bumpver>=2023.1129",
|
|
47
|
+
]
|
|
48
|
+
|
|
20
49
|
[project.urls]
|
|
21
50
|
Homepage = "https://www.glazetech.dk/"
|
|
22
51
|
Documentation = "https://glazetech.github.io/pyglaze/latest"
|
|
@@ -74,7 +103,7 @@ convention = "google"
|
|
|
74
103
|
]
|
|
75
104
|
|
|
76
105
|
[tool.bumpver]
|
|
77
|
-
current_version = "0.
|
|
106
|
+
current_version = "0.5.0"
|
|
78
107
|
version_pattern = "MAJOR.MINOR.PATCH[-TAG]"
|
|
79
108
|
commit_message = "BUMP VERSION {old_version} -> {new_version}"
|
|
80
109
|
tag_message = "v{new_version}"
|
|
@@ -97,3 +126,9 @@ push = false
|
|
|
97
126
|
'**Documentation Version**: {version}'
|
|
98
127
|
]
|
|
99
128
|
|
|
129
|
+
[tool.ty.rules]
|
|
130
|
+
unused-ignore-comment = "error"
|
|
131
|
+
|
|
132
|
+
[tool.ty.terminal]
|
|
133
|
+
error-on-warning = true
|
|
134
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -115,7 +115,7 @@ class Pulse:
|
|
|
115
115
|
|
|
116
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.
|
|
117
117
|
"""
|
|
118
|
-
return cast("float", np.
|
|
118
|
+
return cast("float", np.trapz(self.signal * self.signal, x=self.time)) # noqa: NPY201
|
|
119
119
|
|
|
120
120
|
@classmethod
|
|
121
121
|
def from_dict(
|
|
@@ -585,7 +585,7 @@ def _estimate_bw_idx(x: FloatArray, y: FloatArray, segments: int) -> int:
|
|
|
585
585
|
target = np.log(y)
|
|
586
586
|
|
|
587
587
|
def L1(x: FloatArray, y: FloatArray) -> FloatArray:
|
|
588
|
-
return np.sum(np.abs(y - x))
|
|
588
|
+
return np.sum(np.abs(y - x))
|
|
589
589
|
|
|
590
590
|
def model(pars: list[float]) -> FloatArray:
|
|
591
591
|
idx = np.searchsorted(x, pars[0])
|
|
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from scipy.interpolate import CubicSpline
|
|
8
8
|
|
|
9
|
+
from pyglaze.helpers._lockin import _estimate_IQ_phase, _polar_to_IQ, _rotate_inphase
|
|
10
|
+
|
|
9
11
|
from .pulse import Pulse
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
@@ -43,16 +45,37 @@ class UnprocessedWaveform:
|
|
|
43
45
|
Args:
|
|
44
46
|
time: The time values recorded by the lock-in amp during the scan.
|
|
45
47
|
radius: The radius values recorded by the lock-in amp during the scan.
|
|
46
|
-
theta: The theta values recorded by the lock-in amp during the scan (in
|
|
47
|
-
rotation_angle: The angle to rotate lockin signal to align along x-axis. If not given, will
|
|
48
|
+
theta: The theta values recorded by the lock-in amp during the scan (in radians).
|
|
49
|
+
rotation_angle: The angle to rotate lockin signal to align along x-axis. If not given, will estimate phase from data.
|
|
48
50
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
51
|
+
if rotation_angle is None:
|
|
52
|
+
rotation_angle, _ = _estimate_IQ_phase(*_polar_to_IQ(radius, theta))
|
|
52
53
|
|
|
53
54
|
# rotate such that all signal lies along X
|
|
54
|
-
new_theta = theta -
|
|
55
|
-
signal = radius * np.cos(new_theta
|
|
55
|
+
new_theta = theta - rotation_angle
|
|
56
|
+
signal = radius * np.cos(new_theta)
|
|
57
|
+
return cls(time, signal)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_inphase_quadrature(
|
|
61
|
+
cls: type[UnprocessedWaveform],
|
|
62
|
+
time: FloatArray,
|
|
63
|
+
X: FloatArray,
|
|
64
|
+
Y: FloatArray,
|
|
65
|
+
rotation_angle: float | None = None,
|
|
66
|
+
) -> UnprocessedWaveform:
|
|
67
|
+
"""Create an UnprocessedWaveform object from raw lock-in amp output.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
time: The time values recorded by the lock-in amp during the scan.
|
|
71
|
+
X: The in-phase values recorded by the lock-in amp during the scan.
|
|
72
|
+
Y: The quadrature values recorded by the lock-in amp during the scan.
|
|
73
|
+
rotation_angle: The angle to rotate lockin signal to align along x-axis. If not given, will estimate phase from data.
|
|
74
|
+
"""
|
|
75
|
+
if rotation_angle is None:
|
|
76
|
+
rotation_angle, _ = _estimate_IQ_phase(X, Y)
|
|
77
|
+
|
|
78
|
+
signal = _rotate_inphase(X, Y, rotation_angle)
|
|
56
79
|
return cls(time, signal)
|
|
57
80
|
|
|
58
81
|
@classmethod
|
|
@@ -159,7 +182,9 @@ class _TimestampedWaveform:
|
|
|
159
182
|
Args:
|
|
160
183
|
timestamp: The timestamp of the pulse given by the Toptica server.
|
|
161
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.
|
|
162
186
|
"""
|
|
163
187
|
|
|
164
188
|
timestamp: datetime
|
|
165
189
|
waveform: UnprocessedWaveform
|
|
190
|
+
phase_estimate: float | None = None
|
|
@@ -119,9 +119,7 @@ class _LeAmpCom:
|
|
|
119
119
|
self._encode_send_response(self.START_COMMAND)
|
|
120
120
|
self._await_scan_finished()
|
|
121
121
|
times, Xs, Ys = self._read_scan()
|
|
122
|
-
|
|
123
|
-
radii, angles = self._convert_to_r_angle(Xs, Ys)
|
|
124
|
-
return self.START_COMMAND, np.array(times), np.array(radii), np.array(angles)
|
|
122
|
+
return self.START_COMMAND, np.array(times), np.array(Xs), np.array(Ys)
|
|
125
123
|
|
|
126
124
|
def disconnect(self: _LeAmpCom) -> None:
|
|
127
125
|
"""Closes connection when class instance goes out of scope."""
|
|
@@ -145,13 +143,6 @@ class _LeAmpCom:
|
|
|
145
143
|
"""Intervals squished into effective DAC range."""
|
|
146
144
|
return self.config.scan_intervals or [Interval(lower=0.0, upper=1.0)]
|
|
147
145
|
|
|
148
|
-
def _convert_to_r_angle(
|
|
149
|
-
self: _LeAmpCom, Xs: list, Ys: list
|
|
150
|
-
) -> tuple[FloatArray, FloatArray]:
|
|
151
|
-
r = np.sqrt(np.array(Xs) ** 2 + np.array(Ys) ** 2)
|
|
152
|
-
angle = np.arctan2(np.array(Ys), np.array(Xs))
|
|
153
|
-
return r, np.rad2deg(angle)
|
|
154
|
-
|
|
155
146
|
def _encode_send_response(
|
|
156
147
|
self: _LeAmpCom, command: str, *, check_ack: bool = True
|
|
157
148
|
) -> str:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pyglaze.helpers._types import FloatArray
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _wrap_to_pi(a: float) -> float:
|
|
12
|
+
"""Map angle (in radians) to (-pi, pi]."""
|
|
13
|
+
return (a + np.pi) % (2 * np.pi) - np.pi
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _angular_distance(a: float, b: float) -> float:
|
|
17
|
+
"""Smallest absolute distance between angles a and b (in radians)."""
|
|
18
|
+
return abs(_wrap_to_pi(a - b))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _choose_pi_branch(theta: float, ref: float) -> float:
|
|
22
|
+
"""Choose between theta and theta+pi (mod 2pi) to be closest to ref.
|
|
23
|
+
|
|
24
|
+
This prevents accidental polarity flips.
|
|
25
|
+
"""
|
|
26
|
+
d0 = _angular_distance(theta, ref)
|
|
27
|
+
d1 = _angular_distance(theta + np.pi, ref)
|
|
28
|
+
return theta if d0 <= d1 else (theta + np.pi)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _rotate_inphase(X: FloatArray, Y: FloatArray, phi: float) -> FloatArray:
|
|
32
|
+
"""Compute in-phase after rotation by phi."""
|
|
33
|
+
return X * np.cos(phi) + Y * np.sin(phi)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _polar_to_IQ(r: FloatArray, theta: FloatArray) -> tuple[FloatArray, FloatArray]:
|
|
37
|
+
"""Convert polar coordinates to IQ (X,Y) coordinates."""
|
|
38
|
+
X = r * np.cos(theta)
|
|
39
|
+
Y = r * np.sin(theta)
|
|
40
|
+
return X, Y
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _choose_branch_by_strongest_point(
|
|
44
|
+
theta: float, X: FloatArray, Y: FloatArray, strength: FloatArray | None = None
|
|
45
|
+
) -> float:
|
|
46
|
+
"""Choose between theta and theta+pi so the strongest sample projects positive on the in-phase axis.
|
|
47
|
+
|
|
48
|
+
strength can be r or r² because only ordering matters
|
|
49
|
+
"""
|
|
50
|
+
if strength is None:
|
|
51
|
+
strength = X * X + Y * Y
|
|
52
|
+
k = int(np.argmax(strength))
|
|
53
|
+
proj = X[k] * np.cos(theta) + Y[k] * np.sin(theta) # in-phase at strongest point
|
|
54
|
+
return theta if proj >= 0 else (theta + np.pi)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _eigenvalues_symmetric_2D(
|
|
58
|
+
Sxx: float, Sxy: float, Syy: float
|
|
59
|
+
) -> tuple[float, float]:
|
|
60
|
+
"""Compute the eigenvalues of a symmetric 2D matrix."""
|
|
61
|
+
trace = Sxx + Syy
|
|
62
|
+
D = float(np.sqrt((Sxx - Syy) ** 2 + 4.0 * (Sxy**2)))
|
|
63
|
+
eig1 = 0.5 * (trace + D)
|
|
64
|
+
eig2 = 0.5 * (trace - D)
|
|
65
|
+
# Numerical hygiene: symmetric PSD should be >=0, but float noise can create tiny negatives
|
|
66
|
+
eig1 = float(max(eig1, 0.0))
|
|
67
|
+
eig2 = float(max(eig2, 0.0))
|
|
68
|
+
return eig1, eig2
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _estimate_IQ_phase(
|
|
72
|
+
X: FloatArray, Y: FloatArray, w: FloatArray | None = None
|
|
73
|
+
) -> tuple[float, float]:
|
|
74
|
+
"""Estimate the constant lock-in phase (orientation, modulo π) from many (X, Y) lock-in samples by fitting the dominant axis of the IQ cloud.
|
|
75
|
+
|
|
76
|
+
This treats each sample as a point in the IQ plane: z_i = X_i + j Y_i, and
|
|
77
|
+
finds the rotation angle φ such that rotating all points by -φ makes the
|
|
78
|
+
energy in the quadrature channel minimal (equivalently: maximizes the energy
|
|
79
|
+
along the in-phase axis). Because this estimates an *axis* rather than a
|
|
80
|
+
directed angle, the result is naturally defined modulo π.
|
|
81
|
+
|
|
82
|
+
Estimates phase by calculating a weighted PCA / eigenvector estimate of the first
|
|
83
|
+
principal component direction in 2D. The angle of this direction is the estimated
|
|
84
|
+
lock-in phase.
|
|
85
|
+
"""
|
|
86
|
+
# Weighted second moment matrix components - a weighted, scaled covariance matrix without mean subtraction
|
|
87
|
+
if w is None:
|
|
88
|
+
w = X * X + Y * Y
|
|
89
|
+
|
|
90
|
+
Sxx = np.sum(w * X * X)
|
|
91
|
+
Sxy = np.sum(w * X * Y)
|
|
92
|
+
Syy = np.sum(w * Y * Y)
|
|
93
|
+
|
|
94
|
+
# weighted cov matrix defines an ellipse; find its principal axis angle (see: https://en.wikipedia.org/wiki/Ellipse#General_ellipse)
|
|
95
|
+
phi_estimate = 0.5 * np.arctan2(2.0 * Sxy, (Sxx - Syy))
|
|
96
|
+
|
|
97
|
+
# The squareroot of the eigenvalues correspond to the semi-axis lengths of the ellipse defined by the weighted cov matrix
|
|
98
|
+
# When very line-like, we've succesfully mapped all signal into one axis, corresponding to high confidence in phase estimate
|
|
99
|
+
eig1, eig2 = _eigenvalues_symmetric_2D(Sxx, Sxy, Syy)
|
|
100
|
+
|
|
101
|
+
# If the matrix is (near) zero or non-finite, the phase is not meaningful
|
|
102
|
+
if not np.isfinite(eig1) or not np.isfinite(eig2) or eig1 <= 0.0:
|
|
103
|
+
return phi_estimate, 0.0
|
|
104
|
+
|
|
105
|
+
axis_ratio = float(np.sqrt(eig2 / eig1)) # in [0, 1] ideally
|
|
106
|
+
if not np.isfinite(axis_ratio):
|
|
107
|
+
return phi_estimate, 0.0
|
|
108
|
+
|
|
109
|
+
confidence = np.clip(1.0 - axis_ratio, 0, 1) # 0 (circle) to 1 (line)
|
|
110
|
+
|
|
111
|
+
return phi_estimate, confidence
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _LockinPhaseEstimator:
|
|
115
|
+
def __init__(
|
|
116
|
+
self: _LockinPhaseEstimator,
|
|
117
|
+
confidence_threshold: float = 0.8,
|
|
118
|
+
initial_phase_estimate: float | None = None,
|
|
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
|
+
"""
|
|
127
|
+
self.confidence_threshold = confidence_threshold
|
|
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
|
|
132
|
+
|
|
133
|
+
def update_estimate(
|
|
134
|
+
self: _LockinPhaseEstimator, Xs: FloatArray, Ys: FloatArray
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Update the phase estimate based on new in-phase (X) and quadrature (Y) data.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
Xs: Array of in-phase (X) values from the lock-in amplifier.
|
|
140
|
+
Ys: Array of quadrature (Y) values from the lock-in amplifier.
|
|
141
|
+
"""
|
|
142
|
+
r_squared = Xs * Xs + Ys * Ys
|
|
143
|
+
phase_estimate, confidence = _estimate_IQ_phase(Xs, Ys, r_squared)
|
|
144
|
+
|
|
145
|
+
# Don't update if confidence is too low
|
|
146
|
+
if confidence < self.confidence_threshold:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# First estimate
|
|
150
|
+
if self.phase_estimate is None:
|
|
151
|
+
self._set_estimate(
|
|
152
|
+
_choose_branch_by_strongest_point(phase_estimate, Xs, Ys, r_squared)
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Resolve the pi ambiguity using previous estimate
|
|
157
|
+
branched_phase_estimate = _choose_pi_branch(phase_estimate, self.phase_estimate)
|
|
158
|
+
self._set_estimate(branched_phase_estimate)
|
|
159
|
+
|
|
160
|
+
def _set_estimate(self: _LockinPhaseEstimator, phase: float) -> None:
|
|
161
|
+
self.phase_estimate = _wrap_to_pi(float(phase))
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from functools import wraps
|
|
6
|
-
from typing import TYPE_CHECKING, Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Callable
|
|
7
7
|
|
|
8
8
|
import serial
|
|
9
9
|
import serial.tools.list_ports
|
|
@@ -57,19 +57,20 @@ class _BackoffRetry:
|
|
|
57
57
|
|
|
58
58
|
@wraps(func)
|
|
59
59
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
60
|
+
func_name = getattr(func, "__name__", "function")
|
|
60
61
|
for tries in range(self.max_tries - 1):
|
|
61
62
|
try:
|
|
62
|
-
return
|
|
63
|
+
return func(*args, **kwargs)
|
|
63
64
|
except (KeyboardInterrupt, SystemExit):
|
|
64
65
|
raise
|
|
65
66
|
except Exception as e: # noqa: BLE001
|
|
66
67
|
self._log(
|
|
67
|
-
f"{
|
|
68
|
+
f"{func_name} failed {tries + 1} time(s) with: '{e}'. Trying again"
|
|
68
69
|
)
|
|
69
70
|
backoff = min(self.backoff_base * 2**tries, self.max_backoff)
|
|
70
71
|
time.sleep(backoff)
|
|
71
|
-
self._log(f"{
|
|
72
|
-
return
|
|
72
|
+
self._log(f"{func_name}: Last try ({tries + 2}).")
|
|
73
|
+
return func(*args, **kwargs)
|
|
73
74
|
|
|
74
75
|
return wrapper
|
|
75
76
|
|
|
@@ -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
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
4
|
+
from typing import Generic, TypeVar
|
|
7
5
|
|
|
8
6
|
from pyglaze.datamodels import UnprocessedWaveform
|
|
9
7
|
from pyglaze.device.ampcom import _LeAmpCom
|
|
10
8
|
from pyglaze.device.configuration import DeviceConfiguration, LeDeviceConfiguration
|
|
9
|
+
from pyglaze.helpers._lockin import _LockinPhaseEstimator
|
|
11
10
|
from pyglaze.scanning._exceptions import ScanError
|
|
12
11
|
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from pyglaze.helpers._types import FloatArray
|
|
15
|
-
|
|
16
12
|
TConfig = TypeVar("TConfig", bound=DeviceConfiguration)
|
|
17
13
|
|
|
18
14
|
|
|
@@ -51,13 +47,27 @@ class _ScannerImplementation(ABC, Generic[TConfig]):
|
|
|
51
47
|
def get_firmware_version(self: _ScannerImplementation) -> str:
|
|
52
48
|
pass
|
|
53
49
|
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_phase_estimate(self: _ScannerImplementation) -> float | None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
54
|
|
|
55
55
|
class Scanner:
|
|
56
|
-
"""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
|
+
"""
|
|
57
63
|
|
|
58
|
-
def __init__(
|
|
64
|
+
def __init__(
|
|
65
|
+
self: Scanner,
|
|
66
|
+
config: TConfig,
|
|
67
|
+
initial_phase_estimate: float | None = None,
|
|
68
|
+
) -> None:
|
|
59
69
|
self._scanner_impl: _ScannerImplementation[DeviceConfiguration] = (
|
|
60
|
-
_scanner_factory(config)
|
|
70
|
+
_scanner_factory(config, initial_phase_estimate)
|
|
61
71
|
)
|
|
62
72
|
|
|
63
73
|
@property
|
|
@@ -105,19 +115,35 @@ class Scanner:
|
|
|
105
115
|
"""
|
|
106
116
|
return self._scanner_impl.get_firmware_version()
|
|
107
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
|
+
|
|
108
126
|
|
|
109
127
|
class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
110
128
|
"""Perform synchronous terahertz scanning using a given DeviceConfiguration.
|
|
111
129
|
|
|
112
130
|
Args:
|
|
113
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.
|
|
114
134
|
"""
|
|
115
135
|
|
|
116
|
-
def __init__(
|
|
136
|
+
def __init__(
|
|
137
|
+
self: LeScanner,
|
|
138
|
+
config: LeDeviceConfiguration,
|
|
139
|
+
initial_phase_estimate: float | None = None,
|
|
140
|
+
) -> None:
|
|
117
141
|
self._config: LeDeviceConfiguration
|
|
118
142
|
self._ampcom: _LeAmpCom | None = None
|
|
119
143
|
self.config = config
|
|
120
|
-
self._phase_estimator = _LockinPhaseEstimator(
|
|
144
|
+
self._phase_estimator = _LockinPhaseEstimator(
|
|
145
|
+
initial_phase_estimate=initial_phase_estimate
|
|
146
|
+
)
|
|
121
147
|
|
|
122
148
|
@property
|
|
123
149
|
def config(self: LeScanner) -> LeDeviceConfiguration:
|
|
@@ -154,11 +180,10 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
154
180
|
if self._ampcom is None:
|
|
155
181
|
msg = "Scanner not configured"
|
|
156
182
|
raise ScanError(msg)
|
|
157
|
-
_, time,
|
|
158
|
-
self._phase_estimator.update_estimate(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
time, radius, theta, self._phase_estimator.phase_estimate
|
|
183
|
+
_, time, Xs, Ys = self._ampcom.start_scan()
|
|
184
|
+
self._phase_estimator.update_estimate(Xs=Xs, Ys=Ys)
|
|
185
|
+
return UnprocessedWaveform.from_inphase_quadrature(
|
|
186
|
+
time, Xs, Ys, self._phase_estimator.phase_estimate
|
|
162
187
|
)
|
|
163
188
|
|
|
164
189
|
def update_config(self: LeScanner, new_config: LeDeviceConfiguration) -> None:
|
|
@@ -199,45 +224,20 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
|
|
|
199
224
|
raise ScanError(msg)
|
|
200
225
|
return self._ampcom.get_firmware_version()
|
|
201
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
|
+
|
|
202
235
|
|
|
203
|
-
def _scanner_factory(
|
|
236
|
+
def _scanner_factory(
|
|
237
|
+
config: DeviceConfiguration, initial_phase_estimate: float | None = None
|
|
238
|
+
) -> _ScannerImplementation:
|
|
204
239
|
if isinstance(config, LeDeviceConfiguration):
|
|
205
|
-
return LeScanner(config)
|
|
240
|
+
return LeScanner(config, initial_phase_estimate)
|
|
206
241
|
|
|
207
242
|
msg = f"Unsupported configuration type: {type(config).__name__}"
|
|
208
243
|
raise TypeError(msg)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class _LockinPhaseEstimator:
|
|
212
|
-
def __init__(
|
|
213
|
-
self: _LockinPhaseEstimator,
|
|
214
|
-
r_threshold_for_update: float = 2.0,
|
|
215
|
-
theta_threshold_for_adjustment: float = 1.0,
|
|
216
|
-
) -> None:
|
|
217
|
-
self.r_threshold_for_update = r_threshold_for_update
|
|
218
|
-
self.theta_threshold_for_adjustment = theta_threshold_for_adjustment
|
|
219
|
-
self.phase_estimate: float | None = None
|
|
220
|
-
self._radius_of_est: float | None = None
|
|
221
|
-
|
|
222
|
-
def update_estimate(
|
|
223
|
-
self: _LockinPhaseEstimator, radius: FloatArray, theta: FloatArray
|
|
224
|
-
) -> None:
|
|
225
|
-
r_argmax = np.argmax(radius)
|
|
226
|
-
r_max = radius[r_argmax]
|
|
227
|
-
theta_at_max = theta[r_argmax]
|
|
228
|
-
if self._radius_of_est is None:
|
|
229
|
-
self._set_estimates(theta_at_max, r_max)
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
if r_max > self.r_threshold_for_update * self._radius_of_est or (
|
|
233
|
-
r_max > self._radius_of_est
|
|
234
|
-
and abs(theta_at_max - self.phase_estimate)
|
|
235
|
-
< self.theta_threshold_for_adjustment
|
|
236
|
-
):
|
|
237
|
-
self._set_estimates(theta_at_max, r_max)
|
|
238
|
-
|
|
239
|
-
def _set_estimates(
|
|
240
|
-
self: _LockinPhaseEstimator, phase: float, radius: float
|
|
241
|
-
) -> None:
|
|
242
|
-
self.phase_estimate = phase
|
|
243
|
-
self._radius_of_est = radius
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyglaze
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -38,11 +38,34 @@ Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
|
|
|
38
38
|
Requires-Python: <3.14,>=3.9
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist: numpy
|
|
41
|
+
Requires-Dist: numpy<2.0.0,>=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
|
|
45
45
|
Requires-Dist: typing_extensions>=4.12.2
|
|
46
|
+
Provides-Extra: build
|
|
47
|
+
Requires-Dist: build>=1.2.1; extra == "build"
|
|
48
|
+
Requires-Dist: twine>=5.1.1; extra == "build"
|
|
49
|
+
Requires-Dist: bumpver>=2023.1129; extra == "build"
|
|
50
|
+
Provides-Extra: test
|
|
51
|
+
Requires-Dist: pytest>=7.0.1; extra == "test"
|
|
52
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: mkdocs>=1.2.3; extra == "docs"
|
|
55
|
+
Requires-Dist: mkdocs-material>=8.2.5; extra == "docs"
|
|
56
|
+
Requires-Dist: mkdocstrings[python]>=0.18.1; extra == "docs"
|
|
57
|
+
Requires-Dist: mike>=1.1.2; extra == "docs"
|
|
58
|
+
Requires-Dist: Pygments>=2.12.0; extra == "docs"
|
|
59
|
+
Requires-Dist: matplotlib>=3.7.0; extra == "docs"
|
|
60
|
+
Requires-Dist: bumpver>=2023.1129; extra == "docs"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: ruff>=0.14.10; extra == "dev"
|
|
63
|
+
Requires-Dist: ty>=0.0.13; extra == "dev"
|
|
64
|
+
Requires-Dist: pytest>=7.0.1; extra == "dev"
|
|
65
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
66
|
+
Requires-Dist: semver>=2.13.0; extra == "dev"
|
|
67
|
+
Requires-Dist: types-pyserial>=3.5.0.8; extra == "dev"
|
|
68
|
+
Requires-Dist: bumpver>=2023.1129; extra == "dev"
|
|
46
69
|
Dynamic: license-file
|
|
47
70
|
|
|
48
71
|
# Pyglaze
|
|
@@ -19,6 +19,7 @@ src/pyglaze/devtools/__init__.py
|
|
|
19
19
|
src/pyglaze/devtools/mock_device.py
|
|
20
20
|
src/pyglaze/devtools/thz_pulse.py
|
|
21
21
|
src/pyglaze/helpers/__init__.py
|
|
22
|
+
src/pyglaze/helpers/_lockin.py
|
|
22
23
|
src/pyglaze/helpers/_types.py
|
|
23
24
|
src/pyglaze/helpers/utilities.py
|
|
24
25
|
src/pyglaze/interpolation/__init__.py
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
numpy<2.0.0,>=1.26.4
|
|
2
|
+
pyserial>=3.5
|
|
3
|
+
scipy>=1.7.3
|
|
4
|
+
bitstring>=4.1.2
|
|
5
|
+
typing_extensions>=4.12.2
|
|
6
|
+
|
|
7
|
+
[build]
|
|
8
|
+
build>=1.2.1
|
|
9
|
+
twine>=5.1.1
|
|
10
|
+
bumpver>=2023.1129
|
|
11
|
+
|
|
12
|
+
[dev]
|
|
13
|
+
ruff>=0.14.10
|
|
14
|
+
ty>=0.0.13
|
|
15
|
+
pytest>=7.0.1
|
|
16
|
+
pytest-cov>=4.0.0
|
|
17
|
+
semver>=2.13.0
|
|
18
|
+
types-pyserial>=3.5.0.8
|
|
19
|
+
bumpver>=2023.1129
|
|
20
|
+
|
|
21
|
+
[docs]
|
|
22
|
+
mkdocs>=1.2.3
|
|
23
|
+
mkdocs-material>=8.2.5
|
|
24
|
+
mkdocstrings[python]>=0.18.1
|
|
25
|
+
mike>=1.1.2
|
|
26
|
+
Pygments>=2.12.0
|
|
27
|
+
matplotlib>=3.7.0
|
|
28
|
+
bumpver>=2023.1129
|
|
29
|
+
|
|
30
|
+
[test]
|
|
31
|
+
pytest>=7.0.1
|
|
32
|
+
pytest-cov>=4.0.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.6"
|
|
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
|