Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into dodal_692_convert_det_motion
Browse files Browse the repository at this point in the history
  • Loading branch information
DominicOram committed Aug 14, 2024
2 parents ef860fd + 2384be2 commit 4ea4560
Show file tree
Hide file tree
Showing 20 changed files with 713 additions and 347 deletions.
Binary file added docs/panda-gridscan-layout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ install_requires =
# These dependencies may be issued as pre-release versions and should have a pin constraint
# as by default pip-install will not upgrade to a pre-release.
#
daq-config-server @ git+https://github.com/DiamondLightSource/daq-config-server.git
daq-config-server >= 0.1.1
ophyd == 1.9.0
ophyd-async >= 0.3a5
bluesky >= 1.13.0a4
blueapi >= 0.4.3-rc1
dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@be6e51593e78613e68292fd4af630d138ffbbb01
dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@ce3193e0d6e0400752673b8a843cd4530ce71adb

[options.entry_points]
console_scripts =
Expand Down
2 changes: 1 addition & 1 deletion src/hyperion/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions)
parameters = experiment_internal_param_type(**json.loads(request.data))
except Exception as e:
raise ValueError(
"Supplied parameters don't match the plan for this endpoint"
f"Supplied parameters don't match the plan for this endpoint {request.data}"
) from e
return plan, parameters, plan_name, callback_type

Expand Down
138 changes: 67 additions & 71 deletions src/hyperion/device_setup_plans/setup_panda.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from datetime import datetime
from enum import Enum
from importlib import resources
from pathlib import Path

import bluesky.plan_stubs as bps
Expand All @@ -15,11 +16,12 @@
seq_table_from_rows,
)

import hyperion.resources.panda
from hyperion.log import LOGGER

MM_TO_ENCODER_COUNTS = 200000
GENERAL_TIMEOUT = 60
DETECTOR_TRIGGER_WIDTH = 1e-4
TICKS_PER_MS = 1000 # Panda sequencer prescaler will be set to us


class Enabled(Enum):
Expand All @@ -32,30 +34,35 @@ class PcapArm(Enum):
DISARMED = "Disarm"


def get_seq_table(
parameters: PandAGridScanParams,
exposure_distance_mm,
def _get_seq_table(
parameters: PandAGridScanParams, exposure_distance_mm, time_between_steps_ms
) -> SeqTable:
"""
-Exposure distance is the distance travelled by the sample each time the detector is exposed: exposure time * sample velocity
-Setting a 'signal' means trigger PCAP internally and send signal to Eiger via physical panda output
-When we wait for the position to be greater/lower, give a safe distance (X_STEP_SIZE/2 * MM_TO_ENCODER counts) to ensure the final trigger point
is captured
Generate the sequencer table for the panda.
- Sending a 'trigger' means trigger PCAP internally and send signal to Eiger via physical panda output
SEQUENCER TABLE:
1:Wait for physical trigger from motion script to mark start of scan / change of direction
2:Wait for POSA (X2) to be greater than X_START, then
send a signal out every (minimum eiger exposure time + eiger dead time)
3:Wait for POSA (X2) to be greater than X_START + X_STEP_SIZE + a safe distance for the final trigger, then cut out the signal
4:Wait for physical trigger from motion script to mark change of direction
5:Wait for POSA (X2) to be less than X_START + X_STEP_SIZE + exposure distance, then
send a signal out every (minimum eiger exposure time + eiger dead time)
6:Wait for POSA (X2) to be less than (X_START - safe distance + exposure distance), then cut out signal
7:Go back to step one.
1. Wait for physical trigger from motion script to mark start of scan / change of direction
2. Wait for POSA (X2) to be greater than X_START and send x_steps triggers every time_between_steps_ms
3. Wait for physical trigger from motion script to mark change of direction
4. Wait for POSA (X2) to be less than X_START + X_STEP_SIZE * x_steps + exposure distance, then
send x_steps triggers every time_between_steps_ms
5. Go back to step one.
For a more detailed explanation and a diagram, see https://github.com/DiamondLightSource/hyperion/wiki/PandA-constant%E2%80%90motion-scanning
"""
safe_distance_x_counts = int(MM_TO_ENCODER_COUNTS * parameters.x_step_size / 2)
For documentation on Panda itself, see https://pandablocks.github.io/PandABlocks-FPGA/master/index.html
Args:
exposure_distance_mm: The distance travelled by the sample each time the detector is exposed: exposure time * sample velocity
time_between_steps_ms: The time taken to traverse between each grid step.
parameters: Parameters for the panda gridscan
Returns:
An instance of SeqTable describing the panda sequencer table
"""

start_of_grid_x_counts = int(parameters.x_start * MM_TO_ENCODER_COUNTS)

Expand All @@ -67,42 +74,43 @@ def get_seq_table(

exposure_distance_x_counts = int(exposure_distance_mm * MM_TO_ENCODER_COUNTS)

num_pulses = parameters.x_steps

delay_between_pulses = time_between_steps_ms * TICKS_PER_MS

PULSE_WIDTH_US = 1

assert delay_between_pulses > PULSE_WIDTH_US

# BITA_1 trigger wired from TTLIN1, this is the trigger input

# +ve direction scan
rows = [SeqTableRow(trigger=SeqTrigger.BITA_1, time2=1)]

rows.append(
SeqTableRow(
repeats=num_pulses,
trigger=SeqTrigger.POSA_GT,
position=start_of_grid_x_counts,
time2=1,
time1=PULSE_WIDTH_US,
outa1=True,
outa2=True,
)
)
rows.append(
SeqTableRow(
position=end_of_grid_x_counts + safe_distance_x_counts,
trigger=SeqTrigger.POSA_GT,
time2=1,
time2=delay_between_pulses - PULSE_WIDTH_US,
outa2=False,
)
)

# -ve direction scan
rows.append(SeqTableRow(trigger=SeqTrigger.BITA_1, time2=1))

rows.append(
SeqTableRow(
repeats=num_pulses,
trigger=SeqTrigger.POSA_LT,
position=end_of_grid_x_counts + exposure_distance_x_counts,
time2=1,
time1=PULSE_WIDTH_US,
outa1=True,
outa2=True,
)
)

rows.append(
SeqTableRow(
trigger=SeqTrigger.POSA_LT,
position=start_of_grid_x_counts
- safe_distance_x_counts
+ exposure_distance_x_counts,
time2=1,
time2=delay_between_pulses - PULSE_WIDTH_US,
outa2=False,
)
)

Expand All @@ -113,7 +121,6 @@ def get_seq_table(

def setup_panda_for_flyscan(
panda: HDFPanda,
config_yaml_path: str,
parameters: PandAGridScanParams,
initial_x: float,
exposure_time_s: float,
Expand All @@ -127,19 +134,28 @@ def setup_panda_for_flyscan(
Args:
panda (HDFPanda): The PandA Ophyd device
config_yaml_path (str): Path to the yaml file containing the desired PandA PVs
parameters (PandAGridScanParams): Grid parameters
initial_x (float): Motor positions at time of PandA setup
exposure_time_s (float): Detector exposure time per trigger
time_between_x_steps_ms (float): Time, in ms, between each trigger. Equal to deadtime + exposure time
sample_velocity_mm_per_s (float): Velocity of the sample in mm/s = x_step_size_mm * 1000 /
time_between_x_steps_ms
Returns:
MsgGenerator
Yields:
Iterator[MsgGenerator]
"""
yield from load_device(panda, config_yaml_path)
assert parameters.x_steps > 0
assert time_between_x_steps_ms * 1000 >= exposure_time_s
assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size

yield from bps.stage(panda, group="panda-config")

with resources.as_file(
resources.files(hyperion.resources.panda) / "panda-gridscan.yaml"
) as config_yaml_path:
yield from load_device(panda, str(config_yaml_path))

# Home the PandA X encoder using current motor position
yield from bps.abs_set(
Expand All @@ -148,21 +164,11 @@ def setup_panda_for_flyscan(
wait=True,
)

LOGGER.info(f"Setting PandA clock to period {time_between_x_steps_ms}")

yield from bps.abs_set(
panda.clock[1].period, # type: ignore
time_between_x_steps_ms,
group="panda-config",
)

yield from bps.abs_set(
panda.pulse[1].width, DETECTOR_TRIGGER_WIDTH, group="panda-config"
)
yield from bps.abs_set(panda.pulse[1].width, exposure_time_s, group="panda-config")

exposure_distance_mm = sample_velocity_mm_per_s * exposure_time_s

table = get_seq_table(parameters, exposure_distance_mm)
table = _get_seq_table(parameters, exposure_distance_mm, time_between_x_steps_ms)

yield from bps.abs_set(panda.seq[1].table, table, group="panda-config")

Expand Down Expand Up @@ -195,27 +201,17 @@ def disarm_panda_for_gridscan(panda, group="disarm_panda_gridscan") -> MsgGenera
yield from bps.abs_set(panda.pcap.arm, PcapArm.DISARMED.value, group=group) # type: ignore
yield from bps.abs_set(panda.counter[1].enable, Enabled.DISABLED.value, group=group) # type: ignore
yield from bps.abs_set(panda.seq[1].enable, Enabled.DISABLED.value, group=group)
yield from bps.abs_set(
panda.clock[1].enable, Enabled.DISABLED.value, group=group
) # While disarming the clock shouldn't be necessery,
# it will stop the eiger continuing to trigger if something in the sequencer table goes wrong
yield from bps.abs_set(panda.pulse[1].enable, Enabled.DISABLED.value, group=group)
yield from bps.abs_set(panda.pcap.enable, Enabled.DISABLED.value, group=group) # type: ignore
yield from bps.wait(group=group, timeout=GENERAL_TIMEOUT)


def set_and_create_panda_directory(panda_directory: Path) -> MsgGenerator:
"""Updates and creates the panda subdirectory which is used by the PandA's PCAP.
See https://github.com/DiamondLightSource/hyperion/issues/1385 for a better long
term solution.
"""
def set_panda_directory(panda_directory: Path) -> MsgGenerator:
"""Updates the root folder which is used by the PandA's PCAP."""

if not os.path.isdir(panda_directory):
LOGGER.debug(f"Creating PandA PCAP subdirectory at {panda_directory}")
# Assumes we have permissions, which should be true on Hyperion for now
os.makedirs(panda_directory)
suffix = datetime.now().strftime("_%Y%m%d%H%M%S")

async def set_panda_dir():
await get_directory_provider().update(directory=panda_directory)
await get_directory_provider().update(directory=panda_directory, suffix=suffix)

yield from bps.wait_for([set_panda_dir])
13 changes: 4 additions & 9 deletions src/hyperion/experiment_plans/flyscan_xray_centre_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
)
from hyperion.device_setup_plans.setup_panda import (
disarm_panda_for_gridscan,
set_and_create_panda_directory,
set_panda_directory,
setup_panda_for_flyscan,
)
from hyperion.device_setup_plans.setup_zebra import (
Expand All @@ -77,10 +77,6 @@
)
from hyperion.utils.context import device_composite_from_context

PANDA_SETUP_PATH = (
"/dls_sw/i03/software/daq_configuration/panda_configs/flyscan_pcap_ignore_seq.yaml"
)


class SmargonSpeedException(Exception):
pass
Expand Down Expand Up @@ -471,6 +467,7 @@ def _panda_tidy(fgs_composite: FlyScanXRayCentreComposite):
yield from disarm_panda_for_gridscan(fgs_composite.panda, group)
yield from _generic_tidy(fgs_composite, group, False)
yield from bps.wait(group, timeout=10)
yield from bps.unstage(fgs_composite.panda)


def _zebra_triggering_setup(
Expand Down Expand Up @@ -523,13 +520,11 @@ def _panda_triggering_setup(
time_between_x_steps_ms,
)

panda_directory = Path(parameters.storage_directory, "panda")

yield from set_and_create_panda_directory(panda_directory)
directory_provider_root = Path(parameters.storage_directory)
yield from set_panda_directory(directory_provider_root)

yield from setup_panda_for_flyscan(
fgs_composite.panda,
PANDA_SETUP_PATH,
parameters.panda_FGS_params,
initial_xyz[0],
parameters.exposure_time_s,
Expand Down
19 changes: 6 additions & 13 deletions src/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,6 @@ def detect_grid_and_do_gridscan(
composite: GridDetectThenXRayCentreComposite,
parameters: GridScanWithEdgeDetect,
oav_params: OAVParameters,
):
yield from ispyb_activation_wrapper(
_detect_grid_and_do_gridscan(composite, parameters, oav_params), parameters
)


def _detect_grid_and_do_gridscan(
composite: GridDetectThenXRayCentreComposite,
parameters: GridScanWithEdgeDetect,
oav_params: OAVParameters,
):
assert composite.aperture_scatterguard.aperture_positions is not None

Expand Down Expand Up @@ -202,10 +192,13 @@ def grid_detect_then_xray_centre(

oav_params = OAVParameters("xrayCentring", oav_config)

plan_to_perform = detect_grid_and_do_gridscan(
composite,
plan_to_perform = ispyb_activation_wrapper(
detect_grid_and_do_gridscan(
composite,
parameters,
oav_params,
),
parameters,
oav_params,
)

return start_preparing_data_collection_then_do_plan(
Expand Down
5 changes: 3 additions & 2 deletions src/hyperion/experiment_plans/oav_snapshot_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from blueapi.core import MsgGenerator
from bluesky import plan_stubs as bps
from dodal.devices.aperturescatterguard import AperturePositions, ApertureScatterguard
from dodal.devices.aperturescatterguard import ApertureScatterguard
from dodal.devices.backlight import Backlight, BacklightPosition
from dodal.devices.oav.oav_detector import OAV
from dodal.devices.oav.oav_parameters import OAVParameters
Expand Down Expand Up @@ -39,9 +39,10 @@ def setup_oav_snapshot_plan(
yield from bps.abs_set(
composite.backlight, BacklightPosition.IN, group=OAV_SNAPSHOT_SETUP_GROUP
)
assert composite.aperture_scatterguard.aperture_positions is not None
yield from bps.abs_set(
composite.aperture_scatterguard,
AperturePositions.ROBOT_LOAD,
composite.aperture_scatterguard.aperture_positions.ROBOT_LOAD,
group=OAV_SNAPSHOT_SETUP_GROUP,
)

Expand Down
Loading

0 comments on commit 4ea4560

Please sign in to comment.