pyglaze 0.4.5__tar.gz → 0.4.7__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 (35) hide show
  1. {pyglaze-0.4.5/src/pyglaze.egg-info → pyglaze-0.4.7}/PKG-INFO +25 -2
  2. {pyglaze-0.4.5 → pyglaze-0.4.7}/pyproject.toml +38 -3
  3. pyglaze-0.4.7/src/pyglaze/__init__.py +1 -0
  4. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/datamodels/pulse.py +3 -3
  5. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/datamodels/waveform.py +30 -7
  6. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/device/ampcom.py +1 -10
  7. pyglaze-0.4.7/src/pyglaze/helpers/_lockin.py +150 -0
  8. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/helpers/utilities.py +6 -5
  9. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/scanning/scanner.py +6 -46
  10. {pyglaze-0.4.5 → pyglaze-0.4.7/src/pyglaze.egg-info}/PKG-INFO +25 -2
  11. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze.egg-info/SOURCES.txt +1 -0
  12. pyglaze-0.4.7/src/pyglaze.egg-info/requires.txt +32 -0
  13. pyglaze-0.4.5/src/pyglaze/__init__.py +0 -1
  14. pyglaze-0.4.5/src/pyglaze.egg-info/requires.txt +0 -5
  15. {pyglaze-0.4.5 → pyglaze-0.4.7}/LICENSE +0 -0
  16. {pyglaze-0.4.5 → pyglaze-0.4.7}/MANIFEST.in +0 -0
  17. {pyglaze-0.4.5 → pyglaze-0.4.7}/README.md +0 -0
  18. {pyglaze-0.4.5 → pyglaze-0.4.7}/setup.cfg +0 -0
  19. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/datamodels/__init__.py +0 -0
  20. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/device/__init__.py +0 -0
  21. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/device/configuration.py +0 -0
  22. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/devtools/__init__.py +0 -0
  23. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/devtools/mock_device.py +0 -0
  24. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/devtools/thz_pulse.py +0 -0
  25. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/helpers/__init__.py +0 -0
  26. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/helpers/_types.py +0 -0
  27. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/interpolation/__init__.py +0 -0
  28. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/interpolation/interpolation.py +0 -0
  29. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/py.typed +0 -0
  30. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/scanning/__init__.py +0 -0
  31. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/scanning/_asyncscanner.py +0 -0
  32. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/scanning/_exceptions.py +0 -0
  33. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze/scanning/client.py +0 -0
  34. {pyglaze-0.4.5 → pyglaze-0.4.7}/src/pyglaze.egg-info/dependency_links.txt +0 -0
  35. {pyglaze-0.4.5 → pyglaze-0.4.7}/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.5
3
+ Version: 0.4.7
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>=1.26.4
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.4.5"
3
+ version = "0.4.7"
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.4.5"
106
+ current_version = "0.4.7"
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.4.7"
@@ -62,7 +62,7 @@ class Pulse:
62
62
  @property
63
63
  def fft(self: Pulse) -> ComplexArray:
64
64
  """Return the Fourier Transform of a signal."""
65
- return np.fft.rfft(self.signal, norm="forward")
65
+ return np.fft.rfft(self.signal, norm="forward", axis=0)
66
66
 
67
67
  @property
68
68
  def frequency(self: Pulse) -> FloatArray:
@@ -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.trapezoid(self.signal * self.signal, x=self.time)) # type: ignore[attr-defined, unused-ignore]
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)) # type: ignore[no-any-return]
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 degrees).
47
- rotation_angle: The angle to rotate lockin signal to align along x-axis. If not given, will use the angle at the maximum value of R.
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
- _rot_ang = (
50
- theta[np.argmax(radius)] if rotation_angle is None else rotation_angle
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 - _rot_ang
55
- signal = radius * np.cos(new_theta * np.pi / 180.0)
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
@@ -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,150 @@
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
+ ) -> None:
119
+ self.confidence_threshold = confidence_threshold
120
+ self.phase_estimate: float | None = None
121
+
122
+ def update_estimate(
123
+ self: _LockinPhaseEstimator, Xs: FloatArray, Ys: FloatArray
124
+ ) -> None:
125
+ """Update the phase estimate based on new in-phase (X) and quadrature (Y) data.
126
+
127
+ Args:
128
+ Xs: Array of in-phase (X) values from the lock-in amplifier.
129
+ Ys: Array of quadrature (Y) values from the lock-in amplifier.
130
+ """
131
+ r_squared = Xs * Xs + Ys * Ys
132
+ phase_estimate, confidence = _estimate_IQ_phase(Xs, Ys, r_squared)
133
+
134
+ # Don't update if confidence is too low
135
+ if confidence < self.confidence_threshold:
136
+ return
137
+
138
+ # First estimate
139
+ if self.phase_estimate is None:
140
+ self._set_estimate(
141
+ _choose_branch_by_strongest_point(phase_estimate, Xs, Ys, r_squared)
142
+ )
143
+ return
144
+
145
+ # Resolve the pi ambiguity using previous estimate
146
+ branched_phase_estimate = _choose_pi_branch(phase_estimate, self.phase_estimate)
147
+ self._set_estimate(branched_phase_estimate)
148
+
149
+ def _set_estimate(self: _LockinPhaseEstimator, phase: float) -> None:
150
+ 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, cast
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 cast("T", func(*args, **kwargs))
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"{func.__name__} failed {tries + 1} time(s) with: '{e}'. Trying again"
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"{func.__name__}: Last try ({tries + 2}).")
72
- return cast("T", func(*args, **kwargs))
72
+ self._log(f"{func_name}: Last try ({tries + 2}).")
73
+ return func(*args, **kwargs)
73
74
 
74
75
  return wrapper
75
76
 
@@ -1,18 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Generic, TypeVar
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
 
@@ -154,11 +150,10 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
154
150
  if self._ampcom is None:
155
151
  msg = "Scanner not configured"
156
152
  raise ScanError(msg)
157
- _, time, radius, theta = self._ampcom.start_scan()
158
- self._phase_estimator.update_estimate(radius=radius, theta=theta)
159
-
160
- return UnprocessedWaveform.from_polar_coords(
161
- time, radius, theta, self._phase_estimator.phase_estimate
153
+ _, time, Xs, Ys = self._ampcom.start_scan()
154
+ self._phase_estimator.update_estimate(Xs=Xs, Ys=Ys)
155
+ return UnprocessedWaveform.from_inphase_quadrature(
156
+ time, Xs, Ys, self._phase_estimator.phase_estimate
162
157
  )
163
158
 
164
159
  def update_config(self: LeScanner, new_config: LeDeviceConfiguration) -> None:
@@ -206,38 +201,3 @@ def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
206
201
 
207
202
  msg = f"Unsupported configuration type: {type(config).__name__}"
208
203
  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.4.5
3
+ Version: 0.4.7
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>=1.26.4
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.5"
@@ -1,5 +0,0 @@
1
- numpy>=1.26.4
2
- pyserial>=3.5
3
- scipy>=1.7.3
4
- bitstring>=4.1.2
5
- typing_extensions>=4.12.2
File without changes
File without changes
File without changes
File without changes
File without changes