dls-dodal 1.48.0__py3-none-any.whl → 1.50.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.48.0.dist-info → dls_dodal-1.50.0.dist-info}/METADATA +2 -1
- {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/RECORD +41 -30
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +15 -0
- dodal/beamlines/b16.py +65 -0
- dodal/beamlines/b18.py +38 -0
- dodal/beamlines/i10.py +41 -233
- dodal/beamlines/k11.py +35 -0
- dodal/common/beamlines/device_helpers.py +1 -0
- dodal/devices/apple2_undulator.py +257 -136
- dodal/devices/b16/__init__.py +0 -0
- dodal/devices/b16/detector.py +34 -0
- dodal/devices/bimorph_mirror.py +29 -36
- dodal/devices/electron_analyser/__init__.py +12 -2
- dodal/devices/electron_analyser/abstract/base_detector.py +3 -128
- dodal/devices/electron_analyser/abstract/base_driver_io.py +8 -3
- dodal/devices/electron_analyser/abstract/base_region.py +6 -3
- dodal/devices/electron_analyser/detector.py +141 -0
- dodal/devices/electron_analyser/enums.py +6 -0
- dodal/devices/electron_analyser/specs/__init__.py +2 -0
- dodal/devices/electron_analyser/specs/detector.py +1 -1
- dodal/devices/electron_analyser/specs/driver_io.py +4 -5
- dodal/devices/electron_analyser/specs/enums.py +8 -0
- dodal/devices/electron_analyser/specs/region.py +3 -2
- dodal/devices/electron_analyser/types.py +30 -4
- dodal/devices/electron_analyser/util.py +1 -1
- dodal/devices/electron_analyser/vgscienta/__init__.py +2 -0
- dodal/devices/electron_analyser/vgscienta/detector.py +1 -1
- dodal/devices/electron_analyser/vgscienta/driver_io.py +2 -1
- dodal/devices/electron_analyser/vgscienta/enums.py +19 -0
- dodal/devices/electron_analyser/vgscienta/region.py +7 -22
- dodal/devices/hutch_shutter.py +6 -6
- dodal/devices/i10/__init__.py +0 -0
- dodal/devices/i10/i10_apple2.py +181 -126
- dodal/devices/i22/nxsas.py +1 -1
- dodal/devices/oav/oav_detector.py +45 -7
- dodal/plans/bimorph.py +333 -0
- {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from .detector import VGScientaDetector
|
|
2
2
|
from .driver_io import VGScientaAnalyserDriverIO
|
|
3
|
+
from .enums import AcquisitionMode
|
|
3
4
|
from .region import VGScientaExcitationEnergySource, VGScientaRegion, VGScientaSequence
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
"VGScientaDetector",
|
|
7
8
|
"VGScientaAnalyserDriverIO",
|
|
9
|
+
"AcquisitionMode",
|
|
8
10
|
"VGScientaExcitationEnergySource",
|
|
9
11
|
"VGScientaRegion",
|
|
10
12
|
"VGScientaSequence",
|
|
@@ -14,6 +14,7 @@ from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
|
14
14
|
AbstractAnalyserDriverIO,
|
|
15
15
|
)
|
|
16
16
|
from dodal.devices.electron_analyser.util import to_kinetic_energy
|
|
17
|
+
from dodal.devices.electron_analyser.vgscienta.enums import AcquisitionMode
|
|
17
18
|
from dodal.devices.electron_analyser.vgscienta.region import (
|
|
18
19
|
DetectorMode,
|
|
19
20
|
VGScientaRegion,
|
|
@@ -35,7 +36,7 @@ class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaRegion]):
|
|
|
35
36
|
# Used to read detector data after acqusition.
|
|
36
37
|
self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
|
|
37
38
|
|
|
38
|
-
super().__init__(prefix, name)
|
|
39
|
+
super().__init__(prefix, AcquisitionMode, name)
|
|
39
40
|
|
|
40
41
|
@AsyncStatus.wrap
|
|
41
42
|
async def set(self, region: VGScientaRegion):
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Status(StrictEnum):
|
|
5
|
+
READY = "Ready"
|
|
6
|
+
RUNNING = "Running"
|
|
7
|
+
COMPLETED = "Completed"
|
|
8
|
+
INVALID = "Invalid"
|
|
9
|
+
ABORTED = "Aborted"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectorMode(StrictEnum):
|
|
13
|
+
ADC = "ADC"
|
|
14
|
+
PULSE_COUNTING = "Pulse Counting"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AcquisitionMode(StrictEnum):
|
|
18
|
+
SWEPT = "Swept"
|
|
19
|
+
FIXED = "Fixed"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from enum import Enum
|
|
3
2
|
|
|
4
|
-
from ophyd_async.core import StrictEnum
|
|
5
3
|
from pydantic import Field
|
|
6
4
|
|
|
7
5
|
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
@@ -9,31 +7,18 @@ from dodal.devices.electron_analyser.abstract.base_region import (
|
|
|
9
7
|
AbstractBaseSequence,
|
|
10
8
|
JavaToPythonModel,
|
|
11
9
|
)
|
|
10
|
+
from dodal.devices.electron_analyser.vgscienta.enums import (
|
|
11
|
+
AcquisitionMode,
|
|
12
|
+
DetectorMode,
|
|
13
|
+
Status,
|
|
14
|
+
)
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
class
|
|
15
|
-
READY = "Ready"
|
|
16
|
-
RUNNING = "Running"
|
|
17
|
-
COMPLETED = "Completed"
|
|
18
|
-
INVALID = "Invalid"
|
|
19
|
-
ABORTED = "Aborted"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class DetectorMode(StrictEnum):
|
|
23
|
-
ADC = "ADC"
|
|
24
|
-
PULSE_COUNTING = "Pulse Counting"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class AcquisitionMode(str, Enum):
|
|
28
|
-
SWEPT = "Swept"
|
|
29
|
-
FIXED = "Fixed"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class VGScientaRegion(AbstractBaseRegion):
|
|
17
|
+
class VGScientaRegion(AbstractBaseRegion[AcquisitionMode]):
|
|
33
18
|
# Override defaults of base region class
|
|
34
19
|
lens_mode: str = "Angular45"
|
|
35
20
|
pass_energy: int = 5
|
|
36
|
-
acquisition_mode:
|
|
21
|
+
acquisition_mode: AcquisitionMode = AcquisitionMode.SWEPT
|
|
37
22
|
low_energy: float = 8.0
|
|
38
23
|
high_energy: float = 10.0
|
|
39
24
|
step_time: float = 1.0
|
dodal/devices/hutch_shutter.py
CHANGED
|
@@ -81,14 +81,14 @@ class HutchShutter(StandardReadable, Movable[ShutterDemand]):
|
|
|
81
81
|
|
|
82
82
|
@AsyncStatus.wrap
|
|
83
83
|
async def set(self, value: ShutterDemand):
|
|
84
|
-
interlock_state = await self.interlock.shutter_safe_to_operate()
|
|
85
|
-
if not interlock_state and not TEST_MODE:
|
|
86
|
-
# If not in test mode, fail. If in test mode, the optics hutch may be open.
|
|
87
|
-
raise ShutterNotSafeToOperateError(
|
|
88
|
-
"The hutch has not been locked, not operating shutter."
|
|
89
|
-
)
|
|
90
84
|
if not TEST_MODE:
|
|
91
85
|
if value == ShutterDemand.OPEN:
|
|
86
|
+
interlock_state = await self.interlock.shutter_safe_to_operate()
|
|
87
|
+
if not interlock_state:
|
|
88
|
+
# If not in test mode, fail. If in test mode, the optics hutch may be open.
|
|
89
|
+
raise ShutterNotSafeToOperateError(
|
|
90
|
+
"The hutch has not been locked, not operating shutter."
|
|
91
|
+
)
|
|
92
92
|
await self.control.set(ShutterDemand.RESET, wait=True)
|
|
93
93
|
await self.control.set(value, wait=True)
|
|
94
94
|
return await wait_for_value(
|
|
File without changes
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -8,6 +8,7 @@ import numpy as np
|
|
|
8
8
|
from bluesky.protocols import Movable
|
|
9
9
|
from ophyd_async.core import (
|
|
10
10
|
AsyncStatus,
|
|
11
|
+
Device,
|
|
11
12
|
Reference,
|
|
12
13
|
StandardReadable,
|
|
13
14
|
StandardReadableFormat,
|
|
@@ -15,16 +16,18 @@ from ophyd_async.core import (
|
|
|
15
16
|
soft_signal_rw,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from dodal.
|
|
19
|
+
from dodal.log import LOGGER
|
|
20
|
+
|
|
21
|
+
from ..apple2_undulator import (
|
|
19
22
|
Apple2,
|
|
20
23
|
Apple2Val,
|
|
21
24
|
Lookuptable,
|
|
25
|
+
Pol,
|
|
22
26
|
UndulatorGap,
|
|
23
27
|
UndulatorJawPhase,
|
|
24
28
|
UndulatorPhaseAxes,
|
|
25
29
|
)
|
|
26
|
-
from
|
|
27
|
-
from dodal.log import LOGGER
|
|
30
|
+
from ..pgm import PGM
|
|
28
31
|
|
|
29
32
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
30
33
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -51,22 +54,20 @@ class LookupTableConfig:
|
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
class I10Apple2(Apple2):
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
"""I10Apple2 is the i10 version of Apple2 ID, set and update_lookuptable function
|
|
58
|
+
should be the only part that is I10 specific.
|
|
59
|
+
|
|
60
|
+
A pair of look up tables are needed to provide the conversion betwApple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
|
|
61
|
+
each bank of magnet can move independently to each other,
|
|
62
|
+
which allow the production of different x-ray polarisation as well as energy.
|
|
63
|
+
This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.een motor position and energy.
|
|
57
64
|
|
|
58
|
-
A pair of look up tables are needed to provide the conversion
|
|
59
|
-
between motor position and energy.
|
|
60
65
|
Set is in energy(eV).
|
|
61
66
|
"""
|
|
62
67
|
|
|
63
68
|
def __init__(
|
|
64
69
|
self,
|
|
65
|
-
|
|
66
|
-
id_phase: UndulatorPhaseAxes,
|
|
67
|
-
id_jaw_phase: UndulatorJawPhase,
|
|
68
|
-
energy_gap_table_path: Path,
|
|
69
|
-
energy_phase_table_path: Path,
|
|
70
|
+
look_up_table_dir: str,
|
|
70
71
|
source: tuple[str, str],
|
|
71
72
|
prefix: str = "",
|
|
72
73
|
mode: str = "Mode",
|
|
@@ -78,14 +79,8 @@ class I10Apple2(Apple2):
|
|
|
78
79
|
"""
|
|
79
80
|
Parameters
|
|
80
81
|
----------
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
id_phase:
|
|
84
|
-
An UndulatorPhaseAxes device.
|
|
85
|
-
energy_gap_table_path:
|
|
86
|
-
The path to id gap look up table.
|
|
87
|
-
energy_phase_table_path:
|
|
88
|
-
The path to id phase look up table.
|
|
82
|
+
look_up_table_dir:
|
|
83
|
+
The path to look up table.
|
|
89
84
|
source:
|
|
90
85
|
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
91
86
|
mode:
|
|
@@ -97,11 +92,17 @@ class I10Apple2(Apple2):
|
|
|
97
92
|
poly_deg:
|
|
98
93
|
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
99
94
|
prefix:
|
|
100
|
-
|
|
95
|
+
epic pv for id
|
|
101
96
|
Name:
|
|
102
97
|
Name of the device
|
|
103
98
|
"""
|
|
104
99
|
|
|
100
|
+
energy_gap_table_path = Path(
|
|
101
|
+
look_up_table_dir + "IDEnergy2GapCalibrations.csv",
|
|
102
|
+
)
|
|
103
|
+
energy_phase_table_path = Path(
|
|
104
|
+
look_up_table_dir + "IDEnergy2PhaseCalibrations.csv",
|
|
105
|
+
)
|
|
105
106
|
# A dataclass contains the path to the look up table and the expected column names.
|
|
106
107
|
self.lookup_table_config = LookupTableConfig(
|
|
107
108
|
path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
|
|
@@ -112,44 +113,62 @@ class I10Apple2(Apple2):
|
|
|
112
113
|
poly_deg=poly_deg,
|
|
113
114
|
)
|
|
114
115
|
|
|
115
|
-
super().__init__(
|
|
116
|
-
id_gap=id_gap,
|
|
117
|
-
id_phase=id_phase,
|
|
118
|
-
prefix=prefix,
|
|
119
|
-
name=name,
|
|
120
|
-
)
|
|
121
116
|
with self.add_children_as_readables():
|
|
122
|
-
|
|
117
|
+
super().__init__(
|
|
118
|
+
id_gap=UndulatorGap(name="id_gap", prefix=prefix),
|
|
119
|
+
id_phase=UndulatorPhaseAxes(
|
|
120
|
+
name="id_phase",
|
|
121
|
+
prefix=prefix,
|
|
122
|
+
top_outer="RPQ1",
|
|
123
|
+
top_inner="RPQ2",
|
|
124
|
+
btm_inner="RPQ3",
|
|
125
|
+
btm_outer="RPQ4",
|
|
126
|
+
),
|
|
127
|
+
prefix=prefix,
|
|
128
|
+
name=name,
|
|
129
|
+
)
|
|
130
|
+
self.id_jaw_phase = UndulatorJawPhase(
|
|
131
|
+
prefix=prefix,
|
|
132
|
+
move_pv="RPQ1",
|
|
133
|
+
)
|
|
123
134
|
|
|
124
135
|
@AsyncStatus.wrap
|
|
125
|
-
async def set(self, value:
|
|
136
|
+
async def set(self, value: float) -> None:
|
|
126
137
|
"""
|
|
127
138
|
Check polarisation state and use it together with the energy(value)
|
|
128
139
|
to calculate the required gap and phases before setting it.
|
|
129
140
|
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
|
|
142
|
+
pol = await self.polarisation_setpoint.get_value()
|
|
143
|
+
|
|
144
|
+
if pol == Pol.NONE:
|
|
145
|
+
LOGGER.warning(
|
|
146
|
+
"Found no setpoint for polarisation. Attempting to"
|
|
147
|
+
" determine polarisation from hardware..."
|
|
148
|
+
)
|
|
149
|
+
pol = await self.polarisation.get_value()
|
|
150
|
+
if pol == Pol.NONE:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Polarisation cannot be determined from hardware for {self.name}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self._set_pol_setpoint(pol)
|
|
156
|
+
gap, phase = await self._get_id_gap_phase(value)
|
|
157
|
+
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
141
158
|
id_set_val = Apple2Val(
|
|
142
|
-
top_outer=
|
|
159
|
+
top_outer=f"{phase:.6f}",
|
|
143
160
|
top_inner="0.0",
|
|
144
|
-
btm_inner=
|
|
161
|
+
btm_inner=f"{phase3:.6f}",
|
|
145
162
|
btm_outer="0.0",
|
|
146
|
-
gap=
|
|
163
|
+
gap=f"{gap:.6f}",
|
|
147
164
|
)
|
|
148
|
-
|
|
165
|
+
|
|
166
|
+
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
149
167
|
await self._set(value=id_set_val, energy=value)
|
|
150
|
-
if
|
|
151
|
-
await self.id_jaw_phase
|
|
152
|
-
await self.id_jaw_phase
|
|
168
|
+
if pol != Pol.LA:
|
|
169
|
+
await self.id_jaw_phase.set(0)
|
|
170
|
+
await self.id_jaw_phase.set_move.set(1)
|
|
171
|
+
LOGGER.info(f"Energy set to {value} eV successfully.")
|
|
153
172
|
|
|
154
173
|
def update_lookuptable(self):
|
|
155
174
|
"""
|
|
@@ -175,15 +194,13 @@ class I10Apple2(Apple2):
|
|
|
175
194
|
self._available_pol = list(self.lookup_tables["Gap"].keys())
|
|
176
195
|
|
|
177
196
|
|
|
178
|
-
class
|
|
197
|
+
class EnergySetter(StandardReadable, Movable[float]):
|
|
179
198
|
"""
|
|
180
|
-
Compound device to set both ID and PGM energy at the
|
|
199
|
+
Compound device to set both ID and PGM energy at the same time.
|
|
181
200
|
|
|
182
201
|
"""
|
|
183
202
|
|
|
184
|
-
def __init__(
|
|
185
|
-
self, id: I10Apple2, pgm: PGM, prefix: str = "", name: str = ""
|
|
186
|
-
) -> None:
|
|
203
|
+
def __init__(self, id: I10Apple2, pgm: PGM, name: str = "") -> None:
|
|
187
204
|
"""
|
|
188
205
|
Parameters
|
|
189
206
|
----------
|
|
@@ -191,53 +208,52 @@ class I10Apple2PGM(StandardReadable, Movable[float]):
|
|
|
191
208
|
An Apple2 device.
|
|
192
209
|
pgm:
|
|
193
210
|
A PGM/mono device.
|
|
194
|
-
prefix:
|
|
195
|
-
Not in use but needed for device_instantiation.
|
|
196
211
|
name:
|
|
197
212
|
New device name.
|
|
198
213
|
"""
|
|
199
214
|
super().__init__(name=name)
|
|
200
|
-
self.
|
|
215
|
+
self.id = id
|
|
201
216
|
self.pgm_ref = Reference(pgm)
|
|
202
|
-
|
|
217
|
+
|
|
218
|
+
self.add_readables(
|
|
219
|
+
[self.id.energy, self.pgm_ref().energy.user_readback],
|
|
220
|
+
StandardReadableFormat.HINTED_SIGNAL,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
203
224
|
self.energy_offset = soft_signal_rw(float, initial_value=0)
|
|
204
225
|
|
|
205
226
|
@AsyncStatus.wrap
|
|
206
227
|
async def set(self, value: float) -> None:
|
|
207
228
|
LOGGER.info(f"Moving f{self.name} energy to {value}.")
|
|
208
229
|
await asyncio.gather(
|
|
209
|
-
self.
|
|
230
|
+
self.id.set(value=value + await self.energy_offset.get_value()),
|
|
210
231
|
self.pgm_ref().energy.set(value),
|
|
211
232
|
)
|
|
212
233
|
|
|
213
234
|
|
|
214
|
-
class I10Apple2Pol(StandardReadable, Movable[
|
|
235
|
+
class I10Apple2Pol(StandardReadable, Movable[Pol]):
|
|
215
236
|
"""
|
|
216
237
|
Compound device to set polorisation of ID.
|
|
217
238
|
"""
|
|
218
239
|
|
|
219
|
-
def __init__(self, id: I10Apple2,
|
|
240
|
+
def __init__(self, id: I10Apple2, name: str = "") -> None:
|
|
220
241
|
"""
|
|
221
242
|
Parameters
|
|
222
243
|
----------
|
|
223
244
|
id:
|
|
224
245
|
An I10Apple2 device.
|
|
225
|
-
prefix:
|
|
226
|
-
Not in use but needed for device_instantiation.
|
|
227
246
|
name:
|
|
228
247
|
New device name.
|
|
229
248
|
"""
|
|
230
249
|
super().__init__(name=name)
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
self.id_ref = Reference(id)
|
|
251
|
+
self.add_readables([self.id_ref().polarisation])
|
|
233
252
|
|
|
234
253
|
@AsyncStatus.wrap
|
|
235
|
-
async def set(self, value:
|
|
236
|
-
self.id.pol = value # change polarisation.
|
|
254
|
+
async def set(self, value: Pol) -> None:
|
|
237
255
|
LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
|
|
238
|
-
await self.
|
|
239
|
-
await self.id.energy.get_value()
|
|
240
|
-
) # Move id to new polarisation
|
|
256
|
+
await self.id_ref().polarisation.set(value)
|
|
241
257
|
|
|
242
258
|
|
|
243
259
|
class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
@@ -253,7 +269,6 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
253
269
|
def __init__(
|
|
254
270
|
self,
|
|
255
271
|
id: I10Apple2,
|
|
256
|
-
prefix: str = "",
|
|
257
272
|
name: str = "",
|
|
258
273
|
jaw_phase_limit: float = 12.0,
|
|
259
274
|
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
@@ -264,8 +279,6 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
264
279
|
----------
|
|
265
280
|
id: I10Apple2
|
|
266
281
|
An I10Apple2 device.
|
|
267
|
-
prefix: str
|
|
268
|
-
Not in use but needed for device_instantiation.
|
|
269
282
|
name: str
|
|
270
283
|
New device name.
|
|
271
284
|
jaw_phase_limit: float
|
|
@@ -286,8 +299,8 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
286
299
|
@AsyncStatus.wrap
|
|
287
300
|
async def set(self, value: SupportsFloat) -> None:
|
|
288
301
|
value = float(value)
|
|
289
|
-
pol = self.id_ref().
|
|
290
|
-
if pol !=
|
|
302
|
+
pol = await self.id_ref().polarisation.get_value()
|
|
303
|
+
if pol != Pol.LA:
|
|
291
304
|
raise RuntimeError(
|
|
292
305
|
f"Angle control is not available in polarisation {pol} with {self.id_ref().name}"
|
|
293
306
|
)
|
|
@@ -299,10 +312,60 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
299
312
|
f"jaw_phase position for angle ({value}) is outside permitted range"
|
|
300
313
|
f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
|
|
301
314
|
)
|
|
302
|
-
await self.id_ref().id_jaw_phase
|
|
315
|
+
await self.id_ref().id_jaw_phase.set(jaw_phase)
|
|
303
316
|
self._angle_set(value)
|
|
304
317
|
|
|
305
318
|
|
|
319
|
+
class I10Id(Device):
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
pgm: PGM,
|
|
323
|
+
prefix: str,
|
|
324
|
+
look_up_table_dir: str,
|
|
325
|
+
source: tuple[str, str],
|
|
326
|
+
jaw_phase_limit=12.0,
|
|
327
|
+
jaw_phase_poly_param=DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
328
|
+
angle_threshold_deg=30.0,
|
|
329
|
+
name: str = "",
|
|
330
|
+
) -> None:
|
|
331
|
+
"""I10Id is a compound device that combines the I10-specific Apple2 undulator,
|
|
332
|
+
energy setter, and polarization control.
|
|
333
|
+
This class provides a high-level interface for controlling the undulator's
|
|
334
|
+
energy, polarization, and linear arbitrary angle.
|
|
335
|
+
|
|
336
|
+
Attributes
|
|
337
|
+
----------
|
|
338
|
+
id : I10Apple2
|
|
339
|
+
The I10-specific Apple2 undulator device.
|
|
340
|
+
energy_setter : EnergySetter
|
|
341
|
+
A device for synchronizing the undulator and monochromator energy.
|
|
342
|
+
pol : I10Apple2Pol
|
|
343
|
+
A device for controlling the polarization of the undulator.
|
|
344
|
+
linear_arbitrary_angle : LinearArbitraryAngle
|
|
345
|
+
A device for controlling the linear arbitrary polarization angle.
|
|
346
|
+
"""
|
|
347
|
+
self.energy = EnergySetter(
|
|
348
|
+
id=I10Apple2(
|
|
349
|
+
look_up_table_dir=look_up_table_dir,
|
|
350
|
+
name="id_energy",
|
|
351
|
+
source=source,
|
|
352
|
+
prefix=prefix,
|
|
353
|
+
),
|
|
354
|
+
pgm=pgm,
|
|
355
|
+
name="energy",
|
|
356
|
+
)
|
|
357
|
+
self.pol = I10Apple2Pol(id=self.energy.id, name="pol")
|
|
358
|
+
self.laa = LinearArbitraryAngle(
|
|
359
|
+
id=self.energy.id,
|
|
360
|
+
name="laa",
|
|
361
|
+
jaw_phase_limit=jaw_phase_limit,
|
|
362
|
+
jaw_phase_poly_param=jaw_phase_poly_param,
|
|
363
|
+
angle_threshold_deg=angle_threshold_deg,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
super().__init__(name=name)
|
|
367
|
+
|
|
368
|
+
|
|
306
369
|
def convert_csv_to_lookup(
|
|
307
370
|
file: str,
|
|
308
371
|
source: tuple[str, str],
|
|
@@ -312,38 +375,28 @@ def convert_csv_to_lookup(
|
|
|
312
375
|
poly_deg: list | None = None,
|
|
313
376
|
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
314
377
|
"""
|
|
315
|
-
Convert
|
|
378
|
+
Convert a CSV file to a dictionary compatible with the Apple2 lookup table format.
|
|
316
379
|
|
|
317
380
|
Parameters
|
|
318
|
-
|
|
319
|
-
file: str
|
|
320
|
-
|
|
321
|
-
source: tuple[str, str]
|
|
322
|
-
Tuple
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
{mode: {'Energies': {Any: {'Low': float,
|
|
338
|
-
'High': float,
|
|
339
|
-
'Poly':np.poly1d
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
'Limit': {'Minimum': float,
|
|
343
|
-
'Maximum': float
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
381
|
+
----------
|
|
382
|
+
file : str
|
|
383
|
+
Path to the CSV file.
|
|
384
|
+
source : tuple[str, str]
|
|
385
|
+
Tuple specifying the column name and source name (e.g., ("Source", "idu")).
|
|
386
|
+
mode : str, optional
|
|
387
|
+
Column name for the available modes (e.g., "lv", "lh", "pc", "nc"), by default "Mode".
|
|
388
|
+
min_energy : str, optional
|
|
389
|
+
Column name for the minimum energy, by default "MinEnergy".
|
|
390
|
+
max_energy : str, optional
|
|
391
|
+
Column name for the maximum energy, by default "MaxEnergy".
|
|
392
|
+
poly_deg : list, optional
|
|
393
|
+
Column names for polynomial coefficients, starting with the least significant term.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
dict
|
|
398
|
+
A dictionary conforming to the Apple2 lookup table format.
|
|
399
|
+
|
|
347
400
|
"""
|
|
348
401
|
if poly_deg is None:
|
|
349
402
|
poly_deg = [
|
|
@@ -356,15 +409,15 @@ def convert_csv_to_lookup(
|
|
|
356
409
|
"1st-order",
|
|
357
410
|
"b",
|
|
358
411
|
]
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
412
|
+
lookup_table = {}
|
|
413
|
+
polarisations = set()
|
|
414
|
+
|
|
415
|
+
def process_row(row: dict) -> None:
|
|
416
|
+
"""Process a single row from the CSV file and update the lookup table."""
|
|
417
|
+
mode_value = row[mode]
|
|
418
|
+
if mode_value not in polarisations:
|
|
419
|
+
polarisations.add(mode_value)
|
|
420
|
+
lookup_table[mode_value] = {
|
|
368
421
|
"Energies": {},
|
|
369
422
|
"Limit": {
|
|
370
423
|
"Minimum": float(row[min_energy]),
|
|
@@ -372,20 +425,22 @@ def convert_csv_to_lookup(
|
|
|
372
425
|
},
|
|
373
426
|
}
|
|
374
427
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
|
|
428
|
+
# Create polynomial object for energy-to-gap/phase conversion
|
|
429
|
+
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
430
|
+
polynomial = np.poly1d(coefficients)
|
|
378
431
|
|
|
379
|
-
|
|
432
|
+
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
380
433
|
"Low": float(row[min_energy]),
|
|
381
434
|
"High": float(row[max_energy]),
|
|
382
|
-
"Poly":
|
|
435
|
+
"Poly": polynomial,
|
|
383
436
|
}
|
|
384
|
-
|
|
385
|
-
|
|
437
|
+
|
|
438
|
+
# Update energy limits
|
|
439
|
+
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
440
|
+
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
386
441
|
)
|
|
387
|
-
|
|
388
|
-
|
|
442
|
+
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
443
|
+
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
389
444
|
)
|
|
390
445
|
|
|
391
446
|
with open(file, newline="") as csvfile:
|
|
@@ -393,7 +448,7 @@ def convert_csv_to_lookup(
|
|
|
393
448
|
for row in reader:
|
|
394
449
|
# If there are multiple source only convert requested.
|
|
395
450
|
if row[source[0]] == source[1]:
|
|
396
|
-
|
|
397
|
-
if not
|
|
451
|
+
process_row(row=row)
|
|
452
|
+
if not lookup_table:
|
|
398
453
|
raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
|
|
399
|
-
return
|
|
454
|
+
return lookup_table
|