Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2802e7e
Merge branch 'main' into 1663-i09-energy
Relm-Arrowny Oct 29, 2025
2cc7b93
Merge branch 'create_apple2_energy_motor_lookUP' into 1663-i09-energy
Relm-Arrowny Oct 30, 2025
9f70022
group and reuse fixture, add J09 controller and tests
Relm-Arrowny Oct 30, 2025
a8ebdc8
there is no reason to set mock value to zero
Relm-Arrowny Oct 30, 2025
cbc2a65
Merge remote-tracking branch 'origin/create_apple2_energy_motor_lookU…
Relm-Arrowny Oct 30, 2025
8d05913
remove print
Relm-Arrowny Oct 30, 2025
f0e6dfd
add gap tests
Relm-Arrowny Oct 30, 2025
4b61dc9
add controller, energy and polarisation to i09_2 configuration
Relm-Arrowny Oct 30, 2025
782de6b
move path to top
Relm-Arrowny Oct 30, 2025
071f3e4
add model_validation
Relm-Arrowny Oct 30, 2025
8b3f1a7
remove debug
Relm-Arrowny Oct 30, 2025
03db439
Update docstring for energy_jid function
Relm-Arrowny Oct 31, 2025
b13e35b
Update nc parameter and phase calculation logic
Relm-Arrowny Oct 31, 2025
9d94308
Merge branch 'create_apple2_energy_motor_lookUP' into 1663-i09-energy
Relm-Arrowny Oct 31, 2025
38649cf
update expected lookup table after sign change
Relm-Arrowny Oct 31, 2025
b22a1c2
Merge branch 'create_apple2_energy_motor_lookUP' into 1663-i09-energy
Relm-Arrowny Nov 6, 2025
dc3afed
Merge remote-tracking branch 'origin/create_apple2_energy_motor_lookU…
Relm-Arrowny Nov 6, 2025
e6b3f3e
fix tests
Relm-Arrowny Nov 14, 2025
018130f
Merge remote-tracking branch 'origin/main' into 1663-i09-energy
Relm-Arrowny Nov 14, 2025
25c6756
fix i09 controller and test
Relm-Arrowny Nov 14, 2025
bcc1815
reuse fixture
Relm-Arrowny Nov 14, 2025
e92ce85
Merge remote-tracking branch 'origin/main' into 1663-i09-energy
Relm-Arrowny Nov 17, 2025
14c8886
added gap and phase lookup
Relm-Arrowny Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/dodal/beamlines/i09_2.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
from daq_config_server.client import ConfigServer

from dodal.common.beamlines.beamline_utils import (
device_factory,
)
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.devices.apple2_undulator import (
Apple2,
BeamEnergy,
InsertionDeviceEnergy,
InsertionDevicePolarisation,
UndulatorGap,
UndulatorPhaseAxes,
)
from dodal.devices.i09.enums import Grating
from dodal.devices.i09_2_shared.i09_apple2 import J09Apple2Controller
from dodal.devices.pgm import PlaneGratingMonochromator
from dodal.devices.synchrotron import Synchrotron
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import BeamlinePrefix, get_beamline_name

J09_CONF_CLIENT = ConfigServer(url="https://daq-config.diamond.ac.uk")
LOOK_UPTABLE_DIR = "/dls_sw/i09-2/software/gda/workspace_git/gda-diamond.git/configurations/i09-2-shared/lookupTables/"


BL = get_beamline_name("i09-2")
PREFIX = BeamlinePrefix(BL, suffix="J")
set_log_beamline(BL)
Expand Down Expand Up @@ -55,3 +65,29 @@ def jid() -> Apple2:
id_gap=jid_gap(),
id_phase=jid_phase(),
)


@device_factory()
def jid_controller() -> J09Apple2Controller:
"""J09 insertion device controller."""
return J09Apple2Controller(
apple2=jid(),
lookuptable_dir=LOOK_UPTABLE_DIR,
config_client=J09_CONF_CLIENT,
)


@device_factory()
def jid_energy() -> InsertionDeviceEnergy:
return InsertionDeviceEnergy(id_controller=jid_controller())


@device_factory()
def jid_polarisation() -> InsertionDevicePolarisation:
return InsertionDevicePolarisation(id_controller=jid_controller())


@device_factory()
def energy_jid() -> BeamEnergy:
"""Beam energy."""
return BeamEnergy(id_energy=jid_energy(), mono=pgm().energy)
5 changes: 3 additions & 2 deletions src/dodal/devices/apple2_undulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ def __init__(
self,
apple2: Apple2Type,
energy_to_motor_converter: EnergyMotorConvertor,
units: str = "eV",
name: str = "",
) -> None:
"""
Expand All @@ -465,14 +466,14 @@ def __init__(

# Store the set energy for readback.
self._energy, self._energy_set = soft_signal_r_and_setter(
float, initial_value=None, units="eV"
float, initial_value=None, units=units
)
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
self.energy = derived_signal_rw(
raw_to_derived=self._read_energy,
set_derived=self._set_energy,
energy=self._energy,
derived_units="eV",
derived_units=units,
)

# Store the polarisation for setpoint. And provide readback for LH3.
Expand Down
Empty file.
166 changes: 166 additions & 0 deletions src/dodal/devices/i09_2_shared/i09_apple2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from daq_config_server.client import ConfigServer

from dodal.devices.apple2_undulator import (
Apple2,
Apple2Controller,
Apple2PhasesVal,
Apple2Val,
Pol,
)
from dodal.devices.util.lookup_tables_apple2 import (
BaseEnergyMotorLookup,
LookupPath,
LookupTableConfig,
make_phase_tables,
)
from dodal.log import LOGGER

ROW_PHASE_MOTOR_TOLERANCE = 0.004
ROW_PHASE_CIRCULAR = 15
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
MAXIMUM_GAP_MOTOR_POSITION = 100

J09PhasePoly1dParameters = {
"lh": [0],
"lv": [MAXIMUM_ROW_PHASE_MOTOR_POSITION],
"pc": [ROW_PHASE_CIRCULAR],
"nc": [-ROW_PHASE_CIRCULAR],
"lh3": [0],
}


J09DefaultLookupTableConfig = LookupTableConfig(
path=LookupPath.create(
path="i09_apple2",
gap_file="i09_apple2/j09_energy2gap_calibrations.csv",
phase_file=None,
),
mode="Mode",
min_energy="MinEnergy",
max_energy="MaxEnergy",
poly_deg=[
"9th-order",
"8th-order",
"7th-order",
"6th-order",
"5th-order",
"4th-order",
"3rd-order",
"2nd-order",
"1st-order",
"0th-order",
],
)


class J09EnergyMotorLookup(BaseEnergyMotorLookup):
"""
Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
and phase. Fetches and parses lookup tables from a config server, supports dynamic
updates, and validates input.
"""

def __init__(
self,
config_client: ConfigServer,
lut_config: LookupTableConfig = J09DefaultLookupTableConfig,
):
"""Initialise the I10EnergyMotorLookup class with lookup table headers provided.

Parameters
----------
look_up_table_dir:
The path to look up table.
source:
The column name and the name of the source in look up table. e.g. ( "source", "idu")
config_client:
The config server client to fetch the look up table.
mode:
The column name of the mode in look up table.
min_energy:
The column name that contain the maximum energy in look up table.
max_energy:
The column name that contain the maximum energy in look up table.
poly_deg:
The column names for the parameters for the energy conversion polynomial, starting with the least significant.

"""

super().__init__(
config_client=config_client,
lut_config=lut_config,
)

def update_lookuptable(self):
"""
Update lookup tables from files and validate their format.
"""
self.update_gap_lookuptable()
mix_energies = []
max_energies = []
pols = []
poly1d_params = []
for key in self.lookup_tables[LookupTableKeys.GAP].keys():
if key is not None:
pols.append(Pol(key.lower()))
mix_energies.append(
self.lookup_tables[LookupTableKeys.GAP][key][LookupTableKeys.LIMIT][
LookupTableKeys.MIN
]
)
max_energies.append(
self.lookup_tables[LookupTableKeys.GAP][key][LookupTableKeys.LIMIT][
LookupTableKeys.MAX
]
)
poly1d_params.append(J09PhasePoly1dParameters[key])
self.lookup_tables[LookupTableKeys.PHASE] = make_phase_tables(
pols=pols,
min_energies=mix_energies,
max_energies=max_energies,
poly1d_params=poly1d_params,
)
Lookuptable.model_validate(self.lookup_tables[LookupTableKeys.PHASE])


class J09Apple2Controller(Apple2Controller[Apple2]):
def __init__(
self,
apple2: Apple2,
lookuptable_dir: str,
config_client: ConfigServer,
poly_deg: list[str] | None = None,
units: str = "keV",
name: str = "",
) -> None:
self.lookup_table_client = J09EnergyMotorLookup(
lookuptable_dir=lookuptable_dir,
config_client=config_client,
poly_deg=poly_deg,
)
super().__init__(
apple2=apple2,
energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
units=units,
name=name,
)

async def _set_motors_from_energy(self, value: float) -> None:
"""
Set the undulator motors for a given energy and polarisation.
"""

pol = await self._check_and_get_pol_setpoint()
gap, phase = self.energy_to_motor(energy=value, pol=pol)
id_set_val = Apple2Val(
gap=f"{gap:.6f}",
phase=Apple2PhasesVal(
top_outer=f"{phase:.6f}",
top_inner="0.0",
btm_inner=f"{phase:.6f}",
btm_outer="0.0",
),
)

LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
await self.apple2().set(id_motor_values=id_set_val)
2 changes: 2 additions & 0 deletions src/dodal/devices/i10/i10_apple2.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def update_lookuptable(self):
self.available_pol = list(self.lookup_tables.gap.root.keys())

LOGGER.info("Updating lookup dictionary from file for phase.")
if self.lut_config.path.phase is None:
raise RuntimeError("Phase lookup table is required for I10 Apple2.")
phase_csv_file = self.config_client.get_file_contents(
self.lut_config.path.phase, reset_cached_result=True
)
Expand Down
39 changes: 35 additions & 4 deletions src/dodal/devices/util/lookup_tables_apple2.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,19 @@
)

from dodal.devices.apple2_undulator import Pol
from dodal.log import LOGGER


class LookupPath(BaseModel):
gap: Path
phase: Path
phase: Path | None = None

@classmethod
def create(
cls,
path: str,
gap_file: str = "IDEnergy2GapCalibrations.csv",
phase_file: str = "IDEnergy2PhaseCalibrations.csv",
phase_file: str | None = "IDEnergy2PhaseCalibrations.csv",
) -> "LookupPath":
"""
Factory method to easily create LookupPath using some default file names.
Expand All @@ -71,7 +72,10 @@ def create(
Returns:
LookupPath instance.
"""
return cls(gap=Path(path, gap_file), phase=Path(path, phase_file))
return cls(
gap=Path(path, gap_file),
phase=Path(path, phase_file) if phase_file else None,
)


DEFAULT_POLY_DEG = [
Expand Down Expand Up @@ -233,7 +237,7 @@ def get_poly(
Parameters:
-----------
energy:
Energy value in the same units used to create the lookup table (eV).
Energy value in the same units used to create the lookup table.
pol:
Polarisation mode (Pol enum).
lookup_table:
Expand Down Expand Up @@ -350,6 +354,33 @@ def update_lookuptable(self):
Update lookup tables from files and validate their format.
"""

def update_gap_lookuptable(self):
"""
Update lookup tables from files and validate their format.
"""
LOGGER.info("Updating lookup dictionary from file for gap.")
gap_csv_file = self.config_client.get_file_contents(
self.lut_config.path.gap, reset_cached_result=True
)
self.lookup_tables.gap = convert_csv_to_lookup(
file_contents=gap_csv_file, lut_config=self.lut_config
)
self.available_pol = list(self.lookup_tables.gap.root.keys())

def update_phase_lookuptable(self):
"""
Update lookup tables from files and validate their format.
"""
LOGGER.info("Updating lookup dictionary from file for phase.")
if self.lut_config.path.phase is None:
raise RuntimeError("Phase lookup table path is not provided.")
phase_csv_file = self.config_client.get_file_contents(
self.lut_config.path.phase, reset_cached_result=True
)
self.lookup_tables.phase = convert_csv_to_lookup(
file_contents=phase_csv_file, lut_config=self.lut_config
)

def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
"""
Convert energy and polarisation to gap and phase motor positions.
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions tests/devices/i09_2_shared/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from tests.devices.test_apple2_undulator import (
mock_id_gap,
mock_phase_axes,
)
from tests.devices.util.test_lookup_tables_apple2 import mock_config_client

__all__ = ["mock_id_gap", "mock_phase_axes", "mock_config_client"]
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#I09 Soft X-ray ID fitting parameters, created 9th Sept 2020,,,,,,,,,,,
Mode,MinEnergy,MaxEnergy,0th-order,1st-order,2nd-order,3rd-order,4th-order,5th-order,6th-order,7th-order,8th-order,9th-order
LH,0.104,1.2,0.52071,238.56372,-1169.06966,4273.03275,-10497.36261,17156.91928,-18309.05195,12222.50318,-4623.70738,755.90853
LV,0.22,1,5.33595,72.53678,-133.96826,179.99229,-128.83048,39.34346,0,0,0,0
CR,0.145,1.2,5.32869,101.28316,-192.74788,249.91788,-167.93323,47.22008,0,0,0,0
CL,0.145,1.2,5.25639,101.22916,-192.74788,249.91788,-167.93323,47.22008,0,0,0,0
LH3,0.7,2,10.98969,25.8301,-9.36535,1.74461,0,0,0,0,0,0
Binary file not shown.
16 changes: 16 additions & 0 deletions tests/devices/i09_2_shared/test_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from os import fspath
from os.path import join
from pathlib import Path

LOOKUP_TABLE_PATH = fspath(Path(__file__).parent)
TEST_SOFT_UNDULATOR_LUT = join(LOOKUP_TABLE_PATH, "JIDEnergy2GapCalibrations.csv")
TEST_EXPECTED_UNDULATOR_LUT = join(LOOKUP_TABLE_PATH, "JIDEnergy2GapCalibrations.pkl")
TEST_EXPECTED_ENERGY_MOTOR_LOOKUP = join(
LOOKUP_TABLE_PATH, "ExpectedJ09EnergyMotorLookup.pkl"
)
__all__ = [
"LOOKUP_TABLE_PATH",
"TEST_SOFT_UNDULATOR_LUT",
"TEST_EXPECTED_UNDULATOR_LUT",
"TEST_EXPECTED_ENERGY_MOTOR_LOOKUP",
]
Loading
Loading