dls-dodal 1.51.0__py3-none-any.whl → 1.52.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.
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/METADATA +3 -2
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/RECORD +45 -41
- dodal/_version.py +2 -2
- dodal/beamlines/b07.py +7 -6
- dodal/beamlines/b07_1.py +7 -6
- dodal/beamlines/i03.py +4 -1
- dodal/beamlines/i04.py +2 -2
- dodal/beamlines/i09.py +7 -5
- dodal/beamlines/i09_1.py +6 -3
- dodal/beamlines/i09_2.py +2 -2
- dodal/beamlines/i22.py +11 -0
- dodal/beamlines/i24.py +2 -2
- dodal/beamlines/p60.py +6 -4
- dodal/beamlines/p99.py +14 -0
- dodal/common/device_utils.py +45 -0
- dodal/devices/attenuator/attenuator.py +5 -3
- dodal/devices/b07/__init__.py +2 -2
- dodal/devices/b07/enums.py +24 -0
- dodal/devices/b07_1/__init__.py +2 -2
- dodal/devices/b07_1/enums.py +18 -0
- dodal/devices/electron_analyser/abstract/__init__.py +4 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +21 -4
- dodal/devices/electron_analyser/abstract/base_region.py +20 -7
- dodal/devices/electron_analyser/detector.py +1 -1
- dodal/devices/electron_analyser/specs/detector.py +18 -4
- dodal/devices/electron_analyser/specs/driver_io.py +17 -5
- dodal/devices/electron_analyser/specs/region.py +9 -5
- dodal/devices/electron_analyser/types.py +21 -5
- dodal/devices/electron_analyser/vgscienta/detector.py +16 -7
- dodal/devices/electron_analyser/vgscienta/driver_io.py +13 -4
- dodal/devices/electron_analyser/vgscienta/region.py +11 -5
- dodal/devices/i09/__init__.py +2 -2
- dodal/devices/i09/enums.py +15 -0
- dodal/devices/i09_1/__init__.py +3 -0
- dodal/devices/i09_1/enums.py +19 -0
- dodal/devices/linkam3.py +25 -81
- dodal/devices/oav/pin_image_recognition/__init__.py +11 -14
- dodal/devices/p60/__init__.py +3 -2
- dodal/devices/p60/enums.py +10 -0
- dodal/devices/tetramm.py +134 -150
- dodal/devices/xbpm_feedback.py +6 -3
- dodal/devices/b07/grating.py +0 -9
- dodal/devices/b07_1/grating.py +0 -10
- dodal/devices/i09/grating.py +0 -7
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LensMode(StrictEnum):
|
|
5
|
+
TRANSMISSION = "Transmission"
|
|
6
|
+
ANGULAR14 = "Angular14"
|
|
7
|
+
ANGULAR7NF = "Angular7NF"
|
|
8
|
+
ANGULAR30 = "Angular30"
|
|
9
|
+
ANGULAR30_SMALLSPOT = "Angular30_SmallSpot"
|
|
10
|
+
ANGULAR14_SMALLSPOT = "Angular14_SmallSpot"
|
dodal/devices/tetramm.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Annotated as A
|
|
2
4
|
|
|
3
|
-
from bluesky.protocols import Hints
|
|
4
5
|
from ophyd_async.core import (
|
|
6
|
+
DEFAULT_TIMEOUT,
|
|
7
|
+
AsyncStatus,
|
|
5
8
|
DatasetDescriber,
|
|
6
9
|
DetectorController,
|
|
7
10
|
DetectorTrigger,
|
|
8
|
-
Device,
|
|
9
11
|
PathProvider,
|
|
12
|
+
SignalR,
|
|
13
|
+
SignalRW,
|
|
10
14
|
StandardDetector,
|
|
11
15
|
StrictEnum,
|
|
12
16
|
TriggerInfo,
|
|
@@ -15,15 +19,12 @@ from ophyd_async.core import (
|
|
|
15
19
|
)
|
|
16
20
|
from ophyd_async.epics.adcore import (
|
|
17
21
|
ADHDFWriter,
|
|
22
|
+
NDArrayBaseIO,
|
|
18
23
|
NDFileHDFIO,
|
|
19
24
|
NDPluginBaseIO,
|
|
20
25
|
stop_busy_record,
|
|
21
26
|
)
|
|
22
|
-
from ophyd_async.epics.core import
|
|
23
|
-
epics_signal_r,
|
|
24
|
-
epics_signal_rw,
|
|
25
|
-
epics_signal_rw_rbv,
|
|
26
|
-
)
|
|
27
|
+
from ophyd_async.epics.core import PvSuffix
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class TetrammRange(StrictEnum):
|
|
@@ -54,208 +55,191 @@ class TetrammGeometry(StrictEnum):
|
|
|
54
55
|
SQUARE = "Square"
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
class TetrammDriver(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
|
|
73
|
-
|
|
74
|
-
# this PV is special, for some reason it doesn't have a _RBV suffix...
|
|
75
|
-
self.overflows = epics_signal_r(int, prefix + "RingOverflows")
|
|
76
|
-
|
|
77
|
-
self.num_channels = epics_signal_rw_rbv(TetrammChannels, prefix + "NumChannels")
|
|
78
|
-
self.resolution = epics_signal_rw_rbv(TetrammResolution, prefix + "Resolution")
|
|
79
|
-
self.trigger_mode = epics_signal_rw_rbv(TetrammTrigger, prefix + "TriggerMode")
|
|
80
|
-
self.bias = epics_signal_rw_rbv(bool, prefix + "BiasState")
|
|
81
|
-
self.bias_volts = epics_signal_rw_rbv(float, prefix + "BiasVoltage")
|
|
82
|
-
self.geometry = epics_signal_rw_rbv(TetrammGeometry, prefix + "Geometry")
|
|
83
|
-
self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
|
|
84
|
-
|
|
85
|
-
super().__init__(name=name)
|
|
58
|
+
class TetrammDriver(NDArrayBaseIO):
|
|
59
|
+
range = A[SignalRW[TetrammRange], PvSuffix.rbv("Range")]
|
|
60
|
+
sample_time: A[SignalR[float], PvSuffix("SampleTime_RBV")]
|
|
61
|
+
values_per_reading: A[SignalRW[int], PvSuffix.rbv("ValuesPerRead")]
|
|
62
|
+
averaging_time: A[SignalRW[float], PvSuffix.rbv("AveragingTime")]
|
|
63
|
+
to_average: A[SignalR[int], PvSuffix("NumAverage_RBV")]
|
|
64
|
+
averaged: A[SignalR[int], PvSuffix("NumAveraged_RBV")]
|
|
65
|
+
overflows: A[SignalR[int], PvSuffix("RingOverflows")]
|
|
66
|
+
num_channels: A[SignalRW[TetrammChannels], PvSuffix.rbv("NumChannels")]
|
|
67
|
+
resolution: A[SignalRW[TetrammResolution], PvSuffix.rbv("Resolution")]
|
|
68
|
+
trigger_mode: A[SignalRW[TetrammTrigger], PvSuffix.rbv("TriggerMode")]
|
|
69
|
+
bias: A[SignalRW[bool], PvSuffix.rbv("BiasState")]
|
|
70
|
+
bias_volts: A[SignalRW[float], PvSuffix.rbv("BiasVoltage")]
|
|
71
|
+
geometry: A[SignalRW[TetrammGeometry], PvSuffix.rbv("Geometry")]
|
|
72
|
+
read_format: A[SignalRW[bool], PvSuffix.rbv("ReadFormat")]
|
|
86
73
|
|
|
87
74
|
|
|
88
75
|
class TetrammController(DetectorController):
|
|
89
|
-
"""Controller for a TetrAMM current monitor
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
readings_per_frame (int): Actual number of readings per frame.
|
|
99
|
-
|
|
76
|
+
"""Controller for a TetrAMM current monitor"""
|
|
77
|
+
|
|
78
|
+
_supported_trigger_types = {
|
|
79
|
+
DetectorTrigger.EDGE_TRIGGER: TetrammTrigger.EXT_TRIGGER,
|
|
80
|
+
DetectorTrigger.CONSTANT_GATE: TetrammTrigger.EXT_TRIGGER,
|
|
81
|
+
}
|
|
82
|
+
""""On the TetrAMM ASCII mode requires a minimum value of ValuesPerRead of 500,
|
|
83
|
+
[...] binary mode the minimum value of ValuesPerRead is 5."
|
|
84
|
+
https://millenia.cars.aps.anl.gov/software/epics/quadEMDoc.html
|
|
100
85
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
_minimal_values_per_reading = {0: 5, 1: 500}
|
|
87
|
+
"""The TetrAMM always digitizes at 100 kHz"""
|
|
88
|
+
_base_sample_rate: int = 100_000
|
|
103
89
|
|
|
104
90
|
def __init__(
|
|
105
91
|
self,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
):
|
|
111
|
-
# TODO: Are any of these also fixed by hardware constraints?
|
|
112
|
-
self._drv = drv
|
|
113
|
-
self.maximum_readings_per_frame = maximum_readings_per_frame
|
|
114
|
-
self.minimum_values_per_reading = minimum_values_per_reading
|
|
115
|
-
self.readings_per_frame = readings_per_frame
|
|
92
|
+
driver: TetrammDriver,
|
|
93
|
+
) -> None:
|
|
94
|
+
self.driver = driver
|
|
95
|
+
self._arm_status: AsyncStatus | None = None
|
|
116
96
|
|
|
117
97
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
118
98
|
# 2 internal clock cycles. Best effort approximation
|
|
119
|
-
return 2 / self.
|
|
99
|
+
return 2 / self._base_sample_rate
|
|
120
100
|
|
|
121
|
-
async def prepare(self, trigger_info: TriggerInfo):
|
|
122
|
-
|
|
123
|
-
|
|
101
|
+
async def prepare(self, trigger_info: TriggerInfo) -> None:
|
|
102
|
+
if trigger_info.trigger not in self._supported_trigger_types:
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"{self.__class__.__name__} only supports the following trigger "
|
|
105
|
+
f"types: {[k.name for k in self._supported_trigger_types]} but was asked to "
|
|
106
|
+
f"use {trigger_info.trigger}"
|
|
107
|
+
)
|
|
108
|
+
if trigger_info.livetime is None:
|
|
109
|
+
raise ValueError(f"{self.__class__.__name__} requires that livetime is set")
|
|
124
110
|
|
|
125
111
|
# trigger mode must be set first and on its own!
|
|
126
|
-
await self.
|
|
127
|
-
|
|
112
|
+
await self.driver.trigger_mode.set(
|
|
113
|
+
self._supported_trigger_types[trigger_info.trigger]
|
|
114
|
+
)
|
|
128
115
|
await asyncio.gather(
|
|
129
|
-
self.
|
|
116
|
+
self.driver.averaging_time.set(trigger_info.livetime),
|
|
130
117
|
self.set_exposure(trigger_info.livetime),
|
|
131
118
|
)
|
|
132
119
|
|
|
133
120
|
async def arm(self):
|
|
134
|
-
self._arm_status = await
|
|
135
|
-
self._drv.acquire, True, wait_for_set_completion=False
|
|
136
|
-
)
|
|
121
|
+
self._arm_status = await self.start_acquiring_driver_and_ensure_status()
|
|
137
122
|
|
|
138
123
|
async def wait_for_idle(self):
|
|
139
124
|
if self._arm_status and not self._arm_status.done:
|
|
140
125
|
await self._arm_status
|
|
141
126
|
self._arm_status = None
|
|
142
127
|
|
|
143
|
-
def _validate_trigger(self, trigger: DetectorTrigger) -> None:
|
|
144
|
-
supported_trigger_types = {
|
|
145
|
-
DetectorTrigger.EDGE_TRIGGER,
|
|
146
|
-
DetectorTrigger.CONSTANT_GATE,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if trigger not in supported_trigger_types:
|
|
150
|
-
raise ValueError(
|
|
151
|
-
f"{self.__class__.__name__} only supports the following trigger "
|
|
152
|
-
f"types: {supported_trigger_types} but was asked to "
|
|
153
|
-
f"use {trigger}"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
128
|
async def disarm(self):
|
|
157
|
-
|
|
129
|
+
# We can't use caput callback as we already used it in arm() and we can't have
|
|
130
|
+
# 2 or they will deadlock
|
|
131
|
+
await stop_busy_record(self.driver.acquire, False, timeout=1)
|
|
158
132
|
|
|
159
|
-
async def set_exposure(self, exposure: float):
|
|
160
|
-
"""
|
|
133
|
+
async def set_exposure(self, exposure: float) -> None:
|
|
134
|
+
"""Set the exposure time and acquire period.
|
|
161
135
|
|
|
162
136
|
As during the exposure time, the device must collect an integer number
|
|
163
137
|
of readings, in the case where the exposure is not a multiple of the base
|
|
164
138
|
sample rate, it will be lowered to the prior multiple ot ensure triggers
|
|
165
139
|
are not missed.
|
|
166
140
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Raises:
|
|
171
|
-
ValueError: If exposure is too low to collect the required number
|
|
172
|
-
of readings per frame.
|
|
141
|
+
:param exposure: Desired exposure time.
|
|
142
|
+
:type exposure: How long to wait for the exposure time and acquire
|
|
143
|
+
period to be set.
|
|
173
144
|
"""
|
|
145
|
+
sample_time = await self.driver.sample_time.get_value()
|
|
146
|
+
minimum_samples = self._minimal_values_per_reading[
|
|
147
|
+
await self.driver.read_format.get_value()
|
|
148
|
+
]
|
|
149
|
+
samples_per_reading = int(exposure / sample_time)
|
|
150
|
+
if samples_per_reading < minimum_samples:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"Tetramm exposure time must be at least "
|
|
153
|
+
f"{minimum_samples * sample_time}s, asked to set it to {exposure}s"
|
|
154
|
+
)
|
|
155
|
+
await self.driver.averaging_time.set(samples_per_reading * sample_time)
|
|
174
156
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
self._set_minimum_exposure(exposure)
|
|
178
|
-
values_per_reading: int = int(
|
|
179
|
-
exposure * self.base_sample_rate / self.readings_per_frame
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
await self._drv.values_per_reading.set(values_per_reading)
|
|
157
|
+
async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
|
|
158
|
+
"""Start acquiring driver, raising ValueError if the detector is in a bad state.
|
|
183
159
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return 1 / self.minimum_exposure
|
|
160
|
+
This sets driver.acquire to True, and waits for it to be True up to a timeout.
|
|
161
|
+
Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES,
|
|
162
|
+
and otherwise raises a ValueError.
|
|
188
163
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
164
|
+
:returns AsyncStatus:
|
|
165
|
+
An AsyncStatus that can be awaited to set driver.acquire to True and perform
|
|
166
|
+
subsequent raising (if applicable) due to detector state.
|
|
167
|
+
"""
|
|
168
|
+
status = await set_and_wait_for_value(
|
|
169
|
+
self.driver.acquire,
|
|
170
|
+
True,
|
|
171
|
+
timeout=DEFAULT_TIMEOUT,
|
|
172
|
+
wait_for_set_completion=False,
|
|
173
|
+
)
|
|
192
174
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return self.readings_per_frame * time_per_reading
|
|
175
|
+
async def complete_acquisition() -> None:
|
|
176
|
+
# NOTE: possible race condition here between the callback from
|
|
177
|
+
# set_and_wait_for_value and the detector state updating.
|
|
178
|
+
await status
|
|
198
179
|
|
|
199
|
-
|
|
200
|
-
time_per_reading = self.minimum_values_per_reading / self.base_sample_rate
|
|
201
|
-
if exposure < time_per_reading:
|
|
202
|
-
raise ValueError(
|
|
203
|
-
"Tetramm exposure time must be at least "
|
|
204
|
-
f"{time_per_reading}s, asked to set it to {exposure}s"
|
|
205
|
-
)
|
|
206
|
-
self.readings_per_frame = int(
|
|
207
|
-
min(self.maximum_readings_per_frame, exposure / time_per_reading)
|
|
208
|
-
)
|
|
180
|
+
return AsyncStatus(complete_acquisition())
|
|
209
181
|
|
|
210
182
|
|
|
211
183
|
class TetrammDatasetDescriber(DatasetDescriber):
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def __init__(self, controller: TetrammController) -> None:
|
|
215
|
-
self.controller = controller
|
|
184
|
+
def __init__(self, driver: TetrammDriver) -> None:
|
|
185
|
+
self._driver = driver
|
|
216
186
|
|
|
217
187
|
async def np_datatype(self) -> str:
|
|
218
188
|
return "<f8" # IEEE 754 double precision floating point
|
|
219
189
|
|
|
220
190
|
async def shape(self) -> tuple[int, int]:
|
|
221
|
-
return (
|
|
191
|
+
return (
|
|
192
|
+
int(await self._driver.num_channels.get_value()),
|
|
193
|
+
int(
|
|
194
|
+
await self._driver.averaging_time.get_value()
|
|
195
|
+
/ await self._driver.sample_time.get_value(),
|
|
196
|
+
),
|
|
197
|
+
)
|
|
222
198
|
|
|
223
199
|
|
|
224
|
-
# TODO: Support MeanValue signals https://github.com/DiamondLightSource/dodal/issues/337
|
|
225
200
|
class TetrammDetector(StandardDetector):
|
|
226
201
|
def __init__(
|
|
227
202
|
self,
|
|
228
203
|
prefix: str,
|
|
229
204
|
path_provider: PathProvider,
|
|
205
|
+
drv_suffix: str = "DRV:",
|
|
206
|
+
fileio_suffix: str = "HDF5:",
|
|
230
207
|
name: str = "",
|
|
231
|
-
type: str | None = None,
|
|
232
208
|
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
209
|
+
config_sigs: Sequence[SignalR] = (),
|
|
210
|
+
type: str | None = None,
|
|
211
|
+
):
|
|
212
|
+
self.driver = TetrammDriver(prefix + drv_suffix)
|
|
213
|
+
self.file_io = NDFileHDFIO(prefix + fileio_suffix)
|
|
214
|
+
controller = TetrammController(self.driver)
|
|
215
|
+
|
|
216
|
+
writer = ADHDFWriter(
|
|
217
|
+
fileio=self.file_io,
|
|
218
|
+
path_provider=path_provider,
|
|
219
|
+
dataset_describer=TetrammDatasetDescriber(self.driver),
|
|
220
|
+
plugins=plugins,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
config_sigs = [
|
|
224
|
+
self.driver.values_per_reading,
|
|
225
|
+
self.driver.averaging_time,
|
|
226
|
+
self.driver.sample_time,
|
|
227
|
+
*config_sigs,
|
|
241
228
|
]
|
|
229
|
+
|
|
242
230
|
if type:
|
|
243
231
|
self.type, _ = soft_signal_r_and_setter(str, type)
|
|
244
|
-
|
|
232
|
+
config_sigs.append(self.type)
|
|
245
233
|
else:
|
|
246
234
|
self.type = None
|
|
235
|
+
|
|
236
|
+
if plugins is not None:
|
|
237
|
+
for plugin_name, plugin in plugins.items():
|
|
238
|
+
setattr(self, plugin_name, plugin)
|
|
239
|
+
|
|
247
240
|
super().__init__(
|
|
248
|
-
controller,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
dataset_describer=TetrammDatasetDescriber(controller),
|
|
253
|
-
plugins=plugins,
|
|
254
|
-
),
|
|
255
|
-
config_signals,
|
|
256
|
-
name,
|
|
241
|
+
controller=controller,
|
|
242
|
+
writer=writer,
|
|
243
|
+
name=name,
|
|
244
|
+
config_sigs=config_sigs,
|
|
257
245
|
)
|
|
258
|
-
|
|
259
|
-
@property
|
|
260
|
-
def hints(self) -> Hints:
|
|
261
|
-
return {"fields": [self.name]}
|
dodal/devices/xbpm_feedback.py
CHANGED
|
@@ -2,6 +2,8 @@ from bluesky.protocols import Triggerable
|
|
|
2
2
|
from ophyd_async.core import AsyncStatus, Device, StrictEnum, observe_value
|
|
3
3
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
4
4
|
|
|
5
|
+
from dodal.common.device_utils import periodic_reminder
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class Pause(StrictEnum):
|
|
7
9
|
PAUSE = "Paused" # 0
|
|
@@ -22,6 +24,7 @@ class XBPMFeedback(Device, Triggerable):
|
|
|
22
24
|
|
|
23
25
|
@AsyncStatus.wrap
|
|
24
26
|
async def trigger(self):
|
|
25
|
-
async
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
async with periodic_reminder("Waiting for XBPM"):
|
|
28
|
+
async for value in observe_value(self.pos_stable):
|
|
29
|
+
if value:
|
|
30
|
+
return
|
dodal/devices/b07/grating.py
DELETED
dodal/devices/b07_1/grating.py
DELETED
dodal/devices/i09/grating.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|