diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index c62626f..4087d59 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -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 diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index 5a1820b..82345d9 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -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) diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 3a08126..29e16f5 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -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 diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index ab5e70c..547bc6e 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -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 diff --git a/tomcat_bec/scans/advanced_scans.py b/tomcat_bec/scans/advanced_scans.py new file mode 100644 index 0000000..8bb0808 --- /dev/null +++ b/tomcat_bec/scans/advanced_scans.py @@ -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") +