Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description = "Plans to use at spectroscopy beamlines at DLS"
dependencies = [
"bluesky",
"ophyd",
"ophyd_async==0.17a1",
"ophyd_async@git+https://github.com/bluesky/ophyd-async.git@xspress-odin-dev",
"pandas",
"matplotlib",
"databroker",
Expand Down
56 changes: 49 additions & 7 deletions scripts/startup.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
from pathlib import PurePath

from bluesky import RunEngine
from dodal.beamlines.p51 import panda, turbo_slit
from dodal.beamlines.p51 import panda1, panda2, turbo_slit_x
from dodal.utils import BeamlinePrefix, get_beamline_name
from ophyd_async.core import (
AutoIncrementFilenameProvider,
PathProvider,
StaticFilenameProvider,
StaticPathProvider,
)
from ophyd_async.fastcs.xspress import XspressDetector
from ophyd_async.plan_stubs import ensure_connected

from spectroscopy_bluesky.p51.plans.seq_table_scans import (
seq_non_linear, # noqa: F401
seq_table, # noqa: F401
)
from spectroscopy_bluesky.p51.plans.common import restore_panda_settings
from spectroscopy_bluesky.p51.plans.detector_scans import xsp_scan # noqa: F401

BL = get_beamline_name("P51")
PREFIX = BeamlinePrefix(BL)


def static_panda_path_provider1() -> PathProvider:
return StaticPathProvider(
StaticFilenameProvider("panda_seq"),
PurePath("/dls/p51/data/2026/cm44254-2/tmp/"),
)


def static_panda_path_provider2() -> PathProvider:
return StaticPathProvider(
StaticFilenameProvider("panda_fast"),
PurePath("/dls/p51/data/2026/cm44254-2/tmp/"),
)


def static_xsp_path_provider() -> PathProvider:
return StaticPathProvider(
AutoIncrementFilenameProvider("xsp"),
PurePath("/dls/p51/data/2026/cm44254-2/tmp/"),
)


RE = RunEngine()
p = panda()
RE(ensure_connected(turbo_slit(), p))
p_encoder = panda1(static_panda_path_provider1())
p_debug = panda2(static_panda_path_provider2())
xsp = XspressDetector(
prefix="BL51P-EA-XSP-01:",
path_provider=static_xsp_path_provider(),
name="xspress",
)

m = turbo_slit_x()
RE(ensure_connected(m, p_encoder, p_debug, xsp))
restore_panda_settings([p_encoder, p_debug], False, True, False)
2 changes: 2 additions & 0 deletions src/spectroscopy_bluesky/p51/plans/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
plan_store_settings,
restore_panda_settings,
)
from .detector_scans import xsp_scan
from .seq_table_scans import (
prepare_seq_table,
seq_table_energy_scan,
Expand All @@ -28,4 +29,5 @@
"prepare_seq_table",
"restore_panda_settings",
"plan_store_settings",
"xsp_scan",
]
50 changes: 50 additions & 0 deletions src/spectroscopy_bluesky/p51/plans/detector_scans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import bluesky.plan_stubs as bps
import bluesky.preprocessors as bpp
from bluesky.utils import MsgGenerator
from ophyd_async.core import (
DetectorTrigger,
)
from ophyd_async.fastcs.xspress import XspressDetector, XspressTriggerInfo
from ophyd_async.plan_stubs import ensure_connected


def xsp_scan(
xspress: XspressDetector,
num_frames: int = 10,
time_per_frame: float = 1,
chunk: int | None = None,
) -> MsgGenerator:
"""Scan using only Xspress detector.

Args:
xspress (XspressDetector): FastCS-Xspress object to use in the scan.
num_frames (int): number of frames to acquire.

time_per_frame (float): acquisition time of each frame in seconds.

chunk (int, Optional): how many frames to bundle before sending down the pipeline.
If no value is present chunks will be calculated
Returns:
Callable[[], MsgGenerator]: _description_

Yields:
Iterator[Callable[[], MsgGenerator]]: _description_

"""
yield from ensure_connected(xspress)

@bpp.run_decorator()
@bpp.stage_decorator([xspress])
def inner_plan():
xsp_trigger = XspressTriggerInfo(
number_of_events=num_frames,
trigger=DetectorTrigger.INTERNAL,
livetime=time_per_frame,
chunk=int(1 / time_per_frame) if chunk is None else chunk,
)

yield from bps.prepare(xspress, xsp_trigger, wait=True)
yield from bps.kickoff(xspress, wait=True)
yield from bps.complete_all(xspress, wait=True)

yield from inner_plan()
104 changes: 86 additions & 18 deletions src/spectroscopy_bluesky/p51/plans/seq_table_scans.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def prepare_seq_table(
seq_table_number: int = 1,
num_repeats: int = 1,
prescale_as_us: float = 1,
prepare_panda: bool = True,
) -> Callable[[], MsgGenerator]:
"""Return a function that can be used to prepare and arm (kickoff) a
panda sequence table
Expand All @@ -65,8 +64,6 @@ def prepare_seq_table(
Defaults to 1.
num_repeats (int, optional): Number of repeats of sequence table. Defaults to 1.
prescale_as_us (float, optional): _description_. Defaults to 1.
prepare_panda (bool, optional): If true, add calls to also arm the panda as well
as the sequence table. Defaults to True.

Returns:
Callable[[], MsgGenerator]: _description_
Expand All @@ -83,20 +80,39 @@ def prepare_seq_table(
StaticSeqTableTriggerLogic(panda.seq[seq_table_number])
)

def inner_plan():
yield from bps.prepare(seqtable_flyer, seq_table_info, wait=True)

yield from bps.kickoff(seqtable_flyer)
# panda is kicked off later - in seq_table_scan

return inner_plan


def prepare_panda_data(
panda: HDFPanda,
) -> Callable[[], MsgGenerator]:
"""Return a function that can be used to prepare and arm (kickoff) a
panda data.

Args:
panda (HDFPanda): Panda object to be operated on

Returns:
Callable[[], MsgGenerator]: _description_

Yields:
Iterator[Callable[[], MsgGenerator]]: _description_
"""
trigger_info = TriggerInfo(
number_of_events=len(seq_table),
number_of_events=0,
trigger=DetectorTrigger.EXTERNAL_LEVEL,
livetime=1e-5,
deadtime=1e-5,
)

def inner_plan():
if prepare_panda:
yield from bps.prepare(panda, trigger_info)
yield from bps.prepare(seqtable_flyer, seq_table_info, wait=True)

yield from bps.kickoff(seqtable_flyer)
# panda is kicked off later - in seq_table_scan
yield from bps.prepare(panda, trigger_info, wait=True)

return inner_plan

Expand Down Expand Up @@ -186,7 +202,7 @@ def seq_table_two_panda_scan(
prepare_triggers_seqtable = prepare_seq_table(
panda2, seq_table, 1, num_seqtable_repeats
)
panda_dict[panda2] = [prepare_triggers_seqtable]
panda_dict[panda2] = [prepare_triggers_seqtable, prepare_panda_data(panda2)]

yield from seq_table_uniform_scan(
start,
Expand Down Expand Up @@ -215,7 +231,6 @@ def seq_table_uniform_scan(
number_of_sweeps: int = 4,
panda_dict: dict[HDFPanda, list[Callable[[], MsgGenerator]]] | None = None,
) -> MsgGenerator:

capture_positions = np.arange(start, stop + 0.5 * stepsize, stepsize)

# setup a second seq table for 'spectrum based' triggering :
Expand All @@ -228,10 +243,8 @@ def seq_table_uniform_scan(
.get_seq_table()
)

prepare_triggers_seqtable = prepare_seq_table(
panda, seq_table, 2, prepare_panda=False
)
panda_dict[panda] = [prepare_triggers_seqtable]
prepare_triggers_seqtable = prepare_seq_table(panda, seq_table, 2)
panda_dict[panda] = [prepare_triggers_seqtable, prepare_panda_data(panda)]

yield from seq_table_position_scan(
start,
Expand Down Expand Up @@ -259,7 +272,6 @@ def seq_table_position_scan(
number_of_sweeps: int = 4,
panda_dict: dict[HDFPanda, list[Callable[[], MsgGenerator]]] | None = None,
) -> MsgGenerator:

time_per_traj_point = time_per_sweep / num_trajectory_points

print(
Expand Down Expand Up @@ -303,8 +315,64 @@ def seq_table_position_scan(
)
# append position sequence table setup to panda entry (make empty list first
# if not already present).
panda_dict.setdefault(panda, []).append(prepare_position_seqtable)
panda_dict[panda] = [prepare_position_seqtable, prepare_panda_data(panda)]
yield from seq_table_scan(spec, panda_dict, motor=motor)


def debug_scan(
start: float,
stop: float,
stepsize: float,
time_per_sweep: float,
motor: Motor,
panda_seq: HDFPanda,
panda_debug: HDFPanda,
num_trajectory_points: int = 10,
number_of_sweeps: int = 4,
panda_dict: dict[HDFPanda, list[Callable[[], MsgGenerator]]] | None = None,
) -> MsgGenerator:
time_per_traj_point = time_per_sweep / num_trajectory_points
capture_positions = np.arange(start, stop + 0.5 * stepsize, stepsize)

print(
f"Num trajectorypoints : {num_trajectory_points}, "
f"time per traj point : {time_per_traj_point} "
f"Num capture positions: {len(capture_positions)}"
)

# Prepare motor info using trajectory scanning
spec = Fly(
time_per_traj_point
@ (number_of_sweeps * ~Line(motor, start, stop, num_trajectory_points))
)

# add points to capture positions on the reverse sweep
if number_of_sweeps > 1:
num_captures = capture_positions.size
positions = np.zeros(2 * num_captures)
positions[0:num_captures] = capture_positions
positions[num_captures : num_captures * 2] = np.flip(capture_positions)
else:
positions = capture_positions

num_seqtable_repeats = 1
if number_of_sweeps > 1:
num_seqtable_repeats = mt.ceil(number_of_sweeps / 2)

# Sequence table has position triggers for one back-and-forth sweep.
# Use multiple repetitions of seq table to capture subsequent sweeps.
seqTable_builder = SeqTableBuilder()
seqTable_builder.convert_to_encoder = get_encoder_counts
seqTable_builder.add_positions(positions, time1=1, outa1=True, time2=1, outa2=False)
# initialise if nothing has been passed in
if panda_dict is None:
panda_dict = {}

prepare_position_seqtable = prepare_seq_table(
panda_seq, seqTable_builder.get_seq_table(), 1, num_seqtable_repeats
)
panda_dict[panda_seq] = [prepare_position_seqtable, prepare_panda_data(panda_seq)]
panda_dict[panda_debug] = [prepare_panda_data(panda=panda_debug)]
yield from seq_table_scan(spec, panda_dict, motor=motor)


Expand Down
Loading