Demo step scans working

This commit is contained in:
gac-x05la
2024-10-10 17:15:11 +02:00
parent c08d175425
commit f14205e490
7 changed files with 150 additions and 202 deletions
@@ -89,12 +89,19 @@ es1_tasks:
es1_psod:
description: 'AA1 PSO output interface'
description: 'AA1 PSO output interface (trigger)'
deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'}
onFailure: buffer
deviceConfig:
prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
deviceTags:
- es1
- trigger
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: false
es1_ddaq:
description: 'Automation1 position recording interface'
@@ -153,16 +160,15 @@ daq_stream0:
readoutPriority: monitored
softwareTrigger: false
# Unthrottled stream
# daq_stream1:
# description: stdDAQ preview (4 at 10 Hz)
# deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
# deviceConfig:
# url: 'tcp://129.129.95.111:20001'
# deviceTags:
# - std-daq
# enabled: true
# onFailure: buffer
# readOnly: false
# 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
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: false
@@ -512,8 +512,8 @@ class aa1AxisPsoBase(Device):
USER_ACCESS = ["configure"]
# ########################################################################
# General module status
status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.hinted)
output = Component(EpicsSignalRO, "OUTPUT-RBV", auto_monitor=True, kind=Kind.hinted)
status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.normal)
output = Component(EpicsSignalRO, "OUTPUT-RBV", auto_monitor=True, kind=Kind.normal)
address = Component(EpicsSignalRO, "ARRAY-ADDR", kind=Kind.config)
_eventSingle = Component(EpicsSignal, "EVENT:SINGLE", put_complete=True, kind=Kind.omitted)
@@ -561,9 +561,12 @@ class aa1AxisPsoBase(Device):
outPin = Component(EpicsSignalRO, "PIN", auto_monitor=True, kind=Kind.config)
outSource = Component(EpicsSignal, "SOURCE", put_complete=True, kind=Kind.config)
def trigger(self, settle_time=0.1):
def trigger(self, settle_time=0.1) -> DeviceStatus:
"""Fire a single PSO event (i.e. manual software trigger)"""
self._eventSingle.set(1, settle_time=settle_time).wait()
status = DeviceStatus(self)
status.set_finished()
return status
def toggle(self):
""" Toggle waveform outup"""
@@ -658,6 +661,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
pso.kickoff().wait()
"""
_distance_value = None
USER_ACCESS = ["configure", "prepare", "kickoff", "complete"]
# ########################################################################
# PSO high level interface
@@ -698,27 +702,35 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
return (old, new)
# ########################################################################
# Bluesky step scan interface
# Bluesky step scan interface gutted
def stage(self):
""" Stage the device: just ignore it... """
return super().stage()
# ########################################################################
# Bluesky flyer interface
def prepare(self):
""" Rearms the configured array"""
# Rearm the configured array
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
def prepare(self) -> DeviceStatus:
""" Arm trigger for a synchronous or asynchronous acquisition"""
if self._distance_value is not None:
# Rearm the configured array
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
# Start monitoring the counters
self.dstEventsEna.set("On").wait()
self.dstCounterEna.set("On").wait()
status = DeviceStatus(self)
status.set_finished()
return status
def kickoff(self) -> DeviceStatus:
""" Kick off an asynchronous acquisition"""
# Rearm the configured array
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
# Start monitoring the counters
self.dstEventsEna.set("On").wait()
self.dstCounterEna.set("On").wait()
""" Kick off asynchronous triggering on the controller"""
if self._distance_value is not None:
# Rearm the configured array
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
# Start monitoring the counters
self.dstEventsEna.set("On").wait()
self.dstCounterEna.set("On").wait()
status = DeviceStatus(self)
status.set_finished()
print("PSO kicked off")
@@ -883,7 +895,7 @@ class aa1AxisDriveDataCollection(Device):
# ########################################################################
# General module status
state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.hinted)
state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.normal)
nsamples_rbv = Component(EpicsSignalRO, "SAMPLES_RBV", auto_monitor=True, kind=Kind.hinted)
_switch = Component(EpicsSignal, "ACQUIRE", put_complete=True, kind=Kind.omitted)
_input0 = Component(EpicsSignal, "INPUT0", put_complete=True, kind=Kind.config)
@@ -430,12 +430,8 @@ class GigaFrostCamera(PSIDetectorBase):
super().trigger()
# There's no status readback from the camera, so we just wait
status = DeviceStatus(self)
sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050
sleep(sleep_time)
logger.debug(f"[{self.name}] Slept for {sleep_time} seconds")
status.set_finished()
return status
return DeviceStatus(self, done=True, success=True, settle_time=sleep_time)
def configure(self, d: dict = None):
"""Configure the next scan with the GigaFRoST camera
@@ -143,9 +143,10 @@ class StdDaqPreviewDetector(PSIDetectorBase):
url = Component(Signal, kind=Kind.config)
throttle = Component(Signal, value=0.25, kind=Kind.config)
status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted)
image = Component(Signal, kind=Kind.normal)
frame = Component(Signal, kind=Kind.hinted)
image_shape = Component(Signal, kind=Kind.normal)
# FIXME: The BEC client caches the read()s from the last 50 scans
image = Component(Signal, kind=Kind.config)
def __init__(
self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs
+1 -1
View File
@@ -1,2 +1,2 @@
from .gigafrost_test import GigaFrostStepScan
from .tomcat_scanbase import SnapAndStepScanBase, SimpleStepScanBase
from .tomcat_scanbase import SnapAndStepScanBase, GigaStepScanBase
+72 -154
View File
@@ -55,8 +55,6 @@ class TemplatedScanBase(AsyncFlyScanBase):
scan_name = "aeroscript_scan_base"
scan_report_hint = "table"
required_kwargs = ["filename", "subs"]
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
def __init__(self, *args, parameter: dict = None, **kwargs):
"""Executes an AeroScript template as a flyer
@@ -170,179 +168,99 @@ class TemplatedScanBase(AsyncFlyScanBase):
class GigaStepScanBase(ScanBase):
""" Gigafrost Step scan base class
class SimpleStepScanBase(TemplatedScanBase):
""" Simple Step scan base class
Example temporary base class for AeroScript-based step scans. As a temporary
setup until the final cabling is done, the scan is in a soft-trigger mode.
The Gigafrost is just software triggered, and the position readback is wired
to the internal trigger.
Example base class for AeroScript-based step scans using the low-level API.
Until the final cabling is done, the scan is in soft-trigger mode.
Example
-------
>>> scans.simplestep(range=(0, 180), steps=1800, exp_time=20, exp_burst=5)
Parameters
----------
range : (float, float)
Scan range of the axis.
steps : int, optional
Number of scan steps to cover the range. (default = 10)
exp_time : float, optional [0.01 ... 40]
Exposure time for each frame in [ms]. The IOC fixes the exposure
period to be 2x this long so it doesnt matter. (default = 20)
exp_burst : float, optional
Number of images to be taken for each scan point. (default=1)
>>> scans.gigastep(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5)
"""
scan_name = "simplestep"
scan_report_hint = "table"
required_kwargs = ["range","steps"]
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
scan_name = "gigastep"
required_kwargs = ["scan_start", "scan_end","steps"]
gui_config = {
"Movement parameters": ["steps"],
"Acquisition parameters" : ["exp_time", "burst_at_each_point"]
}
def __init__(self, *args, parameter: dict = None, **kwargs):
"""Example step scan
def __init__(
self,
scan_start: float,
scan_end: float,
steps: int,
exp_time = 0.005,
settling_time = 0.2,
burst_at_each_point = 1,
**kwargs):
super().__init__(
exp_time=exp_time,
settling_time=settling_time,
relative=False,
burst_at_each_point=1,
optim_trajectory=None,
**kwargs
)
Perform a simple step scan with a motor while software triggering the
gigafrost burst sequence at each point and recording it to the StdDAQ.
Actually only the configuration is gigafrost specific, everything else
is just using standard Bluesky event model.
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
self.scan_ntotal = burst_at_each_point * (steps + 1)
self.scan_nburst = burst_at_each_point
"""
# Auto-setup configuration parameters from input
logger.info(parameter)
self.scan_axis = "es1_roty"
self.scan_range = parameter['kwargs'].get("range")
self.scan_stepnum = int(parameter['kwargs'].get("steps"))
self.scan_exptime = float(parameter['kwargs'].get("exp_time", 20))
self.scan_expburst = float(parameter['kwargs'].get("exp_burst", 1))
self.scan_roix = int(parameter['kwargs'].get("roix", 2016))
self.scan_roiy = int(parameter['kwargs'].get("roiy", 2016))
# Override wait time with actual latency
# NOTE : The BEC does not wait for trigger() status done
self.exp_time = settling_time + exp_time * 2 * burst_at_each_point
# Synthetic values
self.scan_startpos = self.scan_range[0]
self.scan_stepsize = (self.scan_range[1]-self.scan_range[0])/self.scan_stepnum
self.scan_ntotal = self.scan_stepnum * self.scan_expburst
# Aerotech DDC settings:
# PsoOut = 0
# PsoEvent = 1
# PsoHsInp0_rise = 2
# PsoHsInp0_fall = 3
# PsoHsInp1_rise = 4
# PsoHsInp1_fall = 5
parameter['kwargs']['daqname'] = "es1_ddaq"
parameter['kwargs']['daqcfg'] = {'ntotal': self.scan_ntotal, 'trigger': 1}
# Gigafrost config
parameter['kwargs']['camcfg'] = {
# Prepare configuration dicts
self.psocfg = {}
self.daqcfg = {'ntotal': self.scan_ntotal, 'trigger': 1}
self.camcfg = {
"ntotal": self.scan_ntotal,
"nimages": self.scan_expburst,
"exposure": self.scan_exptime,
"period": 2*self.scan_exptime,
"pixel_width": self.scan_roix,
"pixel_height": self.scan_roiy
"nimages": burst_at_each_point,
"exposure": 1000*exp_time,
"period": 2000*exp_time,
"pixel_width": 2016,
"pixel_height": 2016
}
# Parameter validation before scan
if self.scan_stepnum <=0:
raise RuntimeError(f"Requires at least one scan step, got {self.scan_stepnum}")
# Call super()
self.filename = "foo"
self.filetext = "bar"
super().__init__(parameter=parameter, **kwargs)
def prepare_positions(self):
def _calculate_positions(self) -> None:
""" Pre-calculate scan positions """
self.positions = [self.scan_startpos]
for ii in range(self.scan_stepnum):
self.positions.append(self.scan_startpos + ii*self.scan_stepsize)
yield None
def pre_scan(self):
# Move roughly to start position
logger.info(f"\n\nScan start position: {self.scan_startpos} with axis {self.scan_axis}\n\n")
yield from self.stubs.send_rpc_and_wait(self.scan_axis, "motor_enable.set", 1)
st = yield from self.stubs.send_rpc_and_wait(self.scan_axis, "move", self.scan_startpos)
st.wait()
yield None
self.positions = [self.scan_start]
for ii in range(self.scan_steps):
self.positions.append(self.scan_start + ii*self.scan_stepsize)
def stage(self):
""" Configure and stage all devices"""
""" First do the actual configuration then the gutted staging"""
print(f"TOMCAT Staging step scan with motors {self.scan_axis}")
print(f"TOMCAT Staging step scan with motors {self.motor}")
# Configure the camera (usually together wit the DAQ)
yield from self.stubs.send_rpc_and_wait(self.camera, "configure", self.camcfg)
# Configure the camera (usually together wit the DAQ)
if self.daqname is not None:
yield from self.stubs.send_rpc_and_wait(self.daqname, "configure", self.daqcfg)
# ###################################################################################
# Staging
if self.camera is not None:
yield from self.stubs.send_rpc_and_wait(self.camera, "stage")
if self.daqname:
yield from self.stubs.send_rpc_and_wait(self.daqname, "stage")
if self.preview is not None:
yield from self.stubs.send_rpc_and_wait(self.preview, "stage")
# For God, NO!
# yield from super().stage()
def scan_core(self):
""" Start"""
t_start = time.time()
for ii in range(self.scan_stepnum):
print(f"Point: {ii}")
st = yield from self.stubs.send_rpc_and_wait(self.scan_axis, "move", self.positions[ii])
st.wait()
st = yield from self.stubs.send_rpc_and_wait("gfclient", "trigger")
st = yield from self.stubs.send_rpc_and_wait("es1_psod", "trigger")
time.sleep(0.1)
t_end = time.time()
t_elapsed = t_end - t_start
print(f"Elapsed scan time: {t_elapsed}")
time.sleep(0.5)
# Collect
if self.daqmode=="collect":
positions = yield from self.stubs.send_rpc_and_wait(self.daqname, "collect")
logger.info(f"Finished scan with collected positions: {positions}")
logger.info("Scan done\n\n")
yield from self.stubs.send_rpc_and_wait("gfclient", "configure", self.camcfg)
# Configure the position readback
yield from self.stubs.send_rpc_and_wait("es1_ddaq", "configure", self.daqcfg)
# Configure the manual triggers
yield from self.stubs.send_rpc_and_wait("es1_psod", "configure", self.psocfg)
# Explicitly arm trigger
yield from self.stubs.send_rpc_and_wait("es1_psod", "prepare")
# TODO: Explicitly arm detector
# Explicitly arm trigger
yield from self.stubs.send_rpc_and_wait("gfclient", "stage")
yield from self.stubs.send_rpc_and_wait("es1_ddaq", "stage")
yield from self.stubs.send_rpc_and_wait("es1_psod", "stage")
yield from self.stubs.send_rpc_and_wait("daq_stream1", "stage")
# FIXME: Shouldn't this stage() everything?
return super().stage()
def unstage(self):
""" Wait for DAQ before unstaging"""
time.sleep(5)
yield from super().unstage()
+23 -8
View File
@@ -1,7 +1,19 @@
""" Demo scans for Tomcat at the microXAS test bench
"""
def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1):
def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1, settling_time=0):
""" Demo step scan with GigaFrost
This is a small user-space demo step scan at the microXAS testbench using
the gigafrost in software triggering mode. It tries to be as standard as
possible, while still setting up the environment.
Example:
--------
demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, burst_at_each_point=5)
"""
scan_ntotal = burst_at_each_point * (steps + 1)
print("NI")
# Move to start position
cfg = {'ntotal': scan_ntotal, 'trigger': 1}
@@ -11,17 +23,20 @@ def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_poin
"ntotal": scan_ntotal, "nimages": burst_at_each_point,
"exposure": 1000*exp_time, "period": 2000*exp_time}
dev.gfclient.configure(d=cfg)
# Configure PSO trigger
cfg = {}
dev.es1_psod.configure(d=cfg)
# Configure PSO trigger (trigger is normally disabled in fly mode)
dev.es1_psod.configure(d={})
dev.es1_psod.software_trigger = True
# Explicitly arm trigger (and detector in future)
dev.es1_psod.prepare()
print("Handing over to 'scans.line_scan'")
wait_time = 0.1 + 2*exp_time * burst_at_each_point
scans.line_scan(
dev.es1_roty, scan_start, scan_end,
steps=steps, exp_time=2*exp_time, relative=False)
steps=steps, exp_time=wait_time, relative=False, settling_time=settling_time)
# Move to start position
umv(dev.es1_roty, scan_start)
# Cleanup: disable SW trigger
dev.es1_psod.software_trigger = False