Skip to content

Commit f50aaaa

Browse files
1663 i09 energy (#1672)
* 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 * correct pytest_plugin location * undo unwanted change in i10_optics --------- Co-authored-by: oliwenmandiamond <[email protected]> Co-authored-by: Oli Wenman <[email protected]>
1 parent 8967593 commit f50aaaa

File tree

15 files changed

+881
-173
lines changed

15 files changed

+881
-173
lines changed

src/dodal/beamlines/i09_2.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
1+
from pathlib import Path
2+
3+
from daq_config_server.client import ConfigServer
4+
15
from dodal.common.beamlines.beamline_utils import (
26
device_factory,
37
)
48
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
59
from dodal.devices.apple2_undulator import (
610
Apple2,
11+
BeamEnergy,
12+
InsertionDeviceEnergy,
13+
InsertionDevicePolarisation,
714
UndulatorGap,
815
UndulatorPhaseAxes,
916
)
1017
from dodal.devices.i09.enums import Grating
18+
from dodal.devices.i09_2_shared.i09_apple2 import (
19+
J09_POLY_DEG,
20+
EnergyMotorLookup,
21+
J09Apple2Controller,
22+
)
1123
from dodal.devices.pgm import PlaneGratingMonochromator
1224
from dodal.devices.synchrotron import Synchrotron
25+
from dodal.devices.util.lookup_tables_apple2 import LookupTableConfig
1326
from dodal.log import set_beamline as set_log_beamline
1427
from dodal.utils import BeamlinePrefix, get_beamline_name
1528

29+
J09_CONF_CLIENT = ConfigServer(url="https://daq-config.diamond.ac.uk")
30+
LOOK_UPTABLE_DIR = "/dls_sw/i09-2/software/gda/workspace_git/gda-diamond.git/configurations/i09-2-shared/lookupTables/"
31+
GAP_LOOKUP_FILE_NAME = "JIDEnergy2GapCalibrations.csv"
32+
1633
BL = get_beamline_name("i09-2")
1734
PREFIX = BeamlinePrefix(BL, suffix="J")
1835
set_log_beamline(BL)
@@ -55,3 +72,32 @@ def jid() -> Apple2:
5572
id_gap=jid_gap(),
5673
id_phase=jid_phase(),
5774
)
75+
76+
77+
@device_factory()
78+
def jid_controller() -> J09Apple2Controller:
79+
"""J09 insertion device controller."""
80+
return J09Apple2Controller(
81+
apple2=jid(),
82+
energy_motor_lut=EnergyMotorLookup(
83+
lut_config=LookupTableConfig(poly_deg=J09_POLY_DEG),
84+
config_client=J09_CONF_CLIENT,
85+
gap_path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME),
86+
),
87+
)
88+
89+
90+
@device_factory()
91+
def jid_energy() -> InsertionDeviceEnergy:
92+
return InsertionDeviceEnergy(id_controller=jid_controller())
93+
94+
95+
@device_factory()
96+
def jid_polarisation() -> InsertionDevicePolarisation:
97+
return InsertionDevicePolarisation(id_controller=jid_controller())
98+
99+
100+
@device_factory()
101+
def energy_jid() -> BeamEnergy:
102+
"""Beam energy."""
103+
return BeamEnergy(id_energy=jid_energy(), mono=pgm().energy)

src/dodal/devices/apple2_undulator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ def __init__(
449449
self,
450450
apple2: Apple2Type,
451451
energy_to_motor_converter: EnergyMotorConvertor,
452+
units: str = "eV",
452453
name: str = "",
453454
) -> None:
454455
"""
@@ -465,14 +466,14 @@ def __init__(
465466

466467
# Store the set energy for readback.
467468
self._energy, self._energy_set = soft_signal_r_and_setter(
468-
float, initial_value=None, units="eV"
469+
float, initial_value=None, units=units
469470
)
470471
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
471472
self.energy = derived_signal_rw(
472473
raw_to_derived=self._read_energy,
473474
set_derived=self._set_energy,
474475
energy=self._energy,
475-
derived_units="eV",
476+
derived_units=units,
476477
)
477478

478479
# Store the polarisation for setpoint. And provide readback for LH3.

src/dodal/devices/i09_2_shared/__init__.py

Whitespace-only changes.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from dodal.devices.apple2_undulator import (
2+
MAXIMUM_MOVE_TIME,
3+
Apple2,
4+
Apple2Controller,
5+
Apple2PhasesVal,
6+
Apple2Val,
7+
Pol,
8+
)
9+
from dodal.devices.util.lookup_tables_apple2 import (
10+
EnergyMotorLookup,
11+
)
12+
from dodal.log import LOGGER
13+
14+
J09_POLY_DEG = [
15+
"9th-order",
16+
"8th-order",
17+
"7th-order",
18+
"6th-order",
19+
"5th-order",
20+
"4th-order",
21+
"3rd-order",
22+
"2nd-order",
23+
"1st-order",
24+
"0th-order",
25+
]
26+
27+
28+
class J09Apple2Controller(Apple2Controller[Apple2]):
29+
def __init__(
30+
self,
31+
apple2: Apple2,
32+
energy_motor_lut: EnergyMotorLookup,
33+
units: str = "keV",
34+
name: str = "",
35+
) -> None:
36+
self.lookup_table_client = energy_motor_lut
37+
super().__init__(
38+
apple2=apple2,
39+
energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
40+
units=units,
41+
name=name,
42+
)
43+
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(
52+
gap=f"{gap:.6f}",
53+
phase=Apple2PhasesVal(
54+
top_outer=f"{phase:.6f}",
55+
top_inner=f"{0.0:.6f}",
56+
btm_inner=f"{phase:.6f}",
57+
btm_outer=f"{0.0:.6f}",
58+
),
59+
)
60+
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+
64+
async def _set_pol(
65+
self,
66+
value: Pol,
67+
) -> None:
68+
# I09 require all palarisation change to go via LH.
69+
target_energy = await self.energy.get_value()
70+
if value is not Pol.LH:
71+
self._polarisation_setpoint_set(Pol.LH)
72+
max_lh_energy = float(
73+
self.lookup_table_client.lookup_tables.gap.root[Pol.LH].limit.maximum
74+
)
75+
lh_setpoint = (
76+
max_lh_energy if target_energy > max_lh_energy else target_energy
77+
)
78+
await self.energy.set(lh_setpoint, timeout=MAXIMUM_MOVE_TIME)
79+
self._polarisation_setpoint_set(value)
80+
await self.energy.set(target_energy, timeout=MAXIMUM_MOVE_TIME)

src/dodal/devices/util/lookup_tables_apple2.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@
4646
from dodal.devices.apple2_undulator import Pol
4747
from dodal.log import LOGGER
4848

49-
DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
50-
DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
51-
52-
5349
DEFAULT_POLY_DEG = [
5450
"7th-order",
5551
"6th-order",
@@ -61,7 +57,22 @@
6157
"b",
6258
]
6359

64-
MODE_NAME_CONVERT = {"CR": "pc", "CL": "nc"}
60+
MODE_NAME_CONVERT = {"cr": "pc", "cl": "nc"}
61+
DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
62+
DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
63+
64+
ROW_PHASE_MOTOR_TOLERANCE = 0.004
65+
ROW_PHASE_CIRCULAR = 15
66+
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
67+
MAXIMUM_GAP_MOTOR_POSITION = 100
68+
69+
PhASE_POLY1D_PARAMETERS = {
70+
Pol.LH: [0],
71+
Pol.LV: [MAXIMUM_ROW_PHASE_MOTOR_POSITION],
72+
Pol.PC: [ROW_PHASE_CIRCULAR],
73+
Pol.NC: [-ROW_PHASE_CIRCULAR],
74+
Pol.LH3: [0],
75+
}
6576

6677

6778
class LookupTableConfig(BaseModel):
@@ -216,7 +227,7 @@ def get_poly(
216227
Parameters:
217228
-----------
218229
energy:
219-
Energy value in the same units used to create the lookup table (eV).
230+
Energy value in the same units used to create the lookup table.
220231
pol:
221232
Polarisation mode (Pol enum).
222233
lookup_table:
@@ -305,8 +316,8 @@ def __init__(
305316
self,
306317
config_client: ConfigServer,
307318
lut_config: LookupTableConfig,
308-
gap_path: Path,
309-
phase_path: Path,
319+
gap_path: Path | None = None,
320+
phase_path: Path | None = None,
310321
):
311322
"""Initialise the EnergyMotorLookup class with lookup table headers provided.
312323
@@ -321,11 +332,11 @@ def __init__(
321332
phase_path:
322333
File path to the phase lookup table.
323334
"""
335+
self.gap_path = gap_path
336+
self.phase_path = phase_path
324337
self.lookup_tables = GapPhaseLookupTables()
325338
self.config_client = config_client
326339
self.lut_config = lut_config
327-
self.gap_path = gap_path
328-
self.phase_path = phase_path
329340
self._available_pol = []
330341

331342
@property
@@ -337,6 +348,8 @@ def available_pol(self, value: list[Pol]) -> None:
337348
self._available_pol = value
338349

339350
def _update_gap_lut(self) -> None:
351+
if self.gap_path is None:
352+
raise RuntimeError("Gap path is not provided!")
340353
file_contents = self.config_client.get_file_contents(
341354
self.gap_path, reset_cached_result=True
342355
)
@@ -346,6 +359,8 @@ def _update_gap_lut(self) -> None:
346359
self.available_pol = list(self.lookup_tables.gap.root.keys())
347360

348361
def _update_phase_lut(self) -> None:
362+
if self.phase_path is None:
363+
raise RuntimeError("Phase path is not provided!")
349364
file_contents = self.config_client.get_file_contents(
350365
self.phase_path, reset_cached_result=True
351366
)
@@ -357,10 +372,24 @@ def update_lookuptables(self):
357372
"""
358373
Update lookup tables from files and validate their format.
359374
"""
360-
LOGGER.info("Updating lookup table from file for gap.")
375+
LOGGER.info("Updating lookup table for gap.")
361376
self._update_gap_lut()
362-
LOGGER.info("Updating lookup table from file for phase.")
363-
self._update_phase_lut()
377+
if self.phase_path is None:
378+
LOGGER.info("Generating lookup table for phase.")
379+
self._generate_phase_lut()
380+
381+
else:
382+
LOGGER.info("Updating lookup table for phase.")
383+
self._update_phase_lut()
384+
385+
def _generate_phase_lut(self):
386+
for key in self.lookup_tables.gap.root.keys():
387+
if key is not None:
388+
self.lookup_tables.phase.root[key] = generate_lookup_table_entry(
389+
min_energy=self.lookup_tables.gap.root[key].limit.minimum,
390+
max_energy=self.lookup_tables.gap.root[key].limit.maximum,
391+
poly1d_param=(PhASE_POLY1D_PARAMETERS[key]),
392+
)
364393

365394
def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
366395
"""
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from unittest.mock import MagicMock
2+
3+
import pytest
4+
from daq_config_server.client import ConfigServer
5+
from ophyd_async.core import (
6+
init_devices,
7+
set_mock_value,
8+
)
9+
10+
from dodal.devices.apple2_undulator import (
11+
EnabledDisabledUpper,
12+
UndulatorGap,
13+
UndulatorGateStatus,
14+
UndulatorJawPhase,
15+
UndulatorPhaseAxes,
16+
)
17+
18+
19+
@pytest.fixture
20+
def mock_config_client() -> ConfigServer:
21+
mock_config_client = ConfigServer()
22+
23+
mock_config_client.get_file_contents = MagicMock(spec=["get_file_contents"])
24+
25+
def my_side_effect(file_path, reset_cached_result) -> str:
26+
assert reset_cached_result is True
27+
with open(file_path) as f:
28+
return f.read()
29+
30+
mock_config_client.get_file_contents.side_effect = my_side_effect
31+
return mock_config_client
32+
33+
34+
@pytest.fixture
35+
async def mock_id_gap(prefix: str = "BLXX-EA-DET-007:") -> UndulatorGap:
36+
async with init_devices(mock=True):
37+
mock_id_gap = UndulatorGap(prefix, "mock_id_gap")
38+
assert mock_id_gap.name == "mock_id_gap"
39+
set_mock_value(mock_id_gap.gate, UndulatorGateStatus.CLOSE)
40+
set_mock_value(mock_id_gap.velocity, 1)
41+
set_mock_value(mock_id_gap.user_readback, 1)
42+
set_mock_value(mock_id_gap.user_setpoint, "1")
43+
set_mock_value(mock_id_gap.status, EnabledDisabledUpper.ENABLED)
44+
return mock_id_gap
45+
46+
47+
@pytest.fixture
48+
async def mock_phase_axes(prefix: str = "BLXX-EA-DET-007:") -> UndulatorPhaseAxes:
49+
async with init_devices(mock=True):
50+
mock_phase_axes = UndulatorPhaseAxes(
51+
prefix=prefix,
52+
top_outer="RPQ1",
53+
top_inner="RPQ2",
54+
btm_outer="RPQ3",
55+
btm_inner="RPQ4",
56+
)
57+
assert mock_phase_axes.name == "mock_phase_axes"
58+
set_mock_value(mock_phase_axes.gate, UndulatorGateStatus.CLOSE)
59+
set_mock_value(mock_phase_axes.top_outer.velocity, 2)
60+
set_mock_value(mock_phase_axes.top_inner.velocity, 2)
61+
set_mock_value(mock_phase_axes.btm_outer.velocity, 2)
62+
set_mock_value(mock_phase_axes.btm_inner.velocity, 2)
63+
set_mock_value(mock_phase_axes.status, EnabledDisabledUpper.ENABLED)
64+
return mock_phase_axes
65+
66+
67+
@pytest.fixture
68+
async def mock_jaw_phase(prefix: str = "BLXX-EA-DET-007:") -> UndulatorJawPhase:
69+
async with init_devices(mock=True):
70+
mock_jaw_phase = UndulatorJawPhase(
71+
prefix=prefix, move_pv="RPQ1", jaw_phase="JAW"
72+
)
73+
set_mock_value(mock_jaw_phase.gate, UndulatorGateStatus.CLOSE)
74+
set_mock_value(mock_jaw_phase.jaw_phase.velocity, 2)
75+
set_mock_value(mock_jaw_phase.jaw_phase.user_readback, 0)
76+
set_mock_value(mock_jaw_phase.jaw_phase.user_setpoint_readback, 0)
77+
set_mock_value(mock_jaw_phase.status, EnabledDisabledUpper.ENABLED)
78+
return mock_jaw_phase

tests/devices/i09_2_shared/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)