SnapNStep works, Sequence fails on Aerotech axis

This commit is contained in:
gac-x05la
2025-02-14 17:39:36 +01:00
parent d1e072b8d9
commit c3cf12b8e2
9 changed files with 109 additions and 163 deletions

View File

@@ -64,18 +64,18 @@ es1_roty:
# readoutPriority: monitored
# softwareTrigger: false
# es1_tasks:
# description: 'Automation1 task management interface'
# deviceClass: tomcat_bec.devices.aa1Tasks
# deviceConfig:
# prefix: 'X02DA-ES1-SMP1:TASK:'
# deviceTags:
# - es1
# enabled: true
# onFailure: buffer
# readOnly: false
# readoutPriority: monitored
# softwareTrigger: false
es1_tasks:
description: 'Automation1 task management interface'
deviceClass: tomcat_bec.devices.aa1Tasks
deviceConfig:
prefix: 'X02DA-ES1-SMP1:TASK:'
deviceTags:
- es1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: false
# es1_psod:
@@ -92,18 +92,18 @@ es1_roty:
# softwareTrigger: true
# es1_ddaq:
# description: 'Automation1 position recording interface'
# deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
# deviceConfig:
# prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
# deviceTags:
# - es1
# enabled: true
# onFailure: buffer
# readOnly: false
# readoutPriority: monitored
# softwareTrigger: false
es1_ddaq:
description: 'Automation1 position recording interface'
deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
deviceConfig:
prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
deviceTags:
- es1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: false
#camera:
@@ -152,7 +152,7 @@ gfdaq:
readoutPriority: monitored
softwareTrigger: false
daq_stream0:
gf_stream0:
description: stdDAQ preview (2 every 555)
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
deviceConfig:
@@ -166,21 +166,6 @@ daq_stream0:
readoutPriority: monitored
softwareTrigger: false
daq_stream1:
description: stdDAQ preview (1 at 5 Hz)
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
deviceConfig:
url: 'tcp://129.129.95.111:20001'
deviceTags:
- std-daq
- gfcam
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: false
pcocam:
description: PCO.edge camera client
deviceClass: tomcat_bec.devices.PcoEdge5M

View File

@@ -36,19 +36,22 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
# NOTE: Scans don't have to fully configure the device
if "ddc_trigger" in scanargs:
d["ddc_trigger"] = scanargs["ddc_trigger"]
if "ddc_num_points" in scanargs:
if "ddc_num_points" in scanargs and scanargs["ddc_num_points"] is not None:
d["num_points_total"] = scanargs["ddc_num_points"]
elif "daq_num_points" in scanargs and scanargs["daq_num_points"] is not None:
d["num_points_total"] = scanargs["daq_num_points"]
else:
# Try to figure out number of points
num_points = 1
points_valid = False
if "steps" in scanargs and scanargs['steps'] is not None:
if "steps" in scanargs and scanargs["steps"] is not None:
num_points *= scanargs["steps"]
points_valid = True
elif "exp_burst" in scanargs and scanargs['exp_burst'] is not None:
if "exp_burst" in scanargs and scanargs["exp_burst"] is not None:
num_points *= scanargs["exp_burst"]
points_valid = True
elif "repeats" in scanargs and scanargs['repeats'] is not None:
if "repeats" in scanargs and scanargs["repeats"] is not None:
num_points *= scanargs["repeats"]
points_valid = True
if points_valid:
@@ -143,6 +146,10 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
"""Bluesky-style stage"""
self._switch.set("Start", settle_time=0.2).wait()
def blueunstage(self):
"""Bluesky-style unstage"""
self._switch.set("Stop", settle_time=0.2).wait()
def reset(self):
"""Reset incremental readback"""
self._switch.set("ResetRB", settle_time=0.1).wait()
@@ -170,6 +177,8 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
elif index == 1:
status = SubscriptionStatus(self._readstatus1, neg_edge, settle_time=0.5)
self._readback1.set(1).wait()
else:
raise ValueError(f"[{self.name}] Invalid data acquisition channel {index}")
# Start asynchronous readback
status.wait()

View File

@@ -34,7 +34,8 @@ program
//////////////////////////////////////////////////////////////////////////
// Internal parameters - dont use
var $axis as axis = ROTY
var $ii as integer
var $ii as integer
var $axisFaults as integer = 0
var $iDdcSafeSpace as integer = 4096
// Set acceleration
@@ -126,7 +127,12 @@ program
if $eScanType == ScanType.POS || $eScanType == ScanType.NEG
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0)
MoveAbsolute($axis, $fPosEnd, $fVelScan)
WaitForInPosition($axis)
WaitForMotionDone($axis)
$axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault)
if $axisFaults
TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY")
end
elseif $eScanType == ScanType.POSNEG || $eScanType == ScanType.NEGPOS
for $ii = 0 to ($iNumRepeat-1)
// Feedback on progress
@@ -134,11 +140,15 @@ program
if ($ii % 2) == 0
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0)
MoveAbsolute($axis, $fPosEnd, $fVelScan)
WaitForInPosition($axis)
elseif ($ii % 2) == 1
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayNegAddr, $iPsoArrayNegSize, 0)
MoveAbsolute($axis, $fPosStart, $fVelScan)
WaitForInPosition($axis)
end
WaitForMotionDone($axis)
$axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault)
if $axisFaults
TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY")
end
Dwell(0.2)
end

View File

@@ -78,6 +78,7 @@ program
///////////////////////////////////////////////////////////
$iglobal[2] = $iNumSteps
for $ii = 0 to ($iNumSteps-1)
$rglobal[4] = $ii
MoveAbsolute($axis, $fStartPosition + $ii * $fStepSize, $fVelScan)
WaitForMotionDone($axis)

View File

@@ -47,7 +47,7 @@ class AerotechTasksMixin(CustomDeviceMixin):
# Perform bluesky-style configuration
if len(d) > 0:
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
# logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
self.parent.configure(d=d)
# The actual staging
@@ -55,12 +55,16 @@ class AerotechTasksMixin(CustomDeviceMixin):
def on_unstage(self):
"""Stop the currently selected task"""
self.parent.switch.set("Stop").wait()
self.parent.blueunstage()
def on_stop(self):
"""Stop the currently selected task"""
self.parent.switch.set("Stop").wait()
def on_kickoff(self):
"""Start execution of the selected task"""
self.parent.bluekickoff()
class aa1Tasks(PSIDeviceBase):
"""Task management API
@@ -146,6 +150,10 @@ class aa1Tasks(PSIDeviceBase):
raise RuntimeError("Failed to load task, please check the Aerotech IOC")
return status
def blueunstage(self):
"""Bluesky style unstage, stops execution"""
self.switch.set("Stop").wait()
def bluekickoff(self):
"""Bluesky style kickoff"""
# Launch and check success
@@ -155,6 +163,10 @@ class aa1Tasks(PSIDeviceBase):
raise RuntimeError("Failed to load task, please check the Aerotech IOC")
return status
def kickoff(self):
"""Missing kickoff, for real?"""
self.custom_prepare.on_kickoff()
##########################################################################
# Bluesky flyer interface
def complete(self) -> DeviceStatus:

View File

@@ -7,7 +7,7 @@ Created on Thu Jun 27 17:28:43 2024
@author: mohacsi_i
"""
from time import sleep
from ophyd import Signal, SignalRO, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus
from ophyd import Signal, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus
from ophyd.device import DynamicDeviceComponent
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
@@ -174,6 +174,10 @@ class GigaFrostCameraMixin(CustomDetectorMixin):
d["image_height"] = scanargs["image_height"]
if "exp_time" in scanargs and scanargs["exp_time"] is not None:
d["exposure_time_ms"] = scanargs["exp_time"]
if "acq_time" in scanargs and scanargs["acq_time"] is not None:
d["exposure_time_ms"] = scanargs["acq_time"]
if "acq_period" in scanargs and scanargs["acq_period"] is not None:
d["exposure_period_ms"] = scanargs["acq_period"]
if "exp_burst" in scanargs and scanargs["exp_burst"] is not None:
d["exposure_num_burst"] = scanargs["exp_burst"]
if "acq_mode" in scanargs and scanargs["acq_mode"] is not None:
@@ -497,6 +501,7 @@ class GigaFrostCamera(PSIDetectorBase):
# There's no status readback from the camera, so we just wait
sleep_time = self.cfgExposure.value * self.cfgCntNum.value * 0.001 + 0.2
logger.warning(f"Gigafrost sleeping for {sleep_time} sec")
sleep(sleep_time)
return DeviceStatus(self, done=True, success=True, settle_time=sleep_time)

View File

@@ -7,7 +7,7 @@ Created on Wed Dec 6 11:33:54 2023
import time
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
from ophyd.status import SubscriptionStatus, DeviceStatus
from ophyd_devices import BECDeviceBase
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as BECDeviceBase
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin as CustomPrepare,
)
@@ -54,6 +54,10 @@ class PcoEdgeCameraMixin(CustomPrepare):
d["exposure_time_ms"] = scanargs["exp_time"]
if "exp_period" in scanargs and scanargs["exp_period"] is not None:
d["exposure_period_ms"] = scanargs["exp_period"]
if "acq_time" in scanargs and scanargs["acq_time"] is not None:
d["exposure_time_ms"] = scanargs["acq_time"]
if "acq_period" in scanargs and scanargs["acq_period"] is not None:
d["exposure_period_ms"] = scanargs["acq_period"]
# if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None:
# d['exposure_num_burst'] = scanargs['exp_burst']
# if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None:
@@ -469,5 +473,5 @@ class PcoEdge5M(HelgeCameraBase):
if __name__ == "__main__":
# Drive data collection
cam = PcoEdge5M("X02DA-CCDCAM2:", name="mcpcam")
cam = PcoEdge5M(prefix="X02DA-CCDCAM2:", name="mcpcam")
cam.wait_for_connection()

View File

@@ -1,2 +1,2 @@
from .tutorial_fly_scan import AcquireDark, AcquireWhite, AcquireRefs, TutorialFlyScanContLine
from .tomcat_scans import TomcatStepScan, TomcatSnapNStep, TomcatSimpleSequence
from .tomcat_scans import TomcatSnapNStep, TomcatSimpleSequence

View File

@@ -18,95 +18,11 @@ import os
import time
from bec_lib import bec_logger
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanBase, ScanArgType
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType
logger = bec_logger.logger
class TomcatStepScan(ScanBase):
"""Simple software step scan for Tomcat
Example class for simple BEC-based step scan using the low-level API. It's just a standard
'line_scan' with the only difference that overrides burst behavior to use camera burst instead
of individual software triggers.
NOTE: As decided by Tomcat, the scans should not manage the scope of devices
- All enabled devices are expected to be configured for acquisition by the end of stage
- Some devices can configure themselves from mandatory scan parameters (steps, burst)
- Other devices can be optionally configured by keyword arguments
- Devices will try to stage using whatever was set on them before
Example:
--------
>>> scans.tomcatstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5)
Common keyword arguments:
-------------------------
image_width : int
image_height : int
ddc_trigger : str
"""
scan_name = "tomcatstepscan"
scan_type = "step"
required_kwargs = ["scan_start", "scan_end", "steps"]
gui_config = {
"Movement parameters": ["steps"],
"Acquisition parameters": ["exp_time", "exp_burst"],
}
def update_scan_motors(self):
self.scan_motors = ["es1_roty"]
def _get_scan_motors(self):
self.scan_motors = ["es1_roty"]
def __init__(
self,
scan_start: float,
scan_end: float,
steps: int,
exp_time: float=0.005,
settling_time: float=0.2,
exp_burst: int=1,
**kwargs,
):
# Converting generic kwargs to tomcat device configuration parameters
super().__init__(
exp_time=exp_time,
settling_time=settling_time,
burst_at_each_point=1,
optim_trajectory=None,
**kwargs,
)
# For position calculation
self.motor = "es1_roty"
self.scan_start = scan_start
self.scan_end = scan_end
self.scan_steps = steps
self.scan_stepsize = (scan_end - scan_start) / steps
def _calculate_positions(self) -> None:
"""Pre-calculate scan positions"""
for ii in range(self.scan_steps + 1):
self.positions.append(self.scan_start + ii * self.scan_stepsize)
def _at_each_point(self, ind=None, pos=None):
""" Overriden at_each_point, using detector burst instaead of manual triggering"""
yield from self._move_scan_motors_and_wait(pos)
time.sleep(self.settling_time)
trigger_time = 0.001*self.exp_time * self.burst_at_each_point
yield from self.stubs.trigger(min_wait=trigger_time)
# yield from self.stubs.trigger(group='trigger', point_id=self.point_id)
# time.sleep(trigger_time)
yield from self.stubs.read(group="monitored", point_id=self.point_id)
# yield from self.stubs.read(group="monitored", point_id=self.point_id, wait_group=None)
self.point_id += 1
class TomcatSnapNStep(AsyncFlyScanBase):
"""Simple software step scan forTomcat
@@ -115,7 +31,7 @@ class TomcatSnapNStep(AsyncFlyScanBase):
Example
-------
>>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5)
>>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, acq_time=5, exp_burst=5)
"""
scan_name = "tomcatsnapnstepscan"
@@ -126,10 +42,10 @@ class TomcatSnapNStep(AsyncFlyScanBase):
required_kwargs = ["scan_start", "scan_end", "steps"]
gui_config = {
"Movement parameters": ["steps"],
"Acquisition parameters": ["exp_time", "exp_burst"],
"Acquisition parameters": ["acq_time", "exp_burst"],
}
def _get_scan_motors(self):
def _update_scan_motors(self):
self.scan_motors = ["es1_roty"]
def __init__(
@@ -137,8 +53,8 @@ class TomcatSnapNStep(AsyncFlyScanBase):
scan_start: float,
scan_end: float,
steps: int,
exp_time:float=0.005,
settling_time:float=0.2,
acq_time:float=5.0,
exp_burst:int=1,
sync:str="event",
**kwargs,
@@ -147,14 +63,12 @@ class TomcatSnapNStep(AsyncFlyScanBase):
self.scan_start = scan_start
self.scan_end = scan_end
self.scan_steps = steps
self.scan_stepsize = (scan_end - scan_start) / steps
self.scan_ntotal = exp_burst * (steps + 1)
self.exp_time = exp_time
self.exp_time = acq_time
self.exp_burst = exp_burst
self.settling_time = settling_time
self.scan_sync = sync
# General device configuration
# Gigafrost trigger mode
kwargs["parameter"]["kwargs"]["acq_mode"] = "ext_enable"
# Used for Aeroscript file substitutions for the task interface
@@ -165,7 +79,8 @@ class TomcatSnapNStep(AsyncFlyScanBase):
kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript"
super().__init__(
exp_time=exp_time,
acq_time=acq_time,
exp_burst=exp_burst,
settling_time=settling_time,
burst_at_each_point=1,
optim_trajectory=None,
@@ -182,12 +97,12 @@ class TomcatSnapNStep(AsyncFlyScanBase):
}
filesubs = {
"startpos": self.scan_start,
"stepsize": self.scan_stepsize,
"stepsize": (self.scan_end - self.scan_start) / self.scan_steps,
"numsteps": self.scan_steps,
"exptime": 2 * self.exp_time * self.exp_burst,
"exptime": 0.002 * self.exp_time * self.exp_burst,
"settling": self.settling_time,
"psotrigger": p_modes[self.scan_sync],
"npoints": self.scan_ntotal,
"npoints": self.exp_burst * (self.scan_steps + 1),
}
return filesubs
@@ -196,7 +111,7 @@ class TomcatSnapNStep(AsyncFlyScanBase):
# Load the test file
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
logger.info(f"Attempting to load file {filename}")
with open(filename) as f:
with open(filename, "r", encoding="utf-8") as f:
templatetext = f.read()
# Substitute jinja template
@@ -208,12 +123,15 @@ class TomcatSnapNStep(AsyncFlyScanBase):
"""The actual scan routine"""
print("TOMCAT Running scripted scan (via Jinjad AeroScript)")
t_start = time.time()
# Kickoff
yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff")
# Complete
# FIXME: this will swallow errors
# yield from self.stubs.complete(device="es1_tasks")
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
# st.wait()
yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
# Check final state
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
if task_states[4] == 8:
raise RuntimeError(f"Task {4} finished in ERROR state")
@@ -243,7 +161,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
Example
-------
>>> scans.tomcatsimplesequencescan(33, 180, 180, exp_time=0.005, exp_burst=1800, repeats=10)
>>> scans.tomcatsimplesequencescan(33, 180, 180, acq_time=5, exp_burst=1800, repeats=10)
"""
scan_name = "tomcatsimplesequencescan"
@@ -255,7 +173,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
"Acquisition parameters": [
"gate_high",
"gate_low",
"exp_time",
"acq_time",
"exp_burst",
"sync",
],
@@ -271,7 +189,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
gate_low: float,
repeats: int = 1,
repmode: str = "PosNeg",
exp_time: float = 0.005,
acq_time: float = 5,
exp_burst: float = 180,
sync: str = "pso",
**kwargs,
@@ -282,13 +200,13 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
self.gate_low = gate_low
self.scan_repnum = repeats
self.scan_repmode = repmode.upper()
self.exp_time = exp_time
self.exp_time = acq_time
self.exp_burst = exp_burst
self.scan_sync = sync
# Synthetic values
self.scan_ntotal = exp_burst * repeats
self.scan_velocity = gate_high / (exp_time * exp_burst)
self.scan_velocity = gate_high / (0.001*acq_time * exp_burst)
self.scan_acceleration = 500
self.scan_safedistance = 10
self.scan_accdistance = (
@@ -314,7 +232,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript"
super().__init__(
exp_time=exp_time,
acq_time=acq_time,
settling_time=0.5,
relative=False,
burst_at_each_point=1,
@@ -327,7 +245,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
# Load the test file
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
logger.info(f"Attempting to load file {filename}")
with open(filename) as f:
with open(filename, 'r', encoding="utf-8") as f:
templatetext = f.read()
# Substitute jinja template
@@ -339,12 +257,14 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
"""The actual scan routine"""
print("TOMCAT Running scripted scan (via Jinjad AeroScript)")
t_start = time.time()
# Kickoff
yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff")
# Complete
# FIXME: this will swallow errors
# yield from self.stubs.complete(device="es1_tasks")
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
# st.wait()
yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
if task_states[4] == 8:
raise RuntimeError(f"Task {4} finished in ERROR state")