This commit is contained in:
gac-x05la
2025-02-25 14:31:46 +01:00
parent f15fd00712
commit 4266798e30
5 changed files with 562 additions and 168 deletions

View File

@@ -11,6 +11,19 @@ eyex:
readOnly: false
softwareTrigger: false
eyey:
readoutPriority: baseline
description: X-ray eye axis Y
deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC
deviceConfig:
prefix: MTEST-X05LA-ES2-XRAYEYE:M2
deviceTags:
- xray-eye
onFailure: buffer
enabled: true
readOnly: false
softwareTrigger: false
eyez:
readoutPriority: baseline
description: X-ray eye axis Z
@@ -38,18 +51,18 @@ femto_mean_curr:
readOnly: true
softwareTrigger: false
es1_roty:
readoutPriority: monitored
description: 'Test rotation stage'
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X02DA-ES1-SMP1:ROTY
deviceTags:
- es1-sam
onFailure: buffer
enabled: true
readOnly: false
softwareTrigger: false
# es1_roty:
# readoutPriority: monitored
# description: 'Test rotation stage'
# deviceClass: ophyd.EpicsMotor
# deviceConfig:
# prefix: X02DA-ES1-SMP1:ROTY
# deviceTags:
# - es1-sam
# onFailure: buffer
# enabled: true
# readOnly: false
# softwareTrigger: false
# es1_ismc:
# description: 'Automation1 iSMC interface'
@@ -64,18 +77,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: false
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: false
# onFailure: buffer
# readOnly: false
# readoutPriority: monitored
# softwareTrigger: false
# es1_psod:
@@ -92,18 +105,18 @@ es1_tasks:
# 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:
@@ -145,6 +158,7 @@ gfdaq:
data_source_name: 'gfcam'
deviceTags:
- std-daq
- daq
- gfcam
enabled: true
onFailure: buffer
@@ -159,6 +173,7 @@ gf_stream0:
url: 'tcp://129.129.95.111:20000'
deviceTags:
- std-daq
- preview
- gfcam
enabled: true
onFailure: buffer
@@ -187,8 +202,10 @@ pcodaq:
deviceConfig:
ws_url: 'ws://129.129.95.111:8081'
rest_url: 'http://129.129.95.111:5010'
data_source_name: 'pcocam'
deviceTags:
- std-daq
- daq
- pcocam
enabled: true
onFailure: buffer
@@ -203,6 +220,7 @@ pco_stream0:
url: 'tcp://129.129.95.111:20010'
deviceTags:
- std-daq
- preview
- pcocam
enabled: true
onFailure: buffer

View File

@@ -41,9 +41,9 @@ class StdDaqMixin(CustomDeviceMixin):
# Fish out our configuration from scaninfo (via explicit or generic addressing)
# NOTE: Scans don't have to fully configure the device
d = {}
scan_parameters = self.parent.scaninfo.scan_msg.scan_parameters
std_daq_params = scan_parameters.get("std_daq_params")
# scan_parameters = self.parent.scaninfo.scan_msg.scan_parameters
# std_daq_params = scan_parameters.get("std_daq_params")
if "kwargs" in self.parent.scaninfo.scan_msg.info:
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
if "image_width" in scanargs and scanargs["image_width"] is not None:
@@ -53,8 +53,8 @@ class StdDaqMixin(CustomDeviceMixin):
if "nr_writers" in scanargs and scanargs["nr_writers"] is not None:
d["nr_writers"] = scanargs["nr_writers"]
if "system_config" in scanargs and scanargs["system_config"] is not None:
if scanargs['system_config']['file_directory']:
file_directory = scanargs['system_config']['file_directory']
if scanargs["system_config"]["file_directory"]:
file_directory = scanargs["system_config"]["file_directory"]
### to be used in the future to substitute the procedure using file path
if "file_path" in scanargs and scanargs["file_path"] is not None:
self.parent.file_path.set(scanargs["file_path"].replace("data", "gpfs")).wait()
@@ -91,7 +91,6 @@ class StdDaqMixin(CustomDeviceMixin):
if points_valid:
d["num_points_total"] = num_points
# Perform bluesky-style configuration
if len(d) > 0:
# Configure new run (will restart the stdDAQ)
@@ -101,9 +100,9 @@ class StdDaqMixin(CustomDeviceMixin):
sleep(0.5)
# Try to start a new run (reconnects)
if std_daq_params.get("reconnect",True):
self.parent.bluestage()
# if std_daq_params.get("reconnect", True):
self.parent.bluestage()
# And start status monitoring
self._mon = Thread(target=self.monitor, daemon=True)
self._mon.start()
@@ -179,6 +178,7 @@ class StdDaqClient(PSIDeviceBase):
file_prefix = Component(Signal, value="file", kind=Kind.config)
# Configuration attributes
rest_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
datasource = Component(Signal, kind=Kind.config, metadata={"write_access": False})
cfg_detector_name = Component(Signal, kind=Kind.config)
cfg_detector_type = Component(Signal, kind=Kind.config)
cfg_bit_depth = Component(Signal, kind=Kind.config)
@@ -213,7 +213,7 @@ class StdDaqClient(PSIDeviceBase):
)
self.ws_url.set(ws_url, force=True).wait()
self.rest_url.set(rest_url, force=True).wait()
self.data_source_name = data_source_name
self.datasource.set(data_source_name, force=True).wait()
# Connect to the DAQ and initialize values
try:
@@ -335,15 +335,6 @@ class StdDaqClient(PSIDeviceBase):
sleep(1)
self.get_daq_config(update=True)
# def configure(self, d:dict):
# if "num_points_total" in d:
# num_points = d.pop("num_points_total")
# self.num_images.set(num_points).wait()
# super().configure(d)
# self.set_daq_config()
# sleep(1)
# self.get_daq_config(update=True)
def bluestage(self):
"""Stages the stdDAQ
@@ -355,27 +346,12 @@ class StdDaqClient(PSIDeviceBase):
if self.state() != "idle":
raise RuntimeError(f"[{self.name}] stdDAQ can't stage from state: {self.state()}")
# Must make sure that image size matches the data source
if self.data_source_name is not None:
cam_img_w = self.device_manager.devices[self.data_source_name].cfgRoiX.get()
cam_img_h = self.device_manager.devices[self.data_source_name].cfgRoiY.get()
daq_img_w = self.cfg_pixel_width.get()
daq_img_h = self.cfg_pixel_height.get()
if not (daq_img_w == cam_img_w and daq_img_h == cam_img_h):
raise RuntimeError(
f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) does not match camera with ({cam_img_w} , {cam_img_h})"
)
else:
logger.warning(
f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) matches camera with ({cam_img_w} , {cam_img_h})"
)
# Ensure expected shape
self.validate()
file_path = self.file_path.get()
num_images = self.num_images.get()
file_prefix = self.name
file_prefix = self.file_prefix.get()
print(file_prefix)
num_images = self.num_images.get()
# New connection
self._wsclient = self.connect()
@@ -407,9 +383,7 @@ class StdDaqClient(PSIDeviceBase):
print(f"[{self.name}] Started stdDAQ in: {reply['status']}")
return
raise RuntimeError(
f"[{self.name}] Failed to start the stdDAQ in 1 tries, reason: {reply['reason']}"
)
raise RuntimeError(f"[{self.name}] Failed to start the stdDAQ, reason: {reply['reason']}")
def blueunstage(self):
"""Unstages the stdDAQ
@@ -464,6 +438,25 @@ class StdDaqClient(PSIDeviceBase):
status = SubscriptionStatus(self.runstatus, is_running, settle_time=0.5)
return status
def validate(self):
"""Validate camera state
Ensure that data source shape matches with the shape expected by the stdDAQ.
"""
# Must make sure that image size matches the data source
source = self.datasource.get()
if source is not None and len(source) > 0:
if source == "gfcam":
cam_img_w = self.device_manager.devices[source].cfgRoiX.get()
cam_img_h = self.device_manager.devices[source].cfgRoiY.get()
daq_img_w = self.cfg_pixel_width.get()
daq_img_h = self.cfg_pixel_height.get()
if not (daq_img_w == cam_img_w and daq_img_h == cam_img_h):
raise RuntimeError(
f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) does not match camera with ({cam_img_w} , {cam_img_h})"
)
def get_daq_config(self, update=False) -> dict:
"""Read the current configuration from the DAQ"""
r = requests.get(self.rest_url.get() + "/api/config/get", params={"user": "ioc"}, timeout=2)

View File

@@ -23,107 +23,23 @@ logger = bec_logger.logger
ZMQ_TOPIC_FILTER = b''
class StdDaqPreviewState(enum.IntEnum):
"""Standard DAQ ophyd device states"""
UNKNOWN = 0
DETACHED = 1
MONITORING = 2
class StdDaqPreviewMixin(CustomDetectorMixin):
"""Setup class for the standard DAQ preview stream
Parent class: CustomDetectorMixin
"""
_mon = None
def on_stage(self):
"""Start listening for preview data stream"""
if self._mon is not None:
self.parent.unstage()
sleep(0.5)
self.parent.connect()
self._stop_polling = False
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
self.parent.arm()
def on_unstage(self):
"""Stop a running preview"""
if self._mon is not None:
self._stop_polling = True
# Might hang on recv_multipart
self._mon.join(timeout=1)
# So also disconnect the socket
self.parent._socket.disconnect(self.parent.url.get())
self.parent.disarm()
def on_stop(self):
"""Stop a running preview"""
self.on_unstage()
def poll(self):
"""Collect streamed updates"""
self.parent.status.set(StdDaqPreviewState.MONITORING, force=True)
try:
t_last = time()
while True:
try:
# Exit loop and finish monitoring
if self._stop_polling:
logger.info(f"[{self.parent.name}]\tDetaching monitor")
break
# pylint: disable=no-member
r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK)
# Length and throtling checks
if len(r) != 2:
logger.warning(
f"[{self.parent.name}] Received malformed array of length {len(r)}")
t_curr = time()
t_elapsed = t_curr - t_last
if t_elapsed < self.parent.throttle.get():
sleep(0.1)
continue
# Unpack the Array V1 reply to metadata and array data
meta, data = r
# Update image and update subscribers
header = json.loads(meta)
if header["type"] == "uint16":
image = np.frombuffer(data, dtype=np.uint16)
if image.size != np.prod(header['shape']):
err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err)
image = image.reshape(header['shape'])
# Update image and update subscribers
self.parent.frame.put(header['frame'], force=True)
self.parent.image_shape.put(header['shape'], force=True)
self.parent.image.put(image, force=True)
self.parent._last_image = image
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
t_last = t_curr
logger.info(
f"[{self.parent.name}] Updated frame {header['frame']}\t"
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
)
except ValueError:
# Happens when ZMQ partially delivers the multipart message
pass
except zmq.error.Again:
# Happens when receive queue is empty
sleep(0.1)
except Exception as ex:
logger.info(f"[{self.parent.name}]\t{str(ex)}")
raise
finally:
self._mon = None
self.parent.status.set(StdDaqPreviewState.DETACHED, force=True)
logger.info(f"[{self.parent.name}]\tDetaching monitor")
class StdDaqPreviewDetector(PSIDetectorBase):
"""Detector wrapper class around the StdDaq preview image stream.
@@ -135,7 +51,7 @@ class StdDaqPreviewDetector(PSIDetectorBase):
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
"""
# Subscriptions for plotting image
USER_ACCESS = ["kickoff", "get_last_image"]
USER_ACCESS = ["arm", "disarm", "get_last_image"]
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
@@ -144,19 +60,19 @@ class StdDaqPreviewDetector(PSIDetectorBase):
# Status attributes
url = Component(Signal, kind=Kind.config)
throttle = Component(Signal, value=0.25, kind=Kind.config)
status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted)
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.omitted)
_last_image = None
_stop_polling = True
_mon = None
def __init__(
self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs
) -> None:
super().__init__(*args, parent=parent, **kwargs)
self.url._metadata["write_access"] = False
self.status._metadata["write_access"] = False
self.image._metadata["write_access"] = False
self.frame._metadata["write_access"] = False
self.image_shape._metadata["write_access"] = False
@@ -185,9 +101,92 @@ class StdDaqPreviewDetector(PSIDetectorBase):
def get_image(self):
return self._last_image
def kickoff(self) -> DeviceStatus:
""" The DAQ was not meant to be toggled"""
return DeviceStatus(self, done=True, success=True, settle_time=0.1)
def arm(self):
"""Start listening for preview data stream"""
if self._mon is not None:
self.unstage()
sleep(0.5)
self.connect()
self._stop_polling = False
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
def disarm(self):
"""Stop a running preview"""
if self._mon is not None:
self._stop_polling = True
# Might hang on recv_multipart
self._mon.join(timeout=1)
# So also disconnect the socket (if not already disconnected)
try:
self._socket.disconnect(self.url.get())
except zmq.error.ZMQError:
pass
def poll(self):
"""Collect streamed updates"""
try:
t_last = time()
while True:
try:
# Exit loop and finish monitoring
if self._stop_polling:
break
# pylint: disable=no-member
r = self._socket.recv_multipart(flags=zmq.NOBLOCK)
# Length and throtling checks
if len(r) != 2:
logger.warning(
f"[{self.name}] Received malformed array of length {len(r)}")
t_curr = time()
t_elapsed = t_curr - t_last
if t_elapsed < self.throttle.get():
sleep(0.1)
continue
# Unpack the Array V1 reply to metadata and array data
meta, data = r
# Update image and update subscribers
header = json.loads(meta)
image = None
if header["type"] == "uint16":
image = np.frombuffer(data, dtype=np.uint16)
if image.size != np.prod(header['shape']):
err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err)
image = image.reshape(header['shape'])
# Update image and update subscribers
self.frame.put(header['frame'], force=True)
self.image_shape.put(header['shape'], force=True)
self.image.put(image, force=True)
self._last_image = image
self._run_subs(sub_type=self.SUB_MONITOR, value=image)
t_last = t_curr
logger.info(
f"[{self.name}] Updated frame {header['frame']}\t"
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
)
except ValueError:
# Happens when ZMQ partially delivers the multipart message
pass
except zmq.error.Again:
# Happens when receive queue is empty
sleep(0.1)
except Exception as ex:
logger.info(f"[{self.name}]\t{str(ex)}")
raise
finally:
self._mon = None
logger.info(f"[{self.name}]\tDetaching monitor")
# Automatically connect to MicroSAXS testbench if directly invoked

View File

@@ -1,2 +1,3 @@
from .tutorial_fly_scan import AcquireDark, AcquireWhite, AcquireRefs, AcquireProjections, TutorialFlyScanContLine
from .tomcat_scans import TomcatSnapNStep, TomcatSimpleSequence
from .advanced_scans import AcquireRefsV2, AcquireDarkV2, AcquireWhiteV2

View File

@@ -0,0 +1,383 @@
import time
import numpy as np
from bec_lib.device import DeviceBase
from bec_server.scan_server.scans import Acquire, AsyncFlyScanBase
from bec_lib import bec_logger
logger = bec_logger.logger
class Shutter:
""" Shutter status """
CLOSED = 0
OPEN = 1
class AcquireDarkV2(Acquire):
scan_name = "acquire_dark_v2"
required_kwargs = ["exp_burst"]
gui_config = {"Acquisition parameters": ["exp_burst"]}
def __init__(self, exp_burst: int, file_prefix="", **kwargs):
"""
Acquire dark images. This scan is used to acquire dark images. Dark images are images taken with the shutter
closed and no beam on the sample. Dark images are used to correct the data images for dark current.
NOTE: this scan has a special operation mode that does not call
Args:
exp_burst : int
Number of dark images to acquire (no default)
file_prefix : str
File prefix
Examples:
>>> scans.acquire_dark(5)
"""
super().__init__(exp_burst=exp_burst, file_prefix="", **kwargs)
self.burst_at_each_point = 1 # At each point, how many times I want to individually trigger
self.exp_burst = exp_burst
self.file_prefix = file_prefix
def pre_scan(self):
""" Close the shutter before scan"""
yield from self.stubs.set(device=["eyex"], value=[Shutter.CLOSED])
return super().pre_scan()
def direct(self):
""" Direct scan procedure call"""
# Collect relevant devices
self.cams = [cam.name for cam in self.device_manager.devices.get_devices_with_tags("camera") if cam.enabled]
self.prev = [pre.name for pre in self.device_manager.devices.get_devices_with_tags("preview") if pre.enabled]
self.daqs = [daq.name for daq in self.device_manager.devices.get_devices_with_tags("daq") if daq.enabled]
# Do not call stage, as there's no ScanInfo emitted for direct call
for daq in self.daqs:
cam = yield from self.stubs.send_rpc_and_wait(daq, "datasource.get")
prefix = f"{self.file_prefix}_{cam}_dark"
yield from self.stubs.send_rpc_and_wait(daq, "file_prefix.set", prefix)
yield from self.stubs.send_rpc_and_wait(daq, "num_images.set", self.exp_burst)
yield from self.stubs.send_rpc_and_wait(daq, "bluestage")
for prev in self.prev:
yield from self.stubs.send_rpc_and_wait(prev, "arm")
for cam in self.cams:
yield from self.stubs.send_rpc_and_wait(cam, "configure", {'exposure_num_burst': self.exp_burst})
yield from self.stubs.send_rpc_and_wait(cam, "bluestage")
yield from self.pre_scan()
yield from self.scan_core()
yield from self.finalize()
yield from self.unstage()
yield from self.cleanup()
class AcquireWhiteV2(Acquire):
scan_name = "acquire_white_v2"
gui_config = {"Acquisition parameters": ["exp_burst"]}
def __init__(self, motor: DeviceBase, exp_burst: int, sample_position_out: float, sample_angle_out: float, file_prefix: str="", **kwargs):
"""
Acquire flat field images. This scan is used to acquire flat field images. The flat field image is an image taken
with the shutter open but the sample out of the beam. Flat field images are used to correct the data images for
non-uniformity in the detector.
Args:
motor : DeviceBase
Motor to be moved to move the sample out of beam
exp_burst : int
Number of flat field images to acquire (no default)
sample_position_out : float
Position to move the sample stage to position the sample out of beam and take flat field images
sample_angle_out : float
Angular position where to take the flat field images
Examples:
>>> scans.acquire_white(dev.samx, 5, 20)
"""
super().__init__(exp_burst=exp_burst, **kwargs)
self.exp_burst = exp_burst
self.file_prefix = file_prefix
self.burst_at_each_point = 1
self.scan_motors = [motor, "eyez"]
# self.scan_motors = [motor, "es1_roty"]
self.out_position = [sample_position_out, sample_angle_out]
def pre_scan(self):
""" Open the shutter before scan"""
# Move sample out
yield from self._move_scan_motors_and_wait(self.out_position)
# Open the main shutter (TODO change to the correct shutter device)
yield from self.stubs.set(device=["eyex"], value=[Shutter.OPEN])
return super().pre_scan()
def cleanup(self):
""" Close the shutter after scan"""
# Close fast shutter
yield from self.stubs.set(device=["eyex"], value=[Shutter.CLOSED])
return super().cleanup()
def direct(self):
""" Direct scan procedure call"""
# Collect relevant devices
self.cams = [cam.name for cam in self.device_manager.devices.get_devices_with_tags("camera") if cam.enabled]
self.prev = [pre.name for pre in self.device_manager.devices.get_devices_with_tags("preview") if pre.enabled]
self.daqs = [daq.name for daq in self.device_manager.devices.get_devices_with_tags("daq") if daq.enabled]
# Do not call stage, as there's no ScanInfo emitted for direct call
for daq in self.daqs:
cam = yield from self.stubs.send_rpc_and_wait(daq, "datasource.get")
prefix = f"{self.file_prefix}_{cam}_white"
yield from self.stubs.send_rpc_and_wait(daq, "file_prefix.set", prefix)
yield from self.stubs.send_rpc_and_wait(daq, "num_images.set", self.exp_burst)
yield from self.stubs.send_rpc_and_wait(daq, "bluestage")
for prev in self.prev:
yield from self.stubs.send_rpc_and_wait(prev, "arm")
for cam in self.cams:
yield from self.stubs.send_rpc_and_wait(cam, "configure", {'exposure_num_burst': self.exp_burst})
yield from self.stubs.send_rpc_and_wait(cam, "bluestage")
yield from self.pre_scan()
yield from self.scan_core()
yield from self.finalize()
yield from self.unstage()
yield from self.cleanup()
# class AcquireProjections(AsyncFlyScanBase):
# scan_name = "acquire_projections"
# gui_config = {
# "Motor": ["motor"],
# "Acquisition parameters": ["sample_position_in", "start_angle", "angular_range" ],
# "Camera": ["exp_time", "exp_burst"]
# }
# def __init__(self,
# motor: DeviceBase,
# exp_burst: int,
# sample_position_in: float,
# start_angle: float,
# angular_range: float,
# exp_time:float,
# **kwargs):
# """
# Acquire projection images.
# Args:
# motor : DeviceBase
# Motor to move continuously from start to stop position
# exp_burst : int
# Number of flat field images to acquire (no default)
# sample_position_in : float
# Position to move the sample stage to position the sample in the beam
# start_angle : float
# Angular start position for the scan
# angular_range : float
# Angular range
# exp_time : float, optional
# Exposure time [ms]. If not specified, the currently configured value on the camera will be used
# exp_period : float, optional
# Exposure period [ms]. If not specified, the currently configured value on the camera will be used
# image_width : int, optional
# ROI size in the x-direction [pixels]. If not specified, the currently configured value on the camera will be used
# image_height : int, optional
# ROI size in the y-direction [pixels]. If not specified, the currently configured value on the camera will be used
# acq_mode : str, optional
# Predefined acquisition mode (default= 'default')
# file_path : str, optional
# File path for standard daq
# ddc_trigger : int, optional
# Drive Data Capture Trigger
# ddc_source0 : int, optional
# Drive Data capture Input0
# Returns:
# ScanReport
# Examples:
# >>> scans.acquire_projections()
# """
# self.motor = motor
# super().__init__(exp_time=exp_time,**kwargs)
# self.burst_at_each_point = 1
# self.sample_position_in = sample_position_in
# self.start_angle = start_angle
# self.angular_range = angular_range
# self.dark_shutter_pos_out = 1 ### change with a variable
# self.dark_shutter_pos_in = 0 ### change with a variable
# def update_scan_motors(self):
# return [self.motor]
# def prepare_positions(self):
# self.positions = np.array([[self.start_angle], [self.start_angle+self.angular_range]])
# self.num_pos = None
# yield from self._set_position_offset()
# def scan_core(self):
# # move to in position and go to start angular position
# yield from self.stubs.set(device=["eyez", self.motor], value=[self.sample_position_in, self.positions[0][0]])
# # open the shutter
# yield from self.stubs.set(device="eyex", value=self.dark_shutter_pos_out)
# # TODO add opening of fast shutter
# # start the flyer
# flyer_request = yield from self.stubs.set(
# device=self.motor, value=self.positions[1][0], wait=False
# )
# self.connector.send_client_info(
# "Starting the scan", show_asap=True, rid=self.metadata.get("RID")
# )
# yield from self.stubs.trigger()
# while not flyer_request.done:
# yield from self.stubs.read(
# group="monitored", point_id=self.point_id
# )
# time.sleep(1)
# # increase the point id
# self.point_id += 1
# self.num_pos = self.point_id
class AcquireRefsV2(Acquire):
scan_name = "acquire_refs_v2"
gui_config = {}
def __init__(
self,
motor: DeviceBase,
num_darks: int = 0,
num_flats: int = 0,
sample_angle_out: float = 0,
sample_position_in: float = 0,
sample_position_out: float = 1,
file_prefix_dark: str = 'tmp_dark',
file_prefix_white: str = 'tmp_white',
**kwargs
):
"""
Acquire reference images (darks + whites) and return to beam position.
Reference images are acquired automatically in an optimized sequence and
the sample is returned to the sample_in_position afterwards.
Args:
motor : DeviceBase
Motor to be moved to move the sample out of beam
num_darks : int , optional
Number of dark field images to acquire
num_flats : int , optional
Number of white field images to acquire
sample_angle_out : float , optional
Angular position where to take the flat field images
sample_position_in : float , optional
Sample stage X position for sample in beam [um]
sample_position_out : float ,optional
Sample stage X position for sample out of the beam [um]
exp_time : float, optional
Exposure time [ms]. If not specified, the currently configured value
on the camera will be used
exp_period : float, optional
Exposure period [ms]. If not specified, the currently configured value
on the camera will be used
image_width : int, optional
ROI size in the x-direction [pixels]. If not specified, the currently
configured value on the camera will be used
image_height : int, optional
ROI size in the y-direction [pixels]. If not specified, the currently
configured value on the camera will be used
acq_mode : str, optional
Predefined acquisition mode (default= 'default')
file_path : str, optional
File path for standard daq
Returns:
ScanReport
Examples:
>>> scans.acquire_refs(sample_angle_out=90, sample_position_in=10, num_darks=5, num_flats=5, exp_time=0.1)
"""
self.motor = motor
super().__init__(**kwargs)
self.sample_position_in = sample_position_in
self.sample_position_out = sample_position_out
self.sample_angle_out = sample_angle_out
self.num_darks = num_darks
self.num_flats = num_flats
self.file_prefix_dark = file_prefix_dark
self.file_prefix_white = file_prefix_white
self.scan_parameters["std_daq_params"] = {"reconnect": False}
def stage(self):
"""Wrapped scan doesn't need staging"""
yield None
def scan_core(self):
if self.num_darks:
msg = f"Acquiring {self.num_darks} dark images"
logger.warning(msg)
self.connector.send_client_info(
msg,
show_asap=True,
rid=self.metadata.get("RID"),
)
darks = AcquireDarkV2(
exp_burst=self.num_darks,
# file_prefix=self.file_prefix_dark,
device_manager=self.device_manager,
metadata=self.metadata,
instruction_handler=self.stubs._instruction_handler,
**self.caller_kwargs,
)
yield from darks.direct()
self.point_id = darks.point_id
if self.num_flats:
msg = f"Acquiring {self.num_flats} flat field images"
logger.warning(msg)
self.connector.send_client_info(
msg,
show_asap=True,
rid=self.metadata.get("RID"),
)
logger.warning("Calling AcquireWhite")
flats = AcquireWhiteV2(
motor=self.motor,
exp_burst=self.num_flats,
sample_position_out=self.sample_position_out,
# sample_angle_out=self.sample_angle_out,
device_manager=self.device_manager,
metadata=self.metadata,
instruction_handler=self.stubs._instruction_handler,
**self.caller_kwargs,
)
flats.point_id = self.point_id
yield from flats.direct()
self.point_id = flats.point_id
## TODO move sample in beam and do not wait
## TODO move rotation to angle and do not wait
logger.warning("[AcquireRefsV2] Done with scan_core")