dls-dodal 1.57.0__py3-none-any.whl → 1.59.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/METADATA +2 -1
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/RECORD +63 -46
- dodal/_version.py +2 -2
- dodal/beamlines/b07.py +10 -5
- dodal/beamlines/b07_1.py +10 -5
- dodal/beamlines/b21.py +22 -0
- dodal/beamlines/i02_1.py +80 -0
- dodal/beamlines/i03.py +7 -4
- dodal/beamlines/i04.py +20 -3
- dodal/beamlines/i09.py +10 -9
- dodal/beamlines/i09_1.py +10 -5
- dodal/beamlines/i10-1.py +25 -0
- dodal/beamlines/i10.py +17 -1
- dodal/beamlines/i11.py +0 -17
- dodal/beamlines/i19_2.py +20 -0
- dodal/beamlines/i21.py +27 -0
- dodal/beamlines/i22.py +12 -2
- dodal/beamlines/i24.py +32 -3
- dodal/beamlines/k07.py +31 -0
- dodal/beamlines/p60.py +10 -9
- dodal/common/beamlines/commissioning_mode.py +33 -0
- dodal/common/watcher_utils.py +1 -1
- dodal/devices/apple2_undulator.py +18 -142
- dodal/devices/attenuator/attenuator.py +48 -2
- dodal/devices/attenuator/filter.py +3 -0
- dodal/devices/attenuator/filter_selections.py +26 -0
- dodal/devices/baton.py +4 -0
- dodal/devices/eiger.py +2 -1
- dodal/devices/electron_analyser/__init__.py +4 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +30 -18
- dodal/devices/electron_analyser/energy_sources.py +101 -0
- dodal/devices/electron_analyser/specs/detector.py +6 -6
- dodal/devices/electron_analyser/specs/driver_io.py +7 -15
- dodal/devices/electron_analyser/vgscienta/detector.py +6 -6
- dodal/devices/electron_analyser/vgscienta/driver_io.py +7 -14
- dodal/devices/fast_grid_scan.py +130 -64
- dodal/devices/focusing_mirror.py +30 -0
- dodal/devices/i02_1/__init__.py +0 -0
- dodal/devices/i02_1/fast_grid_scan.py +61 -0
- dodal/devices/i02_1/sample_motors.py +19 -0
- dodal/devices/i04/murko_results.py +69 -23
- dodal/devices/i10/i10_apple2.py +282 -140
- dodal/devices/i19/backlight.py +17 -0
- dodal/devices/i21/__init__.py +3 -0
- dodal/devices/i21/enums.py +8 -0
- dodal/devices/i22/nxsas.py +2 -0
- dodal/devices/i24/commissioning_jungfrau.py +114 -0
- dodal/devices/smargon.py +0 -56
- dodal/devices/temperture_controller/__init__.py +3 -0
- dodal/devices/temperture_controller/lakeshore/__init__.py +0 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore.py +204 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +112 -0
- dodal/devices/tetramm.py +38 -16
- dodal/devices/undulator.py +13 -9
- dodal/devices/v2f.py +39 -0
- dodal/devices/xbpm_feedback.py +12 -6
- dodal/devices/zebra/zebra.py +1 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -1
- dodal/parameters/experiment_parameter_base.py +1 -5
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/WHEEL +0 -0
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.57.0.dist-info → dls_dodal-1.59.1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import pickle
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import TypedDict
|
|
5
6
|
|
|
@@ -21,9 +22,6 @@ from dodal.log import LOGGER
|
|
|
21
22
|
|
|
22
23
|
NO_MURKO_RESULT = (-1, -1)
|
|
23
24
|
|
|
24
|
-
MurkoResult = dict
|
|
25
|
-
FullMurkoResults = dict[str, list[MurkoResult]]
|
|
26
|
-
|
|
27
25
|
|
|
28
26
|
class MurkoMetadata(TypedDict):
|
|
29
27
|
zoom_percentage: float
|
|
@@ -42,6 +40,19 @@ class Coord(Enum):
|
|
|
42
40
|
z = 2
|
|
43
41
|
|
|
44
42
|
|
|
43
|
+
@dataclass
|
|
44
|
+
class MurkoResult:
|
|
45
|
+
centre_px: tuple
|
|
46
|
+
x_dist_mm: float
|
|
47
|
+
y_dist_mm: float
|
|
48
|
+
omega: float
|
|
49
|
+
uuid: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NoResultsFound(ValueError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
45
56
|
class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
46
57
|
"""Device that takes crystal centre values from Murko and uses them to set the
|
|
47
58
|
x, y, z coordinate of the sample to be in line with the beam centre.
|
|
@@ -59,6 +70,8 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
59
70
|
"""
|
|
60
71
|
|
|
61
72
|
TIMEOUT_S = 2
|
|
73
|
+
PERCENTAGE_TO_USE = 25
|
|
74
|
+
NUMBER_OF_WRONG_RESULTS_TO_LOG = 5
|
|
62
75
|
|
|
63
76
|
def __init__(
|
|
64
77
|
self,
|
|
@@ -74,12 +87,10 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
74
87
|
db=redis_db,
|
|
75
88
|
)
|
|
76
89
|
self.pubsub = self.redis_client.pubsub()
|
|
77
|
-
self._last_omega = 0
|
|
78
90
|
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
79
91
|
self.stop_angle = stop_angle
|
|
80
|
-
|
|
81
|
-
self.
|
|
82
|
-
self.omegas = []
|
|
92
|
+
|
|
93
|
+
self._reset()
|
|
83
94
|
|
|
84
95
|
with self.add_children_as_readables():
|
|
85
96
|
# Diffs from current x/y/z
|
|
@@ -88,6 +99,10 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
88
99
|
self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
|
|
89
100
|
super().__init__(name=name)
|
|
90
101
|
|
|
102
|
+
def _reset(self):
|
|
103
|
+
self._last_omega = 0
|
|
104
|
+
self.results: list[MurkoResult] = []
|
|
105
|
+
|
|
91
106
|
@AsyncStatus.wrap
|
|
92
107
|
async def stage(self):
|
|
93
108
|
await self.pubsub.subscribe("murko-results")
|
|
@@ -97,6 +112,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
97
112
|
|
|
98
113
|
@AsyncStatus.wrap
|
|
99
114
|
async def unstage(self):
|
|
115
|
+
self._reset()
|
|
100
116
|
await self.pubsub.unsubscribe()
|
|
101
117
|
|
|
102
118
|
@AsyncStatus.wrap
|
|
@@ -106,19 +122,26 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
106
122
|
while self._last_omega < self.stop_angle:
|
|
107
123
|
# waits here for next batch to be received
|
|
108
124
|
message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
|
|
109
|
-
if message is None:
|
|
110
|
-
|
|
125
|
+
if message is None:
|
|
126
|
+
continue
|
|
111
127
|
await self.process_batch(message, sample_id)
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
if not self.results:
|
|
130
|
+
raise NoResultsFound("No results retrieved from Murko")
|
|
131
|
+
|
|
132
|
+
for result in self.results:
|
|
133
|
+
LOGGER.debug(result)
|
|
134
|
+
|
|
135
|
+
self.filter_outliers()
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
x_dists_mm = [result.x_dist_mm for result in self.results]
|
|
138
|
+
y_dists_mm = [result.y_dist_mm for result in self.results]
|
|
139
|
+
omegas = [result.omega for result in self.results]
|
|
140
|
+
|
|
141
|
+
LOGGER.info(f"Using average of x beam distances: {x_dists_mm}")
|
|
142
|
+
avg_x = float(np.mean(x_dists_mm))
|
|
143
|
+
LOGGER.info(f"Finding least square y and z from y distances: {y_dists_mm}")
|
|
144
|
+
best_y, best_z = get_yz_least_squares(y_dists_mm, omegas)
|
|
122
145
|
# x, y, z are relative to beam centre. Need to move negative these values to get centred.
|
|
123
146
|
self._x_mm_setter(-avg_x)
|
|
124
147
|
self._y_mm_setter(-best_y)
|
|
@@ -163,15 +186,38 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
163
186
|
centre_px[0],
|
|
164
187
|
centre_px[1],
|
|
165
188
|
)
|
|
166
|
-
self.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
self.results.append(
|
|
190
|
+
MurkoResult(
|
|
191
|
+
centre_px=centre_px,
|
|
192
|
+
x_dist_mm=beam_dist_px[0] * metadata["microns_per_x_pixel"] / 1000,
|
|
193
|
+
y_dist_mm=beam_dist_px[1] * metadata["microns_per_y_pixel"] / 1000,
|
|
194
|
+
omega=omega,
|
|
195
|
+
uuid=metadata["uuid"],
|
|
196
|
+
)
|
|
171
197
|
)
|
|
172
|
-
self.omegas.append(omega)
|
|
173
198
|
self._last_omega = omega
|
|
174
199
|
|
|
200
|
+
def filter_outliers(self):
|
|
201
|
+
"""Whilst murko is not fully trained it often gives us poor results.
|
|
202
|
+
When it is wrong it usually picks up the base of the pin, rather than the tip,
|
|
203
|
+
meaning that by keeping only a percentage of the results with the smallest X we
|
|
204
|
+
remove many of the outliers.
|
|
205
|
+
"""
|
|
206
|
+
LOGGER.info(f"Number of results before filtering: {len(self.results)}")
|
|
207
|
+
sorted_results = sorted(self.results, key=lambda item: item.centre_px[0])
|
|
208
|
+
|
|
209
|
+
worst_results = [
|
|
210
|
+
r.uuid for r in sorted_results[-self.NUMBER_OF_WRONG_RESULTS_TO_LOG :]
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
LOGGER.info(
|
|
214
|
+
f"Worst {self.NUMBER_OF_WRONG_RESULTS_TO_LOG} murko results were {worst_results}"
|
|
215
|
+
)
|
|
216
|
+
cutoff = max(1, int(len(sorted_results) * self.PERCENTAGE_TO_USE / 100))
|
|
217
|
+
smallest_x = sorted_results[:cutoff]
|
|
218
|
+
self.results = smallest_x
|
|
219
|
+
LOGGER.info(f"Number of results after filtering: {len(self.results)}")
|
|
220
|
+
|
|
175
221
|
|
|
176
222
|
def get_yz_least_squares(vertical_dists: list, omegas: list) -> tuple[float, float]:
|
|
177
223
|
"""Get the least squares solution for y and z from the vertical distances and omega angles.
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import csv
|
|
3
|
+
import io
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, SupportsFloat
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
from bluesky.protocols import Movable
|
|
10
|
+
from daq_config_server.client import ConfigServer
|
|
9
11
|
from ophyd_async.core import (
|
|
10
12
|
AsyncStatus,
|
|
11
13
|
Device,
|
|
@@ -15,13 +17,14 @@ from ophyd_async.core import (
|
|
|
15
17
|
soft_signal_r_and_setter,
|
|
16
18
|
soft_signal_rw,
|
|
17
19
|
)
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, RootModel
|
|
18
21
|
|
|
19
22
|
from dodal.log import LOGGER
|
|
20
23
|
|
|
21
24
|
from ..apple2_undulator import (
|
|
22
25
|
Apple2,
|
|
23
26
|
Apple2Val,
|
|
24
|
-
|
|
27
|
+
EnergyMotorConvertor,
|
|
25
28
|
Pol,
|
|
26
29
|
UndulatorGap,
|
|
27
30
|
UndulatorJawPhase,
|
|
@@ -34,6 +37,7 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
|
34
37
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
35
38
|
DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
|
|
36
39
|
ALPHA_OFFSET = 180
|
|
40
|
+
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
# data class to store the lookup table configuration that is use in convert_csv_to_lookup
|
|
@@ -53,36 +57,75 @@ class LookupTableConfig:
|
|
|
53
57
|
poly_deg: list | None
|
|
54
58
|
|
|
55
59
|
|
|
56
|
-
class
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
class EnergyMinMax(BaseModel):
|
|
61
|
+
Minimum: float
|
|
62
|
+
Maximum: float
|
|
59
63
|
|
|
60
|
-
A pair of look up tables are needed to provide the conversion betwApple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
|
|
61
|
-
each bank of magnet can move independently to each other,
|
|
62
|
-
which allow the production of different x-ray polarisation as well as energy.
|
|
63
|
-
This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.een motor position and energy.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
class EnergyCoverageEntry(BaseModel):
|
|
66
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
67
|
+
Low: float
|
|
68
|
+
High: float
|
|
69
|
+
Poly: np.poly1d
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EnergyCoverage(RootModel):
|
|
73
|
+
root: dict[str, EnergyCoverageEntry]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LookupTableEntries(BaseModel):
|
|
77
|
+
Energies: EnergyCoverage
|
|
78
|
+
Limit: EnergyMinMax
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Lookuptable(RootModel):
|
|
82
|
+
"""BaseModel class for the lookup table.
|
|
83
|
+
Apple2 lookup table should be in this format.
|
|
84
|
+
|
|
85
|
+
{mode: {'Energies': {Any: {'Low': float,
|
|
86
|
+
'High': float,
|
|
87
|
+
'Poly':np.poly1d
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
'Limit': {'Minimum': float,
|
|
91
|
+
'Maximum': float
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
root: dict[str, LookupTableEntries]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class I10EnergyMotorLookup:
|
|
101
|
+
"""
|
|
102
|
+
Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
|
|
103
|
+
and phase. Fetches and parses lookup tables from a config server, supports dynamic
|
|
104
|
+
updates, and validates input.
|
|
66
105
|
"""
|
|
67
106
|
|
|
68
107
|
def __init__(
|
|
69
108
|
self,
|
|
70
109
|
look_up_table_dir: str,
|
|
71
110
|
source: tuple[str, str],
|
|
72
|
-
|
|
111
|
+
config_client: ConfigServer,
|
|
73
112
|
mode: str = "Mode",
|
|
74
113
|
min_energy: str = "MinEnergy",
|
|
75
114
|
max_energy: str = "MaxEnergy",
|
|
115
|
+
gap_file_name: str = "IDEnergy2GapCalibrations.csv",
|
|
116
|
+
phase_file_name: str = "IDEnergy2PhaseCalibrations.csv",
|
|
76
117
|
poly_deg: list | None = None,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
):
|
|
119
|
+
"""Initialise the I10EnergyMotorLookup class with lookup table headers provided.
|
|
120
|
+
|
|
80
121
|
Parameters
|
|
81
122
|
----------
|
|
82
123
|
look_up_table_dir:
|
|
83
124
|
The path to look up table.
|
|
84
125
|
source:
|
|
85
|
-
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
126
|
+
The column name and the name of the source in look up table. e.g. ( "source", "idu")
|
|
127
|
+
config_client:
|
|
128
|
+
The config server client to fetch the look up table.
|
|
86
129
|
mode:
|
|
87
130
|
The column name of the mode in look up table.
|
|
88
131
|
min_energy:
|
|
@@ -91,17 +134,14 @@ class I10Apple2(Apple2):
|
|
|
91
134
|
The column name that contain the maximum energy in look up table.
|
|
92
135
|
poly_deg:
|
|
93
136
|
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
94
|
-
prefix:
|
|
95
|
-
epic pv for id
|
|
96
|
-
Name:
|
|
97
|
-
Name of the device
|
|
98
|
-
"""
|
|
99
137
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
"""
|
|
139
|
+
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
140
|
+
"Gap": {},
|
|
141
|
+
"Phase": {},
|
|
142
|
+
}
|
|
143
|
+
energy_gap_table_path = Path(look_up_table_dir, gap_file_name)
|
|
144
|
+
energy_phase_table_path = Path(look_up_table_dir, phase_file_name)
|
|
105
145
|
self.lookup_table_config = LookupTableConfig(
|
|
106
146
|
path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
|
|
107
147
|
source=source,
|
|
@@ -110,6 +150,212 @@ class I10Apple2(Apple2):
|
|
|
110
150
|
max_energy=max_energy,
|
|
111
151
|
poly_deg=poly_deg,
|
|
112
152
|
)
|
|
153
|
+
self.config_client = config_client
|
|
154
|
+
self._available_pol = []
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def available_pol(self) -> list[str | None]:
|
|
158
|
+
return self._available_pol
|
|
159
|
+
|
|
160
|
+
@available_pol.setter
|
|
161
|
+
def available_pol(self, value: list[str | None]) -> None:
|
|
162
|
+
self._available_pol = value
|
|
163
|
+
|
|
164
|
+
def update_lookuptable(self):
|
|
165
|
+
"""
|
|
166
|
+
Update lookup tables from files and validate their format.
|
|
167
|
+
"""
|
|
168
|
+
LOGGER.info("Updating lookup dictionary from file.")
|
|
169
|
+
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
170
|
+
self.lookup_tables[key] = self.convert_csv_to_lookup(
|
|
171
|
+
file=path,
|
|
172
|
+
source=self.lookup_table_config.source,
|
|
173
|
+
mode=self.lookup_table_config.mode,
|
|
174
|
+
min_energy=self.lookup_table_config.min_energy,
|
|
175
|
+
max_energy=self.lookup_table_config.max_energy,
|
|
176
|
+
poly_deg=self.lookup_table_config.poly_deg,
|
|
177
|
+
)
|
|
178
|
+
Lookuptable.model_validate(self.lookup_tables[key])
|
|
179
|
+
|
|
180
|
+
self.available_pol = list(self.lookup_tables["Gap"].keys())
|
|
181
|
+
|
|
182
|
+
def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
|
|
183
|
+
"""
|
|
184
|
+
Convert energy and polarisation to gap and phase motor positions.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
energy : float
|
|
189
|
+
Desired energy in eV.
|
|
190
|
+
pol : Pol
|
|
191
|
+
Polarisation mode.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
tuple[float, float]
|
|
196
|
+
(gap, phase) motor positions.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
if self.available_pol == []:
|
|
200
|
+
self.update_lookuptable()
|
|
201
|
+
|
|
202
|
+
gap_poly = self._get_poly(
|
|
203
|
+
lookup_table=self.lookup_tables["Gap"], energy=energy, pol=pol
|
|
204
|
+
)
|
|
205
|
+
phase_poly = self._get_poly(
|
|
206
|
+
lookup_table=self.lookup_tables["Phase"], energy=energy, pol=pol
|
|
207
|
+
)
|
|
208
|
+
return gap_poly(energy), phase_poly(energy)
|
|
209
|
+
|
|
210
|
+
def _get_poly(
|
|
211
|
+
self,
|
|
212
|
+
energy: float,
|
|
213
|
+
pol: Pol,
|
|
214
|
+
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
215
|
+
) -> np.poly1d:
|
|
216
|
+
"""
|
|
217
|
+
Get polynomial for a given energy and polarisation.
|
|
218
|
+
|
|
219
|
+
Raises
|
|
220
|
+
------
|
|
221
|
+
ValueError
|
|
222
|
+
If energy is out of bounds or coefficients are missing.
|
|
223
|
+
"""
|
|
224
|
+
if (
|
|
225
|
+
energy < lookup_table[pol]["Limit"]["Minimum"]
|
|
226
|
+
or energy > lookup_table[pol]["Limit"]["Maximum"]
|
|
227
|
+
):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"Demanding energy must lie between {} and {} eV!".format(
|
|
230
|
+
lookup_table[pol]["Limit"]["Minimum"],
|
|
231
|
+
lookup_table[pol]["Limit"]["Maximum"],
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
for energy_range in lookup_table[pol]["Energies"].values():
|
|
236
|
+
if energy >= energy_range["Low"] and energy < energy_range["High"]:
|
|
237
|
+
return energy_range["Poly"]
|
|
238
|
+
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"""Cannot find polynomial coefficients for your requested energy.
|
|
241
|
+
There might be gap in the calibration lookup table."""
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def convert_csv_to_lookup(
|
|
245
|
+
self,
|
|
246
|
+
file: str,
|
|
247
|
+
source: tuple[str, str],
|
|
248
|
+
mode: str | None = "Mode",
|
|
249
|
+
min_energy: str | None = "MinEnergy",
|
|
250
|
+
max_energy: str | None = "MaxEnergy",
|
|
251
|
+
poly_deg: list | None = None,
|
|
252
|
+
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
253
|
+
"""
|
|
254
|
+
Convert a CSV file to a lookup table dictionary.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
dict
|
|
259
|
+
Dictionary in Apple2 lookup table format.
|
|
260
|
+
|
|
261
|
+
Raises
|
|
262
|
+
------
|
|
263
|
+
RuntimeError
|
|
264
|
+
If the CSV cannot be converted.
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
if poly_deg is None:
|
|
268
|
+
poly_deg = [
|
|
269
|
+
"7th-order",
|
|
270
|
+
"6th-order",
|
|
271
|
+
"5th-order",
|
|
272
|
+
"4th-order",
|
|
273
|
+
"3rd-order",
|
|
274
|
+
"2nd-order",
|
|
275
|
+
"1st-order",
|
|
276
|
+
"b",
|
|
277
|
+
]
|
|
278
|
+
lookup_table = {}
|
|
279
|
+
polarisations = set()
|
|
280
|
+
|
|
281
|
+
def process_row(row: dict) -> None:
|
|
282
|
+
"""Process a single row from the CSV file and update the lookup table."""
|
|
283
|
+
mode_value = row[mode]
|
|
284
|
+
if mode_value not in polarisations:
|
|
285
|
+
polarisations.add(mode_value)
|
|
286
|
+
lookup_table[mode_value] = {
|
|
287
|
+
"Energies": {},
|
|
288
|
+
"Limit": {
|
|
289
|
+
"Minimum": float(row[min_energy]),
|
|
290
|
+
"Maximum": float(row[max_energy]),
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Create polynomial object for energy-to-gap/phase conversion
|
|
295
|
+
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
296
|
+
polynomial = np.poly1d(coefficients)
|
|
297
|
+
|
|
298
|
+
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
299
|
+
"Low": float(row[min_energy]),
|
|
300
|
+
"High": float(row[max_energy]),
|
|
301
|
+
"Poly": polynomial,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Update energy limits
|
|
305
|
+
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
306
|
+
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
307
|
+
)
|
|
308
|
+
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
309
|
+
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
csv_file = self.config_client.get_file_contents(file, reset_cached_result=True)
|
|
313
|
+
reader = csv.DictReader(io.StringIO(csv_file))
|
|
314
|
+
for row in reader:
|
|
315
|
+
# If there are multiple source only convert requested.
|
|
316
|
+
if row[source[0]] == source[1]:
|
|
317
|
+
process_row(row=row)
|
|
318
|
+
if not lookup_table:
|
|
319
|
+
raise RuntimeError(f"Unable to convert lookup table:\t{file}")
|
|
320
|
+
return lookup_table
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class I10Apple2(Apple2):
|
|
324
|
+
"""I10Apple2 is the i10 version of Apple2 ID, set and energy_motor_convertor
|
|
325
|
+
should be the only part that is I10 specific.
|
|
326
|
+
|
|
327
|
+
A EnergyMotorConvertor function is needed to provide the conversion between
|
|
328
|
+
x-ray motor position and energy.
|
|
329
|
+
|
|
330
|
+
Set is in energy(eV).
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(
|
|
334
|
+
self,
|
|
335
|
+
prefix: str,
|
|
336
|
+
energy_motor_convertor: EnergyMotorConvertor,
|
|
337
|
+
name: str = "",
|
|
338
|
+
) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
look_up_table_dir:
|
|
343
|
+
The path to look up table.
|
|
344
|
+
source:
|
|
345
|
+
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
346
|
+
mode:
|
|
347
|
+
The column name of the mode in look up table.
|
|
348
|
+
min_energy:
|
|
349
|
+
The column name that contain the maximum energy in look up table.
|
|
350
|
+
max_energy:
|
|
351
|
+
The column name that contain the maximum energy in look up table.
|
|
352
|
+
poly_deg:
|
|
353
|
+
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
354
|
+
prefix:
|
|
355
|
+
epic pv for id
|
|
356
|
+
Name:
|
|
357
|
+
Name of the device
|
|
358
|
+
"""
|
|
113
359
|
|
|
114
360
|
with self.add_children_as_readables():
|
|
115
361
|
super().__init__(
|
|
@@ -122,6 +368,7 @@ class I10Apple2(Apple2):
|
|
|
122
368
|
btm_inner="RPQ3",
|
|
123
369
|
btm_outer="RPQ4",
|
|
124
370
|
),
|
|
371
|
+
energy_motor_convertor=energy_motor_convertor,
|
|
125
372
|
name=name,
|
|
126
373
|
)
|
|
127
374
|
self.id_jaw_phase = UndulatorJawPhase(
|
|
@@ -150,7 +397,7 @@ class I10Apple2(Apple2):
|
|
|
150
397
|
)
|
|
151
398
|
|
|
152
399
|
self._set_pol_setpoint(pol)
|
|
153
|
-
gap, phase =
|
|
400
|
+
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
154
401
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
155
402
|
id_set_val = Apple2Val(
|
|
156
403
|
top_outer=f"{phase:.6f}",
|
|
@@ -167,29 +414,6 @@ class I10Apple2(Apple2):
|
|
|
167
414
|
await self.id_jaw_phase.set_move.set(1)
|
|
168
415
|
LOGGER.info(f"Energy set to {value} eV successfully.")
|
|
169
416
|
|
|
170
|
-
def update_lookuptable(self):
|
|
171
|
-
"""
|
|
172
|
-
Update the stored lookup tabled from file.
|
|
173
|
-
|
|
174
|
-
"""
|
|
175
|
-
LOGGER.info("Updating lookup dictionary from file.")
|
|
176
|
-
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
177
|
-
if path.exists():
|
|
178
|
-
self.lookup_tables[key] = convert_csv_to_lookup(
|
|
179
|
-
file=path,
|
|
180
|
-
source=self.lookup_table_config.source,
|
|
181
|
-
mode=self.lookup_table_config.mode,
|
|
182
|
-
min_energy=self.lookup_table_config.min_energy,
|
|
183
|
-
max_energy=self.lookup_table_config.max_energy,
|
|
184
|
-
poly_deg=self.lookup_table_config.poly_deg,
|
|
185
|
-
)
|
|
186
|
-
# ensure the importing lookup table is the correct format
|
|
187
|
-
Lookuptable.model_validate(self.lookup_tables[key])
|
|
188
|
-
else:
|
|
189
|
-
raise FileNotFoundError(f"{key} look up table is not in path: {path}")
|
|
190
|
-
|
|
191
|
-
self._available_pol = list(self.lookup_tables["Gap"].keys())
|
|
192
|
-
|
|
193
417
|
|
|
194
418
|
class EnergySetter(StandardReadable, Movable[float]):
|
|
195
419
|
"""
|
|
@@ -250,7 +474,8 @@ class I10Apple2Pol(StandardReadable, Movable[Pol]):
|
|
|
250
474
|
@AsyncStatus.wrap
|
|
251
475
|
async def set(self, value: Pol) -> None:
|
|
252
476
|
LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
|
|
253
|
-
|
|
477
|
+
# Timeout is determined internally by the set method later, so we set it to max here.
|
|
478
|
+
await self.id_ref().polarisation.set(value, timeout=MAXIMUM_MOVE_TIME)
|
|
254
479
|
|
|
255
480
|
|
|
256
481
|
class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
@@ -320,6 +545,7 @@ class I10Id(Device):
|
|
|
320
545
|
prefix: str,
|
|
321
546
|
look_up_table_dir: str,
|
|
322
547
|
source: tuple[str, str],
|
|
548
|
+
config_client: ConfigServer,
|
|
323
549
|
jaw_phase_limit=12.0,
|
|
324
550
|
jaw_phase_poly_param=DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
325
551
|
angle_threshold_deg=30.0,
|
|
@@ -341,12 +567,16 @@ class I10Id(Device):
|
|
|
341
567
|
linear_arbitrary_angle : LinearArbitraryAngle
|
|
342
568
|
A device for controlling the linear arbitrary polarization angle.
|
|
343
569
|
"""
|
|
570
|
+
self.lookup_table_client = I10EnergyMotorLookup(
|
|
571
|
+
look_up_table_dir=look_up_table_dir,
|
|
572
|
+
source=source,
|
|
573
|
+
config_client=config_client,
|
|
574
|
+
)
|
|
344
575
|
self.energy = EnergySetter(
|
|
345
576
|
id=I10Apple2(
|
|
346
|
-
look_up_table_dir=look_up_table_dir,
|
|
347
|
-
name="id_energy",
|
|
348
|
-
source=source,
|
|
349
577
|
prefix=prefix,
|
|
578
|
+
energy_motor_convertor=self.lookup_table_client.get_motor_from_energy,
|
|
579
|
+
name="id_energy",
|
|
350
580
|
),
|
|
351
581
|
pgm=pgm,
|
|
352
582
|
name="energy",
|
|
@@ -361,91 +591,3 @@ class I10Id(Device):
|
|
|
361
591
|
)
|
|
362
592
|
|
|
363
593
|
super().__init__(name=name)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def convert_csv_to_lookup(
|
|
367
|
-
file: str,
|
|
368
|
-
source: tuple[str, str],
|
|
369
|
-
mode: str | None = "Mode",
|
|
370
|
-
min_energy: str | None = "MinEnergy",
|
|
371
|
-
max_energy: str | None = "MaxEnergy",
|
|
372
|
-
poly_deg: list | None = None,
|
|
373
|
-
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
374
|
-
"""
|
|
375
|
-
Convert a CSV file to a dictionary compatible with the Apple2 lookup table format.
|
|
376
|
-
|
|
377
|
-
Parameters
|
|
378
|
-
----------
|
|
379
|
-
file : str
|
|
380
|
-
Path to the CSV file.
|
|
381
|
-
source : tuple[str, str]
|
|
382
|
-
Tuple specifying the column name and source name (e.g., ("Source", "idu")).
|
|
383
|
-
mode : str, optional
|
|
384
|
-
Column name for the available modes (e.g., "lv", "lh", "pc", "nc"), by default "Mode".
|
|
385
|
-
min_energy : str, optional
|
|
386
|
-
Column name for the minimum energy, by default "MinEnergy".
|
|
387
|
-
max_energy : str, optional
|
|
388
|
-
Column name for the maximum energy, by default "MaxEnergy".
|
|
389
|
-
poly_deg : list, optional
|
|
390
|
-
Column names for polynomial coefficients, starting with the least significant term.
|
|
391
|
-
|
|
392
|
-
Returns
|
|
393
|
-
-------
|
|
394
|
-
dict
|
|
395
|
-
A dictionary conforming to the Apple2 lookup table format.
|
|
396
|
-
|
|
397
|
-
"""
|
|
398
|
-
if poly_deg is None:
|
|
399
|
-
poly_deg = [
|
|
400
|
-
"7th-order",
|
|
401
|
-
"6th-order",
|
|
402
|
-
"5th-order",
|
|
403
|
-
"4th-order",
|
|
404
|
-
"3rd-order",
|
|
405
|
-
"2nd-order",
|
|
406
|
-
"1st-order",
|
|
407
|
-
"b",
|
|
408
|
-
]
|
|
409
|
-
lookup_table = {}
|
|
410
|
-
polarisations = set()
|
|
411
|
-
|
|
412
|
-
def process_row(row: dict) -> None:
|
|
413
|
-
"""Process a single row from the CSV file and update the lookup table."""
|
|
414
|
-
mode_value = row[mode]
|
|
415
|
-
if mode_value not in polarisations:
|
|
416
|
-
polarisations.add(mode_value)
|
|
417
|
-
lookup_table[mode_value] = {
|
|
418
|
-
"Energies": {},
|
|
419
|
-
"Limit": {
|
|
420
|
-
"Minimum": float(row[min_energy]),
|
|
421
|
-
"Maximum": float(row[max_energy]),
|
|
422
|
-
},
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
# Create polynomial object for energy-to-gap/phase conversion
|
|
426
|
-
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
427
|
-
polynomial = np.poly1d(coefficients)
|
|
428
|
-
|
|
429
|
-
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
430
|
-
"Low": float(row[min_energy]),
|
|
431
|
-
"High": float(row[max_energy]),
|
|
432
|
-
"Poly": polynomial,
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
# Update energy limits
|
|
436
|
-
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
437
|
-
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
438
|
-
)
|
|
439
|
-
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
440
|
-
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
with open(file, newline="") as csvfile:
|
|
444
|
-
reader = csv.DictReader(csvfile)
|
|
445
|
-
for row in reader:
|
|
446
|
-
# If there are multiple source only convert requested.
|
|
447
|
-
if row[source[0]] == source[1]:
|
|
448
|
-
process_row(row=row)
|
|
449
|
-
if not lookup_table:
|
|
450
|
-
raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
|
|
451
|
-
return lookup_table
|