Skip to content

Commit 26a67c6

Browse files
Merge pull request #31 from DiamondLightSource/add_initial_analyserscan
Add initial analyserscan
2 parents 4378e5a + 31bc7f1 commit 26a67c6

File tree

20 files changed

+713
-6
lines changed

20 files changed

+713
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,6 @@ lockfiles/
7373

7474
# ruff cache
7575
.ruff_cache/
76+
77+
# temp files
78+
tmp/

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ dependencies = [
1717
# it will be auto-pinned to the latest release version by the pre-release workflow
1818
#
1919
"bluesky",
20-
"dls-dodal",
21-
"ophyd-async >= 0.10.0a2",
20+
"dls-dodal@git+https://github.com/DiamondLightSource/dodal.git@main",
21+
"ophyd-async[sim] >= 0.10.0a2",
2222
"scanspec",
2323
]
2424
dynamic = ["version"]

src/sm_bluesky/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Version number as calculated by https://github.com/pypa/setuptools_scm
77
"""
88

9-
from . import beamlines, common
9+
from . import beamlines, common, electron_analyser
1010
from ._version import __version__
1111

12-
__all__ = ["__version__", "beamlines", "common"]
12+
__all__ = ["__version__", "beamlines", "common", "electron_analyser"]
File renamed without changes.
File renamed without changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .analyser_per_step import analyser_nd_step, analyser_shot
2+
3+
__all__ = ["analyser_shot", "analyser_nd_step"]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from collections.abc import Mapping, Sequence
2+
from typing import Any
3+
4+
from bluesky.plan_stubs import (
5+
abs_set,
6+
move_per_step,
7+
trigger_and_read,
8+
)
9+
from bluesky.protocols import Movable, Readable
10+
from bluesky.utils import (
11+
MsgGenerator,
12+
plan,
13+
)
14+
from dodal.devices.electron_analyser import (
15+
ElectronAnalyserRegionDetector,
16+
GenericElectronAnalyserRegionDetector,
17+
)
18+
from dodal.log import LOGGER
19+
20+
21+
@plan
22+
def analyser_shot(detectors: Sequence[Readable], *args) -> MsgGenerator:
23+
yield from analyser_nd_step(detectors, {}, {}, *args)
24+
25+
26+
@plan
27+
def analyser_nd_step(
28+
detectors: Sequence[Readable],
29+
step: Mapping[Movable, Any],
30+
pos_cache: dict[Movable, Any],
31+
*args,
32+
) -> MsgGenerator:
33+
"""
34+
Inner loop of an N-dimensional step scan
35+
36+
Modified default function for ``per_step`` param in ND plans. Performs an extra for
37+
loop for each ElectronAnalyserRegionDetector present so they can be collected one by
38+
one.
39+
40+
Parameters
41+
----------
42+
detectors : iterable
43+
devices to read
44+
step : dict
45+
mapping motors to positions in this step
46+
pos_cache : dict
47+
mapping motors to their last-set positions
48+
"""
49+
50+
analyser_detectors: list[GenericElectronAnalyserRegionDetector] = []
51+
other_detectors = []
52+
for det in detectors:
53+
if isinstance(det, ElectronAnalyserRegionDetector):
54+
analyser_detectors.append(det)
55+
else:
56+
other_detectors.append(det)
57+
58+
# Step provides the map of motors to single position to move to. Move motors to
59+
# required positions.
60+
yield from move_per_step(step, pos_cache)
61+
62+
# This is to satisfy type checking. Motors are Moveable and Readable, so make
63+
# them Readable so positions can be measured.
64+
motors: list[Readable] = [s for s in step.keys() if isinstance(s, Readable)]
65+
66+
# To get energy sources and open paired shutters, they need to be given in this
67+
# plan. They could possibly come from step but we then have to extract them out.
68+
# It would also mean forcefully adding in the devices at the wrapper level.
69+
# It would easier if they are part of the detector and the plan just calls the
70+
# common methods so it is more dynamic and configuration only for device.
71+
for analyser_det in analyser_detectors:
72+
dets = [analyser_det] + list(other_detectors) + list(motors)
73+
74+
# This is a work around until we can find a way to get the energy sources to use
75+
# the prepare method.
76+
yield from abs_set(analyser_det.driver, analyser_det.region)
77+
LOGGER.info(f"Scanning region {analyser_det.region.name}.")
78+
yield from trigger_and_read(
79+
dets,
80+
name=analyser_det.region.name,
81+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .analyser_scans import analysercount, analyserscan, grid_analyserscan
2+
3+
__all__ = ["analysercount", "analyserscan", "grid_analyserscan"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from collections.abc import Iterable, Sequence
2+
from typing import Any
3+
4+
from bluesky.plans import PerShot, PerStep, count, grid_scan, scan
5+
from bluesky.protocols import (
6+
Movable,
7+
Readable,
8+
)
9+
from bluesky.utils import (
10+
CustomPlanMetadata,
11+
MsgGenerator,
12+
ScalarOrIterableFloat,
13+
plan,
14+
)
15+
from dodal.devices.electron_analyser import (
16+
ElectronAnalyserDetector,
17+
)
18+
19+
from sm_bluesky.electron_analyser.plan_stubs import (
20+
analyser_nd_step,
21+
analyser_shot,
22+
)
23+
24+
25+
def process_detectors_for_analyserscan(
26+
detectors: Sequence[Readable],
27+
sequence_file: str,
28+
) -> Sequence[Readable]:
29+
"""
30+
Check for instance of ElectronAnalyserDetector in the detector list. Provide it with
31+
sequence file to read and create list of ElectronAnalyserRegionDetector's. Replace
32+
ElectronAnalyserDetector in list of detectors with the
33+
list[ElectronAnalyserRegionDetector] and flatten.
34+
35+
Args:
36+
detectors:
37+
Devices to measure with for a scan.
38+
sequence_file:
39+
The file to read to create list[ElectronAnalyserRegionDetector].
40+
41+
Returns:
42+
list of detectors, with any instances of ElectronAnalyserDetector replaced by
43+
ElectronAnalyserRegionDetector by the number of regions in the sequence file.
44+
45+
"""
46+
analyser_detector = None
47+
region_detectors = []
48+
for det in detectors:
49+
if isinstance(det, ElectronAnalyserDetector):
50+
analyser_detector = det
51+
region_detectors = det.create_region_detector_list(sequence_file)
52+
break
53+
54+
expansions = (
55+
region_detectors if e == analyser_detector else [e] for e in detectors
56+
)
57+
return [v for vals in expansions for v in vals]
58+
59+
60+
def analysercount(
61+
detectors: Sequence[Readable],
62+
sequence_file: str,
63+
num: int = 1,
64+
delay: ScalarOrIterableFloat = 0.0,
65+
*,
66+
per_shot: PerShot | None = None,
67+
md: CustomPlanMetadata | None = None,
68+
) -> MsgGenerator:
69+
yield from count(
70+
process_detectors_for_analyserscan(detectors, sequence_file),
71+
num,
72+
delay,
73+
per_shot=analyser_shot,
74+
md=md,
75+
)
76+
77+
78+
@plan
79+
def analyserscan(
80+
detectors: Sequence[Readable],
81+
sequence_file: str,
82+
*args: Movable | Any,
83+
num: int | None = None,
84+
per_step: PerStep | None = None,
85+
md: CustomPlanMetadata | None = None,
86+
) -> MsgGenerator:
87+
yield from scan(
88+
process_detectors_for_analyserscan(detectors, sequence_file),
89+
*args,
90+
num,
91+
per_step=analyser_nd_step,
92+
md=md,
93+
)
94+
95+
96+
@plan
97+
def grid_analyserscan(
98+
detectors: Sequence[Readable],
99+
sequence_file: str,
100+
*args,
101+
snake_axes: Iterable | bool | None = None,
102+
md: CustomPlanMetadata | None = None,
103+
) -> MsgGenerator:
104+
yield from grid_scan(
105+
process_detectors_for_analyserscan(detectors, sequence_file),
106+
*args,
107+
snake_axes=snake_axes,
108+
per_step=analyser_nd_step,
109+
md=md,
110+
)

tests/electron_analyser/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)