pyglaze 0.2.2__py3-none-any.whl → 0.4.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.
@@ -1,6 +1,9 @@
1
+ from typing import cast
2
+
1
3
  import numpy as np
4
+ from scipy.interpolate import CubicSpline
2
5
 
3
- from pyglaze.helpers.types import FloatArray
6
+ from pyglaze.helpers._types import FloatArray
4
7
 
5
8
 
6
9
  def ws_interpolate(
@@ -21,4 +24,21 @@ def ws_interpolate(
21
24
  # times must be zero-centered for formula to work
22
25
  sinc = np.sinc((interp_times[:, np.newaxis] - times[0] - dt * _range) / dt)
23
26
 
24
- return np.asarray(np.sum(pulse * sinc, axis=1))
27
+ return cast("FloatArray", np.sum(pulse * sinc, axis=1))
28
+
29
+
30
+ def cubic_spline_interpolate(
31
+ times: FloatArray, pulse: FloatArray, interp_times: FloatArray
32
+ ) -> FloatArray:
33
+ """Performs cubic spline interpolation at the supplied times given a pulse.
34
+
35
+ Args:
36
+ times: Sampling times
37
+ pulse: A sampled pulse satisfying the Nyquist criterion
38
+ interp_times: Array of times at which to interpolate
39
+
40
+ Returns:
41
+ FloatArray: Interpolated values
42
+ """
43
+ spline = CubicSpline(times, pulse, bc_type="natural")
44
+ return cast("FloatArray", spline(interp_times))
@@ -6,7 +6,7 @@ from multiprocessing import Event, Pipe, Process, Queue, synchronize
6
6
  from queue import Empty, Full
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from serial import serialutil
9
+ from serial import SerialException, serialutil
10
10
 
11
11
  from pyglaze.datamodels.waveform import UnprocessedWaveform, _TimestampedWaveform
12
12
  from pyglaze.scanning.scanner import Scanner
@@ -25,6 +25,12 @@ class _ScannerHealth:
25
25
  error: Exception | None
26
26
 
27
27
 
28
+ @dataclass
29
+ class _ScannerMetadata:
30
+ serial_number: str
31
+ firmware_version: str
32
+
33
+
28
34
  @dataclass
29
35
  class _AsyncScanner:
30
36
  """Used by GlazeClient to starts a scanner in a new process and read scans from shared memory."""
@@ -34,6 +40,7 @@ class _AsyncScanner:
34
40
  logger: logging.Logger | None = None
35
41
  is_scanning: bool = False
36
42
  _child_process: Process = field(init=False)
43
+ _metadata: _ScannerMetadata = field(init=False)
37
44
  _shared_mem: Queue[_TimestampedWaveform] = field(init=False)
38
45
  _SCAN_TIMEOUT: float = field(init=False)
39
46
  _stop_signal: synchronize.Event = field(init=False)
@@ -72,6 +79,10 @@ class _AsyncScanner:
72
79
  self.logger.error(str(msg.error))
73
80
  raise msg.error
74
81
 
82
+ # As part of startup, metadata is sent from scanner
83
+ metadata: _ScannerMetadata = self._scanner_conn.recv()
84
+ self._metadata = metadata
85
+
75
86
  def stop_scan(self: _AsyncScanner) -> None:
76
87
  self._stop_signal.set()
77
88
  self._child_process.join()
@@ -87,10 +98,21 @@ class _AsyncScanner:
87
98
 
88
99
  return [self._get_scan().waveform for _ in range(n_pulses)]
89
100
 
90
- def get_next(self: _AsyncScanner, averaged_over_n: int = 1) -> UnprocessedWaveform:
91
- return UnprocessedWaveform.average(
92
- [self._get_scan().waveform for _ in range(averaged_over_n)]
93
- )
101
+ def get_next(self: _AsyncScanner) -> UnprocessedWaveform:
102
+ return self._get_scan().waveform
103
+
104
+ def get_serial_number(self: _AsyncScanner) -> str:
105
+ if not self.is_scanning:
106
+ msg = "Scanner not connected"
107
+ raise SerialException(msg)
108
+ return self._metadata.serial_number
109
+
110
+ def get_firmware_version(self: _AsyncScanner) -> str:
111
+ if not self.is_scanning:
112
+ msg = "Scanner not connected"
113
+ raise SerialException(msg)
114
+
115
+ return self._metadata.firmware_version
94
116
 
95
117
  def _get_scan(self: _AsyncScanner) -> _TimestampedWaveform:
96
118
  try:
@@ -115,7 +137,12 @@ class _AsyncScanner:
115
137
  ) -> None:
116
138
  try:
117
139
  scanner = Scanner(config=config)
140
+ device_metadata = _ScannerMetadata(
141
+ serial_number=scanner.get_serial_number(),
142
+ firmware_version=scanner.get_firmware_version(),
143
+ )
118
144
  parent_conn.send(_ScannerHealth(is_alive=True, is_healthy=True, error=None))
145
+ parent_conn.send(device_metadata)
119
146
  except (serialutil.SerialException, TimeoutError) as e:
120
147
  parent_conn.send(_ScannerHealth(is_alive=False, is_healthy=False, error=e))
121
148
  return
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from serial import serialutil
6
+ from serial import SerialException, serialutil
7
7
  from typing_extensions import Self
8
8
 
9
9
  from ._asyncscanner import _AsyncScanner
@@ -57,3 +57,19 @@ class GlazeClient:
57
57
  n_pulses: The number of terahertz pulses to read from the CCS server.
58
58
  """
59
59
  return self._scanner.get_scans(n_pulses)
60
+
61
+ def get_serial_number(self: GlazeClient) -> str:
62
+ """Get the serial number of the connected device."""
63
+ try:
64
+ return self._scanner.get_serial_number()
65
+ except AttributeError as e:
66
+ msg = "No connection to device."
67
+ raise SerialException(msg) from e
68
+
69
+ def get_firmware_version(self: GlazeClient) -> str:
70
+ """Get the firmware version of the connected device."""
71
+ try:
72
+ return self._scanner.get_firmware_version()
73
+ except AttributeError as e:
74
+ msg = "No connection to device."
75
+ raise SerialException(msg) from e
@@ -4,19 +4,14 @@ from abc import ABC, abstractmethod
4
4
  from typing import TYPE_CHECKING, Generic, TypeVar
5
5
 
6
6
  import numpy as np
7
- from serial import SerialException
8
7
 
9
8
  from pyglaze.datamodels import UnprocessedWaveform
10
- from pyglaze.device.ampcom import _ForceAmpCom, _LeAmpCom
11
- from pyglaze.device.configuration import (
12
- DeviceConfiguration,
13
- ForceDeviceConfiguration,
14
- LeDeviceConfiguration,
15
- )
9
+ from pyglaze.device.ampcom import _LeAmpCom
10
+ from pyglaze.device.configuration import DeviceConfiguration, LeDeviceConfiguration
16
11
  from pyglaze.scanning._exceptions import ScanError
17
12
 
18
13
  if TYPE_CHECKING:
19
- from pyglaze.helpers.types import FloatArray
14
+ from pyglaze.helpers._types import FloatArray
20
15
 
21
16
  TConfig = TypeVar("TConfig", bound=DeviceConfiguration)
22
17
 
@@ -48,6 +43,14 @@ class _ScannerImplementation(ABC, Generic[TConfig]):
48
43
  def disconnect(self: _ScannerImplementation) -> None:
49
44
  pass
50
45
 
46
+ @abstractmethod
47
+ def get_serial_number(self: _ScannerImplementation) -> str:
48
+ pass
49
+
50
+ @abstractmethod
51
+ def get_firmware_version(self: _ScannerImplementation) -> str:
52
+ pass
53
+
51
54
 
52
55
  class Scanner:
53
56
  """A synchronous scanner for Glaze terahertz devices."""
@@ -86,92 +89,21 @@ class Scanner:
86
89
  """Close serial connection."""
87
90
  self._scanner_impl.disconnect()
88
91
 
89
-
90
- class ForceScanner(_ScannerImplementation[ForceDeviceConfiguration]):
91
- """Perform synchronous terahertz scanning using a given DeviceConfiguration.
92
-
93
- Args:
94
- config: A DeviceConfiguration to use for the scan.
95
-
96
- """
97
-
98
- def __init__(self: ForceScanner, config: ForceDeviceConfiguration) -> None:
99
- self._config: ForceDeviceConfiguration
100
- self._ampcom: _ForceAmpCom | None = None
101
- self.config = config
102
- self._phase_estimator = _LockinPhaseEstimator()
103
-
104
- @property
105
- def config(self: ForceScanner) -> ForceDeviceConfiguration:
106
- """The device configuration to use for the scan.
92
+ def get_serial_number(self: Scanner) -> str:
93
+ """Get the serial number of the connected device.
107
94
 
108
95
  Returns:
109
- DeviceConfiguration: a DeviceConfiguration.
96
+ str: The serial number of the connected device.
110
97
  """
111
- return self._config
98
+ return self._scanner_impl.get_serial_number()
112
99
 
113
- @config.setter
114
- def config(self: ForceScanner, new_config: ForceDeviceConfiguration) -> None:
115
- amp = _ForceAmpCom(new_config)
116
- if getattr(self, "_config", None):
117
- if (
118
- self._config.integration_periods != new_config.integration_periods
119
- or self._config.modulation_frequency != new_config.modulation_frequency
120
- ):
121
- amp.write_period_and_frequency()
122
- if self._config.sweep_length_ms != new_config.sweep_length_ms:
123
- amp.write_sweep_length()
124
- if self._config.modulation_waveform != new_config.modulation_waveform:
125
- amp.write_waveform()
126
- if (
127
- self._config.min_modulation_voltage != new_config.min_modulation_voltage
128
- or self._config.max_modulation_voltage
129
- != new_config.max_modulation_voltage
130
- ):
131
- amp.write_modulation_voltage()
132
- if self._config.scan_intervals != new_config.scan_intervals:
133
- amp.write_list()
134
- else:
135
- amp.write_all()
136
-
137
- self._config = new_config
138
- self._ampcom = amp
139
-
140
- def scan(self: ForceScanner) -> UnprocessedWaveform:
141
- """Perform a scan.
100
+ def get_firmware_version(self: Scanner) -> str:
101
+ """Get the firmware version of the connected device.
142
102
 
143
103
  Returns:
144
- Unprocessed scan.
104
+ str: The firmware version of the connected device.
145
105
  """
146
- if self._ampcom is None:
147
- msg = "Scanner not configured"
148
- raise ScanError(msg)
149
- _, responses = self._ampcom.start_scan()
150
-
151
- time = responses[:, 0]
152
- radius = responses[:, 1]
153
- theta = responses[:, 2]
154
- self._phase_estimator.update_estimate(radius=radius, theta=theta)
155
-
156
- return UnprocessedWaveform.from_polar_coords(
157
- time, radius, theta, self._phase_estimator.phase_estimate
158
- )
159
-
160
- def update_config(self: ForceScanner, new_config: ForceDeviceConfiguration) -> None:
161
- """Update the DeviceConfiguration used in the scan.
162
-
163
- Args:
164
- new_config: A DeviceConfiguration to use for the scan.
165
- """
166
- self.config = new_config
167
-
168
- def disconnect(self: ForceScanner) -> None:
169
- """Close serial connection."""
170
- if self._ampcom is None:
171
- msg = "Scanner not connected"
172
- raise SerialException(msg)
173
- self._ampcom.disconnect()
174
- self._ampcom = None
106
+ return self._scanner_impl.get_firmware_version()
175
107
 
176
108
 
177
109
  class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
@@ -245,10 +177,30 @@ class LeScanner(_ScannerImplementation[LeDeviceConfiguration]):
245
177
  self._ampcom.disconnect()
246
178
  self._ampcom = None
247
179
 
180
+ def get_serial_number(self: LeScanner) -> str:
181
+ """Get the serial number of the connected device.
182
+
183
+ Returns:
184
+ str: The serial number of the connected device.
185
+ """
186
+ if self._ampcom is None:
187
+ msg = "Scanner not connected"
188
+ raise ScanError(msg)
189
+ return self._ampcom.get_serial_number()
190
+
191
+ def get_firmware_version(self: LeScanner) -> str:
192
+ """Get the firmware version of the connected device.
193
+
194
+ Returns:
195
+ str: The firmware version of the connected device.
196
+ """
197
+ if self._ampcom is None:
198
+ msg = "Scanner not connected"
199
+ raise ScanError(msg)
200
+ return self._ampcom.get_firmware_version()
201
+
248
202
 
249
203
  def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
250
- if isinstance(config, ForceDeviceConfiguration):
251
- return ForceScanner(config)
252
204
  if isinstance(config, LeDeviceConfiguration):
253
205
  return LeScanner(config)
254
206
 
@@ -258,10 +210,13 @@ def _scanner_factory(config: DeviceConfiguration) -> _ScannerImplementation:
258
210
 
259
211
  class _LockinPhaseEstimator:
260
212
  def __init__(
261
- self: _LockinPhaseEstimator, r_threshold_for_update: float = 2.0
213
+ self: _LockinPhaseEstimator,
214
+ r_threshold_for_update: float = 2.0,
215
+ theta_threshold_for_adjustment: float = 1.0,
262
216
  ) -> None:
263
- self.phase_estimate: float | None = None
264
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
265
220
  self._radius_of_est: float | None = None
266
221
 
267
222
  def update_estimate(
@@ -274,7 +229,11 @@ class _LockinPhaseEstimator:
274
229
  self._set_estimates(theta_at_max, r_max)
275
230
  return
276
231
 
277
- if r_max > self.r_threshold_for_update * self._radius_of_est:
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
+ ):
278
237
  self._set_estimates(theta_at_max, r_max)
279
238
 
280
239
  def _set_estimates(
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyglaze
3
- Version: 0.2.2
3
+ Version: 0.4.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,12 @@ Project-URL: Issues, https://github.com/GlazeTech/pyglaze/issues
38
38
  Requires-Python: <3.13,>=3.9
39
39
  Description-Content-Type: text/markdown
40
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
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
+ Dynamic: license-file
46
47
 
47
48
  # Pyglaze
48
49
  Pyglaze is a python library used to operate the devices of [Glaze Technologies](https://www.glazetech.dk/).
@@ -0,0 +1,26 @@
1
+ pyglaze/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,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=ygC9Vfdn7ULzu-JndtODuEwmD8cDXnlOoEcliwJPsxg,22565
5
+ pyglaze/datamodels/waveform.py,sha256=T0wV7saJNPowwQs518VLypul-p1bg_1REPTflw8UNzM,5810
6
+ pyglaze/device/__init__.py,sha256=rxF1h54dHTwq5JVvLjxDeirY4njMfr8c9qs0yJ5GRhE,119
7
+ pyglaze/device/ampcom.py,sha256=ecnkrMI1wFatTvPQjlZ5LTG2xkrTLIVkOw1hU_Hojso,10714
8
+ pyglaze/device/configuration.py,sha256=YmIOWY-K1ixSWZgrpEElE7zBVr4EbFWtPIZ0sRn4RpU,4899
9
+ pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
10
+ pyglaze/devtools/mock_device.py,sha256=B-CF5ZG1GZ5P_6LwtURbx9lysbm-nCgtRl43FBJCg6k,9880
11
+ pyglaze/devtools/thz_pulse.py,sha256=WUAz3QTdw_ak0yVyU1zzABrqyCzWhUFRUHP5K_i3ppU,963
12
+ pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pyglaze/helpers/_types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
14
+ pyglaze/helpers/utilities.py,sha256=FkY-jLi62JxsfS6KFCMhzZ9OO34b4nx7-m2ebPqrCy4,2436
15
+ pyglaze/interpolation/__init__.py,sha256=BZU-mIAO7XxrPGG7tsUks-ghbylOaIANJGIYrtF5Y_Y,126
16
+ pyglaze/interpolation/interpolation.py,sha256=x2hgS9jiQ359Ho7klaBec1L-eXZpFe0NVluTksuPLLA,1345
17
+ pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
18
+ pyglaze/scanning/_asyncscanner.py,sha256=cS-XOGWaFsYJ5ShkchyHQo8HtPCADGEGnf-isHx_JL0,6183
19
+ pyglaze/scanning/_exceptions.py,sha256=vS28Dijj76jVuF6cSDBKqM9SQIa9rbIyUaHF-RA3PyM,213
20
+ pyglaze/scanning/client.py,sha256=xgsZLRFVe_rcLQD7AIcztKZ_wY6mp3v4ID3Zq40SbP0,2459
21
+ pyglaze/scanning/scanner.py,sha256=69Bawx3m6toHH3bhHSRduVN527-aeveFI9l6WcaKWs8,7712
22
+ pyglaze-0.4.0.dist-info/licenses/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
23
+ pyglaze-0.4.0.dist-info/METADATA,sha256=SIF4aktj9-ogBuAb9AraranYA5POn6O4aVhlKUnz01w,3515
24
+ pyglaze-0.4.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
+ pyglaze-0.4.0.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
26
+ pyglaze-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,26 +0,0 @@
1
- pyglaze/__init__.py,sha256=m6kyaNpwBcP1XYcqrelX2oS3PJuOnElOcRdBa9pEb8c,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=BoW_GDvkwEpn_UYUFPoUfi5E_oCnvv8cU4nZKG7Ph2U,20911
5
- pyglaze/datamodels/waveform.py,sha256=n31DhJHFeBNZ3hHqQUiCGXssm5Dc8wV6tGPkhmFYB4Q,5809
6
- pyglaze/device/__init__.py,sha256=5RjCHuFKMi9g2KLUkxixO9hNpAgkUBcOURNTuhAdoUk,177
7
- pyglaze/device/ampcom.py,sha256=9smeYg5-3accfYWRg3K2wg1tGfirnWmPxPa4IYSvyRI,17465
8
- pyglaze/device/configuration.py,sha256=gh_eerX8TdXx3LnFxHieJqOpfDfE9cV6Xgm5WYVnvO0,7994
9
- pyglaze/devtools/__init__.py,sha256=9EW20idoaZv_5GuSgDmfpTPjfCZ-Rl27EV3oJebmwnQ,90
10
- pyglaze/devtools/mock_device.py,sha256=Fz0ZCRIslokBz6gFEztv1-V1UJhdTDqujl7-l8seX7U,14394
11
- pyglaze/devtools/thz_pulse.py,sha256=xp-T9psdOrUMtSUFu8HEwQJVu_aMixJdZHtg_BCVu_k,923
12
- pyglaze/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pyglaze/helpers/types.py,sha256=p9xSAP5Trr1FcCWl7ynCWqDOUZKgMQYzMUXSwDpAKHg,599
14
- pyglaze/helpers/utilities.py,sha256=n_x9Tqm305MUorS29O6CoJM8Mi4apo2bsN_odrRaVAw,2423
15
- pyglaze/interpolation/__init__.py,sha256=WCxHPsiI7zvJykp-jfytoEbO4Tla-YIF6A7fjDfcDvU,72
16
- pyglaze/interpolation/interpolation.py,sha256=rQWzPD7W8TXETps7VZI0gcfAOCWO8pGL1HhhBnyxaMw,735
17
- pyglaze/scanning/__init__.py,sha256=uCBaeDTufOrC9KWf30ICqcmvFg_YT85olb3M9jkvZRg,99
18
- pyglaze/scanning/_asyncscanner.py,sha256=blnpdKBieSrUyTqmm1wZtBqp7X66WE-sFae_PpimFNU,5314
19
- pyglaze/scanning/_exceptions.py,sha256=vS28Dijj76jVuF6cSDBKqM9SQIa9rbIyUaHF-RA3PyM,213
20
- pyglaze/scanning/client.py,sha256=3qrQStkeLQzCeu4yMHJ_ENLGQ7E5GMc4CP9J55rk-ug,1817
21
- pyglaze/scanning/scanner.py,sha256=5KYVUboK4f8DWp-qRHWbvdtQ0x46AIdnCkquAO4vQOI,9205
22
- pyglaze-0.2.2.dist-info/LICENSE,sha256=LCP3sGBX7LxuQopcjeug1fW4tngWCHF4zB7QCgB28xM,1504
23
- pyglaze-0.2.2.dist-info/METADATA,sha256=fZcMCJSLAWa1ZrZDV6CPQ0jdGjhQLt5jBJ9srEgYjQM,3498
24
- pyglaze-0.2.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
25
- pyglaze-0.2.2.dist-info/top_level.txt,sha256=X7d5rqVVuWNmtK4-Uh4sgOLlqye8vaHZOr5RYba0REo,8
26
- pyglaze-0.2.2.dist-info/RECORD,,
File without changes