Skip to content

Commit 54b1822

Browse files
Simplified ID lookup logic (#1716)
* 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 * spacing fix * Refactor Lookuptable class documentation Removed outdated docstring from Lookuptable class and updated initialization docstring for I10EnergyMotorLookup class. * 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 * 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 * 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 --------- Co-authored-by: Relm-Arrowny <[email protected]>
1 parent bc36da8 commit 54b1822

File tree

5 files changed

+169
-207
lines changed

5 files changed

+169
-207
lines changed

src/dodal/beamlines/i10_optics.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
idd == id1, idu == id2.
77
"""
88

9+
from pathlib import Path
10+
911
from daq_config_server.client import ConfigServer
1012

1113
from dodal.common.beamlines.beamline_utils import device_factory
@@ -35,7 +37,9 @@
3537
from dodal.devices.pgm import PlaneGratingMonochromator
3638
from dodal.devices.synchrotron import Synchrotron
3739
from dodal.devices.util.lookup_tables_apple2 import (
38-
LookupPath,
40+
DEFAULT_GAP_FILE,
41+
DEFAULT_PHASE_FILE,
42+
EnergyMotorLookup,
3943
LookupTableConfig,
4044
)
4145
from dodal.log import set_beamline as set_log_beamline
@@ -119,13 +123,13 @@ def idd() -> I10Apple2:
119123
@device_factory()
120124
def idd_controller() -> I10Apple2Controller:
121125
"""I10 downstream insertion device controller."""
122-
return I10Apple2Controller(
123-
apple2=idd(),
126+
idd_energy_motor_lut = EnergyMotorLookup(
124127
config_client=I10_CONF_CLIENT,
125-
lut_config=LookupTableConfig(
126-
source=("Source", "idd"), path=LookupPath.create(LOOK_UPTABLE_DIR)
127-
),
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),
128131
)
132+
return I10Apple2Controller(apple2=idd(), energy_motor_lut=idd_energy_motor_lut)
129133

130134

131135
@device_factory()
@@ -184,13 +188,13 @@ def idu() -> I10Apple2:
184188
@device_factory()
185189
def idu_controller() -> I10Apple2Controller:
186190
"""I10 upstream insertion device controller."""
187-
return I10Apple2Controller(
188-
apple2=idu(),
191+
idu_energy_motor_lut = EnergyMotorLookup(
189192
config_client=I10_CONF_CLIENT,
190-
lut_config=LookupTableConfig(
191-
source=("Source", "idu"), path=LookupPath.create(LOOK_UPTABLE_DIR)
192-
),
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),
193196
)
197+
return I10Apple2Controller(apple2=idd(), energy_motor_lut=idu_energy_motor_lut)
194198

195199

196200
@device_factory()

src/dodal/devices/i10/i10_apple2.py

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import numpy as np
44
from bluesky.protocols import Movable
5-
from daq_config_server.client import ConfigServer
65
from ophyd_async.core import (
76
AsyncStatus,
87
Reference,
@@ -23,11 +22,7 @@
2322
UndulatorJawPhase,
2423
UndulatorPhaseAxes,
2524
)
26-
from dodal.devices.util.lookup_tables_apple2 import (
27-
BaseEnergyMotorLookup,
28-
LookupTableConfig,
29-
convert_csv_to_lookup,
30-
)
25+
from dodal.devices.util.lookup_tables_apple2 import EnergyMotorLookup
3126
from dodal.log import LOGGER
3227

3328
ROW_PHASE_MOTOR_TOLERANCE = 0.004
@@ -37,35 +32,6 @@
3732
ALPHA_OFFSET = 180
3833

3934

40-
class I10EnergyMotorLookup(BaseEnergyMotorLookup):
41-
"""
42-
Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
43-
and phase. Fetches and parses lookup tables from a config server, supports dynamic
44-
updates, and validates input.
45-
"""
46-
47-
def update_lookuptable(self):
48-
"""
49-
Update lookup tables from files and validate their format.
50-
"""
51-
LOGGER.info("Updating lookup dictionary from file for gap.")
52-
gap_csv_file = self.config_client.get_file_contents(
53-
self.lut_config.path.gap, reset_cached_result=True
54-
)
55-
self.lookup_tables.gap = convert_csv_to_lookup(
56-
file_contents=gap_csv_file, lut_config=self.lut_config
57-
)
58-
self.available_pol = list(self.lookup_tables.gap.root.keys())
59-
60-
LOGGER.info("Updating lookup dictionary from file for phase.")
61-
phase_csv_file = self.config_client.get_file_contents(
62-
self.lut_config.path.phase, reset_cached_result=True
63-
)
64-
self.lookup_tables.phase = convert_csv_to_lookup(
65-
file_contents=phase_csv_file, lut_config=self.lut_config
66-
)
67-
68-
6935
class I10Apple2(Apple2[UndulatorPhaseAxes]):
7036
"""I10Apple2 device is an apple2 with extra jaw phase motor."""
7137

@@ -102,8 +68,7 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
10268
def __init__(
10369
self,
10470
apple2: I10Apple2,
105-
config_client: ConfigServer,
106-
lut_config: LookupTableConfig,
71+
energy_motor_lut: EnergyMotorLookup,
10772
jaw_phase_limit: float = 12.0,
10873
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
10974
angle_threshold_deg=30.0,
@@ -114,10 +79,8 @@ def __init__(
11479
-----------
11580
apple2 : I10Apple2
11681
An I10Apple2 device.
117-
config_client : ConfigServer
118-
The config server client to fetch the look up table.
119-
lut_config:
120-
Configuration that defines where the lookup table is and how to read it.
82+
energy_motor_lut: EnergyMotorLookup
83+
The class that handles the look up table logic for the insertion device.
12184
jaw_phase_limit : float, optional
12285
The maximum allowed jaw_phase movement., by default 12.0
12386
jaw_phase_poly_param : list[float], optional
@@ -128,13 +91,10 @@ def __init__(
12891
New device name.
12992
"""
13093

131-
self.lookup_table_client = I10EnergyMotorLookup(
132-
lut_config=lut_config,
133-
config_client=config_client,
134-
)
94+
self.energy_motor_lut = energy_motor_lut
13595
super().__init__(
13696
apple2=apple2,
137-
energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
97+
energy_to_motor_converter=self.energy_motor_lut.get_motor_from_energy,
13898
name=name,
13999
)
140100

src/dodal/devices/util/lookup_tables_apple2.py

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
2828
"""
2929

30-
import abc
3130
import csv
3231
import io
3332
from collections.abc import Generator
@@ -45,34 +44,10 @@
4544
)
4645

4746
from dodal.devices.apple2_undulator import Pol
47+
from dodal.log import LOGGER
4848

49-
50-
class LookupPath(BaseModel):
51-
gap: Path
52-
phase: Path
53-
54-
@classmethod
55-
def create(
56-
cls,
57-
path: str,
58-
gap_file: str = "IDEnergy2GapCalibrations.csv",
59-
phase_file: str = "IDEnergy2PhaseCalibrations.csv",
60-
) -> "LookupPath":
61-
"""
62-
Factory method to easily create LookupPath using some default file names.
63-
Parameters:
64-
-----------
65-
path:
66-
The file path to the lookup tables.
67-
gap_file:
68-
The gap lookup table file name.
69-
phase_file:
70-
The phase lookup table file name.
71-
72-
Returns:
73-
LookupPath instance.
74-
"""
75-
return cls(gap=Path(path, gap_file), phase=Path(path, phase_file))
49+
DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
50+
DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
7651

7752

7853
DEFAULT_POLY_DEG = [
@@ -90,7 +65,6 @@ def create(
9065

9166

9267
class LookupTableConfig(BaseModel):
93-
path: LookupPath
9468
source: tuple[str, str] | None = None
9569
mode: str = "Mode"
9670
min_energy: str = "MinEnergy"
@@ -139,7 +113,7 @@ def __init__(self, root: dict[Pol, LookupTableEntries] | None = None):
139113
super().__init__(root=root or {})
140114

141115

142-
class GapPhaseLookupTable(BaseModel):
116+
class GapPhaseLookupTables(BaseModel):
143117
gap: LookupTable = Field(default_factory=lambda: LookupTable())
144118
phase: LookupTable = Field(default_factory=lambda: LookupTable())
145119

@@ -155,9 +129,9 @@ def convert_csv_to_lookup(
155129
Parameters:
156130
-----------
157131
file_contents:
158-
The CSV file content as a string.
132+
The CSV file contents as string.
159133
lut_config:
160-
The configuration that defines how to read the file_contents into a LookupTable
134+
The configuration that how to process the file_contents into a LookupTable.
161135
skip_line_start_with
162136
Lines beginning with this prefix are skipped (default "#").
163137
@@ -315,49 +289,78 @@ def make_phase_tables(
315289
return lookuptable_phase
316290

317291

318-
class BaseEnergyMotorLookup:
292+
class EnergyMotorLookup:
319293
"""
320-
Abstract base for energy->motor lookup.
321-
322-
Subclasses should implement `update_lookuptable()` to populate `self.lookup_tables`
323-
from the configured file sources. After update_lookuptable() has populated the
324-
'gap' and 'phase' tables, `get_motor_from_energy()` can be used to compute
325-
(gap, phase) for a requested (energy, pol) pair.
294+
Handles lookup tables for Apple2 ID, converting energy and polarisation to gap
295+
and phase. Fetches and parses lookup tables from a config server, supports dynamic
296+
updates, and validates input. If custom logic is required for lookup tables, sub
297+
classes should override the _update_gap_lut and _update_phase_lut methods.
298+
299+
After update_lookuptable() has populated the 'gap' and 'phase' tables,
300+
`get_motor_from_energy()` can be used to compute (gap, phase) for a requested
301+
(energy, pol) pair.
326302
"""
327303

328304
def __init__(
329305
self,
330306
config_client: ConfigServer,
331307
lut_config: LookupTableConfig,
308+
gap_path: Path,
309+
phase_path: Path,
332310
):
333311
"""Initialise the EnergyMotorLookup class with lookup table headers provided.
334312
335313
Parameters:
336314
-----------
337-
lut_config:
338-
The configuration that contains the lookup table file paths and how to read
339-
them.
340315
config_client:
341-
The config server client to fetch the look up table.
316+
The config server client to fetch the look up table data.
317+
lut_config:
318+
Configuration that defines how to process file contents into a LookupTable
319+
gap_path:
320+
File path to the gap lookup table.
321+
phase_path:
322+
File path to the phase lookup table.
342323
"""
343-
self.lookup_tables = GapPhaseLookupTable()
344-
self.lut_config = lut_config
324+
self.lookup_tables = GapPhaseLookupTables()
345325
self.config_client = config_client
326+
self.lut_config = lut_config
327+
self.gap_path = gap_path
328+
self.phase_path = phase_path
346329
self._available_pol = []
347330

348331
@property
349-
def available_pol(self) -> list[str | None]:
332+
def available_pol(self) -> list[Pol]:
350333
return self._available_pol
351334

352335
@available_pol.setter
353-
def available_pol(self, value: list[str | None]) -> None:
336+
def available_pol(self, value: list[Pol]) -> None:
354337
self._available_pol = value
355338

356-
@abc.abstractmethod
357-
def update_lookuptable(self):
339+
def _update_gap_lut(self) -> None:
340+
file_contents = self.config_client.get_file_contents(
341+
self.gap_path, reset_cached_result=True
342+
)
343+
self.lookup_tables.gap = convert_csv_to_lookup(
344+
file_contents, lut_config=self.lut_config
345+
)
346+
self.available_pol = list(self.lookup_tables.gap.root.keys())
347+
348+
def _update_phase_lut(self) -> None:
349+
file_contents = self.config_client.get_file_contents(
350+
self.phase_path, reset_cached_result=True
351+
)
352+
self.lookup_tables.phase = convert_csv_to_lookup(
353+
file_contents, lut_config=self.lut_config
354+
)
355+
356+
def update_lookuptables(self):
358357
"""
359358
Update lookup tables from files and validate their format.
360359
"""
360+
LOGGER.info("Updating lookup table from file for gap.")
361+
self._update_gap_lut()
362+
LOGGER.info("Updating lookup table from file for phase.")
363+
self._update_phase_lut()
361364

362365
def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
363366
"""
@@ -376,7 +379,7 @@ def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
376379
(gap, phase) motor positions.
377380
"""
378381
if self.available_pol == []:
379-
self.update_lookuptable()
382+
self.update_lookuptables()
380383

381384
gap_poly = get_poly(lookup_table=self.lookup_tables.gap, energy=energy, pol=pol)
382385
phase_poly = get_poly(

0 commit comments

Comments
 (0)