dls-dodal 1.58.0__py3-none-any.whl → 1.60.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.58.0.dist-info → dls_dodal-1.60.0.dist-info}/METADATA +3 -3
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/RECORD +71 -47
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +1 -0
- 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 +5 -3
- dodal/beamlines/i04.py +5 -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/i15.py +242 -0
- dodal/beamlines/i15_1.py +156 -0
- dodal/beamlines/i19_1.py +3 -1
- dodal/beamlines/i19_2.py +12 -1
- 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/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/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/i15/dcm.py +77 -0
- dodal/devices/i15/focussing_mirror.py +71 -0
- dodal/devices/i15/jack.py +39 -0
- dodal/devices/i15/laue.py +18 -0
- dodal/devices/i15/motors.py +27 -0
- dodal/devices/i15/multilayer_mirror.py +25 -0
- dodal/devices/i15/rail.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/motors.py +52 -1
- dodal/devices/slits.py +18 -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/v2f.py +39 -0
- 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.58.0.dist-info → dls_dodal-1.60.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i24.py
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
from pathlib import PurePath
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import AutoIncrementingPathProvider, StaticFilenameProvider
|
|
4
|
+
|
|
1
5
|
from dodal.common.beamlines.beamline_utils import (
|
|
2
6
|
BL,
|
|
3
7
|
device_factory,
|
|
4
8
|
)
|
|
5
9
|
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
6
|
-
from dodal.devices.attenuator.attenuator import
|
|
10
|
+
from dodal.devices.attenuator.attenuator import EnumFilterAttenuator
|
|
11
|
+
from dodal.devices.attenuator.filter_selections import (
|
|
12
|
+
I24_FilterOneSelections,
|
|
13
|
+
I24_FilterTwoSelections,
|
|
14
|
+
)
|
|
7
15
|
from dodal.devices.hutch_shutter import HutchShutter
|
|
8
16
|
from dodal.devices.i24.aperture import Aperture
|
|
9
17
|
from dodal.devices.i24.beam_center import DetectorBeamCenter
|
|
10
18
|
from dodal.devices.i24.beamstop import Beamstop
|
|
19
|
+
from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
|
|
11
20
|
from dodal.devices.i24.dcm import DCM
|
|
12
21
|
from dodal.devices.i24.dual_backlight import DualBacklight
|
|
13
22
|
from dodal.devices.i24.focus_mirrors import FocusMirrorsMode
|
|
@@ -44,12 +53,13 @@ PREFIX = BeamlinePrefix(BL)
|
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
@device_factory()
|
|
47
|
-
def attenuator() ->
|
|
56
|
+
def attenuator() -> EnumFilterAttenuator:
|
|
48
57
|
"""Get a read-only attenuator device for i24, instantiate it if it hasn't already
|
|
49
58
|
been. If this is called when already instantiated in i24, it will return the
|
|
50
59
|
existing object."""
|
|
51
|
-
return
|
|
60
|
+
return EnumFilterAttenuator(
|
|
52
61
|
f"{PREFIX.beamline_prefix}-OP-ATTN-01:",
|
|
62
|
+
filter_selection=(I24_FilterOneSelections, I24_FilterTwoSelections),
|
|
53
63
|
)
|
|
54
64
|
|
|
55
65
|
|
|
@@ -187,3 +197,22 @@ def eiger_beam_center() -> DetectorBeamCenter:
|
|
|
187
197
|
f"{PREFIX.beamline_prefix}-EA-EIGER-01:CAM:",
|
|
188
198
|
"eiger_bc",
|
|
189
199
|
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@device_factory()
|
|
203
|
+
def commissioning_jungfrau(
|
|
204
|
+
path_to_dir: str = "/tmp/jf", # Device factory doesn't allow for required args,
|
|
205
|
+
filename: str = "jf_output", # but these should be manually entered when commissioning
|
|
206
|
+
) -> CommissioningJungfrau:
|
|
207
|
+
"""Get the commissionning Jungfrau 9M device, which uses a temporary filewriter
|
|
208
|
+
device in place of Odin while the detector is in commissioning.
|
|
209
|
+
Instantiates the device if it hasn't already been.
|
|
210
|
+
If this is called when already instantiated, it will return the existing object."""
|
|
211
|
+
|
|
212
|
+
return CommissioningJungfrau(
|
|
213
|
+
f"{PREFIX.beamline_prefix}-EA-JFRAU-01:",
|
|
214
|
+
f"{PREFIX.beamline_prefix}-JUNGFRAU-META:FD:",
|
|
215
|
+
AutoIncrementingPathProvider(
|
|
216
|
+
StaticFilenameProvider(filename), PurePath(path_to_dir)
|
|
217
|
+
),
|
|
218
|
+
)
|
dodal/beamlines/k07.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
2
|
+
|
|
3
|
+
from dodal.common.beamlines.beamline_utils import (
|
|
4
|
+
device_factory,
|
|
5
|
+
)
|
|
6
|
+
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
7
|
+
from dodal.devices.pgm import PGM
|
|
8
|
+
from dodal.devices.synchrotron import Synchrotron
|
|
9
|
+
from dodal.log import set_beamline as set_log_beamline
|
|
10
|
+
from dodal.utils import BeamlinePrefix, get_beamline_name
|
|
11
|
+
|
|
12
|
+
BL = get_beamline_name("k07")
|
|
13
|
+
PREFIX = BeamlinePrefix(BL)
|
|
14
|
+
set_log_beamline(BL)
|
|
15
|
+
set_utils_beamline(BL)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@device_factory()
|
|
19
|
+
def synchrotron() -> Synchrotron:
|
|
20
|
+
return Synchrotron()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Grating does not exist yet - this class is a placeholder for when it does
|
|
24
|
+
class Grating(StrictEnum):
|
|
25
|
+
NO_GRATING = "No Grating"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Grating does not exist yet - this class is a placeholder for when it does
|
|
29
|
+
@device_factory(skip=True)
|
|
30
|
+
def pgm() -> PGM:
|
|
31
|
+
return PGM(prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:", grating=Grating)
|
dodal/beamlines/p60.py
CHANGED
|
@@ -2,8 +2,8 @@ from dodal.common.beamlines.beamline_utils import (
|
|
|
2
2
|
device_factory,
|
|
3
3
|
)
|
|
4
4
|
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
5
|
-
from dodal.devices.electron_analyser import
|
|
6
|
-
from dodal.devices.electron_analyser.vgscienta import
|
|
5
|
+
from dodal.devices.electron_analyser import DualEnergySource
|
|
6
|
+
from dodal.devices.electron_analyser.vgscienta import VGScientaDetector
|
|
7
7
|
from dodal.devices.p60 import (
|
|
8
8
|
LabXraySource,
|
|
9
9
|
LabXraySourceReadable,
|
|
@@ -30,18 +30,19 @@ def mg_kalpha_source() -> LabXraySourceReadable:
|
|
|
30
30
|
return LabXraySourceReadable(LabXraySource.MG_KALPHA)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
@device_factory()
|
|
34
|
+
def energy_source() -> DualEnergySource:
|
|
35
|
+
return DualEnergySource(al_kalpha_source().energy_ev, mg_kalpha_source().energy_ev)
|
|
36
|
+
|
|
37
|
+
|
|
33
38
|
# Connect will work again after this work completed
|
|
34
39
|
# https://jira.diamond.ac.uk/browse/P60-13
|
|
35
40
|
@device_factory()
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
SelectedSource.SOURCE1: al_kalpha_source().energy_ev,
|
|
39
|
-
SelectedSource.SOURCE2: mg_kalpha_source().energy_ev,
|
|
40
|
-
}
|
|
41
|
-
return VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy](
|
|
41
|
+
def r4000() -> VGScientaDetector[LensMode, PsuMode, PassEnergy]:
|
|
42
|
+
return VGScientaDetector[LensMode, PsuMode, PassEnergy](
|
|
42
43
|
prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
|
|
43
44
|
lens_mode_type=LensMode,
|
|
44
45
|
psu_mode_type=PsuMode,
|
|
45
46
|
pass_energy_type=PassEnergy,
|
|
46
|
-
|
|
47
|
+
energy_source=energy_source(),
|
|
47
48
|
)
|
dodal/common/watcher_utils.py
CHANGED
|
@@ -12,7 +12,6 @@ class _LogOnPercentageProgressWatcher(Watcher[Number]):
|
|
|
12
12
|
message_prefix: str,
|
|
13
13
|
percent_interval: Number = 25,
|
|
14
14
|
):
|
|
15
|
-
status.watch(self)
|
|
16
15
|
self.percent_interval = percent_interval
|
|
17
16
|
self._current_percent_interval = 0
|
|
18
17
|
self.message_prefix = message_prefix
|
|
@@ -20,6 +19,7 @@ class _LogOnPercentageProgressWatcher(Watcher[Number]):
|
|
|
20
19
|
raise ValueError(
|
|
21
20
|
f"Percent interval on class _LogOnPercentageProgressWatcher must be a positive number, but received {self.percent_interval}"
|
|
22
21
|
)
|
|
22
|
+
status.watch(self)
|
|
23
23
|
|
|
24
24
|
def __call__(
|
|
25
25
|
self,
|
|
@@ -2,7 +2,7 @@ import abc
|
|
|
2
2
|
import asyncio
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from math import isclose
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Generic, Protocol, TypeVar
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
from bluesky.protocols import Movable
|
|
@@ -18,7 +18,6 @@ from ophyd_async.core import (
|
|
|
18
18
|
wait_for_value,
|
|
19
19
|
)
|
|
20
20
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
21
|
-
from pydantic import BaseModel, ConfigDict, RootModel
|
|
22
21
|
|
|
23
22
|
from dodal.log import LOGGER
|
|
24
23
|
|
|
@@ -49,46 +48,6 @@ class Apple2Val:
|
|
|
49
48
|
btm_outer: str
|
|
50
49
|
|
|
51
50
|
|
|
52
|
-
class EnergyMinMax(BaseModel):
|
|
53
|
-
Minimum: float
|
|
54
|
-
Maximum: float
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class EnergyCoverageEntry(BaseModel):
|
|
58
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
59
|
-
Low: float
|
|
60
|
-
High: float
|
|
61
|
-
Poly: np.poly1d
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class EnergyCoverage(RootModel):
|
|
65
|
-
root: dict[str, EnergyCoverageEntry]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class LookupTableEntries(BaseModel):
|
|
69
|
-
Energies: EnergyCoverage
|
|
70
|
-
Limit: EnergyMinMax
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class Lookuptable(RootModel):
|
|
74
|
-
"""BaseModel class for the lookup table.
|
|
75
|
-
Apple2 lookup table should be in this format.
|
|
76
|
-
|
|
77
|
-
{mode: {'Energies': {Any: {'Low': float,
|
|
78
|
-
'High': float,
|
|
79
|
-
'Poly':np.poly1d
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
'Limit': {'Minimum': float,
|
|
83
|
-
'Maximum': float
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
root: dict[str, LookupTableEntries]
|
|
90
|
-
|
|
91
|
-
|
|
92
51
|
class Pol(StrictEnum):
|
|
93
52
|
NONE = "None"
|
|
94
53
|
LH = "lh"
|
|
@@ -342,6 +301,12 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
|
342
301
|
)
|
|
343
302
|
|
|
344
303
|
|
|
304
|
+
class EnergyMotorConvertor(Protocol):
|
|
305
|
+
def __call__(self, energy: float, pol: Pol) -> tuple[float, float]:
|
|
306
|
+
"""Protocol to provide energy to motor position convertion"""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
|
|
345
310
|
class Apple2(abc.ABC, StandardReadable, Movable):
|
|
346
311
|
"""
|
|
347
312
|
Apple2 Undulator Device
|
|
@@ -353,9 +318,8 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
353
318
|
The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
|
|
354
319
|
abstracting hardware interactions and providing a high-level interface for beamline operations.
|
|
355
320
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
and energy.
|
|
321
|
+
The class is abstract and requires beamline-specific implementations for set motor
|
|
322
|
+
positions based on energy and polarisation.
|
|
359
323
|
|
|
360
324
|
Attributes
|
|
361
325
|
----------
|
|
@@ -371,27 +335,22 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
371
335
|
A hardware-backed signal for polarisation readback and control.
|
|
372
336
|
lookup_tables : dict
|
|
373
337
|
A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
|
|
374
|
-
|
|
375
|
-
A
|
|
338
|
+
energy_to_motor : EnergyMotorConvertor
|
|
339
|
+
A callable that converts energy and polarisation to motor positions.
|
|
376
340
|
|
|
377
341
|
Abstract Methods
|
|
378
342
|
----------------
|
|
379
343
|
set(value: float) -> None
|
|
380
344
|
Abstract method to set motor positions for a given energy and polarisation.
|
|
381
|
-
update_lookuptable() -> None
|
|
382
|
-
Abstract method to load and validate lookup tables from external sources.
|
|
383
345
|
|
|
384
346
|
Methods
|
|
385
347
|
-------
|
|
386
|
-
_set_pol_setpoint(pol: Pol) -> None
|
|
387
|
-
Sets the polarisation setpoint without moving hardware.
|
|
388
348
|
determine_phase_from_hardware(...) -> tuple[Pol, float]
|
|
389
349
|
Determines the polarisation and phase value based on motor positions.
|
|
390
350
|
|
|
391
351
|
Notes
|
|
392
352
|
-----
|
|
393
353
|
- This class requires beamline-specific implementations of the abstract methods.
|
|
394
|
-
- The lookup tables must follow the `Lookuptable` format and be validated before use.
|
|
395
354
|
- The device supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
|
|
396
355
|
positive circular (PC), negative circular (NC), and linear arbitrary (LA).
|
|
397
356
|
|
|
@@ -406,6 +365,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
406
365
|
self,
|
|
407
366
|
id_gap: UndulatorGap,
|
|
408
367
|
id_phase: UndulatorPhaseAxes,
|
|
368
|
+
energy_motor_convertor: EnergyMotorConvertor,
|
|
409
369
|
name: str = "",
|
|
410
370
|
) -> None:
|
|
411
371
|
"""
|
|
@@ -414,16 +374,13 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
414
374
|
----------
|
|
415
375
|
id_gap: An UndulatorGap device.
|
|
416
376
|
id_phase: An UndulatorPhaseAxes device.
|
|
417
|
-
|
|
377
|
+
energy_motor_convertor: A callable that converts energy and polarisation to motor positions.
|
|
418
378
|
name: Name of the device.
|
|
419
379
|
"""
|
|
420
|
-
super().__init__(name)
|
|
421
380
|
|
|
422
|
-
# Attributes are set after super call so they are not renamed to
|
|
423
|
-
# <name>-undulator, etc.
|
|
424
381
|
self.gap = id_gap
|
|
425
382
|
self.phase = id_phase
|
|
426
|
-
|
|
383
|
+
self.energy_to_motor = energy_motor_convertor
|
|
427
384
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
428
385
|
# Store the set energy for readback.
|
|
429
386
|
self.energy, self._set_energy_rbv = soft_signal_r_and_setter(
|
|
@@ -435,11 +392,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
435
392
|
self.polarisation_setpoint, self._polarisation_setpoint_set = (
|
|
436
393
|
soft_signal_r_and_setter(Pol)
|
|
437
394
|
)
|
|
438
|
-
|
|
439
|
-
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
440
|
-
"Gap": {},
|
|
441
|
-
"Phase": {},
|
|
442
|
-
}
|
|
395
|
+
|
|
443
396
|
# Hardware backed read/write for polarisation.
|
|
444
397
|
self.polarisation = derived_signal_rw(
|
|
445
398
|
raw_to_derived=self._read_pol,
|
|
@@ -451,13 +404,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
451
404
|
btm_outer=self.phase.btm_outer.user_readback,
|
|
452
405
|
gap=id_gap.user_readback,
|
|
453
406
|
)
|
|
454
|
-
|
|
455
|
-
self._available_pol = []
|
|
456
|
-
"""
|
|
457
|
-
Abstract method that run at start up to load lookup tables into self.lookup_tables
|
|
458
|
-
and set available_pol.
|
|
459
|
-
"""
|
|
460
|
-
self.update_lookuptable()
|
|
407
|
+
super().__init__(name)
|
|
461
408
|
|
|
462
409
|
def _set_pol_setpoint(self, pol: Pol) -> None:
|
|
463
410
|
"""Set the polarisation setpoint without moving hardware. The polarisation
|
|
@@ -488,8 +435,8 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
488
435
|
|
|
489
436
|
Examples
|
|
490
437
|
--------
|
|
491
|
-
|
|
492
|
-
|
|
438
|
+
RE( id.set(888.0)) # This will set the ID to 888 eV
|
|
439
|
+
RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
|
|
493
440
|
"""
|
|
494
441
|
|
|
495
442
|
def _read_pol(
|
|
@@ -551,77 +498,6 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
551
498
|
await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
552
499
|
self._set_energy_rbv(energy) # Update energy after move for readback.
|
|
553
500
|
|
|
554
|
-
async def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
|
|
555
|
-
"""
|
|
556
|
-
Converts energy and polarisation to gap and phase.
|
|
557
|
-
"""
|
|
558
|
-
gap_poly = await self._get_poly(
|
|
559
|
-
lookup_table=self.lookup_tables["Gap"], new_energy=energy
|
|
560
|
-
)
|
|
561
|
-
phase_poly = await self._get_poly(
|
|
562
|
-
lookup_table=self.lookup_tables["Phase"], new_energy=energy
|
|
563
|
-
)
|
|
564
|
-
return gap_poly(energy), phase_poly(energy)
|
|
565
|
-
|
|
566
|
-
async def _get_poly(
|
|
567
|
-
self,
|
|
568
|
-
new_energy: float,
|
|
569
|
-
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
570
|
-
) -> np.poly1d:
|
|
571
|
-
"""
|
|
572
|
-
Get the correct polynomial for a given energy form lookuptable
|
|
573
|
-
for the current polarisation setpoint.
|
|
574
|
-
Parameters
|
|
575
|
-
----------
|
|
576
|
-
new_energy : float
|
|
577
|
-
The energy in eV for which the polynomial is requested.
|
|
578
|
-
lookup_table : dict[str | None, dict[str, dict[str, Any]]]
|
|
579
|
-
The lookup table containing polynomial coefficients for different energies
|
|
580
|
-
and polarisations.
|
|
581
|
-
Returns
|
|
582
|
-
-------
|
|
583
|
-
np.poly1d
|
|
584
|
-
The polynomial coefficients for the requested energy and polarisation.
|
|
585
|
-
Raises
|
|
586
|
-
------
|
|
587
|
-
ValueError
|
|
588
|
-
If the requested energy is outside the limits defined in the lookup table
|
|
589
|
-
or if no polynomial coefficients are found for the requested energy.
|
|
590
|
-
"""
|
|
591
|
-
pol = await self.polarisation_setpoint.get_value()
|
|
592
|
-
if (
|
|
593
|
-
new_energy < lookup_table[pol]["Limit"]["Minimum"]
|
|
594
|
-
or new_energy > lookup_table[pol]["Limit"]["Maximum"]
|
|
595
|
-
):
|
|
596
|
-
raise ValueError(
|
|
597
|
-
"Demanding energy must lie between {} and {} eV!".format(
|
|
598
|
-
lookup_table[pol]["Limit"]["Minimum"],
|
|
599
|
-
lookup_table[pol]["Limit"]["Maximum"],
|
|
600
|
-
)
|
|
601
|
-
)
|
|
602
|
-
else:
|
|
603
|
-
for energy_range in lookup_table[pol]["Energies"].values():
|
|
604
|
-
if (
|
|
605
|
-
new_energy >= energy_range["Low"]
|
|
606
|
-
and new_energy < energy_range["High"]
|
|
607
|
-
):
|
|
608
|
-
return energy_range["Poly"]
|
|
609
|
-
|
|
610
|
-
raise ValueError(
|
|
611
|
-
"""Cannot find polynomial coefficients for your requested energy.
|
|
612
|
-
There might be gap in the calibration lookup table."""
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
@abc.abstractmethod
|
|
616
|
-
def update_lookuptable(self) -> None:
|
|
617
|
-
"""
|
|
618
|
-
Abstract method to update the stored lookup tabled from file.
|
|
619
|
-
This function should include check to ensure the lookuptable is in the correct format:
|
|
620
|
-
# ensure the importing lookup table is the correct format
|
|
621
|
-
Lookuptable.model_validate(<loockuptable>)
|
|
622
|
-
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
501
|
def determine_phase_from_hardware(
|
|
626
502
|
self,
|
|
627
503
|
top_outer: float,
|
|
@@ -7,6 +7,7 @@ from ophyd_async.core import (
|
|
|
7
7
|
DeviceVector,
|
|
8
8
|
SignalR,
|
|
9
9
|
StandardReadable,
|
|
10
|
+
StrictEnum,
|
|
10
11
|
SubsetEnum,
|
|
11
12
|
wait_for_value,
|
|
12
13
|
)
|
|
@@ -26,6 +27,9 @@ class ReadOnlyAttenuator(StandardReadable):
|
|
|
26
27
|
|
|
27
28
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
28
29
|
with self.add_children_as_readables():
|
|
30
|
+
# Closest obtainable transmission to the current desired transmission given the specific
|
|
31
|
+
# set of filters in the attenuator. This value updates immediately after setting desired
|
|
32
|
+
# transmission, before the motors may have finished moving. It is not a readback value.
|
|
29
33
|
self.actual_transmission = epics_signal_r(float, prefix + "MATCH")
|
|
30
34
|
|
|
31
35
|
super().__init__(name)
|
|
@@ -92,7 +96,18 @@ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
|
|
|
92
96
|
)
|
|
93
97
|
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
# Replace with ophyd async enum after https://github.com/bluesky/ophyd-async/pull/1067
|
|
100
|
+
class YesNo(StrictEnum):
|
|
101
|
+
YES = "YES"
|
|
102
|
+
NO = "NO"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Time given to allow for motors to begin moving after the desired transmission has been set,
|
|
106
|
+
# so that we can work out when the set is complete.
|
|
107
|
+
ENUM_ATTENUATOR_SETTLE_TIME_S = 0.15
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EnumFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
|
|
96
111
|
"""The attenuator will insert filters into the beam to reduce its transmission.
|
|
97
112
|
|
|
98
113
|
This device is currently working, but feature incomplete. See https://github.com/DiamondLightSource/dodal/issues/972
|
|
@@ -107,11 +122,42 @@ class EnumFilterAttenuator(ReadOnlyAttenuator):
|
|
|
107
122
|
filter_selection: tuple[type[SubsetEnum], ...],
|
|
108
123
|
name: str = "",
|
|
109
124
|
):
|
|
125
|
+
self._auto_move_on_desired_transmission_set = epics_signal_rw(
|
|
126
|
+
YesNo, prefix + "AUTOMOVE"
|
|
127
|
+
)
|
|
128
|
+
self._desired_transmission = epics_signal_rw(float, prefix + "T2A:SETVAL1")
|
|
129
|
+
self._use_current_energy = epics_signal_x(prefix + "E2WL:USECURRENTENERGY.PROC")
|
|
130
|
+
|
|
110
131
|
with self.add_children_as_readables():
|
|
111
|
-
self.
|
|
132
|
+
self._filters: DeviceVector[FilterMotor] = DeviceVector(
|
|
112
133
|
{
|
|
113
134
|
index: FilterMotor(f"{prefix}MP{index + 1}:", filter, name)
|
|
114
135
|
for index, filter in enumerate(filter_selection)
|
|
115
136
|
}
|
|
116
137
|
)
|
|
117
138
|
super().__init__(prefix, name=name)
|
|
139
|
+
|
|
140
|
+
@AsyncStatus.wrap
|
|
141
|
+
async def set(self, value: float):
|
|
142
|
+
"""Set the transmission to the fractional (0-1) value given.
|
|
143
|
+
|
|
144
|
+
The attenuator IOC will then insert filters to reach the desired transmission for
|
|
145
|
+
the current beamline energy, the set will only complete when they have all been
|
|
146
|
+
applied.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
# auto move should normally be on, but check here incase it was manually turned off
|
|
150
|
+
await self._auto_move_on_desired_transmission_set.set(YesNo.YES)
|
|
151
|
+
|
|
152
|
+
# Currently uncertain if _use_current_energy correctly waits for completion: https://github.com/DiamondLightSource/dodal/issues/1588
|
|
153
|
+
await self._use_current_energy.trigger()
|
|
154
|
+
await self._desired_transmission.set(value)
|
|
155
|
+
|
|
156
|
+
# Give EPICS a chance to start moving the filter motors. Not needed after
|
|
157
|
+
# a transmission readback PV is added at the controls level: https://jira.diamond.ac.uk/browse/I24-725
|
|
158
|
+
await asyncio.sleep(ENUM_ATTENUATOR_SETTLE_TIME_S)
|
|
159
|
+
coros = [
|
|
160
|
+
wait_for_value(self._filters[i].done_move, 1, timeout=DEFAULT_TIMEOUT)
|
|
161
|
+
for i in self._filters
|
|
162
|
+
]
|
|
163
|
+
await asyncio.gather(*coros)
|
|
@@ -8,4 +8,7 @@ class FilterMotor(StandardReadable):
|
|
|
8
8
|
):
|
|
9
9
|
with self.add_children_as_readables():
|
|
10
10
|
self.user_setpoint = epics_signal_rw(filter_selections, f"{prefix}SELECT")
|
|
11
|
+
self.done_move = epics_signal_rw(
|
|
12
|
+
int, f"{prefix}DMOV"
|
|
13
|
+
) # 1 for yes, 0 for no
|
|
11
14
|
super().__init__(name=name)
|
|
@@ -70,3 +70,29 @@ class I02_1FilterFourSelections(SubsetEnum):
|
|
|
70
70
|
TI300 = "Ti300"
|
|
71
71
|
TI400 = "Ti400"
|
|
72
72
|
TI500 = "Ti500"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class I24_FilterOneSelections(SubsetEnum):
|
|
76
|
+
EMPTY = "Empty"
|
|
77
|
+
AL12_5 = "Al12.5"
|
|
78
|
+
AL25 = "Al25"
|
|
79
|
+
AL50 = "Al50"
|
|
80
|
+
AL75 = "Al75"
|
|
81
|
+
AL1000 = "Al1000"
|
|
82
|
+
AL2000 = "Al2000"
|
|
83
|
+
AL3000 = "Al3000"
|
|
84
|
+
PT25 = "Pt25"
|
|
85
|
+
TI500 = "Ti500"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class I24_FilterTwoSelections(SubsetEnum):
|
|
89
|
+
EMPTY = "Empty"
|
|
90
|
+
AL100 = "Al100"
|
|
91
|
+
AL200 = "Al200"
|
|
92
|
+
AL300 = "Al300"
|
|
93
|
+
AL400 = "Al400"
|
|
94
|
+
AL500 = "Al500"
|
|
95
|
+
AL600 = "Al600"
|
|
96
|
+
AL700 = "Al700"
|
|
97
|
+
AL800 = "Al800"
|
|
98
|
+
AL900 = "Al900"
|
dodal/devices/eiger.py
CHANGED
|
@@ -23,6 +23,7 @@ class EigerTimeouts:
|
|
|
23
23
|
meta_file_ready_timeout: int = 30
|
|
24
24
|
all_frames_timeout: int = 120
|
|
25
25
|
arming_timeout: int = 60
|
|
26
|
+
odin_stop_timeout: int = 30
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class InternalEigerTriggerMode(Enum):
|
|
@@ -139,7 +140,7 @@ class EigerDetector(Device, Stageable):
|
|
|
139
140
|
).wait(self.timeouts.all_frames_timeout)
|
|
140
141
|
finally:
|
|
141
142
|
LOGGER.info("Stopping Odin")
|
|
142
|
-
self.odin.stop().wait(
|
|
143
|
+
self.odin.stop().wait(self.timeouts.odin_stop_timeout)
|
|
143
144
|
|
|
144
145
|
def unstage(self) -> bool:
|
|
145
146
|
assert self.detector_params is not None
|
|
@@ -4,6 +4,7 @@ from .detector import (
|
|
|
4
4
|
TElectronAnalyserDetector,
|
|
5
5
|
TElectronAnalyserRegionDetector,
|
|
6
6
|
)
|
|
7
|
+
from .energy_sources import DualEnergySource, EnergySource
|
|
7
8
|
from .enums import EnergyMode, SelectedSource
|
|
8
9
|
from .types import (
|
|
9
10
|
ElectronAnalyserDetectorImpl,
|
|
@@ -16,6 +17,9 @@ from .util import to_binding_energy, to_kinetic_energy
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"to_binding_energy",
|
|
18
19
|
"to_kinetic_energy",
|
|
20
|
+
"DualEnergySource",
|
|
21
|
+
"SelectedSource",
|
|
22
|
+
"EnergySource",
|
|
19
23
|
"EnergyMode",
|
|
20
24
|
"SelectedSource",
|
|
21
25
|
"ElectronAnalyserDetector",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Mapping
|
|
3
2
|
from typing import Generic, TypeVar
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
@@ -25,7 +24,11 @@ from dodal.devices.electron_analyser.abstract.types import (
|
|
|
25
24
|
TPassEnergy,
|
|
26
25
|
TPsuMode,
|
|
27
26
|
)
|
|
28
|
-
from dodal.devices.electron_analyser.
|
|
27
|
+
from dodal.devices.electron_analyser.energy_sources import (
|
|
28
|
+
DualEnergySource,
|
|
29
|
+
EnergySource,
|
|
30
|
+
)
|
|
31
|
+
from dodal.devices.electron_analyser.enums import EnergyMode
|
|
29
32
|
from dodal.devices.electron_analyser.util import to_binding_energy
|
|
30
33
|
|
|
31
34
|
|
|
@@ -49,7 +52,7 @@ class AbstractAnalyserDriverIO(
|
|
|
49
52
|
lens_mode_type: type[TLensMode],
|
|
50
53
|
psu_mode_type: type[TPsuMode],
|
|
51
54
|
pass_energy_type: type[TPassEnergy],
|
|
52
|
-
|
|
55
|
+
energy_source: EnergySource | DualEnergySource,
|
|
53
56
|
name: str = "",
|
|
54
57
|
) -> None:
|
|
55
58
|
"""
|
|
@@ -65,11 +68,11 @@ class AbstractAnalyserDriverIO(
|
|
|
65
68
|
pass_energy_type: Can be enum or float, depends on electron analyser model.
|
|
66
69
|
If enum, it determines the available pass energies for
|
|
67
70
|
this device.
|
|
68
|
-
|
|
71
|
+
energy_source: Device that can give us the correct excitation energy and
|
|
72
|
+
switch sources if applicable.
|
|
69
73
|
(in eV).
|
|
70
74
|
name: Name of the device.
|
|
71
75
|
"""
|
|
72
|
-
self.energy_sources = energy_sources
|
|
73
76
|
self.acquisition_mode_type = acquisition_mode_type
|
|
74
77
|
self.lens_mode_type = lens_mode_type
|
|
75
78
|
self.psu_mode_type = psu_mode_type
|
|
@@ -84,7 +87,7 @@ class AbstractAnalyserDriverIO(
|
|
|
84
87
|
self.total_intensity = derived_signal_r(
|
|
85
88
|
self._calculate_total_intensity, spectrum=self.spectrum
|
|
86
89
|
)
|
|
87
|
-
self.
|
|
90
|
+
self.energy_source = energy_source
|
|
88
91
|
|
|
89
92
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
90
93
|
# Read once per scan after data acquired
|
|
@@ -104,7 +107,6 @@ class AbstractAnalyserDriverIO(
|
|
|
104
107
|
self.acquisition_mode = epics_signal_rw(
|
|
105
108
|
acquisition_mode_type, prefix + "ACQ_MODE"
|
|
106
109
|
)
|
|
107
|
-
self.excitation_energy_source = soft_signal_rw(str, initial_value="")
|
|
108
110
|
# This is used by each electron analyser, however it depends on the electron
|
|
109
111
|
# analyser type to know if is moved with region settings.
|
|
110
112
|
self.psu_mode = epics_signal_rw(psu_mode_type, prefix + "PSU_MODE")
|
|
@@ -119,7 +121,7 @@ class AbstractAnalyserDriverIO(
|
|
|
119
121
|
self._calculate_binding_energy_axis,
|
|
120
122
|
"eV",
|
|
121
123
|
energy_axis=self.energy_axis,
|
|
122
|
-
excitation_energy=self.
|
|
124
|
+
excitation_energy=self.energy_source.energy,
|
|
123
125
|
energy_mode=self.energy_mode,
|
|
124
126
|
)
|
|
125
127
|
self.angle_axis = self._create_angle_axis_signal(prefix)
|
|
@@ -137,9 +139,28 @@ class AbstractAnalyserDriverIO(
|
|
|
137
139
|
await self.image_mode.set(ADImageMode.SINGLE)
|
|
138
140
|
await super().stage()
|
|
139
141
|
|
|
140
|
-
@abstractmethod
|
|
141
142
|
@AsyncStatus.wrap
|
|
142
143
|
async def set(self, region: TAbstractBaseRegion):
|
|
144
|
+
"""
|
|
145
|
+
Take a region object and setup the driver with it. If using a DualEnergySource,
|
|
146
|
+
set it to use the source selected by the region. It also converts the region to
|
|
147
|
+
kinetic mode before we move the driver signals to the region parameter values:
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
region: Contains the parameters to setup the driver for a scan.
|
|
151
|
+
"""
|
|
152
|
+
if isinstance(self.energy_source, DualEnergySource):
|
|
153
|
+
self.energy_source.selected_source.set(region.excitation_energy_source)
|
|
154
|
+
excitation_energy = await self.energy_source.energy.get_value()
|
|
155
|
+
|
|
156
|
+
# Copy region so doesn't alter the actual region and switch to kinetic energy
|
|
157
|
+
ke_region = region.model_copy()
|
|
158
|
+
ke_region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
|
|
159
|
+
|
|
160
|
+
await self._set_region(ke_region)
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
async def _set_region(self, ke_region: TAbstractBaseRegion):
|
|
143
164
|
"""
|
|
144
165
|
Move a group of signals defined in a region. Each implementation of this class
|
|
145
166
|
is responsible for implementing this method correctly.
|
|
@@ -148,15 +169,6 @@ class AbstractAnalyserDriverIO(
|
|
|
148
169
|
region: Contains the parameters to setup the driver for a scan.
|
|
149
170
|
"""
|
|
150
171
|
|
|
151
|
-
def _get_energy_source(self, alias_name: SelectedSource) -> SignalR[float]:
|
|
152
|
-
energy_source = self.energy_sources.get(alias_name)
|
|
153
|
-
if energy_source is None:
|
|
154
|
-
raise KeyError(
|
|
155
|
-
f"'{energy_source}' is an invalid energy source. Avaliable energy "
|
|
156
|
-
+ f"sources are '{list(self.energy_sources.keys())}'"
|
|
157
|
-
)
|
|
158
|
-
return energy_source
|
|
159
|
-
|
|
160
172
|
@abstractmethod
|
|
161
173
|
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
162
174
|
"""
|