pyglaze 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ from serial import serialutil
7
+ from typing_extensions import Self
8
+
9
+ from ._asyncscanner import _AsyncScanner
10
+
11
+ if TYPE_CHECKING:
12
+ from pyglaze.datamodels import UnprocessedWaveform
13
+ from pyglaze.device.configuration import DeviceConfiguration
14
+
15
+
16
+ class ScannerStartupError(Exception):
17
+ """Raised when the scanner could not be started."""
18
+
19
+ def __init__(self: ScannerStartupError) -> None:
20
+ super().__init__(
21
+ "Scanner could not be started. Please check the internal server error messages."
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class GlazeClient:
27
+ """Open a connection to and start continuously scanning using the Glaze device.
28
+
29
+ Args:
30
+ config: Configuration to use for scans
31
+ """
32
+
33
+ config: DeviceConfiguration
34
+ _scanner: _AsyncScanner = field(init=False)
35
+
36
+ def __enter__(self: Self) -> Self:
37
+ """Start the scanner and return the client."""
38
+ self._scanner = _AsyncScanner()
39
+ try:
40
+ self._scanner.start_scan(self.config)
41
+ except (TimeoutError, serialutil.SerialException) as e:
42
+ self.__exit__(e)
43
+ return self
44
+
45
+ def __exit__(self: GlazeClient, *args: object) -> None:
46
+ """Stop the scanner and close the connection."""
47
+ if self._scanner.is_scanning:
48
+ self._scanner.stop_scan()
49
+ # Exit is only called with arguments when an error occurs - hence raise.
50
+ if args[0]:
51
+ raise
52
+
53
+ def read(self: GlazeClient, n_pulses: int) -> list[UnprocessedWaveform]:
54
+ """Read a number of pulses from the Glaze system.
55
+
56
+ Args:
57
+ n_pulses: The number of terahertz pulses to read from the CCS server.
58
+ """
59
+ return self._scanner.get_scans(n_pulses)
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Generic, TypeVar
5
+
6
+ import numpy as np
7
+
8
+ from pyglaze.datamodels import UnprocessedWaveform
9
+ from pyglaze.device.ampcom import _ForceAmpCom, _LeAmpCom
10
+ from pyglaze.device.configuration import (
11
+ DeviceConfiguration,
12
+ ForceDeviceConfiguration,
13
+ LeDeviceConfiguration,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from pyglaze.helpers.types import FloatArray
18
+
19
+ TConfig = TypeVar("TConfig", bound=DeviceConfiguration)
20
+
21
+
22
+ class _ScannerImplementation(ABC, Generic[TConfig]):
23
+ @abstractmethod
24
+ def __init__(self: _ScannerImplementation, config: TConfig) -> None:
25
+ pass
26
+
27
+ @property
28
+ @abstractmethod
29
+ def config(self: _ScannerImplementation) -> TConfig:
30
+ pass
31
+
32
+ @config.setter
33
+ @abstractmethod
34
+ def config(self: _ScannerImplementation, new_config: TConfig) -> None:
35
+ pass
36
+
37
+ @abstractmethod
38
+ def scan(self: _ScannerImplementation) -> UnprocessedWaveform:
39
+ pass
40
+
41
+ @abstractmethod
42
+ def update_config(self: _ScannerImplementation, new_config: TConfig) -> None:
43
+ pass
44
+
45
+
46
+ class Scanner:
47
+ """A synchronous scanner for Glaze terahertz devices."""
48
+
49
+ def __init__(self: Scanner, config: TConfig) -> None:
50
+ self._scanner_impl: _ScannerImplementation[DeviceConfiguration] = (
51
+ _scanner_factory(config)
52
+ )
53
+
54
+ @property
55
+ def config(self: Scanner) -> DeviceConfiguration:
56
+ """Configuration used in the scan."""
57
+ return self._scanner_impl.config
58
+
59
+ @config.setter
60
+ def config(self: Scanner, new_config: DeviceConfiguration) -> None:
61
+ self._scanner_impl.config = new_config
62
+
63
+ def scan(self: Scanner) -> UnprocessedWaveform:
64
+ """Perform a scan.
65
+
66
+ Returns:
67
+ UnprocessedWaveform: A raw waveform.
68
+ """
69
+ return self._scanner_impl.scan()
70
+
71
+ def update_config(self: Scanner, new_config: DeviceConfiguration) -> None:
72
+ """Update the DeviceConfiguration used in the scan.
73
+
74
+ Args:
75
+ new_config (DeviceConfiguration): New configuration for scanner
76
+ """
77
+ self._scanner_impl.update_config(new_config)
78
+
79
+
80
+ class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
81
+ """Perform synchronous terahertz scanning using a given DeviceConfiguration.
82
+
83
+ Args:
84
+ config: A DeviceConfiguration to use for the scan.
85
+
86
+ """
87
+
88
+ def __init__(self: ForceScanner, config: ForceDeviceConfiguration) -> None:
89
+ self._config: ForceDeviceConfiguration
90
+ self._ampcom: _ForceAmpCom
91
+ self.config = config
92
+ self._phase_estimator = _LockinPhaseEstimator()
93
+
94
+ @property
95
+ def config(self: ForceScanner) -> ForceDeviceConfiguration:
96
+ """The device configuration to use for the scan.
97
+
98
+ Returns:
99
+ DeviceConfiguration: a DeviceConfiguration.
100
+ """
101
+ return self._config
102
+
103
+ @config.setter
104
+ def config(self: ForceScanner, new_config: ForceDeviceConfiguration) -> None:
105
+ amp = _ForceAmpCom(new_config)
106
+ if getattr(self, "_config", None):
107
+ if (
108
+ self._config.integration_periods != new_config.integration_periods
109
+ or self._config.modulation_frequency != new_config.modulation_frequency
110
+ ):
111
+ amp.write_period_and_frequency()
112
+ if self._config.sweep_length_ms != new_config.sweep_length_ms:
113
+ amp.write_sweep_length()
114
+ if self._config.modulation_waveform != new_config.modulation_waveform:
115
+ amp.write_waveform()
116
+ if (
117
+ self._config.min_modulation_voltage != new_config.min_modulation_voltage
118
+ or self._config.max_modulation_voltage
119
+ != new_config.max_modulation_voltage
120
+ ):
121
+ amp.write_modulation_voltage()
122
+ if self._config.scan_intervals != new_config.scan_intervals:
123
+ amp.write_list()
124
+ else:
125
+ amp.write_all()
126
+
127
+ self._config = new_config
128
+ self._ampcom = amp
129
+
130
+ def scan(self: ForceScanner) -> UnprocessedWaveform:
131
+ """Perform a scan.
132
+
133
+ Returns:
134
+ Unprocessed scan.
135
+ """
136
+ _, responses = self._ampcom.start_scan()
137
+
138
+ time = responses[:, 0]
139
+ radius = responses[:, 1]
140
+ theta = responses[:, 2]
141
+ self._phase_estimator.update_estimate(radius=radius, theta=theta)
142
+
143
+ return UnprocessedWaveform.from_polar_coords(
144
+ time, radius, theta, self._phase_estimator.phase_estimate
145
+ )
146
+
147
+ def update_config(self: ForceScanner, new_config: ForceDeviceConfiguration) -> None:
148
+ """Update the DeviceConfiguration used in the scan.
149
+
150
+ Args:
151
+ new_config: A DeviceConfiguration to use for the scan.
152
+ """
153
+ self.config = new_config
154
+
155
+
156
+ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
157
+ """Perform synchronous terahertz scanning using a given DeviceConfiguration.
158
+
159
+ Args:
160
+ config: A DeviceConfiguration to use for the scan.
161
+ """
162
+
163
+ def __init__(self: LeScanner, config: LeDeviceConfiguration) -> None:
164
+ self._config: LeDeviceConfiguration
165
+ self._ampcom: _LeAmpCom
166
+ self.config = config
167
+ self._phase_estimator = _LockinPhaseEstimator()
168
+
169
+ @property
170
+ def config(self: LeScanner) -> LeDeviceConfiguration:
171
+ """The device configuration to use for the scan.
172
+
173
+ Returns:
174
+ DeviceConfiguration: a DeviceConfiguration.
175
+ """
176
+ return self._config
177
+
178
+ @config.setter
179
+ def config(self: LeScanner, new_config: LeDeviceConfiguration) -> None:
180
+ amp = _LeAmpCom(new_config)
181
+ if getattr(self, "_config", None):
182
+ if (
183
+ self._config.integration_periods != new_config.integration_periods
184
+ or self._config.n_points != new_config.n_points
185
+ ):
186
+ amp.write_list_length_and_integration_periods_and_use_ema()
187
+ if self._config.scan_intervals != new_config.scan_intervals:
188
+ amp.write_list()
189
+ else:
190
+ amp.write_all()
191
+
192
+ self._config = new_config
193
+ self._ampcom = amp
194
+
195
+ def scan(self: LeScanner) -> UnprocessedWaveform:
196
+ """Perform a scan.
197
+
198
+ Returns:
199
+ Unprocessed scan.
200
+ """
201
+ _, responses = self._ampcom.start_scan()
202
+
203
+ time = responses[:, 0]
204
+ radius = responses[:, 1]
205
+ theta = responses[:, 2]
206
+ self._phase_estimator.update_estimate(radius=radius, theta=theta)
207
+
208
+ return UnprocessedWaveform.from_polar_coords(
209
+ time, radius, theta, self._phase_estimator.phase_estimate
210
+ )
211
+
212
+ def update_config(self: LeScanner, new_config: LeDeviceConfiguration) -> None:
213
+ """Update the DeviceConfiguration used in the scan.
214
+
215
+ Args:
216
+ new_config: A DeviceConfiguration to use for the scan.
217
+ """
218
+ self.config = new_config
219
+
220
+
221
+ def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
222
+ if isinstance(config, ForceDeviceConfiguration):
223
+ return ForceScanner(config)
224
+ if isinstance(config, LeDeviceConfiguration):
225
+ return LeScanner(config)
226
+
227
+ msg = f"Unsupported configuration type: {type(config).__name__}"
228
+ raise TypeError(msg)
229
+
230
+
231
+ class _LockinPhaseEstimator:
232
+ def __init__(
233
+ self: _LockinPhaseEstimator, r_threshold_for_update: float = 2.0
234
+ ) -> None:
235
+ self.phase_estimate: float | None = None
236
+ self.r_threshold_for_update = r_threshold_for_update
237
+ self._radius_of_est: float | None = None
238
+
239
+ def update_estimate(
240
+ self: _LockinPhaseEstimator, radius: FloatArray, theta: FloatArray
241
+ ) -> None:
242
+ r_argmax = np.argmax(radius)
243
+ r_max = radius[r_argmax]
244
+ theta_at_max = theta[r_argmax]
245
+ if self._radius_of_est is None:
246
+ self._set_estimates(theta_at_max, r_max)
247
+ return
248
+
249
+ if r_max > self.r_threshold_for_update * self._radius_of_est:
250
+ self._set_estimates(theta_at_max, r_max)
251
+
252
+ def _set_estimates(
253
+ self: _LockinPhaseEstimator, phase: float, radius: float
254
+ ) -> None:
255
+ self.phase_estimate = phase
256
+ self._radius_of_est = radius
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Glaze Technologies
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyglaze
3
+ Version: 0.1.0
4
+ Summary: Pyglaze is a library used to operate the devices of Glaze Technologies
5
+ Author: GLAZE Technologies ApS
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2024, Glaze Technologies
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ Project-URL: Homepage, https://www.glazetech.dk/
35
+ Project-URL: Documentation, https://glazetech.github.io/pyglaze/latest
36
+ Project-URL: Repository, https://github.com/GlazeTech/pyglaze
37
+ Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
38
+ Requires-Python: <3.13,>=3.9
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: numpy <2.0.0,>=1.26.4
42
+ Requires-Dist: pyserial >=3.5
43
+ Requires-Dist: scipy >=1.7.3
44
+ Requires-Dist: bitstring >=4.1.2
45
+ Requires-Dist: typing-extensions >=4.12.2
46
+
47
+ # Pyglaze
48
+ Pyglaze is a python library used to operate the devices of [Glaze Technologies](https://www.glazetech.dk/).
49
+
50
+ Documentation can be found [here](https://glazetech.github.io/pyglaze/latest/).
51
+
52
+ # Installation
53
+
54
+ To install the latest version of the package, simply run
55
+
56
+ ```
57
+ pip install pyglaze
58
+ ```
59
+
60
+ # Usage
61
+ See [our documentation](https://glazetech.github.io/pyglaze/latest/) for usage.
62
+
63
+ # Developers
64
+
65
+ To install the API with development tools in editable mode, first clone the repository from our [public GitHub repository](https://github.com/GlazeTech/pyglaze). Then, from the root of the project, run
66
+
67
+ ```
68
+ python -m pip install --upgrade pip
69
+ pip install -e . --config-settings editable_mode=strict
70
+ pip install -r requirements-dev.txt
71
+ ```
72
+
73
+ ## Documentation - local build
74
+ To build and serve the documentation locally
75
+
76
+ 1. Checkout the repository (or a specific version)
77
+ 2. Install `mkdocs`
78
+ 3. Run `mkdocs serve` while standing in the project root.
79
+
80
+
81
+ # Bug reporting or feature requests
82
+ Please create an issue [here](https://github.com/GlazeTech/pyglaze/issues) and we will look at it ASAP!
@@ -0,0 +1,32 @@
1
+ pyglaze/__init__.py,sha256=pU56ZDjCDnUbINe39hYuKXHy9UI9Tf4xjWgnxBlxEDk,22
2
+ pyglaze/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pyglaze/datamodels/__init__.py,sha256=DJLByl2C7pC4RM4Uh6PW-McM5RIGBjcopzGywCKhSlI,111
4
+ pyglaze/datamodels/pulse.py,sha256=8N0wHHtV2c31E8LKzPOfxWYHialPGXQfXAoX_h7dkBA,20488
5
+ pyglaze/datamodels/waveform.py,sha256=n31DhJHFeBNZ3hHqQUiCGXssm5Dc8wV6tGPkhmFYB4Q,5809
6
+ pyglaze/device/__init__.py,sha256=NbN0I1U3mc_kNz-HyRCGRQQSi5z5peEZOIrH3z-m52Y,445
7
+ pyglaze/device/ampcom.py,sha256=_cPfgXb78uuj-GZkNPNDjifhQvqfkAxYycY8n_E2bvk,16767
8
+ pyglaze/device/configuration.py,sha256=8_q7l4DTsU_dfrZd384XYGyzmA8ZuOho98pFulzByeY,8511
9
+ pyglaze/device/delayunit.py,sha256=TO1FRhB3TepJn4QRNP-AKykWJVD9sx0Q9crL3xMudIk,4415
10
+ pyglaze/device/identifiers.py,sha256=-LBAhlYsxBGSMevUIqW3KxckdOzOT9HcKIBOjT3XN-U,1022
11
+ pyglaze/device/_delayunit_data/carmen-nonuniform-2023-10-20.pickle,sha256=5qcz-YwPfP5s2f5xlQ02er4dtZULWxXXJsac5tQsJRE,20400
12
+ pyglaze/device/_delayunit_data/g1-linearized-2023-04-04.pickle,sha256=N2QX4WSCrh7QihOqfczIYgk_XPx1IxY2zxp0gs9H0RY,209
13
+ pyglaze/device/_delayunit_data/g2-linearized-2023-04-04.pickle,sha256=eJ1A-NsZxN3pDd0UsqCyY9-pEzcXAYdsu_vxBx8ODq0,210
14
+ pyglaze/device/_delayunit_data/g2-nonuniform-2023-04-04.pickle,sha256=1Tyd22YzAqw2r6BPVemfE1XuJ8jZj8XYQik5Ne8-s9g,1784
15
+ pyglaze/device/_delayunit_data/mock_delay.pickle,sha256=tu-Uqytg8bWarBaXXRMD4jVU6MUr1Sz6dswJ6_6nbOA,207
16
+ pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
17
+ pyglaze/devtools/mock_device.py,sha256=TglmqvwX3FtdcE8cB_-3RNu2_QFtuJ-1chW8WTED3rc,13049
18
+ pyglaze/devtools/thz_pulse.py,sha256=xp-T9psdOrUMtSUFu8HEwQJVu_aMixJdZHtg_BCVu_k,923
19
+ pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ pyglaze/helpers/types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
21
+ pyglaze/helpers/utilities.py,sha256=n_x9Tqm305MUorS29O6CoJM8Mi4apo2bsN_odrRaVAw,2423
22
+ pyglaze/interpolation/__init__.py,sha256=WCxHPsiI7zvJykp-jfytoEbO4Tla-YIF6A7fjDfcDvU,72
23
+ pyglaze/interpolation/interpolation.py,sha256=rQWzPD7W8TXETps7VZI0gcfAOCWO8pGL1HhhBnyxaMw,735
24
+ pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
25
+ pyglaze/scanning/_asyncscanner.py,sha256=n4PhRBSZLFSSeGgBkgVwFsdY4rnduXJnQo6TQ0ujVkw,5224
26
+ pyglaze/scanning/client.py,sha256=3qrQStkeLQzCeu4yMHJ_ENLGQ7E5GMc4CP9J55rk-ug,1817
27
+ pyglaze/scanning/scanner.py,sha256=TGHTO4qoLeKzb_hCuqEEAlKicbDvozY-3GiSfxiatXI,8226
28
+ pyglaze-0.1.0.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
29
+ pyglaze-0.1.0.dist-info/METADATA,sha256=OJ_Ck2MLNZ8ropSBGxuzRlDx8akLD_c2sEptwscCHuI,3498
30
+ pyglaze-0.1.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
31
+ pyglaze-0.1.0.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
32
+ pyglaze-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (70.1.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyglaze