dls-dodal 1.65.0__py3-none-any.whl → 1.66.0__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.65.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +56 -50
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +23 -3
- dodal/beamlines/i04.py +18 -3
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +7 -2
- dodal/beamlines/i10_optics.py +18 -8
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/p38.py +3 -3
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/apple2_undulator.py +89 -44
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/fast_grid_scan.py +14 -2
- dodal/devices/i03/beamsize.py +35 -0
- dodal/devices/i03/constants.py +7 -0
- dodal/devices/i03/undulator_dcm.py +2 -2
- dodal/devices/i04/beamsize.py +45 -0
- dodal/devices/i04/murko_results.py +36 -26
- dodal/devices/i04/transfocator.py +23 -29
- dodal/devices/i07/id.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +6 -2
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i10/i10_apple2.py +22 -316
- dodal/devices/i17/i17_apple2.py +7 -4
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +19 -3
- dodal/devices/mx_phase1/beamstop.py +31 -12
- dodal/devices/oav/oav_calculations.py +9 -4
- dodal/devices/oav/oav_detector.py +65 -7
- dodal/devices/oav/oav_parameters.py +3 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
- dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
- dodal/devices/oav/pin_image_recognition/utils.py +23 -1
- dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
- dodal/devices/oav/utils.py +16 -6
- dodal/devices/robot.py +17 -7
- dodal/devices/scintillator.py +36 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +152 -68
- dodal/devices/util/lookup_tables_apple2.py +390 -0
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from ophyd_async.core import Reference, derived_signal_r
|
|
2
|
+
|
|
3
|
+
from dodal.devices.aperturescatterguard import ApertureScatterguard
|
|
4
|
+
from dodal.devices.beamsize.beamsize import BeamsizeBase
|
|
5
|
+
from dodal.devices.i04.transfocator import Transfocator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Beamsize(BeamsizeBase):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
transfocator: Transfocator,
|
|
12
|
+
aperture_scatterguard: ApertureScatterguard,
|
|
13
|
+
name="",
|
|
14
|
+
):
|
|
15
|
+
super().__init__(name=name)
|
|
16
|
+
self._transfocator_ref = Reference(transfocator)
|
|
17
|
+
self._aperture_scatterguard_ref = Reference(aperture_scatterguard)
|
|
18
|
+
|
|
19
|
+
with self.add_children_as_readables():
|
|
20
|
+
self.x_um = derived_signal_r(
|
|
21
|
+
self._get_beamsize_x,
|
|
22
|
+
transfocator_size_x=self._transfocator_ref().current_horizontal_size_rbv,
|
|
23
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
24
|
+
derived_units="µm",
|
|
25
|
+
)
|
|
26
|
+
self.y_um = derived_signal_r(
|
|
27
|
+
self._get_beamsize_y,
|
|
28
|
+
transfocator_size_y=self._transfocator_ref().current_vertical_size_rbv,
|
|
29
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
30
|
+
derived_units="µm",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _get_beamsize_x(
|
|
34
|
+
self,
|
|
35
|
+
transfocator_size_x: float,
|
|
36
|
+
aperture_radius: float,
|
|
37
|
+
) -> float:
|
|
38
|
+
return min(transfocator_size_x, aperture_radius)
|
|
39
|
+
|
|
40
|
+
def _get_beamsize_y(
|
|
41
|
+
self,
|
|
42
|
+
transfocator_size_y: float,
|
|
43
|
+
aperture_radius: float,
|
|
44
|
+
) -> float:
|
|
45
|
+
return min(transfocator_size_y, aperture_radius)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import pickle
|
|
3
|
+
import time
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from typing import TypedDict
|
|
@@ -21,6 +22,7 @@ from dodal.devices.oav.oav_calculations import (
|
|
|
21
22
|
from dodal.log import LOGGER
|
|
22
23
|
|
|
23
24
|
NO_MURKO_RESULT = (-1, -1)
|
|
25
|
+
RESULTS_COMPLETE_MESSAGE = "murko_results_complete"
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class MurkoMetadata(TypedDict):
|
|
@@ -71,7 +73,8 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
71
73
|
solutions for y and z can be calculated using numpy's linear algebra library.
|
|
72
74
|
"""
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
GET_MESSAGE_TIMEOUT_S = 2
|
|
77
|
+
RESULTS_COMPLETE_TIMEOUT_S = 5
|
|
75
78
|
PERCENTAGE_TO_USE = 25
|
|
76
79
|
LEFTMOST_PIXEL_TO_USE = 10
|
|
77
80
|
NUMBER_OF_WRONG_RESULTS_TO_LOG = 5
|
|
@@ -82,7 +85,6 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
82
85
|
redis_password=RedisConstants.REDIS_PASSWORD,
|
|
83
86
|
redis_db=RedisConstants.MURKO_REDIS_DB,
|
|
84
87
|
name="",
|
|
85
|
-
stop_angle=350,
|
|
86
88
|
):
|
|
87
89
|
self.redis_client = StrictRedis(
|
|
88
90
|
host=redis_host,
|
|
@@ -91,7 +93,6 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
91
93
|
)
|
|
92
94
|
self.pubsub = self.redis_client.pubsub()
|
|
93
95
|
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
94
|
-
self.stop_angle = stop_angle
|
|
95
96
|
|
|
96
97
|
self._reset()
|
|
97
98
|
|
|
@@ -103,7 +104,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
103
104
|
super().__init__(name=name)
|
|
104
105
|
|
|
105
106
|
def _reset(self):
|
|
106
|
-
self._last_omega =
|
|
107
|
+
self._last_omega = None
|
|
107
108
|
self._results: list[MurkoResult] = []
|
|
108
109
|
|
|
109
110
|
@AsyncStatus.wrap
|
|
@@ -120,14 +121,27 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
120
121
|
|
|
121
122
|
@AsyncStatus.wrap
|
|
122
123
|
async def trigger(self):
|
|
123
|
-
# Wait for results
|
|
124
124
|
sample_id = await self.sample_id.get_value()
|
|
125
|
-
|
|
125
|
+
t_last_result = time.time()
|
|
126
|
+
while True:
|
|
127
|
+
if time.time() - t_last_result > self.RESULTS_COMPLETE_TIMEOUT_S:
|
|
128
|
+
LOGGER.warning(
|
|
129
|
+
f"Time since last result > {self.RESULTS_COMPLETE_TIMEOUT_S}, expected to receive {RESULTS_COMPLETE_MESSAGE}"
|
|
130
|
+
)
|
|
131
|
+
break
|
|
126
132
|
# waits here for next batch to be received
|
|
127
|
-
message = await self.pubsub.get_message(timeout=self.
|
|
128
|
-
if message
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
message = await self.pubsub.get_message(timeout=self.GET_MESSAGE_TIMEOUT_S)
|
|
134
|
+
if message and message["type"] == "message":
|
|
135
|
+
t_last_result = time.time()
|
|
136
|
+
data = pickle.loads(message["data"])
|
|
137
|
+
|
|
138
|
+
if data == RESULTS_COMPLETE_MESSAGE:
|
|
139
|
+
LOGGER.info(
|
|
140
|
+
f"Received results complete message: {RESULTS_COMPLETE_MESSAGE}"
|
|
141
|
+
)
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
await self.process_batch(data, sample_id)
|
|
131
145
|
|
|
132
146
|
if not self._results:
|
|
133
147
|
raise NoResultsFoundError("No results retrieved from Murko")
|
|
@@ -155,22 +169,18 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
155
169
|
f"murko:{sample_id}:metadata", result.uuid, json.dumps(result.metadata)
|
|
156
170
|
)
|
|
157
171
|
|
|
158
|
-
async def process_batch(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
result, MurkoMetadata(json.loads(metadata_str))
|
|
171
|
-
)
|
|
172
|
-
else:
|
|
173
|
-
LOGGER.info(f"Found no metadata for uuid {uuid}")
|
|
172
|
+
async def process_batch(
|
|
173
|
+
self, batch_results: list[tuple[str, dict]], sample_id: str
|
|
174
|
+
):
|
|
175
|
+
for result_with_uuid in batch_results:
|
|
176
|
+
uuid, result = result_with_uuid
|
|
177
|
+
if metadata_str := await self.redis_client.hget( # type: ignore
|
|
178
|
+
f"murko:{sample_id}:metadata", uuid
|
|
179
|
+
):
|
|
180
|
+
LOGGER.info(f"Found metadata for uuid {uuid}, processing result")
|
|
181
|
+
self.process_result(result, MurkoMetadata(json.loads(metadata_str)))
|
|
182
|
+
else:
|
|
183
|
+
LOGGER.info(f"Found no metadata for uuid {uuid}")
|
|
174
184
|
|
|
175
185
|
def process_result(self, result: dict, metadata: MurkoMetadata):
|
|
176
186
|
"""Uses the 'most_likely_click' coordinates from Murko to calculate the
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import math
|
|
3
2
|
|
|
4
3
|
from ophyd_async.core import (
|
|
5
4
|
AsyncStatus,
|
|
@@ -9,6 +8,7 @@ from ophyd_async.core import (
|
|
|
9
8
|
)
|
|
10
9
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
11
10
|
|
|
11
|
+
from dodal.common.device_utils import periodic_reminder
|
|
12
12
|
from dodal.log import LOGGER
|
|
13
13
|
|
|
14
14
|
|
|
@@ -23,15 +23,15 @@ class Transfocator(StandardReadable):
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
def __init__(self, prefix: str, name: str = ""):
|
|
26
|
+
self._vert_size_calc_sp = epics_signal_rw(float, prefix + "VERT_REQ")
|
|
27
|
+
self._num_lenses_calc_rbv = epics_signal_r(float, prefix + "LENS_PRED")
|
|
28
|
+
self.start = epics_signal_rw(int, prefix + "START.PROC")
|
|
29
|
+
self.start_rbv = epics_signal_r(int, prefix + "START_RBV")
|
|
30
|
+
|
|
26
31
|
with self.add_children_as_readables():
|
|
27
|
-
self.beamsize_set_microns = epics_signal_rw(float, prefix + "VERT_REQ")
|
|
28
|
-
self.predicted_vertical_num_lenses = epics_signal_rw(
|
|
29
|
-
float, prefix + "LENS_PRED"
|
|
30
|
-
)
|
|
31
32
|
self.number_filters_sp = epics_signal_rw(int, prefix + "NUM_FILTERS")
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.vertical_lens_rbv = epics_signal_r(float, prefix + "VER")
|
|
33
|
+
self.current_horizontal_size_rbv = epics_signal_r(float, prefix + "HOR")
|
|
34
|
+
self.current_vertical_size_rbv = epics_signal_r(float, prefix + "VER")
|
|
35
35
|
|
|
36
36
|
self.TIMEOUT = 120
|
|
37
37
|
|
|
@@ -41,14 +41,10 @@ class Transfocator(StandardReadable):
|
|
|
41
41
|
# We can only put an integer number of lenses in the beam but the
|
|
42
42
|
# calculation in the IOC returns the theoretical float number of lenses
|
|
43
43
|
value = round(value)
|
|
44
|
-
LOGGER.info(f"Transfocator setting {value} filters")
|
|
45
44
|
await self.number_filters_sp.set(value)
|
|
46
45
|
await self.start.set(1)
|
|
47
|
-
LOGGER.info("Waiting for start_rbv to change to 1")
|
|
48
46
|
await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
|
|
49
|
-
LOGGER.info("Waiting for start_rbv to change to 0")
|
|
50
47
|
await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
|
|
51
|
-
self.latest_pred_vertical_num_lenses = value
|
|
52
48
|
|
|
53
49
|
@AsyncStatus.wrap
|
|
54
50
|
async def set(self, value: float):
|
|
@@ -59,29 +55,27 @@ class Transfocator(StandardReadable):
|
|
|
59
55
|
4. Start the device moving
|
|
60
56
|
5. Wait for the start_rbv goes high and low again
|
|
61
57
|
"""
|
|
62
|
-
self.latest_pred_vertical_num_lenses = (
|
|
63
|
-
await self.predicted_vertical_num_lenses.get_value()
|
|
64
|
-
)
|
|
65
|
-
|
|
66
58
|
LOGGER.info(f"Transfocator setting {value} beamsize")
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
# Logic in the IOC calculates _num_lenses_calc_rbv when _vert_size_calc_sp changes
|
|
61
|
+
|
|
62
|
+
# Register an observer before setting _vert_size_calc_sp to ensure we don't miss changes
|
|
63
|
+
num_lenses_calc_iterator = observe_value(
|
|
64
|
+
self._num_lenses_calc_rbv, timeout=self.TIMEOUT
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await anext(num_lenses_calc_iterator)
|
|
68
|
+
await self._vert_size_calc_sp.set(value)
|
|
69
|
+
calc_lenses = await anext(num_lenses_calc_iterator)
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
# Keep initial prediction before setting to later compare with change after setting
|
|
76
|
-
current_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
77
|
-
await self.beamsize_set_microns.set(value)
|
|
78
|
-
accepted_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
79
|
-
if not math.isclose(current_prediction, accepted_prediction, abs_tol=1e-8):
|
|
80
|
-
await self.set_based_on_prediction(accepted_prediction)
|
|
71
|
+
async with periodic_reminder(
|
|
72
|
+
f"Waiting for transfocator to insert {calc_lenses} into beam"
|
|
73
|
+
):
|
|
74
|
+
await self.set_based_on_prediction(calc_lenses)
|
|
81
75
|
|
|
82
76
|
number_filters_rbv, vertical_lens_size_rbv = await asyncio.gather(
|
|
83
77
|
self.number_filters_sp.get_value(),
|
|
84
|
-
self.
|
|
78
|
+
self.current_vertical_size_rbv.get_value(),
|
|
85
79
|
)
|
|
86
80
|
|
|
87
81
|
LOGGER.info(
|
dodal/devices/i07/id.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from dodal.devices.undulator import UndulatorInKeV, UndulatorOrder
|
|
4
|
+
from dodal.devices.util.lookup_tables import energy_distance_table
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InsertionDevice(UndulatorInKeV):
|
|
8
|
+
"""
|
|
9
|
+
Insertion device for i07 including beamline-specific energy-gap lookup behaviour
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
name: str,
|
|
15
|
+
prefix: str,
|
|
16
|
+
harmonic: UndulatorOrder,
|
|
17
|
+
id_gap_lookup_table_path: str = "/dls_sw/i07/software/gda/config/lookupTables/"
|
|
18
|
+
+ "IIDCalibrationTable.txt",
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(prefix, id_gap_lookup_table_path, name=name)
|
|
21
|
+
self.harmonic = harmonic
|
|
22
|
+
|
|
23
|
+
async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
|
|
24
|
+
"""
|
|
25
|
+
i07's energy scans remain on a particular harmonic while changing energy. The
|
|
26
|
+
calibration table has one row for each harmonic, row contains max and min
|
|
27
|
+
energies and their corresponding ID gaps. The requested energy is used to
|
|
28
|
+
interpolate between these values, assuming a linear relationship on the relevant
|
|
29
|
+
scale.
|
|
30
|
+
"""
|
|
31
|
+
energy_to_distance_table: np.ndarray = await energy_distance_table(
|
|
32
|
+
self.id_gap_lookup_table_path, comments="#", skiprows=2
|
|
33
|
+
)
|
|
34
|
+
harmonic_value: int = await self.harmonic.value.get_value()
|
|
35
|
+
|
|
36
|
+
row: np.ndarray = energy_to_distance_table[harmonic_value - 1, :]
|
|
37
|
+
gap = np.interp(energy_kev, [row[1], row[2]], [row[3], row[4]])
|
|
38
|
+
return gap
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
from .hard_undulator_functions import
|
|
1
|
+
from .hard_undulator_functions import (
|
|
2
|
+
calculate_energy_i09_hu,
|
|
3
|
+
calculate_gap_i09_hu,
|
|
4
|
+
get_hu_lut_as_dict,
|
|
5
|
+
)
|
|
2
6
|
|
|
3
|
-
__all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict"]
|
|
7
|
+
__all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict", "calculate_energy_i09_hu"]
|
|
@@ -14,11 +14,16 @@ RING_ENERGY_COLUMN = 1
|
|
|
14
14
|
MAGNET_FIELD_COLUMN = 2
|
|
15
15
|
MIN_ENERGY_COLUMN = 3
|
|
16
16
|
MAX_ENERGY_COLUMN = 4
|
|
17
|
+
MIN_GAP_COLUMN = 5
|
|
18
|
+
MAX_GAP_COLUMN = 6
|
|
17
19
|
GAP_OFFSET_COLUMN = 7
|
|
18
20
|
|
|
21
|
+
MAGNET_BLOCKS_PER_PERIOD = 4
|
|
22
|
+
MAGNTE_BLOCK_HEIGHT_MM = 16
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
async def get_hu_lut_as_dict(lut_path: str) -> dict[int, np.ndarray]:
|
|
26
|
+
lut_dict: dict[int, np.ndarray] = {}
|
|
22
27
|
_lookup_table: np.ndarray = await energy_distance_table(
|
|
23
28
|
lut_path,
|
|
24
29
|
comments=LUT_COMMENTS,
|
|
@@ -26,13 +31,44 @@ async def get_hu_lut_as_dict(lut_path: str) -> dict:
|
|
|
26
31
|
)
|
|
27
32
|
for i in range(_lookup_table.shape[0]):
|
|
28
33
|
lut_dict[_lookup_table[i][0]] = _lookup_table[i]
|
|
29
|
-
|
|
34
|
+
LOGGER.debug(f"Loaded lookup table: {lut_dict}")
|
|
30
35
|
return lut_dict
|
|
31
36
|
|
|
32
37
|
|
|
38
|
+
def _validate_order(order: int, look_up_table: dict[int, "np.ndarray"]) -> None:
|
|
39
|
+
"""Validate that the harmonic order exists in the lookup table."""
|
|
40
|
+
if order not in look_up_table.keys():
|
|
41
|
+
raise ValueError(f"Order parameter {order} not found in lookup table")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _calculate_gamma(look_up_table: dict[int, "np.ndarray"], order: int) -> float:
|
|
45
|
+
"""Calculate the Lorentz factor gamma from the lookup table."""
|
|
46
|
+
return 1000 * look_up_table[order][RING_ENERGY_COLUMN] / ELECTRON_REST_ENERGY_MEV
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _calculate_undulator_parameter_max(
|
|
50
|
+
magnet_field: float, undulator_period_mm: int
|
|
51
|
+
) -> float:
|
|
52
|
+
"""
|
|
53
|
+
Calculate the maximum undulator parameter.
|
|
54
|
+
"""
|
|
55
|
+
return (
|
|
56
|
+
(
|
|
57
|
+
2
|
|
58
|
+
* 0.0934
|
|
59
|
+
* undulator_period_mm
|
|
60
|
+
* magnet_field
|
|
61
|
+
* MAGNET_BLOCKS_PER_PERIOD
|
|
62
|
+
/ np.pi
|
|
63
|
+
)
|
|
64
|
+
* np.sin(np.pi / MAGNET_BLOCKS_PER_PERIOD)
|
|
65
|
+
* (1 - np.exp(-2 * np.pi * MAGNTE_BLOCK_HEIGHT_MM / undulator_period_mm))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
33
69
|
def calculate_gap_i09_hu(
|
|
34
70
|
photon_energy_kev: float,
|
|
35
|
-
look_up_table: dict[int,
|
|
71
|
+
look_up_table: dict[int, np.ndarray],
|
|
36
72
|
order: int = 1,
|
|
37
73
|
gap_offset: float = 0.0,
|
|
38
74
|
undulator_period_mm: int = 27,
|
|
@@ -52,13 +88,9 @@ def calculate_gap_i09_hu(
|
|
|
52
88
|
Returns:
|
|
53
89
|
float: Calculated undulator gap in millimeters.
|
|
54
90
|
"""
|
|
55
|
-
magnet_blocks_per_period = 4
|
|
56
|
-
magnet_block_height_mm = 16
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
gamma = 1000 * look_up_table[order][RING_ENERGY_COLUMN] / ELECTRON_REST_ENERGY_MEV
|
|
92
|
+
_validate_order(order, look_up_table)
|
|
93
|
+
gamma = _calculate_gamma(look_up_table, order)
|
|
62
94
|
|
|
63
95
|
# Constructive interference of radiation emitted at different poles
|
|
64
96
|
# lamda = (lambda_u/2*gamma^2)*(1+K^2/2 + gamma^2*theta^2)/n for n=1,2,3...
|
|
@@ -83,17 +115,8 @@ def calculate_gap_i09_hu(
|
|
|
83
115
|
# leading to K = 0.934*B0[T]*lambda_u[cm]*exp(-pi*gap/lambda_u) or
|
|
84
116
|
# K = undulator_parameter_max*exp(-pi*gap/lambda_u)
|
|
85
117
|
# Calculating undulator_parameter_max gives:
|
|
86
|
-
undulator_parameter_max = (
|
|
87
|
-
|
|
88
|
-
2
|
|
89
|
-
* 0.0934
|
|
90
|
-
* undulator_period_mm
|
|
91
|
-
* look_up_table[order][MAGNET_FIELD_COLUMN]
|
|
92
|
-
* magnet_blocks_per_period
|
|
93
|
-
/ np.pi
|
|
94
|
-
)
|
|
95
|
-
* np.sin(np.pi / magnet_blocks_per_period)
|
|
96
|
-
* (1 - np.exp(-2 * np.pi * magnet_block_height_mm / undulator_period_mm))
|
|
118
|
+
undulator_parameter_max = _calculate_undulator_parameter_max(
|
|
119
|
+
look_up_table[order][MAGNET_FIELD_COLUMN], undulator_period_mm
|
|
97
120
|
)
|
|
98
121
|
|
|
99
122
|
# Finnaly, rearranging the equation:
|
|
@@ -109,3 +132,44 @@ def calculate_gap_i09_hu(
|
|
|
109
132
|
)
|
|
110
133
|
|
|
111
134
|
return gap
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def calculate_energy_i09_hu(
|
|
138
|
+
gap: float,
|
|
139
|
+
look_up_table: dict[int, "np.ndarray"],
|
|
140
|
+
order: int = 1,
|
|
141
|
+
gap_offset: float = 0.0,
|
|
142
|
+
undulator_period_mm: int = 27,
|
|
143
|
+
) -> float:
|
|
144
|
+
"""
|
|
145
|
+
Calculate the photon energy produced by the undulator at a given gap and harmonic order.
|
|
146
|
+
Reverse of the calculate_gap_i09_hu function.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
gap (float): Undulator gap in millimeters.
|
|
150
|
+
look_up_table (dict[int, np.ndarray]): Lookup table containing undulator and beamline parameters for each harmonic order.
|
|
151
|
+
order (int, optional): Harmonic order for which to calculate the energy. Defaults to 1.
|
|
152
|
+
gap_offset (float, optional): Additional gap offset to apply (in mm). Defaults to 0.0.
|
|
153
|
+
undulator_period_mm (int, optional): Undulator period in mm. Defaults to 27.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
float: Calculated photon energy in keV.
|
|
157
|
+
"""
|
|
158
|
+
_validate_order(order, look_up_table)
|
|
159
|
+
|
|
160
|
+
gamma = _calculate_gamma(look_up_table, order)
|
|
161
|
+
undulator_parameter_max = _calculate_undulator_parameter_max(
|
|
162
|
+
look_up_table[order][MAGNET_FIELD_COLUMN], undulator_period_mm
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
undulator_parameter = undulator_parameter_max / np.exp(
|
|
166
|
+
(gap - look_up_table[order][GAP_OFFSET_COLUMN] - gap_offset)
|
|
167
|
+
/ (undulator_period_mm / np.pi)
|
|
168
|
+
)
|
|
169
|
+
energy_kev = (
|
|
170
|
+
4.959368e-6
|
|
171
|
+
* order
|
|
172
|
+
* np.square(gamma)
|
|
173
|
+
/ (undulator_period_mm * (np.square(undulator_parameter) + 2))
|
|
174
|
+
)
|
|
175
|
+
return energy_kev
|