Skip to content

Commit dfd2ead

Browse files
Villtordeir17846
andauthored
Add hard undulator energy and hard energy classes (#1713)
* add classes * add beamline objects * remove lut object parameter for now * add energy_readback and tests * fix readables in hardEnergy class * add hard energy tests * remove lut from i09_1 * rename module * fix typo * and reverse function for energy from gap calculation and update tests * fix tests * add test * test * test2 * test_win_laptop_commit * implement review suggestions * mainly remove names parameter from devices, change undulator PV * added issue for LUT --------- Co-authored-by: eir17846 <[email protected]>
1 parent 55d8581 commit dfd2ead

File tree

4 files changed

+389
-3
lines changed

4 files changed

+389
-3
lines changed

src/dodal/beamlines/i09_1.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
from dodal.devices.electron_analyser import EnergySource
1111
from dodal.devices.electron_analyser.specs import SpecsDetector
1212
from dodal.devices.i09_1 import LensMode, PsuMode
13+
from dodal.devices.i09_1_shared.hard_energy import HardEnergy, HardInsertionDeviceEnergy
14+
from dodal.devices.i09_1_shared.hard_undulator_functions import (
15+
calculate_energy_i09_hu,
16+
calculate_gap_i09_hu,
17+
)
1318
from dodal.devices.synchrotron import Synchrotron
1419
from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
1520
from dodal.log import set_beamline as set_log_beamline
@@ -52,9 +57,28 @@ def analyser() -> SpecsDetector[LensMode, PsuMode]:
5257

5358
@device_factory()
5459
def undulator() -> UndulatorInMm:
55-
return UndulatorInMm(prefix=f"{PREFIX.beamline_prefix}-MO-UND-01:")
60+
return UndulatorInMm(prefix=f"{BeamlinePrefix(BL).insertion_prefix}-MO-SERVC-01:")
5661

5762

5863
@device_factory()
5964
def harmonics() -> UndulatorOrder:
60-
return UndulatorOrder(name="harmonics")
65+
return UndulatorOrder()
66+
67+
68+
@device_factory()
69+
def hu_id_energy() -> HardInsertionDeviceEnergy:
70+
return HardInsertionDeviceEnergy(
71+
undulator_order=harmonics(),
72+
undulator=undulator(),
73+
lut={}, # ToDo https://github.com/DiamondLightSource/sm-bluesky/issues/239
74+
gap_to_energy_func=calculate_energy_i09_hu,
75+
energy_to_gap_func=calculate_gap_i09_hu,
76+
)
77+
78+
79+
@device_factory()
80+
def hu_energy() -> HardEnergy:
81+
return HardEnergy(
82+
dcm=dcm(),
83+
undulator_energy=hu_id_energy(),
84+
)
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from .hard_energy import HardEnergy, HardInsertionDeviceEnergy
12
from .hard_undulator_functions import (
23
calculate_energy_i09_hu,
34
calculate_gap_i09_hu,
45
get_hu_lut_as_dict,
56
)
67

7-
__all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict", "calculate_energy_i09_hu"]
8+
__all__ = [
9+
"calculate_gap_i09_hu",
10+
"get_hu_lut_as_dict",
11+
"calculate_energy_i09_hu",
12+
"HardInsertionDeviceEnergy",
13+
"HardEnergy",
14+
]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from asyncio import gather
2+
from collections.abc import Callable
3+
4+
from bluesky.protocols import Locatable, Location, Movable
5+
from numpy import ndarray
6+
from ophyd_async.core import (
7+
AsyncStatus,
8+
Reference,
9+
StandardReadable,
10+
StandardReadableFormat,
11+
derived_signal_rw,
12+
soft_signal_rw,
13+
)
14+
15+
from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
16+
from dodal.devices.i09_1_shared.hard_undulator_functions import (
17+
MAX_ENERGY_COLUMN,
18+
MIN_ENERGY_COLUMN,
19+
)
20+
from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
21+
22+
23+
class HardInsertionDeviceEnergy(StandardReadable, Movable[float]):
24+
"""
25+
Compound device to link hard x-ray undulator gap and order to photon energy.
26+
Setting the energy adjusts the undulator gap accordingly.
27+
"""
28+
29+
def __init__(
30+
self,
31+
undulator_order: UndulatorOrder,
32+
undulator: UndulatorInMm,
33+
lut: dict[int, ndarray],
34+
gap_to_energy_func: Callable[..., float],
35+
energy_to_gap_func: Callable[..., float],
36+
name: str = "",
37+
) -> None:
38+
self._lut = lut
39+
self.gap_to_energy_func = gap_to_energy_func
40+
self.energy_to_gap_func = energy_to_gap_func
41+
self._undulator_order_ref = Reference(undulator_order)
42+
self._undulator_ref = Reference(undulator)
43+
44+
self.add_readables([undulator_order, undulator.current_gap])
45+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
46+
self.energy_demand = soft_signal_rw(float)
47+
self.energy = derived_signal_rw(
48+
raw_to_derived=self._read_energy,
49+
set_derived=self._set_energy,
50+
current_gap=self._undulator_ref().gap_motor.user_readback,
51+
current_order=self._undulator_order_ref().value,
52+
derived_units="keV",
53+
)
54+
super().__init__(name=name)
55+
56+
def _read_energy(self, current_gap: float, current_order: int) -> float:
57+
return self.gap_to_energy_func(
58+
gap=current_gap,
59+
look_up_table=self._lut,
60+
order=current_order,
61+
)
62+
63+
async def _set_energy(self, energy: float) -> None:
64+
current_order = await self._undulator_order_ref().value.get_value()
65+
min_energy, max_energy = self._lut[current_order][
66+
MIN_ENERGY_COLUMN : MAX_ENERGY_COLUMN + 1
67+
]
68+
if not (min_energy <= energy <= max_energy):
69+
raise ValueError(
70+
f"Requested energy {energy} keV is out of range for harmonic {current_order}: "
71+
f"[{min_energy}, {max_energy}] keV"
72+
)
73+
74+
target_gap = self.energy_to_gap_func(
75+
photon_energy_kev=energy, look_up_table=self._lut, order=current_order
76+
)
77+
await self._undulator_ref().set(target_gap)
78+
79+
@AsyncStatus.wrap
80+
async def set(self, value: float) -> None:
81+
self.energy_demand.set(value)
82+
await self.energy.set(value)
83+
84+
85+
class HardEnergy(StandardReadable, Locatable[float]):
86+
"""
87+
Energy compound device that provides combined change of both DCM energy and undulator gap accordingly.
88+
"""
89+
90+
def __init__(
91+
self,
92+
dcm: DoubleCrystalMonochromatorBase,
93+
undulator_energy: HardInsertionDeviceEnergy,
94+
name: str = "",
95+
) -> None:
96+
self._dcm_ref = Reference(dcm)
97+
self._undulator_energy_ref = Reference(undulator_energy)
98+
self.add_readables([undulator_energy, dcm.energy_in_keV])
99+
super().__init__(name=name)
100+
101+
@AsyncStatus.wrap
102+
async def set(self, value: float) -> None:
103+
await gather(
104+
self._dcm_ref().energy_in_keV.set(value),
105+
self._undulator_energy_ref().set(value),
106+
)
107+
108+
async def locate(self) -> Location[float]:
109+
return Location(
110+
setpoint=await self._dcm_ref().energy_in_keV.user_setpoint.get_value(),
111+
readback=await self._dcm_ref().energy_in_keV.user_readback.get_value(),
112+
)

0 commit comments

Comments
 (0)