Skip to content

Commit 9ed11d4

Browse files
Create ConfigServerEnergyMotorLookup and decouple gap and phase (#1733)
* add i09 look up table praiser * move get_poly to lookuptable * extracted EnergyMotorLookup base class from i10EnergyMotorLookup * added new make_phase_tables function * add test for correct table output * remove_i09 * add docstring * add test for helper functions * add test for skipping * group and reuse fixture, add J09 controller and tests * there is no reason to set mock value to zero * spacing fix * remove print * add gap tests * add controller, energy and polarisation to i09_2 configuration * move path to top * add model_validation * remove debug * Update docstring for energy_jid function * Update nc parameter and phase calculation logic Change 'nc' parameter to negative ROW_PHASE_CIRCULAR and adjust phase calculation based on pol. * Refactor Lookuptable class documentation Removed outdated docstring from Lookuptable class and updated initialization docstring for I10EnergyMotorLookup class. * update expected lookup table after sign change * change lookup table schema to snake case * replace dictionary with basemodel * add tying * add gap and phase * fat finger correction * Update ID lookup logic to use type checking * Fix some tests * Improve models to not use shared defaults * undo syntax error * Fixed test to check for success on loading i10 lut * Simplified lut logic * Added back generate_lookup_table function * Fixed all tests but polarisation * Updated doc strings * Moved generic test from i10 to test_lookup_table_apple2 * Renamed lut_column_config to lut_config * Updated more doc strings * Fix default phase_file name and thus tests * Use Pol in LookupTable rather than string * Updated test_convert_csv_to_lookup_overwrite_name_convert_default to use Pol * Update tests to use Pol rather str value * fix tests * fix i09 controller and test * reuse fixture * Improve EnergyCoverageEntry to have a poly serializer and EnergyCoverage to use float as key * Add type checking to i10Apple2 phase * Removed commente out code * Update i10 id tests to use json files rather than pickle files so they are human readable * Remove comments * Fixed poly test * Fixed test_make_phase_tables_multiple_entries * Added test_lookup_table_is_serialisable * Update src/dodal/devices/util/lookup_tables_apple2.py Co-authored-by: Raymond Fan <[email protected]> * Fixed formatting * Improved code coverage * Simplified ID lookup table logic * Updated doc strings, tidy tests and variable names * Made default files a constant * added gap and phase lookup * Update logging messages * Removed duplicate test logic * Decoupled path from LookupTableConfig, updated convert_csv_to_lookup to use file_contents again * Updated doc strings * Clean up imports * fixed test * update and reuse fixture * change I09 to use json data * fix controller * add test for Path not given * correct test * typing * modify _setpol to go to lh first for i09 id * Rename class and update comments for clarity * Remove POLY_DEG list from test_i09_apple2.py Removed the POLY_DEG list from the test file. * fix lint * remove I09EnergyMotorLookup and move phase generation into EnergyMotorlookup * move lookup table path into lut_config * revert lookuptable with path * introduce spacing to data * Update src/dodal/devices/util/lookup_tables_apple2.py Co-authored-by: oliwenmandiamond <[email protected]> * Update src/dodal/devices/util/lookup_tables_apple2.py Co-authored-by: oliwenmandiamond <[email protected]> * remove J09defaultlookuptable * remove default in test * lint * update ophyd * Apply suggestions from code review Co-authored-by: oliwenmandiamond <[email protected]> * fix typo * fix file name and test * Create AbstractEnergyMotorLookup * Moved apple2 fixture to dodal.testing.fixtures.devices.apple2 * Fix test * Remove init for DummyEnergyMotorLookup * Removed unneeded pytest plugins * Adjust i17 implementation * Fix tests * Add PhaseAxes type to Apple2 for i17 * Update lookup_tables_apple2 doc strings * Renamed update_lut to update_lookup_table * Further improve type checking for i09 and i17 apple2 * Reworked GeneratePoly1DFromFileEnergyMotorLookup to ConfiguredEnergyMotorLookup. Update i09 to use phase lookup table * Updated variable names for generate_lookup_table to be lut * Restructured controller logic to reuse more components * Updated doc strings and function names * Add missing types to EnergyMotorLookup * Correct return type for process_row * Remove ConfiguredEnergyMotorLookup and changed this to EnergyMotorLookup, removed abstract * Rename _id_set_value to _get_apple2_value * Renamed get_motor_from_energy to find_value_in_lookup_table * Remove available_pol from EnergyMotorLookup * Fix tests * Renamed parameters from feedback * Renamed configured_energy_motor_lookup to energy_motor_lookup * Removed pol arg from _set_apple2, i10 now extends _set_motors_from_energy_and_polarisation instead * Add csv to ConfigServerEnergyMotorLookup doc stirng * Update ConfigServerEnergyMotorLookup __init__ doc string to remove reference to gap * Removed unneeded test setup * Fixed test * Updated docs for Apple2Controller * Removed _set_apple2 as no longer needed --------- Co-authored-by: Relm-Arrowny <[email protected]>
1 parent 75e94dc commit 9ed11d4

19 files changed

+589
-717
lines changed

src/dodal/beamlines/i09_2.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@
1616
)
1717
from dodal.devices.i09.enums import Grating
1818
from dodal.devices.i09_2_shared.i09_apple2 import (
19-
J09_POLY_DEG,
20-
EnergyMotorLookup,
19+
J09_GAP_POLY_DEG_COLUMNS,
20+
J09_PHASE_POLY_DEG_COLUMNS,
2121
J09Apple2Controller,
2222
)
2323
from dodal.devices.pgm import PlaneGratingMonochromator
2424
from dodal.devices.synchrotron import Synchrotron
25-
from dodal.devices.util.lookup_tables_apple2 import LookupTableConfig
25+
from dodal.devices.util.lookup_tables_apple2 import (
26+
ConfigServerEnergyMotorLookup,
27+
LookupTableConfig,
28+
)
2629
from dodal.log import set_beamline as set_log_beamline
2730
from dodal.utils import BeamlinePrefix, get_beamline_name
2831

2932
J09_CONF_CLIENT = ConfigServer(url="https://daq-config.diamond.ac.uk")
3033
LOOK_UPTABLE_DIR = "/dls_sw/i09-2/software/gda/workspace_git/gda-diamond.git/configurations/i09-2-shared/lookupTables/"
3134
GAP_LOOKUP_FILE_NAME = "JIDEnergy2GapCalibrations.csv"
35+
PHASE_LOOKUP_FILE_NAME = "JIDEnergy2PhaseCalibrations.csv"
3236

3337
BL = get_beamline_name("i09-2")
3438
PREFIX = BeamlinePrefix(BL, suffix="J")
@@ -79,10 +83,15 @@ def jid_controller() -> J09Apple2Controller:
7983
"""J09 insertion device controller."""
8084
return J09Apple2Controller(
8185
apple2=jid(),
82-
energy_motor_lut=EnergyMotorLookup(
83-
lut_config=LookupTableConfig(poly_deg=J09_POLY_DEG),
86+
gap_energy_motor_lut=ConfigServerEnergyMotorLookup(
87+
lut_config=LookupTableConfig(poly_deg=J09_GAP_POLY_DEG_COLUMNS),
88+
config_client=J09_CONF_CLIENT,
89+
path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME),
90+
),
91+
phase_energy_motor_lut=ConfigServerEnergyMotorLookup(
92+
lut_config=LookupTableConfig(poly_deg=J09_PHASE_POLY_DEG_COLUMNS),
8493
config_client=J09_CONF_CLIENT,
85-
gap_path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME),
94+
path=Path(LOOK_UPTABLE_DIR, PHASE_LOOKUP_FILE_NAME),
8695
),
8796
)
8897

src/dodal/beamlines/i10_optics.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from dodal.devices.util.lookup_tables_apple2 import (
4040
DEFAULT_GAP_FILE,
4141
DEFAULT_PHASE_FILE,
42-
EnergyMotorLookup,
42+
ConfigServerEnergyMotorLookup,
4343
LookupTableConfig,
4444
)
4545
from dodal.log import set_beamline as set_log_beamline
@@ -50,8 +50,6 @@
5050
set_utils_beamline(BL)
5151
PREFIX = BeamlinePrefix(BL)
5252

53-
LOOK_UPTABLE_DIR = "/dls_sw/i10/software/blueapi/scratch/i10-config/lookupTables/"
54-
5553

5654
@device_factory()
5755
def synchrotron() -> Synchrotron:
@@ -123,13 +121,22 @@ def idd() -> I10Apple2:
123121
@device_factory()
124122
def idd_controller() -> I10Apple2Controller:
125123
"""I10 downstream insertion device controller."""
126-
idd_energy_motor_lut = EnergyMotorLookup(
124+
source = ("Source", "idd")
125+
idd_gap_energy_motor_lut = ConfigServerEnergyMotorLookup(
126+
config_client=I10_CONF_CLIENT,
127+
lut_config=LookupTableConfig(source=source),
128+
path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
129+
)
130+
idd_phase_energy_motor_lut = ConfigServerEnergyMotorLookup(
127131
config_client=I10_CONF_CLIENT,
128-
lut_config=LookupTableConfig(source=("Source", "idd")),
129-
gap_path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
130-
phase_path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
132+
lut_config=LookupTableConfig(source=source),
133+
path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
134+
)
135+
return I10Apple2Controller(
136+
apple2=idd(),
137+
gap_energy_motor_lut=idd_gap_energy_motor_lut,
138+
phase_energy_motor_lut=idd_phase_energy_motor_lut,
131139
)
132-
return I10Apple2Controller(apple2=idd(), energy_motor_lut=idd_energy_motor_lut)
133140

134141

135142
@device_factory()
@@ -188,13 +195,22 @@ def idu() -> I10Apple2:
188195
@device_factory()
189196
def idu_controller() -> I10Apple2Controller:
190197
"""I10 upstream insertion device controller."""
191-
idu_energy_motor_lut = EnergyMotorLookup(
198+
source = ("Source", "idu")
199+
idu_gap_energy_motor_lut = ConfigServerEnergyMotorLookup(
192200
config_client=I10_CONF_CLIENT,
193-
lut_config=LookupTableConfig(source=("Source", "idu")),
194-
gap_path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
195-
phase_path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
201+
lut_config=LookupTableConfig(source=source),
202+
path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
203+
)
204+
idu_phase_energy_motor_lut = ConfigServerEnergyMotorLookup(
205+
config_client=I10_CONF_CLIENT,
206+
lut_config=LookupTableConfig(source=source),
207+
path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
208+
)
209+
return I10Apple2Controller(
210+
apple2=idd(),
211+
gap_energy_motor_lut=idu_gap_energy_motor_lut,
212+
phase_energy_motor_lut=idu_phase_energy_motor_lut,
196213
)
197-
return I10Apple2Controller(apple2=idd(), energy_motor_lut=idu_energy_motor_lut)
198214

199215

200216
@device_factory()

src/dodal/beamlines/i17.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from dodal.devices.i17.i17_apple2 import I17Apple2Controller
2020
from dodal.devices.pgm import PlaneGratingMonochromator
2121
from dodal.devices.synchrotron import Synchrotron
22+
from dodal.devices.util.lookup_tables_apple2 import (
23+
EnergyMotorLookup,
24+
LookupTable,
25+
)
2226
from dodal.log import set_beamline as set_log_beamline
2327
from dodal.utils import BeamlinePrefix, get_beamline_name
2428

@@ -76,7 +80,9 @@ def id() -> Apple2:
7680
def id_controller() -> Apple2Controller:
7781
"""I17 insertion device controller with dummy energy to motor_converter."""
7882
return I17Apple2Controller(
79-
apple2=id(), energy_to_motor_converter=lambda energy, pol: (0.0, 0.0)
83+
apple2=id(),
84+
gap_energy_motor_lut=EnergyMotorLookup(lut=LookupTable()),
85+
phase_energy_motor_lut=EnergyMotorLookup(lut=LookupTable()),
8086
)
8187

8288

src/dodal/devices/apple2_undulator.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ async def set(self, id_motor_values: Apple2Val) -> None:
398398

399399

400400
class EnergyMotorConvertor(Protocol):
401-
def __call__(self, energy: float, pol: Pol) -> tuple[float, float]:
401+
def __call__(self, energy: float, pol: Pol) -> float:
402402
"""Protocol to provide energy to motor position conversion"""
403403
...
404404

@@ -426,19 +426,18 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
426426
Soft signal for the polarisation setpoint.
427427
polarisation : derived_signal_rw
428428
Hardware-backed signal for polarisation readback and control.
429-
energy_to_motor : EnergyMotorConvertor
430-
Callable that converts energy and polarisation to motor positions.
429+
gap_energy_to_motor_converter : EnergyMotorConvertor
430+
Callable that converts energy and polarisation to gap motor positions.
431+
phase_energy_to_motor_converter : EnergyMotorConvertor
432+
Callable that converts energy and polarisation to phase motor positions.
431433
432434
Abstract Methods
433435
----------------
434-
_set_motors_from_energy(value: float) -> None
435-
Abstract method to set motor positions for a given energy and polarisation.
436-
energy_to_motor : EnergyMotorConvertor
437-
A callable that converts energy and polarisation to motor positions.
438-
436+
_get_apple2_value(gap: float, phase: float) -> Apple2Val
437+
Abstract method to return the Apple2Val used to set the apple2 with.
439438
Notes
440439
-----
441-
- Subclasses must implement `_set_motors_from_energy` for beamline-specific logic.
440+
- Subclasses must implement `_get_apple2_value` for beamline-specific logic.
442441
- LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
443442
- Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
444443
positive circular (PC), negative circular (NC), and linear arbitrary (LA).
@@ -448,7 +447,8 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
448447
def __init__(
449448
self,
450449
apple2: Apple2Type,
451-
energy_to_motor_converter: EnergyMotorConvertor,
450+
gap_energy_motor_converter: EnergyMotorConvertor,
451+
phase_energy_motor_converter: EnergyMotorConvertor,
452452
units: str = "eV",
453453
name: str = "",
454454
) -> None:
@@ -461,8 +461,9 @@ def __init__(
461461
name: str
462462
Name of the device.
463463
"""
464-
self.energy_to_motor = energy_to_motor_converter
465464
self.apple2 = Reference(apple2)
465+
self.gap_energy_motor_converter = gap_energy_motor_converter
466+
self.phase_energy_motor_converter = phase_energy_motor_converter
466467

467468
# Store the set energy for readback.
468469
self._energy, self._energy_set = soft_signal_r_and_setter(
@@ -481,10 +482,11 @@ def __init__(
481482
self.polarisation_setpoint, self._polarisation_setpoint_set = (
482483
soft_signal_r_and_setter(Pol)
483484
)
485+
phase = self.apple2().phase()
484486
# check if undulator phase is unlocked.
485-
if isinstance(self.apple2().phase(), UndulatorPhaseAxes):
486-
top_inner = self.apple2().phase().top_inner.user_readback
487-
btm_outer = self.apple2().phase().btm_outer.user_readback
487+
if isinstance(phase, UndulatorPhaseAxes):
488+
top_inner = phase.top_inner.user_readback
489+
btm_outer = phase.btm_outer.user_readback
488490
else:
489491
# If locked phase axes make the locked phase 0.
490492
top_inner = btm_outer = soft_signal_rw(float, initial_value=0.0)
@@ -496,24 +498,35 @@ def __init__(
496498
raw_to_derived=self._read_pol,
497499
set_derived=self._set_pol,
498500
pol=self.polarisation_setpoint,
499-
top_outer=self.apple2().phase().top_outer.user_readback,
501+
top_outer=phase.top_outer.user_readback,
500502
top_inner=top_inner,
501-
btm_inner=self.apple2().phase().btm_inner.user_readback,
503+
btm_inner=phase.btm_inner.user_readback,
502504
btm_outer=btm_outer,
503505
gap=self.apple2().gap().user_readback,
504506
)
505507
super().__init__(name)
506508

507509
@abc.abstractmethod
508-
async def _set_motors_from_energy(self, value: float) -> None:
510+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
509511
"""
510512
This method should be implemented by the beamline specific ID class as the
511513
motor positions will be different for each beamline depending on the
512-
undulator design and the lookup table used.
514+
undulator design.
513515
"""
514516

517+
async def _set_motors_from_energy_and_polarisation(
518+
self, energy: float, pol: Pol
519+
) -> None:
520+
"""Set the undulator motors for a given energy and polarisation."""
521+
gap = self.gap_energy_motor_converter(energy=energy, pol=pol)
522+
phase = self.phase_energy_motor_converter(energy=energy, pol=pol)
523+
apple2_val = self._get_apple2_value(gap, phase, pol)
524+
LOGGER.info(f"Setting polarisation to {pol}, with values: {apple2_val}")
525+
await self.apple2().set(id_motor_values=apple2_val)
526+
515527
async def _set_energy(self, energy: float) -> None:
516-
await self._set_motors_from_energy(energy)
528+
pol = await self._check_and_get_pol_setpoint()
529+
await self._set_motors_from_energy_and_polarisation(energy, pol)
517530
self._energy_set(energy)
518531

519532
def _read_energy(self, energy: float) -> float:
@@ -525,7 +538,6 @@ async def _check_and_get_pol_setpoint(self) -> Pol:
525538
Check the polarisation setpoint and if it is NONE try to read it from
526539
hardware.
527540
"""
528-
529541
pol = await self.polarisation_setpoint.get_value()
530542

531543
if pol == Pol.NONE:

src/dodal/devices/i09_2_shared/i09_apple2.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
Apple2PhasesVal,
66
Apple2Val,
77
Pol,
8+
UndulatorPhaseAxes,
89
)
9-
from dodal.devices.util.lookup_tables_apple2 import (
10-
EnergyMotorLookup,
11-
)
12-
from dodal.log import LOGGER
10+
from dodal.devices.util.lookup_tables_apple2 import EnergyMotorLookup
1311

14-
J09_POLY_DEG = [
12+
J09_GAP_POLY_DEG_COLUMNS = [
1513
"9th-order",
1614
"8th-order",
1715
"7th-order",
@@ -24,31 +22,44 @@
2422
"0th-order",
2523
]
2624

25+
J09_PHASE_POLY_DEG_COLUMNS = ["0th-order"]
26+
2727

28-
class J09Apple2Controller(Apple2Controller[Apple2]):
28+
class J09Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
2929
def __init__(
3030
self,
31-
apple2: Apple2,
32-
energy_motor_lut: EnergyMotorLookup,
31+
apple2: Apple2[UndulatorPhaseAxes],
32+
gap_energy_motor_lut: EnergyMotorLookup,
33+
phase_energy_motor_lut: EnergyMotorLookup,
3334
units: str = "keV",
3435
name: str = "",
3536
) -> None:
36-
self.lookup_table_client = energy_motor_lut
37+
"""
38+
Parameters:
39+
-----------
40+
apple2 : Apple2
41+
An Apple2 device.
42+
gap_energy_motor_lut: EnergyMotorLookup
43+
The class that handles the gap look up table logic for the insertion device.
44+
phase_energy_motor_lut: EnergyMotorLookup
45+
The class that handles the phase look up table logic for the insertion device.
46+
units:
47+
the units of this device. Defaults to eV.
48+
name : str, optional
49+
New device name.
50+
"""
51+
self.gap_energy_motor_lut = gap_energy_motor_lut
52+
self.phase_energy_motor_lut = phase_energy_motor_lut
3753
super().__init__(
3854
apple2=apple2,
39-
energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
55+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
56+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
4057
units=units,
4158
name=name,
4259
)
4360

44-
async def _set_motors_from_energy(self, value: float) -> None:
45-
"""
46-
Set the undulator motors for a given energy and polarisation.
47-
"""
48-
49-
pol = await self._check_and_get_pol_setpoint()
50-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
51-
id_set_val = Apple2Val(
61+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
62+
return Apple2Val(
5263
gap=f"{gap:.6f}",
5364
phase=Apple2PhasesVal(
5465
top_outer=f"{phase:.6f}",
@@ -58,9 +69,6 @@ async def _set_motors_from_energy(self, value: float) -> None:
5869
),
5970
)
6071

61-
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
62-
await self.apple2().set(id_motor_values=id_set_val)
63-
6472
async def _set_pol(
6573
self,
6674
value: Pol,
@@ -70,7 +78,7 @@ async def _set_pol(
7078
if value is not Pol.LH:
7179
self._polarisation_setpoint_set(Pol.LH)
7280
max_lh_energy = float(
73-
self.lookup_table_client.lookup_tables.gap.root[Pol.LH].limit.maximum
81+
self.gap_energy_motor_lut.lut.root[Pol.LH].limit.maximum
7482
)
7583
lh_setpoint = (
7684
max_lh_energy if target_energy > max_lh_energy else target_energy

0 commit comments

Comments
 (0)