From d9940b2fd21134b47830ad8c2e277e42df38a0f8 Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Mon, 15 Jan 2024 18:57:22 -0500 Subject: [PATCH 01/33] First commit for optimized autoraster --- bluesky_env/devices/auto_center.py | 211 +++++++++ bluesky_env/devices/top_align.py | 343 ++++++++++++++ bluesky_env/devices/zebra.py | 698 ++++++++++++++++++++++++++++ bluesky_env/plans/__init__.py | 2 + bluesky_env/plans/loop_detection.py | 121 +++++ bluesky_env/plans/top_view.py | 133 ++++++ bluesky_env/plans/utils.py | 27 ++ daq_lib.py | 2 +- daq_macros.py | 140 +++++- embl_robot.py | 7 +- start_bs.py | 17 + 11 files changed, 1690 insertions(+), 11 deletions(-) create mode 100644 bluesky_env/devices/auto_center.py create mode 100644 bluesky_env/devices/top_align.py create mode 100644 bluesky_env/devices/zebra.py create mode 100644 bluesky_env/plans/__init__.py create mode 100644 bluesky_env/plans/loop_detection.py create mode 100644 bluesky_env/plans/top_view.py create mode 100644 bluesky_env/plans/utils.py diff --git a/bluesky_env/devices/auto_center.py b/bluesky_env/devices/auto_center.py new file mode 100644 index 00000000..062be9cd --- /dev/null +++ b/bluesky_env/devices/auto_center.py @@ -0,0 +1,211 @@ + +from ophyd.areadetector.filestore_mixins import FileStorePluginBase +from ophyd.areadetector.plugins import JPEGPlugin, register_plugin, PluginBase +from ophyd import (DeviceStatus, Component as Cpt, Signal, EpicsSignal, Device, + DDC_EpicsSignal, DDC_EpicsSignalRO, ADComponent as ADCpt, ImagePlugin, + ROIPlugin, TransformPlugin, StatsPlugin, ProcessPlugin, SingleTrigger, ProsilicaDetector) +from pathlib import Path +import requests + +import os + + +@register_plugin +class CVPlugin(PluginBase): + _default_suffix = "CV1:" + _suffix_re = "CV1\d:" + _default_read_attrs = ["outputs"] + func_sets = DDC_EpicsSignal( + *[(f"func_set{k}", f"CompVisionFunction{k}") for k in range(1, 4)] + ) + inputs = DDC_EpicsSignal( + *[(f"input{k}", f"Input{k}") for k in range(1, 11)] + ) + outputs = DDC_EpicsSignalRO( + *[(f"output{k}", f"Output{k}_RBV") for k in range(1, 11)] + ) + cam_depth = ADCpt(EpicsSignal, "CompVisionCamDepth", kind="config") + + +class StandardProsilica(SingleTrigger, ProsilicaDetector): + image = Cpt(ImagePlugin, "image1:") + roi1 = Cpt(ROIPlugin, "ROI1:") + roi2 = Cpt(ROIPlugin, "ROI2:") + roi3 = Cpt(ROIPlugin, "ROI3:") + roi4 = Cpt(ROIPlugin, "ROI4:") + trans1 = Cpt(TransformPlugin, "Trans1:") + proc1 = Cpt(ProcessPlugin, "Proc1:") + stats1 = Cpt(StatsPlugin, "Stats1:") + stats2 = Cpt(StatsPlugin, "Stats2:") + stats3 = Cpt(StatsPlugin, "Stats3:") + stats4 = Cpt(StatsPlugin, "Stats4:") + stats5 = Cpt(StatsPlugin, "Stats5:") + +class FileStoreJPEG(FileStorePluginBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.filestore_spec = "AD_JPEG" # spec name stored in resource doc + self.stage_sigs.update( + [ + ("file_template", "%s%s_%6.6d.jpg"), + ("file_write_mode", "Single"), + ] + ) + # 'Single' file_write_mode means one image : one file. + # It does NOT mean that 'num_images' is ignored. + + def get_frames_per_point(self): + return self.parent.cam.num_images.get() + + def stage(self): + super().stage() + # this over-rides the behavior is the base stage + self._fn = self._fp + + resource_kwargs = { + "template": self.file_template.get(), + "filename": self.file_name.get(), + "frame_per_point": self.get_frames_per_point(), + } + self._generate_resource(resource_kwargs) + + +class JPEGPluginWithFileStore(JPEGPlugin, FileStoreJPEG): + pass + + +class LoopDetector(Device): + url = Cpt( + Signal, value=f'{os.environ["SAMPLE_DETECT_URL"]}/predict', kind='config' + ) + filename = Cpt(Signal, value=None) + box = Cpt(Signal, value=[]) + thresholded_box = Cpt(Signal, value=[]) + x_start = Cpt(Signal, value=0) + x_end = Cpt(Signal, value=0) + get_threshold = Cpt(Signal, value=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.read_attrs = ['box', 'thresholded_box'] + + def trigger(self): + + filename_dict = {"file": Path(self.filename.get()).open('rb')} + post_data = None + if self.get_threshold.get(): + post_data = {"x_start": self.x_start.get(), "x_end": self.x_end.get()} + response = requests.post(self.url.get(), files=filename_dict, data=post_data) + response.raise_for_status() + json_response = response.json() + if json_response['pred_boxes']: + self.box.put(response.json()['pred_boxes'][0]['box']) + else: + self.box.put([]) + if "threshold" in json_response: + self.thresholded_box.put(response.json()["threshold"]) + else: + self.thresholded_box.put([]) + + self.get_threshold.set(False) + response_status = DeviceStatus(self.box, timeout=10) + response_status.set_finished() + + return response_status + + +class TwoClickLowMag(StandardProsilica): + cv1 = Cpt(CVPlugin, "CV1:") + cam_mode = Cpt(Signal, value=None, kind="config") + pix_per_um = Cpt(Signal, value=1, kind="config") + x_min = Cpt(Signal, value=0, doc="min horizontal pixel", kind="config") + x_max = Cpt(Signal, value=640, doc="max horizontal pixel", kind="config") + + jpeg = Cpt( + JPEGPluginWithFileStore, + "JPEG1:", + write_path_template="/nsls2/data/amx/legacy/topcam", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.read_attrs = ["cv1", "jpeg"] + self.jpeg.read_attrs = ['full_file_name'] + self.cv1.read_attrs = ["outputs"] + self.cv1.outputs.read_attrs = ["output1", "output2", "output3"] + self.cam_mode.subscribe(self._update_stage_sigs, event_type="value") + + def _update_stage_sigs(self, *args, **kwargs): + self.stage_sigs.clear() + self.stage_sigs.update( + [ + ("cam.acquire", 0), + ("cam.image_mode", 1), + ("cam.acquire_time", 0.0022), + ("cam.acquire_period", 1), + ] + ) + if self.cam_mode.get() == "centroid": + self.stage_sigs.update( + [ + ("cv1.enable", 1), + ("cv1.func_sets.func_set2", "Centroid Identification"), + ("cv1.inputs.input1", 1), + ("cv1.inputs.input2", 5), + ("cv1.inputs.input3", 30), + ("cv1.inputs.input4", 3000000), + ("cv1.inputs.input5", 5000), + ] + ) + elif self.cam_mode.get() == "edge_detection": + self.stage_sigs.update( + [ + ("cv1.enable", 1), + ("cv1.nd_array_port", "ROI4"), + ("cv1.func_sets.func_set1", "Canny Edge Detection"), + ("cv1.inputs.input1", 20), + ("cv1.inputs.input2", 8), + ("cv1.inputs.input3", 9), + ("cv1.inputs.input4", 5), + ("roi4.min_xyz.min_y", self.roi1.min_xyz.min_y.get()), + ( + "roi4.min_xyz.min_x", + self.roi1.min_xyz.min_x.get() + 245, + ), + ("roi4.size.x", 240), + ("roi4.size.y", self.roi1.size.y.get()), + ] + ) + elif self.cam_mode.get() == "two_click": + self.stage_sigs.update( + [ + ("jpeg.nd_array_port", "ROI2"), + ("cv1.nd_array_port", "ROI2"), + ("cv1.enable", 1), + ("cv1.func_sets.func_set1", "Threshold"), + ("cv1.func_sets.func_set2", "None"), + ("cv1.func_sets.func_set3", "None"), + ("cv1.inputs.input1", 1), + # x_min; update in plan + ("cv1.inputs.input2", self.x_min.get()), + # x_max; update in plan + ("cv1.inputs.input3", self.x_max.get()), + ("cv1.inputs.input4", 0), # y_min + ("cv1.inputs.input5", 512) # y_max + ] + ) + + def stage(self, *args, **kwargs): + self._update_stage_sigs(*args, **kwargs) + super().stage(*args, **kwargs) + + +class WorkPositions(Device): + gx = Cpt(EpicsSignal, '{Gov:Robot-Dev:gx}Pos:Work-Pos') + gpy = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpy}Pos:Work-Pos') + gpz = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpz}Pos:Work-Pos') + o = Cpt(EpicsSignal, '{Gov:Robot-Dev:go}Pos:Work-Pos') + + + + diff --git a/bluesky_env/devices/top_align.py b/bluesky_env/devices/top_align.py new file mode 100644 index 00000000..c14a8def --- /dev/null +++ b/bluesky_env/devices/top_align.py @@ -0,0 +1,343 @@ + +from ophyd.areadetector.filestore_mixins import FileStoreTIFF +from ophyd.areadetector.filestore_mixins import FileStorePluginBase +from ophyd.areadetector.plugins import TIFFPlugin +from ophyd.areadetector.plugins import JPEGPlugin, register_plugin, PluginBase +from ophyd import (Component as Cpt, Signal, EpicsSignal, EpicsSignalRO, Device, + EpicsMotor, ImagePlugin, ROIPlugin, TransformPlugin, + StatsPlugin, ProcessPlugin, SingleTrigger, ProsilicaDetector, + DDC_EpicsSignal, DDC_EpicsSignalRO, ADComponent as ADCpt) +from ophyd.status import SubscriptionStatus +from pathlib import Path +import requests +from .zebra import Zebra +from enum import Enum +from start_bs import gov_robot + + +class TopPlanLimit(Enum): + """Prevent large goniometer movements during topview plan. Rotating gonio + with piezo stages extended can hit things like the collimator or beamstop. + + ... + Attributes + ---------- + DELTAY : float + Gonio pin Y stage movement limit (microns) + DELTAZ : float + Gonio pin Z stage movement limit (microns) + OMEGAMIN : float + Gonio omega rotation stage movement limit (degrees) + """ + + DELTAY = DELTAZ = 2500 + OMEGAMIN = 400 + +class TIFFPluginWithFileStore(TIFFPlugin, FileStoreTIFF): + pass + +@register_plugin +class CVPlugin(PluginBase): + _default_suffix = "CV1:" + _suffix_re = "CV1\d:" + _default_read_attrs = ["outputs"] + func_sets = DDC_EpicsSignal( + *[(f"func_set{k}", f"CompVisionFunction{k}") for k in range(1, 4)] + ) + inputs = DDC_EpicsSignal( + *[(f"input{k}", f"Input{k}") for k in range(1, 11)] + ) + outputs = DDC_EpicsSignalRO( + *[(f"output{k}", f"Output{k}_RBV") for k in range(1, 11)] + ) + cam_depth = ADCpt(EpicsSignal, "CompVisionCamDepth", kind="config") + + +class StandardProsilica(SingleTrigger, ProsilicaDetector): + image = Cpt(ImagePlugin, "image1:") + roi1 = Cpt(ROIPlugin, "ROI1:") + roi2 = Cpt(ROIPlugin, "ROI2:") + roi3 = Cpt(ROIPlugin, "ROI3:") + roi4 = Cpt(ROIPlugin, "ROI4:") + trans1 = Cpt(TransformPlugin, "Trans1:") + proc1 = Cpt(ProcessPlugin, "Proc1:") + stats1 = Cpt(StatsPlugin, "Stats1:") + stats2 = Cpt(StatsPlugin, "Stats2:") + stats3 = Cpt(StatsPlugin, "Stats3:") + stats4 = Cpt(StatsPlugin, "Stats4:") + stats5 = Cpt(StatsPlugin, "Stats5:") + +class EpicsMotorSPMG(EpicsMotor): + SPMG = Cpt(EpicsSignal, ".SPMG") + + +class TopAlignCam(StandardProsilica): + _default_read_attrs = ["cv1", "tiff"] + cv1 = Cpt(CVPlugin, "CV1:") + tiff = Cpt( + TIFFPluginWithFileStore, + "TIFF1:", + write_path_template="/nsls2/data/amx/legacy/topcam", + ) + cam_mode = Cpt(Signal, value=None, kind="config") + pix_per_um = Cpt(Signal, value=0.164, doc="pixels per um") + out10_buffer = Cpt( + EpicsSignalRO, + "Out10:compress", + doc="circular buffer for ADCompVision output10 (thresholding loop profile)", + ) + out10_reset = Cpt(EpicsSignal, "Out10:compress.RES") + out9_buffer = Cpt(EpicsSignalRO, "Out9:compress") + out9_reset = Cpt(EpicsSignal, "Out9:compress.RES") + + def __init__(self, *args, **kwargs): + super().__init__( + *args, + **kwargs, + ) + #self.tiff.read_attrs = [] + self.cv1.read_attrs = ["outputs"] + self.cv1.outputs.read_attrs = [ + "output8", + "output9", + "output10", + ] + self.cam_mode.subscribe(self._update_stage_sigs, event_type="value") + + def _update_stage_sigs(self, *args, **kwargs): + self.tiff.stage_sigs.update([("enable", 0)]) + self.stage_sigs.clear() + self.stage_sigs.update( + [ + ("cv1.func_sets.func_set1", "Canny Edge Detection"), + ("cv1.inputs.input1", 8), + ("cv1.inputs.input2", 3), + ("cv1.inputs.input3", 1), + ("cv1.inputs.input4", 3), + ("cv1.inputs.input5", 13), + ("cv1.inputs.input6", 1), + ("trans1.nd_array_port", "PROC1"), + ("tiff.nd_array_port", "CV1"), + ] + ) + if self.cam_mode.get() == "coarse_align": + self.stage_sigs.update( + [ + ("cv1.nd_array_port", "ROI2"), + ("roi2.nd_array_port", "TRANS1"), + ] + ) + elif self.cam_mode.get() == "fine_face": + self.stage_sigs.update( + [ + ("cv1.nd_array_port", "ROI1"), + ("roi1.nd_array_port", "TRANS1"), + ] + ) + + def stage(self, *args, **kwargs): + self._update_stage_sigs(*args, **kwargs) + super().stage(*args, **kwargs) + + +class ZebraMXOr(Zebra): + or3 = Cpt(EpicsSignal, "OR3_ENA:B3") + or3loc = Cpt(EpicsSignal, "OR3_INP4") + armsel = Cpt(EpicsSignal, "PC_ARM_SEL") + + +class TopAlignerBase(Device): + + topcam = Cpt(TopAlignCam, "XF:17IDB-ES:AMX{Cam:9}") + gonio_o = Cpt(EpicsMotor, "XF:17IDB-ES:AMX{Gon:1-Ax:O}Mtr", timeout=6) + gonio_py = Cpt( + EpicsMotorSPMG, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Mtr", timeout=6 + ) + gonio_pz = Cpt( + EpicsMotorSPMG, "XF:17IDB-ES:AMX{Gon:1-Ax:PZ}Mtr", timeout=6 + ) + kill_py = Cpt(EpicsSignal, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Cmd:Kill-Cmd") + kill_pz = Cpt(EpicsSignal, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Cmd:Kill-Cmd") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.read_attrs = [] + self._configure_device() + + def _configure_device(self, *args, **kwargs): + raise NotImplementedError( + "Subclasses must implement custom device configuration" + ) + + def stage(self, *args, **kwargs): + if type(self) == TopAlignerBase: + raise NotImplementedError("TopAlignerBase has no stage method") + super().stage(*args, **kwargs) + + def trigger(self): + raise NotImplementedError("Subclasses must implement custom trigger") + + def unstage(self, *args, **kwargs): + if type(self) == TopAlignerBase: + raise NotImplementedError("TopAlignerBase has no unstage method") + super().unstage(*args, **kwargs) + + +class TopAlignerFast(TopAlignerBase): + + zebra = Cpt(ZebraMXOr, "XF:17IDB-ES:AMX{Zeb:2}:") + target_gov_state = Cpt( + Signal, value=None, doc="target governor state used to trigger device" + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.target_gov_state.subscribe( + self._update_stage_sigs, event_type="value" + ) + + def _configure_device(self, *args, **kwargs): + self.read_attrs = ["topcam", "zebra"] + self.stage_sigs.clear() + self.topcam.cam_mode.set("fine_face") + + self.stage_sigs.update( + [ + ("topcam.cam.trigger_mode", 1), + ("topcam.cam.image_mode", 2), + ("topcam.cam.acquire", 1), + ("gonio_o.velocity", 90), # slow down to get picture taken + ] + ) + self.zebra.stage_sigs.update( + [ + ("pos_capt.source", "Enc4"), + ("pos_capt.direction", 1), + ("armsel", 0), + ("pos_capt.gate.start", 0.5), # very important + ("pos_capt.gate.width", 4.5), + ("pos_capt.gate.step", 9), + ("pos_capt.gate.num_gates", 20), + ("pos_capt.pulse.start", 0), + ("pos_capt.pulse.width", 4), + ("pos_capt.pulse.step", 5), + ("pos_capt.pulse.delay", 0), + ("pos_capt.pulse.max_pulses", 1), + ("or3", 1), + ("or3loc", 30), + ] + ) + self.topcam.read_attrs = ["out9_buffer", "out10_buffer"] + self.zebra.read_attrs = ["pos_capt.data.enc4"] + + def _update_stage_sigs(self, *args, **kwargs): + + if self.target_gov_state.get() == "TA": + self.zebra.stage_sigs.update( + [ + ("pos_capt.direction", 0), # positive + ("pos_capt.gate.start", 0.1), + ] + ) + + elif self.target_gov_state.get() == "SA": + self.zebra.stage_sigs.update( + [ + ("pos_capt.direction", 1), # negative + ("pos_capt.gate.start", 180), + ] + ) + + else: + raise Exception("Target gov state not implemented!") + + def stage(self, *args, **kwargs): + self._update_stage_sigs() + super().stage(*args, **kwargs) + + def callback_armed(value, old_value, **kwargs): + if old_value == 0 and value == 1: + return True + else: + return False + + callback_armed_status = SubscriptionStatus( + self.zebra.pos_capt.arm.output, + callback_armed, + run=False, + settle_time=0.5, + ) + self.zebra.pos_capt.arm.arm.set(1) + callback_armed_status.wait(timeout=3) + + def unstage(self, **kwargs): + super().unstage(**kwargs) + # self.zebra.pos_capt.arm.disarm.set(1) + + def trigger(self): + def callback_unarmed(value, old_value, **kwargs): + if old_value == 1 and value == 0: + return True + else: + return False + + callback_unarmed_status = SubscriptionStatus( + self.zebra.pos_capt.arm.output, + callback_unarmed, + run=False, + timeout=6, + ) + + if (self.target_gov_state.get() in gov_robot.reachable.get() or + self.target_gov_state.get() == gov_robot.setpoint.get()): + gov_robot.set(self.target_gov_state.get(), wait=True) + # self.gonio_o.set(0) + return callback_unarmed_status + + else: + raise Exception( + f'{self.target_gov_state.get()} is wrong governor state for transition' + ) + + +class TopAlignerSlow(TopAlignerBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _configure_device(self, *args, **kwargs): + + self.stage_sigs.clear() + self.stage_sigs.update( + [ + ("topcam.cam.trigger_mode", 5), + ("topcam.cam.image_mode", 1), + ("topcam.cam.acquire", 0), + ] + ) + self.topcam.cam_mode.set("coarse_align") + self.read_attrs = [ + "topcam.cv1.outputs.output8", + "topcam.cv1.outputs.output9", + "topcam.cv1.outputs.output10", + "gonio_o", + ] + + def trigger(self): + return self.topcam.trigger() + + def read(self): + return self.topcam.read() + + + + +class GoniometerStack(Device): + gx = Cpt(EpicsMotor, "-Ax:GX}Mtr") + gy = Cpt(EpicsMotor, "-Ax:GY}Mtr") + gz = Cpt(EpicsMotor, "-Ax:GZ}Mtr") + o = Cpt(EpicsMotor, "-Ax:O}Mtr") + py = Cpt(EpicsMotor, "-Ax:PY}Mtr") + pz = Cpt(EpicsMotor, "-Ax:PZ}Mtr") + + + diff --git a/bluesky_env/devices/zebra.py b/bluesky_env/devices/zebra.py new file mode 100644 index 00000000..d2f5c334 --- /dev/null +++ b/bluesky_env/devices/zebra.py @@ -0,0 +1,698 @@ +print(f"Loading {__file__}") + +from enum import IntEnum + +from ophyd import (Device, Component as Cpt, FormattedComponent as FC, + Signal) +from ophyd import (EpicsSignal, EpicsSignalRO, DeviceStatus) +from ophyd.utils import set_and_wait +from bluesky.plans import fly +import pandas as pd + +import uuid +import time +import datetime as dt +import os + + +def _get_configuration_attrs(cls, *, signal_class=Signal): + return [sig_name for sig_name in cls.component_names + if issubclass(getattr(cls, sig_name).cls, signal_class)] + + +class ZebraInputEdge(IntEnum): + FALLING = 1 + RISING = 0 + + +class ZebraAddresses(IntEnum): + DISCONNECT = 0 + IN1_TTL = 1 + IN1_NIM = 2 + IN1_LVDS = 3 + IN2_TTL = 4 + IN2_NIM = 5 + IN2_LVDS = 6 + IN3_TTL = 7 + IN3_OC = 8 + IN3_LVDS = 9 + IN4_TTL = 10 + IN4_CMP = 11 + IN4_PECL = 12 + IN5_ENCA = 13 + IN5_ENCB = 14 + IN5_ENCZ = 15 + IN5_CONN = 16 + IN6_ENCA = 17 + IN6_ENCB = 18 + IN6_ENCZ = 19 + IN6_CONN = 20 + IN7_ENCA = 21 + IN7_ENCB = 22 + IN7_ENCZ = 23 + IN7_CONN = 24 + IN8_ENCA = 25 + IN8_ENCB = 26 + IN8_ENCZ = 27 + IN8_CONN = 28 + PC_ARM = 29 + PC_GATE = 30 + PC_PULSE = 31 + AND1 = 32 + AND2 = 33 + AND3 = 34 + AND4 = 35 + OR1 = 36 + OR2 = 37 + OR3 = 38 + OR4 = 39 + GATE1 = 40 + GATE2 = 41 + GATE3 = 42 + GATE4 = 43 + DIV1_OUTD = 44 + DIV2_OUTD = 45 + DIV3_OUTD = 46 + DIV4_OUTD = 47 + DIV1_OUTN = 48 + DIV2_OUTN = 49 + DIV3_OUTN = 50 + DIV4_OUTN = 51 + PULSE1 = 52 + PULSE2 = 53 + PULSE3 = 54 + PULSE4 = 55 + QUAD_OUTA = 56 + QUAD_OUTB = 57 + CLOCK_1KHZ = 58 + CLOCK_1MHZ = 59 + SOFT_IN1 = 60 + SOFT_IN2 = 61 + SOFT_IN3 = 62 + SOFT_IN4 = 63 + + +class ZebraSignalWithRBV(EpicsSignal): + # An EPICS signal that uses the Zebra convention of 'pvname' being the + # setpoint and 'pvname:RBV' being the read-back + + def __init__(self, prefix, **kwargs): + super().__init__(prefix + ':RBV', write_pv=prefix, **kwargs) + + +class ZebraPulse(Device): + width = Cpt(ZebraSignalWithRBV, 'WID') + input_addr = Cpt(ZebraSignalWithRBV, 'INP') + input_str = Cpt(EpicsSignalRO, 'INP:STR', string=True) + input_status = Cpt(EpicsSignalRO, 'INP:STA') + delay = Cpt(ZebraSignalWithRBV, 'DLY') + delay_sync = Cpt(EpicsSignal, 'DLY:SYNC') + time_units = Cpt(ZebraSignalWithRBV, 'PRE', string=True) + output = Cpt(EpicsSignal, 'OUT') + + input_edge = FC(EpicsSignal, + '{self._zebra_prefix}POLARITY:{self._edge_addr}') + + _edge_addrs = {1: 'BC', + 2: 'BD', + 3: 'BE', + 4: 'BF', + } + + def __init__(self, prefix, *, index=None, parent=None, + configuration_attrs=None, read_attrs=None, **kwargs): + if read_attrs is None: + read_attrs = ['input_status', 'output'] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=ZebraSignalWithRBV) + ['input_edge'] + + zebra = parent + self.index = index + self._zebra_prefix = zebra.prefix + self._edge_addr = self._edge_addrs[index] + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, parent=parent, **kwargs) + + +class ZebraOutputBase(Device): + '''The base of all zebra outputs (1~8) + + Front outputs + # TTL LVDS NIM PECL OC ENC + 1 o o o + 2 o o o + 3 o o o + 4 o o o + + Rear outputs + # TTL LVDS NIM PECL OC ENC + 5 o + 6 o + 7 o + 8 o + + ''' + def __init__(self, prefix, *, index=None, read_attrs=None, + configuration_attrs=None, **kwargs): + self.index = index + + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__) + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraOutputType(Device): + '''Shared by all output types (ttl, lvds, nim, pecl, out)''' + addr = Cpt(ZebraSignalWithRBV, '') + status = Cpt(EpicsSignalRO, ':STA') + string = Cpt(EpicsSignalRO, ':STR', string=True) + sync = Cpt(EpicsSignal, ':SYNC') + write_output = Cpt(EpicsSignal, ':SET') + + def __init__(self, prefix, *, read_attrs=None, configuration_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = ['status'] + if configuration_attrs is None: + configuration_attrs = ['addr'] + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraFrontOutput12(ZebraOutputBase): + ttl = Cpt(ZebraOutputType, 'TTL') + lvds = Cpt(ZebraOutputType, 'LVDS') + nim = Cpt(ZebraOutputType, 'NIM') + + def __init__(self, prefix, *, read_attrs=None, configuration_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=ZebraOutputType) + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraFrontOutput3(ZebraOutputBase): + ttl = Cpt(ZebraOutputType, 'TTL') + lvds = Cpt(ZebraOutputType, 'LVDS') + open_collector = Cpt(ZebraOutputType, 'OC') + + def __init__(self, prefix, *, read_attrs=None, configuration_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=ZebraOutputType) + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraFrontOutput4(ZebraOutputBase): + ttl = Cpt(ZebraOutputType, 'TTL') + nim = Cpt(ZebraOutputType, 'NIM') + pecl = Cpt(ZebraOutputType, 'PECL') + + def __init__(self, prefix, *, read_attrs=None, configuration_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=ZebraOutputType) + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraRearOutput(ZebraOutputBase): + enca = Cpt(ZebraOutputType, 'ENCA') + encb = Cpt(ZebraOutputType, 'ENCB') + encz = Cpt(ZebraOutputType, 'ENCZ') + conn = Cpt(ZebraOutputType, 'CONN') + + def __init__(self, prefix, *, read_attrs=None, configuration_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=ZebraOutputType) + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, **kwargs) + + +class ZebraEncoder(Device): + motor_pos = FC(EpicsSignalRO, '{self._zebra_prefix}M{self.index}:RBV') + zebra_pos = FC(EpicsSignal, '{self._zebra_prefix}POS{self.index}_SET') + encoder_res = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:MRES') + encoder_off = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:OFF') + _copy_pos_signal = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:SETPOS.PROC') + + def __init__(self, prefix, *, index=None, parent=None, + configuration_attrs=None, read_attrs=None, **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = ['encoder_res', 'encoder_off'] + + self.index = index + self._zebra_prefix = parent.prefix + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, **kwargs) + + def copy_position(self): + self._copy_pos_signal.put(1, wait=True) + + +class ZebraGateInput(Device): + addr = Cpt(ZebraSignalWithRBV, '') + string = Cpt(EpicsSignalRO, ':STR', string=True) + status = Cpt(EpicsSignalRO, ':STA') + sync = Cpt(EpicsSignal, ':SYNC') + write_input = Cpt(EpicsSignal, ':SET') + + # Input edge index depends on the gate number (these are set in __init__) + edge = FC(EpicsSignal, + '{self._zebra_prefix}POLARITY:B{self._input_edge_idx}') + + def __init__(self, prefix, *, index=None, parent=None, + configuration_attrs=None, read_attrs=None, **kwargs): + if read_attrs is None: + read_attrs = ['status'] + if configuration_attrs is None: + configuration_attrs = ['addr', 'edge'] + + gate = parent + zebra = gate.parent + + self.index = index + self._zebra_prefix = zebra.prefix + self._input_edge_idx = gate._input_edge_idx[self.index] + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, **kwargs) + + +class ZebraGate(Device): + input1 = Cpt(ZebraGateInput, 'INP1', index=1) + input2 = Cpt(ZebraGateInput, 'INP2', index=2) + output = Cpt(EpicsSignal, 'OUT') + + def __init__(self, prefix, *, index=None, read_attrs=None, + configuration_attrs=None, **kwargs): + self.index = index + self._input_edge_idx = {1: index - 1, + 2: 4 + index - 1 + } + + if read_attrs is None: + read_attrs = ['output'] + if configuration_attrs is None: + configuration_attrs = ['input1', 'input2'] + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + def set_input_edges(self, edge1, edge2): + set_and_wait(self.input1.edge, int(edge1)) + set_and_wait(self.input2.edge, int(edge2)) + + +class ZebraPositionCaptureDeviceBase(Device): + source = Cpt(ZebraSignalWithRBV, 'SEL', put_complete=True) + input_addr = Cpt(ZebraSignalWithRBV, 'INP') + input_str = Cpt(EpicsSignalRO, 'INP:STR', string=True) + input_status = Cpt(EpicsSignalRO, 'INP:STA', auto_monitor=True) + output = Cpt(EpicsSignalRO, 'OUT', auto_monitor=True) + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + + if read_attrs is None: + read_attrs = [] + read_attrs += ['input_status', 'output'] + + if configuration_attrs is None: + configuration_attrs = [] + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + +class ZebraPositionCaptureArm(ZebraPositionCaptureDeviceBase): + + class ZebraArmSignalWithRBV(EpicsSignal): + def __init__(self, prefix, **kwargs): + super().__init__(prefix + 'ARM_OUT', write_pv=prefix+'ARM', **kwargs) + + class ZebraDisarmSignalWithRBV(EpicsSignal): + def __init__(self, prefix, **kwargs): + super().__init__(prefix + 'ARM_OUT', write_pv=prefix+'DISARM', **kwargs) + + arm = FC(ZebraArmSignalWithRBV, '{self._parent_prefix}') + disarm = FC(ZebraDisarmSignalWithRBV, '{self._parent_prefix}') + + def __init__(self, prefix, *, parent=None, + configuration_attrs=None, read_attrs=None, **kwargs): + + self._parent_prefix = parent.prefix + + super().__init__(prefix, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, **kwargs) + + +class ZebraPositionCaptureGate(ZebraPositionCaptureDeviceBase): + num_gates = Cpt(EpicsSignal, 'NGATE') + start = Cpt(EpicsSignal, 'START') + width = Cpt(EpicsSignal, 'WID') + step = Cpt(EpicsSignal, 'STEP') + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=EpicsSignal) + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + +class ZebraPositionCapturePulse(ZebraPositionCaptureDeviceBase): + max_pulses = Cpt(EpicsSignal, 'MAX') + start = Cpt(EpicsSignal, 'START') + width = Cpt(EpicsSignal, 'WID') + step = Cpt(EpicsSignal, 'STEP') + delay = Cpt(EpicsSignal, 'DLY') + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = _get_configuration_attrs(self.__class__, signal_class=EpicsSignal) + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + +class ZebraPositionCaptureData(Device): + num_captured = Cpt(EpicsSignalRO, 'NUM_CAP') + num_downloaded = Cpt(EpicsSignalRO, 'NUM_DOWN') + + time = Cpt(EpicsSignalRO, 'TIME') + + enc1 = Cpt(EpicsSignalRO, 'ENC1') + enc2 = Cpt(EpicsSignalRO, 'ENC2') + enc3 = Cpt(EpicsSignalRO, 'ENC3') + enc4 = Cpt(EpicsSignalRO, 'ENC4') + + sys1 = Cpt(EpicsSignalRO, 'SYS1') + sys2 = Cpt(EpicsSignalRO, 'SYS2') + + div1 = Cpt(EpicsSignalRO, 'DIV1') + div2 = Cpt(EpicsSignalRO, 'DIV2') + div3 = Cpt(EpicsSignalRO, 'DIV3') + div4 = Cpt(EpicsSignalRO, 'DIV4') + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + + if read_attrs is None: + read_attrs = _get_configuration_attrs(self.__class__, signal_class=EpicsSignalRO) + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + +class ZebraPositionCapture(Device): + source = Cpt(ZebraSignalWithRBV, 'ENC') + direction = Cpt(ZebraSignalWithRBV, 'DIR') + time_units = Cpt(ZebraSignalWithRBV, 'TSPRE') + + arm = Cpt(ZebraPositionCaptureArm, 'ARM_') + gate = Cpt(ZebraPositionCaptureGate, 'GATE_') + pulse = Cpt(ZebraPositionCapturePulse, 'PULSE_') + + capture_enc1 = Cpt(EpicsSignal, 'BIT_CAP:B0') + capture_enc2 = Cpt(EpicsSignal, 'BIT_CAP:B1') + capture_enc3 = Cpt(EpicsSignal, 'BIT_CAP:B2') + capture_enc4 = Cpt(EpicsSignal, 'BIT_CAP:B3') + + capture_sys1 = Cpt(EpicsSignal, 'BIT_CAP:B4') + capture_sys2 = Cpt(EpicsSignal, 'BIT_CAP:B5') + + capture_div1 = Cpt(EpicsSignal, 'BIT_CAP:B6') + capture_div2 = Cpt(EpicsSignal, 'BIT_CAP:B7') + capture_div3 = Cpt(EpicsSignal, 'BIT_CAP:B8') + capture_div4 = Cpt(EpicsSignal, 'BIT_CAP:B9') + + data = Cpt(ZebraPositionCaptureData, '') + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + + if read_attrs is None: + read_attrs = ['data'] + if configuration_attrs is None: + configuration_attrs = ( + ['source', 'direction', 'time_units', + 'arm', 'gate', 'pulse'] + + [f'capture_enc{i}' for i in range(1,5)] + + [f'capture_sys{i}' for i in range(1,3)] + + [f'capture_div{i}' for i in range(1,5)] + ) + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + +class ZebraBase(Device): + soft_input1 = Cpt(EpicsSignal, 'SOFT_IN:B0') + soft_input2 = Cpt(EpicsSignal, 'SOFT_IN:B1') + soft_input3 = Cpt(EpicsSignal, 'SOFT_IN:B2') + soft_input4 = Cpt(EpicsSignal, 'SOFT_IN:B3') + + pulse1 = Cpt(ZebraPulse, 'PULSE1_', index=1) + pulse2 = Cpt(ZebraPulse, 'PULSE2_', index=2) + pulse3 = Cpt(ZebraPulse, 'PULSE3_', index=3) + pulse4 = Cpt(ZebraPulse, 'PULSE4_', index=4) + + output1 = Cpt(ZebraFrontOutput12, 'OUT1_', index=1) + output2 = Cpt(ZebraFrontOutput12, 'OUT2_', index=2) + output3 = Cpt(ZebraFrontOutput3, 'OUT3_', index=3) + output4 = Cpt(ZebraFrontOutput4, 'OUT4_', index=4) + + output5 = Cpt(ZebraRearOutput, 'OUT5_', index=5) + output6 = Cpt(ZebraRearOutput, 'OUT6_', index=6) + output7 = Cpt(ZebraRearOutput, 'OUT7_', index=7) + output8 = Cpt(ZebraRearOutput, 'OUT8_', index=8) + + gate1 = Cpt(ZebraGate, 'GATE1_', index=1) + gate2 = Cpt(ZebraGate, 'GATE2_', index=2) + gate3 = Cpt(ZebraGate, 'GATE3_', index=3) + gate4 = Cpt(ZebraGate, 'GATE4_', index=4) + + encoder1 = Cpt(ZebraEncoder, '', index=1) + encoder2 = Cpt(ZebraEncoder, '', index=2) + encoder3 = Cpt(ZebraEncoder, '', index=3) + encoder4 = Cpt(ZebraEncoder, '', index=4) + + pos_capt = Cpt(ZebraPositionCapture, 'PC_') + download_status = Cpt(EpicsSignalRO, 'ARRAY_ACQ') + reset = Cpt(EpicsSignal, 'SYS_RESET.PROC') + + addresses = ZebraAddresses + + def __init__(self, prefix, *, configuration_attrs=None, read_attrs=None, + **kwargs): + if read_attrs is None: + read_attrs = [] + if configuration_attrs is None: + configuration_attrs = ( + [f'soft_input{i}' for i in range(1,5)] + + [f'pulse{i}' for i in range(1,5)] + + [f'output{i}' for i in range(1,9)] + + [f'gate{i}' for i in range(1,5)] + + [f'encoder{i}' for i in range(1,5)] + + ['pos_capt'] + ) + + super().__init__(prefix, configuration_attrs=configuration_attrs, + read_attrs=read_attrs, **kwargs) + + self.pulse = dict(self._get_indexed_devices(ZebraPulse)) + self.output = dict(self._get_indexed_devices(ZebraOutputBase)) + self.gate = dict(self._get_indexed_devices(ZebraGate)) + self.encoder = dict(self._get_indexed_devices(ZebraEncoder)) + + def _get_indexed_devices(self, cls): + for attr in self._sub_devices: + dev = getattr(self, attr) + if isinstance(dev, cls): + yield dev.index, dev + + def trigger(self): + # Re-implement this to trigger as desired in bluesky + status = DeviceStatus(self) + status._finished() + return status + + +class Zebra(ZebraBase): + + def __init__(self, prefix, *args, **kwargs): + self._collection_ts = None + self._disarmed_status = None + self._dl_status = None + super().__init__(prefix, *args, **kwargs) + + def setup(self, master, arm_source, gate_start, gate_width, gate_step, num_gates, + direction, pulse_width, pulse_step, capt_delay, max_pulses, + collect=[True, True, True, True]): + + # arm_source is either 0 (soft) or 1 (external) + # direction is either 0 (positive) or 1 (negative) + # gate_* parameters in motor units + # pulse_*, capt_delay parameters in ms + # collect represents which of the four encoders to collect data from + + # Sanity checks + if master not in range(4): + raise ValueError(f"Invalid master positioner '{master}', must be between 0 and 3") + + if arm_source not in (0, 1): + raise ValueError('arm_source must be either 0 (soft) or 1 (external)') + + if direction not in (0, 1): + raise ValueError('direction must be either 0 (positive) or 1 (negative)') + + if gate_width > gate_step: + raise ValueError('gate_width must be smaller than gate_step') + + if pulse_width > pulse_step: + raise ValueError('pulse_width must be smaller than pulse_step') + + # Reset Zebra state + self.reset.put(1, wait=True) + time.sleep(0.1) + + pc = self.pos_capt + + pc.arm.source.put(arm_source, wait=True) + + pc.time_units.put("ms", wait=True) + pc.gate.source.put("Position", wait=True) + pc.pulse.source.put("Time", wait=True) + + # Setup which encoders to capture + for encoder, do_capture in zip((pc.capture_enc1, pc.capture_enc2, pc.capture_enc3, pc.capture_enc4), collect): + encoder.put(int(do_capture), wait=True) + + # Configure Position Capture + pc.source.put(master, wait=True) + pc.direction.put(direction, wait=True) + + # Configure Position Capture Gate + pc.gate.start.put(gate_start, wait=True) + pc.gate.width.put(gate_width, wait=True) + pc.gate.step.put(gate_step, wait=True) + pc.gate.num_gates.put(num_gates, wait=True) + + # Configure Position Capture Pulses + pc.pulse.start.put(0, wait=True) + pc.pulse.step.put(pulse_step, wait=True) + pc.pulse.width.put(pulse_width, wait=True) + pc.pulse.delay.put(capt_delay, wait=True) + pc.pulse.max_pulses.put(max_pulses, wait=True) + + # Synchronize encoders (do it last) + for encoder in self.encoder.values(): + encoder.copy_position() + + def kickoff(self): + armed_status = DeviceStatus(self) + self._disarmed_status = disarmed_status = DeviceStatus(self) + + pc = self.pos_capt + external = bool(pc.arm.source.get()) # Using external trigger? + + if external: + armed_signal = pc.arm.input_status + else: + armed_signal = pc.arm.output + + disarmed_signal = self.download_status + + self._collection_ts = time.time() + + def armed_status_cb(value, old_value, obj, **kwargs): + if int(old_value) == 0 and int(value) == 1: + armed_status._finished() + obj.clear_sub(armed_status_cb) + + def disarmed_status_cb(value, old_value, obj, **kwargs): + if int(old_value) == 1 and int(value) == 0: + disarmed_status._finished() + obj.clear_sub(disarmed_status_cb) + + armed_signal.subscribe(armed_status_cb, run=False) + disarmed_signal.subscribe(disarmed_status_cb, run=False) + + # Arm it if not External + if not external: + self.pos_capt.arm.arm.put(1) + + return armed_status + + def complete(self): + return self._disarmed_status + + def collect(self): + pc = self.pos_capt + + # Array of timestamps + ts = pc.data.time.get() + self._collection_ts + + # Arrays of captured positions + data = { + f'enc{i}': getattr(pc.data, f'enc{i}').get() + for i in range(1,5) + if getattr(pc, f'capture_enc{i}').get() + } + + for i, timestamp in enumerate(ts): + yield { + 'data': { k: v[i] for k, v in data.items() }, + 'timestamps': { k: timestamp for k in data.keys() }, + 'time' : timestamp + } + + def describe_collect(self): + return { + 'primary': { + f'enc{i}': { + 'source': 'PV:' + getattr(self.pos_capt.data, f'enc{i}').pvname, + 'shape': [], + 'dtype': 'number' + } for i in range(1, 5) if getattr(self.pos_capt, f'capture_enc{i}').get() + } + } + +zebra1 = Zebra('XF:17IDB-ES:AMX{Zeb:1}:', name='zebra1') +zebra2 = Zebra('XF:17IDB-ES:AMX{Zeb:2}:', name='zebra2') \ No newline at end of file diff --git a/bluesky_env/plans/__init__.py b/bluesky_env/plans/__init__.py new file mode 100644 index 00000000..c9d52bed --- /dev/null +++ b/bluesky_env/plans/__init__.py @@ -0,0 +1,2 @@ +from .loop_detection import detect_loop +from .top_view import topview_optimized \ No newline at end of file diff --git a/bluesky_env/plans/loop_detection.py b/bluesky_env/plans/loop_detection.py new file mode 100644 index 00000000..e3d53135 --- /dev/null +++ b/bluesky_env/plans/loop_detection.py @@ -0,0 +1,121 @@ +from typing import Dict +from logging import getLogger + +import numpy as np +import bluesky.plan_stubs as bps +import bluesky.plans as bp + +from bluesky_env.devices.auto_center import two_click_low, loop_detector +from bluesky_env.devices.top_align import gonio + +from start_bs import db + + +logger = getLogger() + +def detect_loop(sample_detection: "Dict[str, float|int]"): + # face on attempt, most features, should work + #yield from bps.abs_set(two_click_low.cam_mode, "two_click", wait=True) + + two_click_low.cam_mode.set("two_click") + + yield from bp.count([two_click_low], 1) + loop_detector.filename.set(two_click_low.jpeg.full_file_name.get()) + + """ + yield from bps.abs_set( + loop_detector.filename, two_click_low.jpeg.full_file_name.get() + ) + """ + + scan_uid = yield from bp.count([loop_detector], 1) + #box_coords_face: "list[int]" = db[scan_uid].table()['loop_detector_box'][1] + box_coords_face: "list[int]" = loop_detector.box.get() + + if len(box_coords_face) != 4: + logger.exception("Exception during loop detection plan. Face on loop not found") + sample_detection["large_box_width"] = 630 * 2 * two_click_low.pix_per_um.get() + sample_detection["large_box_height"] = 390 * 2 * two_click_low.pix_per_um.get() + mean_x = 630/2 + mean_y = 390/2 + else: + sample_detection["large_box_width"] = (box_coords_face[2] - box_coords_face[0]) * 2 * two_click_low.pix_per_um.get() + sample_detection["large_box_height"] = (box_coords_face[3] - box_coords_face[1]) * 2 * two_click_low.pix_per_um.get() + + mean_x = (box_coords_face[0] + box_coords_face[2]) / 2 + mean_y = (box_coords_face[1] + box_coords_face[3]) / 2 + + mean_x = mean_x - 320 + mean_y = mean_y - 256 + + delta_x = mean_x * 2*two_click_low.pix_per_um.get() + delta_cam_y = mean_y * 2*two_click_low.pix_per_um.get() + sample_detection["center_x"], sample_detection["center_y"] = delta_x, delta_cam_y + + + omega: float = gonio.o.user_readback.get() + sample_detection["face_on_omega"] = omega + + d = np.pi/180 + + real_y = delta_cam_y * np.cos(omega * d) + real_z = delta_cam_y * np.sin(omega * d) + + yield from bps.mvr(gonio.gx, delta_x) + yield from bps.mvr(gonio.py, -real_y) + yield from bps.mvr(gonio.pz, -real_z) + + # The sample has moved to the center of the beam (hopefully), need to update co-ordinates + box_coords_face[0] + + # orthogonal face, use loop model only if predicted width matches face on + # otherwise, threshold + yield from bps.mv(gonio.o, sample_detection["face_on_omega"]+90) + + scan_uid = yield from bp.count([two_click_low], 1) + + loop_detector.get_threshold.set(True) + loop_detector.x_start.set(int(box_coords_face[0]-mean_x)) + loop_detector.x_end.set(int(box_coords_face[2]-mean_x)) + loop_detector.filename.set(two_click_low.jpeg.full_file_name.get()) + + scan_uid = yield from bp.count([loop_detector], 1) + box_coords_ortho = db[scan_uid].table()['loop_detector_box'][1] + box_coords_ortho_threshold = db[scan_uid].table()['loop_detector_thresholded_box'][1] + mean_y_threshold = (box_coords_ortho_threshold[1] + box_coords_ortho_threshold[3]) / 2 + small_box_height_threshold = (box_coords_ortho_threshold[3] - box_coords_ortho_threshold[1]) * 2 * two_click_low.pix_per_um.get() + + try: + mean_y = (box_coords_ortho[1] + box_coords_ortho[3]) / 2 + sample_detection["small_box_height"] = (box_coords_ortho[3] - box_coords_ortho[1]) * 2 * two_click_low.pix_per_um.get() + # sum of squared difference, face-on vs. ortho width similarity + ssd_ratio = ( + ((box_coords_face[0]-mean_x - box_coords_ortho[0])**2 + + (box_coords_face[2]-mean_x - box_coords_ortho[2])**2) + ) / (box_coords_face[0]-mean_x - box_coords_face[2]-mean_x)**2 + + except IndexError: + logger.error("Orthogonal loop detection failed") + mean_y = mean_y_threshold + ssd_ratio = 10000 + + if ssd_ratio > 0.2: + logger.info(f'ssd_ratio of {ssd_ratio}, thresholding for loop sideview') + mean_y = mean_y_threshold + sample_detection["small_box_height"] = small_box_height_threshold + if mean_y == -1: + logger.error('threshold of -1 detected, something is wrong') + yield from bps.mvr(gonio.o, -90) + return + + delta_cam_y = (mean_y - 256) * 2*two_click_low.pix_per_um.get() + omega = gonio.o.user_readback.get() + d = np.pi/180 + + real_y = delta_cam_y * np.cos(omega * d) + real_z = delta_cam_y * np.sin(omega * d) + + yield from bps.mvr(gonio.py, -real_y) + yield from bps.mvr(gonio.pz, -real_z) + + yield from bps.mv(gonio.o, sample_detection["face_on_omega"]) diff --git a/bluesky_env/plans/top_view.py b/bluesky_env/plans/top_view.py new file mode 100644 index 00000000..b28aa06c --- /dev/null +++ b/bluesky_env/plans/top_view.py @@ -0,0 +1,133 @@ +import numpy as np +from scipy.interpolate import interp1d + +import bluesky.plans as bp +import bluesky.plan_stubs as bps +from bluesky.utils import FailedStatus +from ophyd.utils import WaitTimeoutError + +from start_bs import db, gov_robot +from bluesky_env.devices.top_align import top_aligner_fast, top_aligner_slow, gonio +from bluesky_env.devices.auto_center import work_pos +from bluesky_env.plans.utils import mv_with_retry, mvr_with_retry +import gov_lib + +def topview_optimized(): + + def inner_pseudo_fly_scan(*args, **kwargs): + scan_uid = yield from bp.count(*args, **kwargs) + omegas = db[scan_uid].table()[ + top_aligner_fast.zebra.pos_capt.data.enc4.name + ][1] + + d = np.pi / 180 + + # rot axis calculation, use linear regression + A_rot = np.matrix( + [[np.cos(omega * d), np.sin(omega * d), 1] for omega in omegas] + ) + + b_rot = db[scan_uid].table()[top_aligner_fast.topcam.out9_buffer.name][ + 1 + ] + p = ( + np.linalg.inv(A_rot.transpose() * A_rot) + * A_rot.transpose() + * np.matrix(b_rot).transpose() + ) + + delta_z_pix, delta_y_pix, rot_axis_pix = p[0], p[1], p[2] + delta_y, delta_z = ( + delta_y_pix / top_aligner_fast.topcam.pix_per_um.get(), + delta_z_pix / top_aligner_fast.topcam.pix_per_um.get(), + ) + + # face on calculation + b = db[scan_uid].table()[top_aligner_fast.topcam.out10_buffer.name][1] + + sample = 300 + f_splines = interp1d(omegas, b) + b_splines = f_splines(np.linspace(omegas[0], omegas[-1], sample)) + omega_min = np.linspace(omegas[0], omegas[-1], sample)[ + b_splines.argmin() + ] + print(f"SPLINES / {omega_min}") + + return delta_y, delta_z, omega_min + + # horizontal bump calculation, don't move just yet to avoid disturbing gov + + yield from bps.abs_set(top_aligner_slow.topcam.cam_mode, 'coarse_align') + scan_uid = yield from bp.count([top_aligner_slow.topcam], 1) + x = db[scan_uid].table()[top_aligner_slow.topcam.cv1.outputs.output8.name][1] + #x = top_aligner_slow.topcam.cv1.outputs.output8.get() + delta_x = ((top_aligner_slow.topcam.roi2.size.x.get() / 2) - + x) / top_aligner_slow.topcam.pix_per_um.get() + + + + # update work positions + yield from bps.abs_set(work_pos.gx, gonio.gx.user_readback.get(), wait=True) + yield from bps.abs_set( + work_pos.gpy, top_aligner_fast.gonio_py.user_readback.get(), wait=True + ) + yield from bps.abs_set( + work_pos.gpz, top_aligner_fast.gonio_pz.user_readback.get(), wait=True + ) + yield from bps.abs_set(work_pos.o, 180, wait=True) + + # SE -> TA + yield from bps.abs_set(top_aligner_fast.target_gov_state, "TA", wait=True) + yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, "coarse_align") + + try: + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( + [top_aligner_fast] + ) + except (FailedStatus, WaitTimeoutError) as error: + print("arming problem during coarse alignment...trying again") + yield from bps.sleep(15) + gov_lib.setGovRobot(gov_robot, 'SE') + yield from bps.abs_set(top_aligner_fast.zebra.reset, 1, wait=True) + yield from bps.sleep(2) + + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( + [top_aligner_fast] + ) + + yield from bps.mvr(gonio.gx, delta_x) + yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) + yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) + + # update work positions + yield from bps.abs_set(work_pos.gx, gonio.gx.user_readback.get(), wait=True) + yield from bps.abs_set( + work_pos.gpy, top_aligner_fast.gonio_py.user_readback.get(), wait=True + ) + yield from bps.abs_set( + work_pos.gpz, top_aligner_fast.gonio_pz.user_readback.get(), wait=True + ) + yield from bps.abs_set(work_pos.o, 0, wait=True) + + # TA -> SA + yield from bps.abs_set(top_aligner_fast.target_gov_state, "SA", wait=True) + yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, "fine_face") + + try: + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( + [top_aligner_fast] + ) + except (FailedStatus, WaitTimeoutError) as error: + print("arming problem during fine alignment...trying again") + yield from bps.sleep(15) + gov_lib.setGovRobot(gov_robot, 'TA') + yield from bps.abs_set(top_aligner_fast.zebra.reset, 1, wait=True) + yield from bps.sleep(2) + + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( + [top_aligner_fast] + ) + + yield from mv_with_retry(top_aligner_fast.gonio_o, omega_min) + yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) + yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) diff --git a/bluesky_env/plans/utils.py b/bluesky_env/plans/utils.py new file mode 100644 index 00000000..8fe1afcd --- /dev/null +++ b/bluesky_env/plans/utils.py @@ -0,0 +1,27 @@ + +from bluesky.utils import FailedStatus +import bluesky.plan_stubs as bps + +def retry_move(func): + def wrapper(*args, **kwargs): + for j in range(2): + try: + yield from func(*args, **kwargs) + break + except FailedStatus: + if j == 0: + print(f"{args[0].name} is stuck, retrying...") + yield from bps.sleep(0.2) + else: + raise RuntimeError( + f"{args[0].name} is really stuck!") + return wrapper + +@retry_move +def mvr_with_retry(*args, **kwargs): + yield from bps.mvr(*args, **kwargs) + + +@retry_move +def mv_with_retry(*args, **kwargs): + yield from bps.mv(*args, **kwargs) \ No newline at end of file diff --git a/daq_lib.py b/daq_lib.py index 1c7864e1..e76df630 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -680,7 +680,7 @@ def collect_detector_seq_hw(sweep_start,range_degrees,image_width,exposure_perio logger.info("data directory = " + data_directory_name) reqObj = currentRequest["request_obj"] protocol = str(reqObj["protocol"]) - sweep_start = sweep_start%360.0 + sweep_start = sweep_start % 360.0 if sweep_start > 0 else sweep_start % -360 if (protocol == "vector" or protocol == "stepVector"): beamline_lib.mvaDescriptor("omega",sweep_start) if (image_width == 0): diff --git a/daq_macros.py b/daq_macros.py index 58be0967..25ace1a0 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -39,6 +39,8 @@ from bluesky.preprocessors import finalize_wrapper from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX +from bluesky_env.plans import detect_loop, topview_optimized + try: import ispybLib except Exception as e: @@ -59,7 +61,7 @@ autoVectorCoarseCoords = {} autoVectorFlag=False - +max_col = None C3D_SEARCH_BASE = f'{os.environ["PROJDIR"]}/software/c3d/c3d_search -p=$CONFIGDIR/' @@ -211,9 +213,54 @@ def changeImageCenterHighMag(x,y,czoom): setPvDesc("highMagMinY",new_minY) setPvDesc("highMagCursorX",noZoomCenterX) setPvDesc("highMagCursorY",noZoomCenterY) - + +sample_detection = { + "large_box_width": 0, + "large_box_height": 0, + "small_box_height": 0, + "center_x" : 0, + "center_y" : 0, + "face_on_omega" : 0 +} + + +def run_top_view_optimized(): + RE(topview_optimized()) + +def loop_center_plan(): + global sample_detection + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') + if gov_status.success: + yield from detect_loop(sample_detection) + else: + print("could not trasition to SA") def autoRasterLoop(currentRequest): + global sample_detection, autoRasterFlag, max_col + RE(loop_center_plan()) + setTrans(getBlConfig("rasterDefaultTrans")) + daq_lib.set_field("xrecRasterFlag","100") + logger.info("auto raster " + str(currentRequest["sample"])) + logger.info(f"sample detection : {sample_detection}") + time.sleep(1) + autoRasterFlag = 1 + # Before collecting the 1st raster, reset max_col + max_col = None + runRasterScan(currentRequest, rasterType="Custom", + width=sample_detection["large_box_width"], + height=sample_detection["large_box_height"]) + time.sleep(1) + bps.mv(gonio.gx, sample_detection["center_x"], + gonio.gy, sample_detection["center_y"]) + + runRasterScan(currentRequest, rasterType="Custom", + width=sample_detection["large_box_width"], + height=sample_detection["small_box_height"], + omega_rel=90) + autoRasterFlag = 0 + return 1 + +def autoRasterLoopOld(currentRequest): global autoRasterFlag @@ -1909,12 +1956,12 @@ def snakeRasterBluesky(rasterReqID, grain=""): targetGovState = 'SA' else: - govStatus = gov_lib.setGovRobot(gov_robot, 'DI') + govStatus = gov_lib.setGovRobot(gov_robot, 'DA') if govStatus.exception(): logger.error(f"Problem during end-of-raster governor move, aborting! exception: {govStatus.exception()}") return - targetGovState = 'DI' + targetGovState = 'DA' # priorities: # 1. make heat map visible to users correctly aligned with sample @@ -1949,10 +1996,10 @@ def snakeRasterBluesky(rasterReqID, grain=""): else: try: # go to start omega for faster heat map display - gotoMaxRaster(rasterResult,omega=omega) + gotoMaxRaster(rasterResult,omega=omega, rasterRequest=rasterRequest) except ValueError: #must go to known position to account for windup dist. - logger.info("moving to raster start") + logger.info("moving to raster start because of value error in gotoMaxRaster") yield from bps.mv(samplexyz.x, rasterStartX) yield from bps.mv(samplexyz.y, rasterStartY) yield from bps.mv(samplexyz.z, rasterStartZ) @@ -2181,7 +2228,7 @@ def gridRaster(currentRequest): RE(snakeRaster(rasterReqID)) -def runRasterScan(currentRequest,rasterType=""): #this actually defines and runs +def runRasterScan(currentRequest,rasterType="", width=0, height=0, step_size=10, omega_rel=0): #this actually defines and runs sampleID = currentRequest["sample"] if (rasterType=="Fine"): daq_lib.set_field("xrecRasterFlag","100") @@ -2201,6 +2248,11 @@ def runRasterScan(currentRequest,rasterType=""): #this actually defines and runs rasterReqID = defineRectRaster(currentRequest,10,290,10) RE(snakeRaster(rasterReqID)) daq_lib.set_field("xrecRasterFlag","100") + elif (rasterType=="Custom"): + daq_lib.set_field("xrecRasterFlag","100") + beamline_lib.mvrDescriptor("omega",omega_rel) + rasterReqID = defineRectRaster(currentRequest, width+step_size, height+step_size, step_size) + RE(snakeRaster(rasterReqID)) else: rasterReqID = getXrecLoopShape(currentRequest) logger.info("snake raster " + str(rasterReqID)) @@ -2208,7 +2260,7 @@ def runRasterScan(currentRequest,rasterType=""): #this actually defines and runs RE(snakeRaster(rasterReqID)) def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): - global autoVectorCoarseCoords,autoVectorFlag + global autoVectorCoarseCoords,autoVectorFlag, max_col requestID = rasterResult["request"] if (rasterResult["result_obj"]["rasterCellResults"]['resultObj'] == None): @@ -2229,6 +2281,7 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): scoreOption = "d_min" else: scoreOption = "total_intensity" + max_index = None for i in range (0,len(cellResults)): try: scoreVal = float(cellResults[i][scoreOption]) @@ -2250,6 +2303,7 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): hotFile = cellResults[i]["cellMapKey"] else: if (scoreVal > ceiling): + max_index = i ceiling = scoreVal hotFile = cellResults[i]["cellMapKey"] if (hotFile != ""): @@ -2260,6 +2314,68 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): z = hotCoords["z"] logger.info("goto " + str(x) + " " + str(y) + " " + str(z)) + if "rasterRequest" in kwargs: + # Update the raster request with the location of the max raster image and co-ordinates. + # NOTE: In the optimized autoraster, two 2D rasters are collected, face on and orthogonal. + # Both rasters have the same number of columns, we save the column number of the hot cell (max_col) + # in the face on raster then look for the hot cell in the orthogonal raster in the same + # column. This is to emulate the original autoraster but leaves room for other ways of selecting + # points for standard collection + rasterDef = kwargs["rasterRequest"]["request_obj"]["rasterDef"] + + num_rows = len(rasterDef["rowDefs"]) + num_cols = rasterDef["rowDefs"]["numsteps"] + row = max_index // num_rows + if row % 2 == 0: + col = num_cols - 1 - (max_index % num_cols) + else: + col = max_index % num_cols + # Set the column index for the face on raster + if max_col is None: + max_col = col + else: + # max_col should be available for orthogonal rasters + # Find maximum in col defined in max_col and then reset max_col + indices = [] + for i in range(num_rows): + # Determine the column index in the flattened array + if i % 2 == 0: + # Even row + column_index = max_col + else: + # Odd row + column_index = num_cols - 1 - max_col + + # Calculate the index in the flattened array + index = i * num_cols + column_index + indices.append(index) + + ceiling = 0 + # Loop through all the cells that belong to the column corresponding to max_col + for index in indices: + try: + scoreVal = float(cellResults[index][scoreOption]) + except TypeError: + scoreVal = 0.0 + if (scoreVal > ceiling): + max_index = index + ceiling = scoreVal + hotFile = cellResults[index]["cellMapKey"] + + max_col = None + + kwargs["rasterRequest"]["request_obj"]["max_raster"]["file"] = hotFile + kwargs["rasterRequest"]["request_obj"]["max_raster"]["coords"] = [x, y, z] + kwargs["rasterRequest"]["request_obj"]["max_raster"]["index"] = max_index + if "omega" in kwargs: + kwargs["rasterRequest"]["request_obj"]["max_raster"]["omega"] = kwargs["omega"] + + db_lib.updateRequest(kwargs["rasterRequest"]) + + #Following lines for testing, remove for production! + req = db_lib.getRequestByID(kwargs["rasterRequest"]["uid"]) + logger.info(f'MAX RASTER INFO: {req["request_obj"]["max_raster"]["file"]} {req["request_obj"]["max_raster"]["coords"]}') + if 'omega' in kwargs: beamline_lib.mvaDescriptor("sampleX",x, "sampleY",y, @@ -2484,6 +2600,14 @@ def defineRectRaster(currentRequest,raster_w_s,raster_h_s,stepsizeMicrons_s,xoff reqObj["xbeam"] = currentRequest['request_obj']["xbeam"] reqObj["ybeam"] = currentRequest['request_obj']["ybeam"] reqObj["wavelength"] = currentRequest['request_obj']["wavelength"] + # request params to save the file and location of max_raster cell + # This data is saved as part of the raster request and not result is because analysisstore does not allow updating + reqObj["max_raster"] = { + "file": None, + "coords": [None, None, None], + "index": None, + "omega": None, + } newRasterRequestUID = db_lib.addRequesttoSample(sampleID,reqObj["protocol"],daq_utils.owner,reqObj,priority=5000,proposalID=propNum) daq_lib.set_field("xrecRasterFlag",newRasterRequestUID) time.sleep(1) diff --git a/embl_robot.py b/embl_robot.py index 622106ec..5786bc85 100644 --- a/embl_robot.py +++ b/embl_robot.py @@ -207,7 +207,7 @@ def preMount(self, gov_robot, puckPos, pinPos, sampID, **kwargs): prefix90 = sampName + "_" + str(puckPos) + "_" + str(pinPos) + "_" + str(reqCount) + "_PA_90" kwargs['prefix1'] = prefix1 kwargs['prefix90'] = prefix90 - top_view.topViewSnap(prefix1,getBlConfig("visitDirectory")+"/pinAlign",1,acquire=0) + # top_view.topViewSnap(prefix1,getBlConfig("visitDirectory")+"/pinAlign",1,acquire=0) except Exception as e: e_s = str(e) message = "TopView check ERROR, will continue: " + e_s @@ -242,7 +242,7 @@ def callAlignPinThread(self, gov_robot, **kwargs): if (omegaCP > 89.5 and omegaCP < 90.5): beamline_lib.mvrDescriptor("omega", 85.0) logger.info("calling thread") - _thread.start_new_thread(top_view.wait90TopviewThread,(gov_robot, prefix1,prefix90)) + # _thread.start_new_thread(top_view.wait90TopviewThread,(gov_robot, prefix1,prefix90)) logger.info("called thread") @@ -344,6 +344,9 @@ def postMount(self, gov_robot, puck, pinPos, sampID): logger.info('not changing anything as governor is active') if (sampYadjust == 0): logger.info("Cannot align pin - Mount next sample.") + if daq_utils.beamline == "amx": + daq_macros.run_top_view_optimized() + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') if not gov_status.success: logger.error('Failure during governor change to SA') diff --git a/start_bs.py b/start_bs.py index 494dd5fd..7efa1d55 100755 --- a/start_bs.py +++ b/start_bs.py @@ -9,6 +9,8 @@ from mxtools.governor import _make_governors from ophyd.signal import EpicsSignalBase EpicsSignalBase.set_defaults(timeout=10, connection_timeout=10) # new style +from bluesky_env.devices.auto_center import WorkPositions, TwoClickLowMag, LoopDetector +from bluesky_env.devices.top_align import TopAlignerFast, TopAlignerSlow, GoniometerStack #12/19 - author unknown. DAMA can help """ @@ -133,6 +135,14 @@ class SampleXYZ(Device): back_light = EpicsSignal(read_pv="XF:17DB-ES:AMX{BL:1}Ch1Value",name="back_light") back_light_range = (0, 100) + work_pos = WorkPositions("XF:17IDB-ES:AMX", name="work_pos") + two_click_low = TwoClickLowMag("XF:17IDB-ES:AMX{Cam:6}", name="two_click_low") + gonio = GoniometerStack("XF:17IDB-ES:AMX{Gon:1", name="gonio") + loop_detector = LoopDetector(name="loop_detector") + top_aligner_fast = TopAlignerFast(name="top_aligner_fast") + top_aligner_slow = TopAlignerSlow(name="top_aligner") + + elif beamline == "fmx": mercury = ABBIXMercury('XF:17IDC-ES:FMX{Det:Mer}', name='mercury') mercury.read_attrs = ['mca.spectrum', 'mca.preset_live_time', 'mca.rois.roi0.count', @@ -156,6 +166,13 @@ class SampleXYZ(Device): back_light = EpicsSignal(read_pv="XF:17DC-ES:FMX{BL:1}Ch1Value",name="back_light") back_light_range = (0, 100) + work_pos = WorkPositions("XF:17DC-ES:FMX", name="work_pos") + two_click_low = TwoClickLowMag("XF:17DC-ES:FMX{Cam:6}", name="two_click_low") + gonio = GoniometerStack("XF:17DC-ES:FMX{Gon:1", name="gonio") + loop_detector = LoopDetector(name="loop_detector") + top_aligner_fast = TopAlignerFast(name="top_aligner_fast") + top_aligner_slow = TopAlignerSlow(name="top_aligner") + import setenergy_lsdc elif beamline=="nyx": From 2f3d14f3038f12759aa833b5087da8771f870a27 Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Thu, 18 Jan 2024 13:26:26 -0500 Subject: [PATCH 02/33] Added error handling in loop detection plan --- bluesky_env/plans/loop_detection.py | 33 ++++++++-------- daq_macros.py | 61 +++++++++++++++++------------ 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/bluesky_env/plans/loop_detection.py b/bluesky_env/plans/loop_detection.py index e3d53135..baded5cd 100644 --- a/bluesky_env/plans/loop_detection.py +++ b/bluesky_env/plans/loop_detection.py @@ -5,11 +5,9 @@ import bluesky.plan_stubs as bps import bluesky.plans as bp -from bluesky_env.devices.auto_center import two_click_low, loop_detector -from bluesky_env.devices.top_align import gonio - -from start_bs import db +from start_bs import db, two_click_low, loop_detector, gonio +from bluesky_env.plans.utils import mvr_with_retry, mv_with_retry logger = getLogger() @@ -34,10 +32,12 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): if len(box_coords_face) != 4: logger.exception("Exception during loop detection plan. Face on loop not found") - sample_detection["large_box_width"] = 630 * 2 * two_click_low.pix_per_um.get() - sample_detection["large_box_height"] = 390 * 2 * two_click_low.pix_per_um.get() - mean_x = 630/2 - mean_y = 390/2 + # 640x512 + sample_detection["large_box_width"] = 430 * 2 * two_click_low.pix_per_um.get() + sample_detection["large_box_height"] = 340 * 2 * two_click_low.pix_per_um.get() + mean_x = 320 + mean_y = 256 + box_coords_face = [105, 85, 535, 425] else: sample_detection["large_box_width"] = (box_coords_face[2] - box_coords_face[0]) * 2 * two_click_low.pix_per_um.get() sample_detection["large_box_height"] = (box_coords_face[3] - box_coords_face[1]) * 2 * two_click_low.pix_per_um.get() @@ -61,12 +61,12 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): real_y = delta_cam_y * np.cos(omega * d) real_z = delta_cam_y * np.sin(omega * d) - yield from bps.mvr(gonio.gx, delta_x) - yield from bps.mvr(gonio.py, -real_y) - yield from bps.mvr(gonio.pz, -real_z) + yield from mvr_with_retry(gonio.gx, delta_x) + yield from mvr_with_retry(gonio.py, -real_y) + yield from mvr_with_retry(gonio.pz, -real_z) # The sample has moved to the center of the beam (hopefully), need to update co-ordinates - box_coords_face[0] + # box_coords_face[0] # orthogonal face, use loop model only if predicted width matches face on # otherwise, threshold @@ -105,6 +105,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): sample_detection["small_box_height"] = small_box_height_threshold if mean_y == -1: logger.error('threshold of -1 detected, something is wrong') + sample_detection["sample_detected"] = False yield from bps.mvr(gonio.o, -90) return @@ -115,7 +116,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): real_y = delta_cam_y * np.cos(omega * d) real_z = delta_cam_y * np.sin(omega * d) - yield from bps.mvr(gonio.py, -real_y) - yield from bps.mvr(gonio.pz, -real_z) - - yield from bps.mv(gonio.o, sample_detection["face_on_omega"]) + yield from mvr_with_retry(gonio.py, -real_y) + yield from mvr_with_retry(gonio.pz, -real_z) + yield from mv_with_retry(gonio.o, sample_detection["face_on_omega"]) + sample_detection["sample_detected"] = True diff --git a/daq_macros.py b/daq_macros.py index 25ace1a0..7f30996e 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -36,7 +36,7 @@ setup_vector_program) import bluesky.plan_stubs as bps import bluesky.plans as bp -from bluesky.preprocessors import finalize_wrapper +from bluesky.preprocessors import finalize_wrapper, finalize_decorator from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX from bluesky_env.plans import detect_loop, topview_optimized @@ -215,6 +215,7 @@ def changeImageCenterHighMag(x,y,czoom): setPvDesc("highMagCursorY",noZoomCenterY) sample_detection = { + "sample_detected": False, "large_box_width": 0, "large_box_height": 0, "small_box_height": 0, @@ -229,36 +230,44 @@ def run_top_view_optimized(): def loop_center_plan(): global sample_detection - gov_status = gov_lib.setGovRobot(gov_robot, 'SA') - if gov_status.success: - yield from detect_loop(sample_detection) + if gov_robot.state.get() == 'M': + bps.sleep(15) + if gov_robot.state.get() == "SE": + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') + if gov_status.success: + yield from detect_loop(sample_detection) + else: + print("could not trasition to SA") else: - print("could not trasition to SA") + yield from detect_loop(sample_detection) def autoRasterLoop(currentRequest): global sample_detection, autoRasterFlag, max_col RE(loop_center_plan()) - setTrans(getBlConfig("rasterDefaultTrans")) - daq_lib.set_field("xrecRasterFlag","100") - logger.info("auto raster " + str(currentRequest["sample"])) - logger.info(f"sample detection : {sample_detection}") - time.sleep(1) - autoRasterFlag = 1 - # Before collecting the 1st raster, reset max_col - max_col = None - runRasterScan(currentRequest, rasterType="Custom", - width=sample_detection["large_box_width"], - height=sample_detection["large_box_height"]) - time.sleep(1) - bps.mv(gonio.gx, sample_detection["center_x"], - gonio.gy, sample_detection["center_y"]) - - runRasterScan(currentRequest, rasterType="Custom", - width=sample_detection["large_box_width"], - height=sample_detection["small_box_height"], - omega_rel=90) - autoRasterFlag = 0 - return 1 + if sample_detection["sample_detected"]: + setTrans(getBlConfig("rasterDefaultTrans")) + daq_lib.set_field("xrecRasterFlag","100") + logger.info("auto raster " + str(currentRequest["sample"])) + logger.info(f"sample detection : {sample_detection}") + # time.sleep(1) + autoRasterFlag = 1 + # Before collecting the 1st raster, reset max_col + max_col = None + runRasterScan(currentRequest, rasterType="Custom", + width=sample_detection["large_box_width"], + height=sample_detection["large_box_height"]) + # time.sleep(1) + bps.mv(gonio.gx, sample_detection["center_x"], + gonio.gy, sample_detection["center_y"]) + + runRasterScan(currentRequest, rasterType="Custom", + width=sample_detection["large_box_width"], + height=sample_detection["small_box_height"], + omega_rel=90) + autoRasterFlag = 0 + return 1 + else: + return 0 def autoRasterLoopOld(currentRequest): global autoRasterFlag From 8819d1b64d416d673032f05abd0fe1405720f053 Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Thu, 18 Jan 2024 13:27:48 -0500 Subject: [PATCH 03/33] Added error handling to top align --- bluesky_env/devices/top_align.py | 90 +++++++++++++--- bluesky_env/plans/top_view.py | 174 ++++++++++++++++++------------- 2 files changed, 181 insertions(+), 83 deletions(-) diff --git a/bluesky_env/devices/top_align.py b/bluesky_env/devices/top_align.py index c14a8def..3188e09f 100644 --- a/bluesky_env/devices/top_align.py +++ b/bluesky_env/devices/top_align.py @@ -7,12 +7,14 @@ EpicsMotor, ImagePlugin, ROIPlugin, TransformPlugin, StatsPlugin, ProcessPlugin, SingleTrigger, ProsilicaDetector, DDC_EpicsSignal, DDC_EpicsSignalRO, ADComponent as ADCpt) +from bluesky.utils import FailedStatus from ophyd.status import SubscriptionStatus from pathlib import Path import requests from .zebra import Zebra from enum import Enum -from start_bs import gov_robot + +from collections import OrderedDict class TopPlanLimit(Enum): @@ -136,9 +138,17 @@ def _update_stage_sigs(self, *args, **kwargs): ) def stage(self, *args, **kwargs): - self._update_stage_sigs(*args, **kwargs) + #self._update_stage_sigs(*args, **kwargs) super().stage(*args, **kwargs) + def trigger(self): + try: + status = super().trigger() + status.wait(6) + except AttributeError: + raise FailedStatus + return status + class ZebraMXOr(Zebra): or3 = Cpt(EpicsSignal, "OR3_ENA:B3") @@ -146,6 +156,10 @@ class ZebraMXOr(Zebra): armsel = Cpt(EpicsSignal, "PC_ARM_SEL") +class GovernorError(Exception): + def __init__(self, message): + super().__init__(self, message) + class TopAlignerBase(Device): topcam = Cpt(TopAlignCam, "XF:17IDB-ES:AMX{Cam:9}") @@ -172,7 +186,7 @@ def _configure_device(self, *args, **kwargs): def stage(self, *args, **kwargs): if type(self) == TopAlignerBase: raise NotImplementedError("TopAlignerBase has no stage method") - super().stage(*args, **kwargs) + return super().stage(*args, **kwargs) def trigger(self): raise NotImplementedError("Subclasses must implement custom trigger") @@ -191,6 +205,7 @@ class TopAlignerFast(TopAlignerBase): ) def __init__(self, *args, **kwargs): + self.gov_robot = kwargs.pop("gov_robot") super().__init__(*args, **kwargs) self.target_gov_state.subscribe( self._update_stage_sigs, event_type="value" @@ -199,7 +214,7 @@ def __init__(self, *args, **kwargs): def _configure_device(self, *args, **kwargs): self.read_attrs = ["topcam", "zebra"] self.stage_sigs.clear() - self.topcam.cam_mode.set("fine_face") + # self.topcam.cam_mode.set("fine_face") self.stage_sigs.update( [ @@ -252,8 +267,56 @@ def _update_stage_sigs(self, *args, **kwargs): raise Exception("Target gov state not implemented!") def stage(self, *args, **kwargs): - self._update_stage_sigs() - super().stage(*args, **kwargs) + if self.gov_robot.state.get() == 'M': + raise GovernorError("Governor busy or in M during staging attempt") + + # Resolve any stage_sigs keys given as strings: 'a.b' -> self.a.b + stage_sigs = OrderedDict() + for k, v in self.stage_sigs.items(): + if isinstance(k, str): + # Device.__getattr__ handles nested attr lookup + stage_sigs[getattr(self, k)] = v + else: + stage_sigs[k] = v + + # Read current values, to be restored by unstage() + original_vals = {sig: sig.get() for sig in stage_sigs} + + # We will add signals and values from original_vals to + # self._original_vals one at a time so that + # we can undo our partial work in the event of an error. + + # Apply settings. + devices_staged = [] + try: + for sig, val in stage_sigs.items(): + self.log.debug( + "Setting %s to %r (original value: %r)", + sig.name, + val, + original_vals[sig], + ) + sig.set(val).wait(10) + # It worked -- now add it to this list of sigs to unstage. + self._original_vals[sig] = original_vals[sig] + devices_staged.append(self) + + # Call stage() on child devices. + for attr in self._sub_devices: + device = getattr(self, attr) + if hasattr(device, "stage"): + device.stage() + devices_staged.append(device) + except Exception: + self.log.debug( + "An exception was raised while staging %s or " + "one of its children. Attempting to restore " + "original settings before re-raising the " + "exception.", + self.name, + ) + self.unstage() + raise def callback_armed(value, old_value, **kwargs): if old_value == 0 and value == 1: @@ -268,13 +331,16 @@ def callback_armed(value, old_value, **kwargs): settle_time=0.5, ) self.zebra.pos_capt.arm.arm.set(1) - callback_armed_status.wait(timeout=3) + callback_armed_status.wait(timeout=6) + return devices_staged def unstage(self, **kwargs): super().unstage(**kwargs) # self.zebra.pos_capt.arm.disarm.set(1) def trigger(self): + print('top aligner triggered') + def callback_unarmed(value, old_value, **kwargs): if old_value == 1 and value == 0: return True @@ -288,14 +354,13 @@ def callback_unarmed(value, old_value, **kwargs): timeout=6, ) - if (self.target_gov_state.get() in gov_robot.reachable.get() or - self.target_gov_state.get() == gov_robot.setpoint.get()): - gov_robot.set(self.target_gov_state.get(), wait=True) + if self.target_gov_state.get() in self.gov_robot.reachable.get(): + self.gov_robot.set(self.target_gov_state.get(), wait=True) # self.gonio_o.set(0) return callback_unarmed_status else: - raise Exception( + raise FailedStatus( f'{self.target_gov_state.get()} is wrong governor state for transition' ) @@ -311,7 +376,7 @@ def _configure_device(self, *args, **kwargs): [ ("topcam.cam.trigger_mode", 5), ("topcam.cam.image_mode", 1), - ("topcam.cam.acquire", 0), + ("topcam.cam.acquire", 1), ] ) self.topcam.cam_mode.set("coarse_align") @@ -319,7 +384,6 @@ def _configure_device(self, *args, **kwargs): "topcam.cv1.outputs.output8", "topcam.cv1.outputs.output9", "topcam.cv1.outputs.output10", - "gonio_o", ] def trigger(self): diff --git a/bluesky_env/plans/top_view.py b/bluesky_env/plans/top_view.py index b28aa06c..cc09b2d8 100644 --- a/bluesky_env/plans/top_view.py +++ b/bluesky_env/plans/top_view.py @@ -5,125 +5,133 @@ import bluesky.plan_stubs as bps from bluesky.utils import FailedStatus from ophyd.utils import WaitTimeoutError +from bluesky.preprocessors import finalize_decorator -from start_bs import db, gov_robot -from bluesky_env.devices.top_align import top_aligner_fast, top_aligner_slow, gonio -from bluesky_env.devices.auto_center import work_pos +from start_bs import db, gov_robot, mount_pos, top_aligner_fast, top_aligner_slow, gonio, work_pos +from bluesky_env.devices.top_align import GovernorError from bluesky_env.plans.utils import mv_with_retry, mvr_with_retry import gov_lib +from logging import getLogger -def topview_optimized(): +logger = getLogger() - def inner_pseudo_fly_scan(*args, **kwargs): - scan_uid = yield from bp.count(*args, **kwargs) - omegas = db[scan_uid].table()[ - top_aligner_fast.zebra.pos_capt.data.enc4.name - ][1] +def cleanup_topcam(): + yield from bps.abs_set(top_aligner_slow.topcam.cam.acquire, 1, wait=True) - d = np.pi / 180 +def inner_pseudo_fly_scan(*args, **kwargs): + scan_uid = yield from bp.count(*args, **kwargs) + omegas = db[scan_uid].table()[ + top_aligner_fast.zebra.pos_capt.data.enc4.name + ][1] - # rot axis calculation, use linear regression - A_rot = np.matrix( - [[np.cos(omega * d), np.sin(omega * d), 1] for omega in omegas] - ) + d = np.pi / 180 - b_rot = db[scan_uid].table()[top_aligner_fast.topcam.out9_buffer.name][ - 1 - ] - p = ( - np.linalg.inv(A_rot.transpose() * A_rot) - * A_rot.transpose() - * np.matrix(b_rot).transpose() - ) + # rot axis calculation, use linear regression + A_rot = np.matrix( + [[np.cos(omega * d), np.sin(omega * d), 1] for omega in omegas] + ) - delta_z_pix, delta_y_pix, rot_axis_pix = p[0], p[1], p[2] - delta_y, delta_z = ( - delta_y_pix / top_aligner_fast.topcam.pix_per_um.get(), - delta_z_pix / top_aligner_fast.topcam.pix_per_um.get(), - ) + b_rot = db[scan_uid].table()[top_aligner_fast.topcam.out9_buffer.name][ + 1 + ] + p = ( + np.linalg.inv(A_rot.transpose() * A_rot) + * A_rot.transpose() + * np.matrix(b_rot).transpose() + ) - # face on calculation - b = db[scan_uid].table()[top_aligner_fast.topcam.out10_buffer.name][1] + delta_z_pix, delta_y_pix, rot_axis_pix = p[0], p[1], p[2] + delta_y, delta_z = ( + delta_y_pix / top_aligner_fast.topcam.pix_per_um.get(), + delta_z_pix / top_aligner_fast.topcam.pix_per_um.get(), + ) - sample = 300 - f_splines = interp1d(omegas, b) - b_splines = f_splines(np.linspace(omegas[0], omegas[-1], sample)) - omega_min = np.linspace(omegas[0], omegas[-1], sample)[ - b_splines.argmin() - ] - print(f"SPLINES / {omega_min}") + # face on calculation + b = db[scan_uid].table()[top_aligner_fast.topcam.out10_buffer.name][1] + + sample = 300 + f_splines = interp1d(omegas, b) + b_splines = f_splines(np.linspace(omegas[0], omegas[-1], sample)) + omega_min = np.linspace(omegas[0], omegas[-1], sample)[ + b_splines.argmin() + ] + print(f"SPLINES / {omega_min}") + + """ + if (-2000 < delta_y[[0]] < 2000) or (-2000 < delta_z[[0]] < 2000): + raise ValueError(f"Calculated Delta y or z too large {delta_y=} {delta_z=}") + """ + - return delta_y, delta_z, omega_min + return delta_y, delta_z, omega_min + + +@finalize_decorator(cleanup_topcam) +def topview_optimized(): + + try: + # horizontal bump calculation, don't move just yet to avoid disturbing gov + scan_uid = yield from bp.count([top_aligner_slow], 1) + except FailedStatus: + scan_uid = yield from bp.count([top_aligner_slow], 1) - # horizontal bump calculation, don't move just yet to avoid disturbing gov - - yield from bps.abs_set(top_aligner_slow.topcam.cam_mode, 'coarse_align') - scan_uid = yield from bp.count([top_aligner_slow.topcam], 1) x = db[scan_uid].table()[top_aligner_slow.topcam.cv1.outputs.output8.name][1] - #x = top_aligner_slow.topcam.cv1.outputs.output8.get() delta_x = ((top_aligner_slow.topcam.roi2.size.x.get() / 2) - x) / top_aligner_slow.topcam.pix_per_um.get() - - # update work positions - yield from bps.abs_set(work_pos.gx, gonio.gx.user_readback.get(), wait=True) - yield from bps.abs_set( - work_pos.gpy, top_aligner_fast.gonio_py.user_readback.get(), wait=True - ) - yield from bps.abs_set( - work_pos.gpz, top_aligner_fast.gonio_pz.user_readback.get(), wait=True - ) - yield from bps.abs_set(work_pos.o, 180, wait=True) + yield from set_TA_work_pos(delta_x=delta_x) # SE -> TA yield from bps.abs_set(top_aligner_fast.target_gov_state, "TA", wait=True) - yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, "coarse_align") + yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, 'coarse_align') + yield from bps.sleep(0.1) try: delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) - except (FailedStatus, WaitTimeoutError) as error: + except (FailedStatus, WaitTimeoutError, GovernorError, ValueError) as error: + print(f"Error: {error}") print("arming problem during coarse alignment...trying again") + yield from bps.sleep(15) - gov_lib.setGovRobot(gov_robot, 'SE') + yield from bps.abs_set(gov_robot, 'SE', wait=True) yield from bps.abs_set(top_aligner_fast.zebra.reset, 1, wait=True) - yield from bps.sleep(2) - + yield from bps.sleep(4) # 2-3 sec will disarm zebra after reset + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) - yield from bps.mvr(gonio.gx, delta_x) yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) # update work positions - yield from bps.abs_set(work_pos.gx, gonio.gx.user_readback.get(), wait=True) - yield from bps.abs_set( - work_pos.gpy, top_aligner_fast.gonio_py.user_readback.get(), wait=True - ) - yield from bps.abs_set( - work_pos.gpz, top_aligner_fast.gonio_pz.user_readback.get(), wait=True - ) - yield from bps.abs_set(work_pos.o, 0, wait=True) + yield from set_SA_work_pos(delta_y, delta_z) # TA -> SA yield from bps.abs_set(top_aligner_fast.target_gov_state, "SA", wait=True) yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, "fine_face") + yield from bps.sleep(0.1) try: delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) - except (FailedStatus, WaitTimeoutError) as error: + except (FailedStatus, WaitTimeoutError, GovernorError, ValueError) as error: + print(f"Error: {error}") print("arming problem during fine alignment...trying again") yield from bps.sleep(15) - gov_lib.setGovRobot(gov_robot, 'TA') + # update work positions for TA reset + yield from set_TA_work_pos() + yield from bps.abs_set(gov_robot, 'TA', wait=True) yield from bps.abs_set(top_aligner_fast.zebra.reset, 1, wait=True) - yield from bps.sleep(2) - + + # update work positions for TA -> SA retry + yield from set_SA_work_pos(delta_y, delta_z) + yield from bps.sleep(4) # 2-3 sec will disarm zebra after reset + delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) @@ -131,3 +139,29 @@ def inner_pseudo_fly_scan(*args, **kwargs): yield from mv_with_retry(top_aligner_fast.gonio_o, omega_min) yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) + + set_SA_work_pos(delta_y, delta_z, gonio.py.user_readback.get(), gonio.pz.user_readback.get(), omega=gonio.o.user_readback.get()) + +def set_TA_work_pos(delta_x=None): + if delta_x: + yield from bps.abs_set(work_pos.gx, mount_pos.gx.get() + delta_x, wait=True) + yield from bps.abs_set( + work_pos.gpy, mount_pos.py.get(), wait=True + ) + yield from bps.abs_set( + work_pos.gpz, mount_pos.pz.get(), wait=True + ) + yield from bps.abs_set(work_pos.o, 180, wait=True) + +def set_SA_work_pos(delta_y, delta_z, current_y=None, current_z= None, omega=0): + if current_y is None: + current_y = mount_pos.py.get() + if current_z is None: + current_z = mount_pos.pz.get() + yield from bps.abs_set( + work_pos.gpy, current_y + delta_y, wait=True + ) + yield from bps.abs_set( + work_pos.gpz, current_z - delta_z, wait=True + ) + yield from bps.abs_set(work_pos.o, omega, wait=True) From 5da6131165746365ab9a38cf642c440753b62058 Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Thu, 18 Jan 2024 13:31:01 -0500 Subject: [PATCH 04/33] Added: - Robot move during unmount for AMX - Handle exception if top_view fails --- bluesky_env/devices/auto_center.py | 6 +++++- daq_lib.py | 6 +++--- embl_robot.py | 22 ++++++++++++++++------ start_bs.py | 12 +++++++----- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/bluesky_env/devices/auto_center.py b/bluesky_env/devices/auto_center.py index 062be9cd..9ded4b02 100644 --- a/bluesky_env/devices/auto_center.py +++ b/bluesky_env/devices/auto_center.py @@ -207,5 +207,9 @@ class WorkPositions(Device): o = Cpt(EpicsSignal, '{Gov:Robot-Dev:go}Pos:Work-Pos') - +class MountPositions(Device): + gx = Cpt(EpicsSignal, '{Gov:Robot-Dev:gx}Pos:Mount-Pos') + py = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpy}Pos:Mount-Pos') + pz = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpz}Pos:Mount-Pos') + o = Cpt(EpicsSignal, '{Gov:Robot-Dev:go}Pos:Mount-Pos') diff --git a/daq_lib.py b/daq_lib.py index e76df630..f2465c9b 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -233,11 +233,11 @@ def getRobotConfig(): return getPvDesc("robotGovConfig",as_string=True) -def setRobotGovState(stateString): +def setRobotGovState(stateString, wait=True): if (getRobotConfig() == "Robot"): - setPvDesc("robotGovGo",stateString) + setPvDesc("robotGovGo",stateString, wait=wait) else: - setPvDesc("humanGovGo",stateString) + setPvDesc("humanGovGo",stateString, wait=wait) def mountSample(sampID): global mountCounter diff --git a/embl_robot.py b/embl_robot.py index 5786bc85..d4214152 100644 --- a/embl_robot.py +++ b/embl_robot.py @@ -345,11 +345,14 @@ def postMount(self, gov_robot, puck, pinPos, sampID): if (sampYadjust == 0): logger.info("Cannot align pin - Mount next sample.") if daq_utils.beamline == "amx": - daq_macros.run_top_view_optimized() - - gov_status = gov_lib.setGovRobot(gov_robot, 'SA') - if not gov_status.success: - logger.error('Failure during governor change to SA') + try: + daq_macros.run_top_view_optimized() + except: + logger.exception("Error running top_view_optimized") + if gov_robot.state.get() != "SA": + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') + if not gov_status.success: + logger.error('Failure during governor change to SA') return MOUNT_SUCCESSFUL @@ -358,12 +361,19 @@ def preUnmount(self, gov_robot, puckPos,pinPos,sampID): #will somehow know where robotOnline = getBlConfig('robot_online') logger.info("robot online = " + str(robotOnline)) if (robotOnline): + logger.info("Checking detector dist") detDist = beamline_lib.motorPosFromDescriptor("detectorDist") if (detDist Date: Tue, 23 Jan 2024 12:25:23 -0500 Subject: [PATCH 05/33] Changes to autorasterloop plans: 1. To save center of the detected loop 2. Save the max raster positions for face on and ortho 3. Set the final gonio position to start standard collection --- bluesky_env/plans/loop_detection.py | 6 ++ daq_macros.py | 110 +++++++++++++++++++--------- utils/raster.py | 93 +++++++++++++++++++++++ 3 files changed, 176 insertions(+), 33 deletions(-) create mode 100644 utils/raster.py diff --git a/bluesky_env/plans/loop_detection.py b/bluesky_env/plans/loop_detection.py index baded5cd..4f035e55 100644 --- a/bluesky_env/plans/loop_detection.py +++ b/bluesky_env/plans/loop_detection.py @@ -119,4 +119,10 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): yield from mvr_with_retry(gonio.py, -real_y) yield from mvr_with_retry(gonio.pz, -real_z) yield from mv_with_retry(gonio.o, sample_detection["face_on_omega"]) + + sample_detection["center_x"] = gonio.gx.user_readback.get() + sample_detection["center_y"] = gonio.py.user_readback.get() + sample_detection["center_z"] = gonio.pz.user_readback.get() + logger.info("Saving gonio x,y,z spositions") + sample_detection["sample_detected"] = True diff --git a/daq_macros.py b/daq_macros.py index 7f30996e..1231ac9f 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -7,6 +7,7 @@ import daq_utils import db_lib from daq_utils import getBlConfig, setBlConfig +from utils.raster import get_raster_max_col, get_flattened_indices_of_max_col import det_lib import math import time @@ -62,6 +63,8 @@ autoVectorFlag=False max_col = None +face_on_max_coords = None +ortho_max_coords = None C3D_SEARCH_BASE = f'{os.environ["PROJDIR"]}/software/c3d/c3d_search -p=$CONFIGDIR/' @@ -221,6 +224,7 @@ def changeImageCenterHighMag(x,y,czoom): "small_box_height": 0, "center_x" : 0, "center_y" : 0, + "center_z" : 0, "face_on_omega" : 0 } @@ -242,7 +246,16 @@ def loop_center_plan(): yield from detect_loop(sample_detection) def autoRasterLoop(currentRequest): - global sample_detection, autoRasterFlag, max_col + global sample_detection, autoRasterFlag, max_col, face_on_max_coords, ortho_max_coords + if daq_utils.beamline == "fmx": + retries = 3 + while retries: + success = loop_center_xrec() + if not success: + retries -= 1 + else: + retries = 0 + RE(loop_center_plan()) if sample_detection["sample_detected"]: setTrans(getBlConfig("rasterDefaultTrans")) @@ -251,19 +264,61 @@ def autoRasterLoop(currentRequest): logger.info(f"sample detection : {sample_detection}") # time.sleep(1) autoRasterFlag = 1 - # Before collecting the 1st raster, reset max_col + # Before collecting the 1st raster, reset max_col and face_on_max_coords max_col = None + face_on_max_coords = None + ortho_max_coords = None + step_size = 10 + if sample_detection["large_box_width"] * sample_detection["large_box_height"] > 150_000: + step_size = 20 + + runRasterScan(currentRequest, rasterType="Custom", width=sample_detection["large_box_width"], - height=sample_detection["large_box_height"]) - # time.sleep(1) - bps.mv(gonio.gx, sample_detection["center_x"], - gonio.gy, sample_detection["center_y"]) + height=sample_detection["large_box_height"], step_size=step_size) + logger.info(f"AUTORASTER LOOP: {sample_detection['face_on_omega']=} ") + RE(bps.mv(gonio.gx, sample_detection["center_x"], + gonio.py, sample_detection["center_y"], + gonio.pz, sample_detection["center_z"])) runRasterScan(currentRequest, rasterType="Custom", width=sample_detection["large_box_width"], height=sample_detection["small_box_height"], + step_size=step_size, omega_rel=90) + # Gonio should be at the hot cell for the ortho raster. + # Now move to the hot cell of the face on raster, then start standard collection + if face_on_max_coords and ortho_max_coords: + logger.info(f"AUTORASTER LOOP: {face_on_max_coords=} {ortho_max_coords=}") + logger.info(f"AUTORASTER LOOP: {sample_detection=} {max_col=}") + _, y_f, z_f = face_on_max_coords + _, y_ort, z_ort = ortho_max_coords + y_center, z_center = sample_detection["center_y"], sample_detection["center_z"] + + omega_face_on = sample_detection["face_on_omega"] + omega_ortho = (sample_detection["face_on_omega"] + 90) + + r_f = (y_f - y_center)*np.cos(np.deg2rad(omega_face_on)) + (z_f - z_center)*np.sin(np.deg2rad(omega_face_on)) + r_o = (y_ort - y_center)*np.cos(np.deg2rad(omega_ortho)) + (z_ort - z_center)*np.sin(np.deg2rad(omega_ortho)) + + logger.info(f"AUTORASTER LOOP: {r_f=} {r_o=}") + + r = np.array([[r_f],[r_o]]) + A = np.matrix([[np.cos(np.deg2rad(omega_face_on)), np.sin(np.deg2rad(omega_face_on))],[np.cos(np.deg2rad(omega_ortho)), np.sin(np.deg2rad(omega_ortho))]]) + logger.info(f"AUTORASTER LOOP: {A=}") + + yz = np.linalg.inv(A)*r + delta_y, delta_z = yz[0,0], yz[1,0] + logger.info(f"AUTORASTER LOOP: {yz=}") + + final_y = y_center + delta_y + final_z = z_center + delta_z + + logger.info(f"AUTORASTER LOOP: {final_y=} {final_z=}") + + + RE(bps.mv(gonio.py, final_y, gonio.pz, final_z)) + autoRasterFlag = 0 return 1 else: @@ -1882,7 +1937,6 @@ def snakeRasterBluesky(rasterReqID, grain=""): if (daq_utils.beamline == "fmx"): setPvDesc("sampleProtect",0) setPvDesc("vectorGo", 0) #set to 0 to allow easier camonitoring vectorGo - data_directory_name, filePrefix, file_number_start, dataFilePrefix, exptimePerCell, img_width_per_cell, wave, detDist, rasterDef, stepsize, omega, rasterStartX, rasterStartY, rasterStartZ, omegaRad, rowCount, numsteps, totalImages, rows = params_from_raster_req_id(rasterReqID) rasterRowResultsList = [{} for i in range(0,rowCount)] processedRasterRowCount = 0 @@ -2269,7 +2323,7 @@ def runRasterScan(currentRequest,rasterType="", width=0, height=0, step_size=10, RE(snakeRaster(rasterReqID)) def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): - global autoVectorCoarseCoords,autoVectorFlag, max_col + global autoVectorCoarseCoords,autoVectorFlag, max_col, face_on_max_coords, ortho_max_coords requestID = rasterResult["request"] if (rasterResult["result_obj"]["rasterCellResults"]['resultObj'] == None): @@ -2332,33 +2386,17 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): # points for standard collection rasterDef = kwargs["rasterRequest"]["request_obj"]["rasterDef"] - num_rows = len(rasterDef["rowDefs"]) - num_cols = rasterDef["rowDefs"]["numsteps"] - row = max_index // num_rows - if row % 2 == 0: - col = num_cols - 1 - (max_index % num_cols) - else: - col = max_index % num_cols + col = get_raster_max_col(rasterDef, max_index) + + # Set the column index for the face on raster if max_col is None: max_col = col + face_on_max_coords = (x, y, z) else: # max_col should be available for orthogonal rasters # Find maximum in col defined in max_col and then reset max_col - indices = [] - for i in range(num_rows): - # Determine the column index in the flattened array - if i % 2 == 0: - # Even row - column_index = max_col - else: - # Odd row - column_index = num_cols - 1 - max_col - - # Calculate the index in the flattened array - index = i * num_cols + column_index - indices.append(index) - + indices = get_flattened_indices_of_max_col(rasterDef, max_col) ceiling = 0 # Loop through all the cells that belong to the column corresponding to max_col for index in indices: @@ -2370,12 +2408,18 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): max_index = index ceiling = scoreVal hotFile = cellResults[index]["cellMapKey"] - + hotCoords = rasterMap[hotFile] + x = hotCoords["x"] + y = hotCoords["y"] + z = hotCoords["z"] + ortho_max_coords = (x, y, z) max_col = None - kwargs["rasterRequest"]["request_obj"]["max_raster"]["file"] = hotFile - kwargs["rasterRequest"]["request_obj"]["max_raster"]["coords"] = [x, y, z] - kwargs["rasterRequest"]["request_obj"]["max_raster"]["index"] = max_index + kwargs["rasterRequest"]["request_obj"]["max_raster"] = { + "file" : hotFile, + "coords": [x, y, z], + "index": max_index + } if "omega" in kwargs: kwargs["rasterRequest"]["request_obj"]["max_raster"]["omega"] = kwargs["omega"] diff --git a/utils/raster.py b/utils/raster.py new file mode 100644 index 00000000..ed869336 --- /dev/null +++ b/utils/raster.py @@ -0,0 +1,93 @@ +import numpy as np + + +def calculate_matrix_index(k, M, N, pattern="horizontal"): + if pattern=="horizontal": + i = k // N + if i % 2 == 0: # odd row + j= k - (i*N) + else: + j = N - (k - (i*N)) - 1 + return i, j + elif pattern == "vertical": + j= k // M + if j % 2 == 0: # odd column + i = k - (j*M) + else: + i= M - (k - (j*M)) - 1 + return i, j + + +def calculate_flattened_index(i, j, M, N, pattern='horizontal'): + if pattern == 'horizontal': + if i % 2 == 0: # odd row + return i * (N) + j + else: # even row + return i * N + (N - 1 - j) + elif pattern == 'vertical': + if j % 2 == 0: # Odd column + return j * M + i + else: # Even column + return j * M + (M - 1 - i) + else: + raise ValueError("Invalid pattern specified") + + +def create_snake_array(flattened, raster_type, M, N): + # Reshape the list to a 2D array + array_2d = np.array(flattened).reshape(M, N) + + + if raster_type == 'horizontal': + # Reverse every even row for horizontal snaking + array_2d[1::2] = np.fliplr(array_2d[1::2]) + elif raster_type == 'vertical': + # Reverse every odd column for vertical snaking + array_2d = array_2d.T + array_2d[:, 0::2] = np.flipud(array_2d[:, 0::2]) + + return array_2d + +def determine_raster_shape(raster_def): + if (raster_def["rowDefs"][0]["start"]["y"] + == raster_def["rowDefs"][0]["end"]["y"] + ): # this is a horizontal raster + raster_dir = "horizontal" + else: + raster_dir = "vertical" + + + num_rows = len(raster_def["rowDefs"]) + num_cols = raster_def["rowDefs"][0]["numsteps"] + if raster_dir == "vertical": + num_rows, num_cols = num_cols, num_rows + + return raster_dir, num_rows, num_cols + + +def get_raster_max_col(raster_def, max_index): + """ + Given the results of a snake raster return a matrix of the + desired metric. The numpy array returned will have M rows and N columns + regardless of how the data was collected + """ + raster_dir, num_rows, num_cols = determine_raster_shape(raster_def) + i, j = calculate_matrix_index(max_index, num_rows, num_cols, pattern=raster_dir) + + return j + + + +def get_flattened_indices_of_max_col(raster_def, max_col): + """ + Given a column in a 2d raster this function returns the indices of all elements + in the column in the flattened list + """ + raster_dir, num_rows, num_cols = determine_raster_shape(raster_def) + + indices = [] + j = max_col + for i in range(num_rows): + indices.append(calculate_flattened_index(i, j, num_rows, num_cols, pattern=raster_dir)) + + return indices \ No newline at end of file From 79b72ad43f699d05b0aadcf64f9d369e1da7e2c7 Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Tue, 23 Jan 2024 12:26:36 -0500 Subject: [PATCH 06/33] FMX specific changes: 1. Fixed Ophyd device names 2. Run top_view only for FMX --- embl_robot.py | 6 ++++-- start_bs.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/embl_robot.py b/embl_robot.py index d4214152..b6ab9145 100644 --- a/embl_robot.py +++ b/embl_robot.py @@ -207,7 +207,8 @@ def preMount(self, gov_robot, puckPos, pinPos, sampID, **kwargs): prefix90 = sampName + "_" + str(puckPos) + "_" + str(pinPos) + "_" + str(reqCount) + "_PA_90" kwargs['prefix1'] = prefix1 kwargs['prefix90'] = prefix90 - # top_view.topViewSnap(prefix1,getBlConfig("visitDirectory")+"/pinAlign",1,acquire=0) + if daq_utils.beamline == "fmx": + top_view.topViewSnap(prefix1,getBlConfig("visitDirectory")+"/pinAlign",1,acquire=0) except Exception as e: e_s = str(e) message = "TopView check ERROR, will continue: " + e_s @@ -242,7 +243,8 @@ def callAlignPinThread(self, gov_robot, **kwargs): if (omegaCP > 89.5 and omegaCP < 90.5): beamline_lib.mvrDescriptor("omega", 85.0) logger.info("calling thread") - # _thread.start_new_thread(top_view.wait90TopviewThread,(gov_robot, prefix1,prefix90)) + if daq_utils.beamline == "fmx": + _thread.start_new_thread(top_view.wait90TopviewThread,(gov_robot, prefix1,prefix90)) logger.info("called thread") diff --git a/start_bs.py b/start_bs.py index 1a834603..0f8164c6 100755 --- a/start_bs.py +++ b/start_bs.py @@ -167,10 +167,10 @@ class SampleXYZ(Device): back_light = EpicsSignal(read_pv="XF:17DC-ES:FMX{BL:1}Ch1Value",name="back_light") back_light_range = (0, 100) - work_pos = WorkPositions("XF:17DC-ES:FMX", name="work_pos") + work_pos = WorkPositions("XF:17IDC-ES:FMX", name="work_pos") mount_pos = MountPositions("XF:17IDC-ES:FMX", name="mount_pos") - two_click_low = TwoClickLowMag("XF:17DC-ES:FMX{Cam:6}", name="two_click_low") - gonio = GoniometerStack("XF:17DC-ES:FMX{Gon:1", name="gonio") + two_click_low = TwoClickLowMag("XF:17IDC-ES:FMX{Cam:7}", name="two_click_low") + gonio = GoniometerStack("XF:17IDC-ES:FMX{Gon:1", name="gonio") loop_detector = LoopDetector(name="loop_detector") top_aligner_fast = TopAlignerFast(name="top_aligner_fast", gov_robot=gov_robot) top_aligner_slow = TopAlignerSlow(name="top_aligner_slow") From 461484cce3b8a9d1dc22a669560e25c0bcbdaac9 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 09:55:51 -0500 Subject: [PATCH 07/33] Moved generic devices to its own file --- bluesky_env/devices/__init__.py | 4 ++++ bluesky_env/devices/auto_center.py | 12 ------------ bluesky_env/devices/generic.py | 25 +++++++++++++++++++++++++ bluesky_env/devices/top_align.py | 8 +------- start_bs.py | 4 ++-- 5 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 bluesky_env/devices/__init__.py create mode 100644 bluesky_env/devices/generic.py diff --git a/bluesky_env/devices/__init__.py b/bluesky_env/devices/__init__.py new file mode 100644 index 00000000..97d9a197 --- /dev/null +++ b/bluesky_env/devices/__init__.py @@ -0,0 +1,4 @@ +from auto_center import * +from generic import * +from top_align import * +from zebra import * diff --git a/bluesky_env/devices/auto_center.py b/bluesky_env/devices/auto_center.py index 9ded4b02..99132806 100644 --- a/bluesky_env/devices/auto_center.py +++ b/bluesky_env/devices/auto_center.py @@ -200,16 +200,4 @@ def stage(self, *args, **kwargs): super().stage(*args, **kwargs) -class WorkPositions(Device): - gx = Cpt(EpicsSignal, '{Gov:Robot-Dev:gx}Pos:Work-Pos') - gpy = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpy}Pos:Work-Pos') - gpz = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpz}Pos:Work-Pos') - o = Cpt(EpicsSignal, '{Gov:Robot-Dev:go}Pos:Work-Pos') - - -class MountPositions(Device): - gx = Cpt(EpicsSignal, '{Gov:Robot-Dev:gx}Pos:Mount-Pos') - py = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpy}Pos:Mount-Pos') - pz = Cpt(EpicsSignal, '{Gov:Robot-Dev:gpz}Pos:Mount-Pos') - o = Cpt(EpicsSignal, '{Gov:Robot-Dev:go}Pos:Mount-Pos') diff --git a/bluesky_env/devices/generic.py b/bluesky_env/devices/generic.py new file mode 100644 index 00000000..2e458240 --- /dev/null +++ b/bluesky_env/devices/generic.py @@ -0,0 +1,25 @@ +from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal + + +class WorkPositions(Device): + gx = Cpt(EpicsSignal, "{Gov:Robot-Dev:gx}Pos:Work-Pos") + gpy = Cpt(EpicsSignal, "{Gov:Robot-Dev:gpy}Pos:Work-Pos") + gpz = Cpt(EpicsSignal, "{Gov:Robot-Dev:gpz}Pos:Work-Pos") + o = Cpt(EpicsSignal, "{Gov:Robot-Dev:go}Pos:Work-Pos") + + +class MountPositions(Device): + gx = Cpt(EpicsSignal, "{Gov:Robot-Dev:gx}Pos:Mount-Pos") + py = Cpt(EpicsSignal, "{Gov:Robot-Dev:gpy}Pos:Mount-Pos") + pz = Cpt(EpicsSignal, "{Gov:Robot-Dev:gpz}Pos:Mount-Pos") + o = Cpt(EpicsSignal, "{Gov:Robot-Dev:go}Pos:Mount-Pos") + + +class GoniometerStack(Device): + gx = Cpt(EpicsMotor, "-Ax:GX}Mtr") + gy = Cpt(EpicsMotor, "-Ax:GY}Mtr") + gz = Cpt(EpicsMotor, "-Ax:GZ}Mtr") + o = Cpt(EpicsMotor, "-Ax:O}Mtr") + py = Cpt(EpicsMotor, "-Ax:PY}Mtr") + pz = Cpt(EpicsMotor, "-Ax:PZ}Mtr") diff --git a/bluesky_env/devices/top_align.py b/bluesky_env/devices/top_align.py index 3188e09f..0b13109c 100644 --- a/bluesky_env/devices/top_align.py +++ b/bluesky_env/devices/top_align.py @@ -395,13 +395,7 @@ def read(self): -class GoniometerStack(Device): - gx = Cpt(EpicsMotor, "-Ax:GX}Mtr") - gy = Cpt(EpicsMotor, "-Ax:GY}Mtr") - gz = Cpt(EpicsMotor, "-Ax:GZ}Mtr") - o = Cpt(EpicsMotor, "-Ax:O}Mtr") - py = Cpt(EpicsMotor, "-Ax:PY}Mtr") - pz = Cpt(EpicsMotor, "-Ax:PZ}Mtr") + diff --git a/start_bs.py b/start_bs.py index 0f8164c6..601c2a62 100755 --- a/start_bs.py +++ b/start_bs.py @@ -9,8 +9,8 @@ from mxtools.governor import _make_governors from ophyd.signal import EpicsSignalBase EpicsSignalBase.set_defaults(timeout=10, connection_timeout=10) # new style -from bluesky_env.devices.auto_center import WorkPositions, TwoClickLowMag, LoopDetector, MountPositions -from bluesky_env.devices.top_align import TopAlignerFast, TopAlignerSlow, GoniometerStack +from bluesky_env.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, + TopAlignerFast, TopAlignerSlow, GoniometerStack) #12/19 - author unknown. DAMA can help """ From 63ccbc857f8414e0fe27da5ee59e7cb547740872 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 09:58:59 -0500 Subject: [PATCH 08/33] Added spline info to logger.debug --- bluesky_env/plans/top_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluesky_env/plans/top_view.py b/bluesky_env/plans/top_view.py index cc09b2d8..7b056341 100644 --- a/bluesky_env/plans/top_view.py +++ b/bluesky_env/plans/top_view.py @@ -55,7 +55,7 @@ def inner_pseudo_fly_scan(*args, **kwargs): omega_min = np.linspace(omegas[0], omegas[-1], sample)[ b_splines.argmin() ] - print(f"SPLINES / {omega_min}") + logger.debug(f"SPLINES / {omega_min}") """ if (-2000 < delta_y[[0]] < 2000) or (-2000 < delta_z[[0]] < 2000): From 0b436cffb19d1a22ffdbaee4312e9f8f4a1247fc Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 10:11:43 -0500 Subject: [PATCH 09/33] Added comment about loop detector device ML API --- bluesky_env/devices/auto_center.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bluesky_env/devices/auto_center.py b/bluesky_env/devices/auto_center.py index 99132806..92951032 100644 --- a/bluesky_env/devices/auto_center.py +++ b/bluesky_env/devices/auto_center.py @@ -75,6 +75,12 @@ class JPEGPluginWithFileStore(JPEGPlugin, FileStoreJPEG): class LoopDetector(Device): + """ + The loop detector device sends the low mag cam image to a specified REST API URL. + The URL accepts an image file as a post request with the key "image" in the form + data. It returns a JSON response that provides the predicted box as well as the + thresholded box based on x_start and x_end + """ url = Cpt( Signal, value=f'{os.environ["SAMPLE_DETECT_URL"]}/predict', kind='config' ) From 40baf5b98e8403948812ecfbf3a69ae573d7777f Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 10:16:34 -0500 Subject: [PATCH 10/33] Changed bluesky_env to mxbluesky --- daq_macros.py | 2 +- .../devices/__init__.py | 0 .../devices/auto_center.py | 0 {bluesky_env => mxbluesky}/devices/generic.py | 0 .../devices/top_align.py | 0 {bluesky_env => mxbluesky}/devices/zebra.py | 0 {bluesky_env => mxbluesky}/plans/__init__.py | 0 .../plans/loop_detection.py | 2 +- {bluesky_env => mxbluesky}/plans/top_view.py | 24 ++++++++++++------- {bluesky_env => mxbluesky}/plans/utils.py | 0 start_bs.py | 2 +- 11 files changed, 19 insertions(+), 11 deletions(-) rename {bluesky_env => mxbluesky}/devices/__init__.py (100%) rename {bluesky_env => mxbluesky}/devices/auto_center.py (100%) rename {bluesky_env => mxbluesky}/devices/generic.py (100%) rename {bluesky_env => mxbluesky}/devices/top_align.py (100%) rename {bluesky_env => mxbluesky}/devices/zebra.py (100%) rename {bluesky_env => mxbluesky}/plans/__init__.py (100%) rename {bluesky_env => mxbluesky}/plans/loop_detection.py (98%) rename {bluesky_env => mxbluesky}/plans/top_view.py (95%) rename {bluesky_env => mxbluesky}/plans/utils.py (100%) diff --git a/daq_macros.py b/daq_macros.py index 1231ac9f..58d5d587 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -40,7 +40,7 @@ from bluesky.preprocessors import finalize_wrapper, finalize_decorator from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX -from bluesky_env.plans import detect_loop, topview_optimized +from mxbluesky.plans import detect_loop, topview_optimized try: import ispybLib diff --git a/bluesky_env/devices/__init__.py b/mxbluesky/devices/__init__.py similarity index 100% rename from bluesky_env/devices/__init__.py rename to mxbluesky/devices/__init__.py diff --git a/bluesky_env/devices/auto_center.py b/mxbluesky/devices/auto_center.py similarity index 100% rename from bluesky_env/devices/auto_center.py rename to mxbluesky/devices/auto_center.py diff --git a/bluesky_env/devices/generic.py b/mxbluesky/devices/generic.py similarity index 100% rename from bluesky_env/devices/generic.py rename to mxbluesky/devices/generic.py diff --git a/bluesky_env/devices/top_align.py b/mxbluesky/devices/top_align.py similarity index 100% rename from bluesky_env/devices/top_align.py rename to mxbluesky/devices/top_align.py diff --git a/bluesky_env/devices/zebra.py b/mxbluesky/devices/zebra.py similarity index 100% rename from bluesky_env/devices/zebra.py rename to mxbluesky/devices/zebra.py diff --git a/bluesky_env/plans/__init__.py b/mxbluesky/plans/__init__.py similarity index 100% rename from bluesky_env/plans/__init__.py rename to mxbluesky/plans/__init__.py diff --git a/bluesky_env/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py similarity index 98% rename from bluesky_env/plans/loop_detection.py rename to mxbluesky/plans/loop_detection.py index 4f035e55..f6900a30 100644 --- a/bluesky_env/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -7,7 +7,7 @@ from start_bs import db, two_click_low, loop_detector, gonio -from bluesky_env.plans.utils import mvr_with_retry, mv_with_retry +from mxbluesky.plans.utils import mvr_with_retry, mv_with_retry logger = getLogger() diff --git a/bluesky_env/plans/top_view.py b/mxbluesky/plans/top_view.py similarity index 95% rename from bluesky_env/plans/top_view.py rename to mxbluesky/plans/top_view.py index 7b056341..692c797e 100644 --- a/bluesky_env/plans/top_view.py +++ b/mxbluesky/plans/top_view.py @@ -1,17 +1,25 @@ -import numpy as np -from scipy.interpolate import interp1d +from logging import getLogger -import bluesky.plans as bp import bluesky.plan_stubs as bps +import bluesky.plans as bp +import numpy as np +from bluesky.preprocessors import finalize_decorator from bluesky.utils import FailedStatus from ophyd.utils import WaitTimeoutError -from bluesky.preprocessors import finalize_decorator +from scipy.interpolate import interp1d -from start_bs import db, gov_robot, mount_pos, top_aligner_fast, top_aligner_slow, gonio, work_pos -from bluesky_env.devices.top_align import GovernorError -from bluesky_env.plans.utils import mv_with_retry, mvr_with_retry import gov_lib -from logging import getLogger +from mxbluesky.devices.top_align import GovernorError +from mxbluesky.plans.utils import mv_with_retry, mvr_with_retry +from start_bs import ( + db, + gonio, + gov_robot, + mount_pos, + top_aligner_fast, + top_aligner_slow, + work_pos, +) logger = getLogger() diff --git a/bluesky_env/plans/utils.py b/mxbluesky/plans/utils.py similarity index 100% rename from bluesky_env/plans/utils.py rename to mxbluesky/plans/utils.py diff --git a/start_bs.py b/start_bs.py index 601c2a62..ac163893 100755 --- a/start_bs.py +++ b/start_bs.py @@ -9,7 +9,7 @@ from mxtools.governor import _make_governors from ophyd.signal import EpicsSignalBase EpicsSignalBase.set_defaults(timeout=10, connection_timeout=10) # new style -from bluesky_env.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, +from mxbluesky.devices import (WorkPositions, TwoClickLowMag, LoopDetector, MountPositions, TopAlignerFast, TopAlignerSlow, GoniometerStack) #12/19 - author unknown. DAMA can help From c6082466618b86020f9ac8595eb0ea3628cef2b7 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 10:20:30 -0500 Subject: [PATCH 11/33] Removed zebra1 and zebra2 since they aren't used anywhere --- mxbluesky/devices/zebra.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mxbluesky/devices/zebra.py b/mxbluesky/devices/zebra.py index d2f5c334..d1e5bf40 100644 --- a/mxbluesky/devices/zebra.py +++ b/mxbluesky/devices/zebra.py @@ -694,5 +694,3 @@ def describe_collect(self): } } -zebra1 = Zebra('XF:17IDB-ES:AMX{Zeb:1}:', name='zebra1') -zebra2 = Zebra('XF:17IDB-ES:AMX{Zeb:2}:', name='zebra2') \ No newline at end of file From 9f39bf7b5ebbca29204e5f96ee226ce0a2ea3238 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 10:27:24 -0500 Subject: [PATCH 12/33] Removed commented out code --- mxbluesky/plans/loop_detection.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mxbluesky/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py index f6900a30..65e65a2a 100644 --- a/mxbluesky/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -19,12 +19,6 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): yield from bp.count([two_click_low], 1) loop_detector.filename.set(two_click_low.jpeg.full_file_name.get()) - - """ - yield from bps.abs_set( - loop_detector.filename, two_click_low.jpeg.full_file_name.get() - ) - """ scan_uid = yield from bp.count([loop_detector], 1) #box_coords_face: "list[int]" = db[scan_uid].table()['loop_detector_box'][1] From b67e7594f6511b88cfef1f3a88358598c57f06a5 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 10:39:03 -0500 Subject: [PATCH 13/33] Used np.deg2rad for angle conversion --- mxbluesky/plans/loop_detection.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mxbluesky/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py index 65e65a2a..bbcc7644 100644 --- a/mxbluesky/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -50,17 +50,14 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): omega: float = gonio.o.user_readback.get() sample_detection["face_on_omega"] = omega - d = np.pi/180 - - real_y = delta_cam_y * np.cos(omega * d) - real_z = delta_cam_y * np.sin(omega * d) + real_y = delta_cam_y * np.cos(np.deg2rad(omega)) + real_z = delta_cam_y * np.sin(np.deg2rad(omega)) yield from mvr_with_retry(gonio.gx, delta_x) yield from mvr_with_retry(gonio.py, -real_y) yield from mvr_with_retry(gonio.pz, -real_z) # The sample has moved to the center of the beam (hopefully), need to update co-ordinates - # box_coords_face[0] # orthogonal face, use loop model only if predicted width matches face on # otherwise, threshold From 1608ff2f2a52dea4959c622a2f9cd604414f0f35 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 24 Jan 2024 20:35:03 -0500 Subject: [PATCH 14/33] Added enum for CamMode in top align device --- mxbluesky/devices/top_align.py | 18 +++++++++++++++--- mxbluesky/plans/top_view.py | 6 +++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index 0b13109c..1f6de81d 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -35,6 +35,13 @@ class TopPlanLimit(Enum): DELTAY = DELTAZ = 2500 OMEGAMIN = 400 +class CamMode(Enum): + """ + Enum to represent the cam modes for TopAlignCam + """ + COARSE_ALIGN = "coarse_align" + FINE_FACE = "fine_face" + class TIFFPluginWithFileStore(TIFFPlugin, FileStoreTIFF): pass @@ -107,6 +114,11 @@ def __init__(self, *args, **kwargs): self.cam_mode.subscribe(self._update_stage_sigs, event_type="value") def _update_stage_sigs(self, *args, **kwargs): + """ + Update signals for coarse align and fine alignment + Coarse align takes place during SE -> TA transition + Fine face takes place during TA -> SA transition + """ self.tiff.stage_sigs.update([("enable", 0)]) self.stage_sigs.clear() self.stage_sigs.update( @@ -122,14 +134,14 @@ def _update_stage_sigs(self, *args, **kwargs): ("tiff.nd_array_port", "CV1"), ] ) - if self.cam_mode.get() == "coarse_align": + if self.cam_mode.get() == CamMode.COARSE_ALIGN.value: self.stage_sigs.update( [ ("cv1.nd_array_port", "ROI2"), ("roi2.nd_array_port", "TRANS1"), ] ) - elif self.cam_mode.get() == "fine_face": + elif self.cam_mode.get() == CamMode.FINE_FACE.value: self.stage_sigs.update( [ ("cv1.nd_array_port", "ROI1"), @@ -379,7 +391,7 @@ def _configure_device(self, *args, **kwargs): ("topcam.cam.acquire", 1), ] ) - self.topcam.cam_mode.set("coarse_align") + self.topcam.cam_mode.set(CamMode.COARSE_ALIGN.value) self.read_attrs = [ "topcam.cv1.outputs.output8", "topcam.cv1.outputs.output9", diff --git a/mxbluesky/plans/top_view.py b/mxbluesky/plans/top_view.py index 692c797e..8f89b3ba 100644 --- a/mxbluesky/plans/top_view.py +++ b/mxbluesky/plans/top_view.py @@ -9,7 +9,7 @@ from scipy.interpolate import interp1d import gov_lib -from mxbluesky.devices.top_align import GovernorError +from mxbluesky.devices.top_align import GovernorError, CamMode from mxbluesky.plans.utils import mv_with_retry, mvr_with_retry from start_bs import ( db, @@ -92,7 +92,7 @@ def topview_optimized(): # SE -> TA yield from bps.abs_set(top_aligner_fast.target_gov_state, "TA", wait=True) - yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, 'coarse_align') + yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, CamMode.COARSE_ALIGN.value) yield from bps.sleep(0.1) try: @@ -120,7 +120,7 @@ def topview_optimized(): # TA -> SA yield from bps.abs_set(top_aligner_fast.target_gov_state, "SA", wait=True) - yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, "fine_face") + yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, CamMode.FINE_FACE.value) yield from bps.sleep(0.1) try: From 51ab981f5a4ca45cada32bc77ed179ee8bd5caed Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 26 Jan 2024 17:13:18 -0500 Subject: [PATCH 15/33] Added docs to raster utils Fixed init for mxbluesky devices --- daq_macros.py | 2 +- mxbluesky/devices/__init__.py | 8 ++-- utils/raster.py | 71 ++++++++++++++++++++++------------- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/daq_macros.py b/daq_macros.py index 58d5d587..6b199abc 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -241,7 +241,7 @@ def loop_center_plan(): if gov_status.success: yield from detect_loop(sample_detection) else: - print("could not trasition to SA") + print("could not transition to SA") else: yield from detect_loop(sample_detection) diff --git a/mxbluesky/devices/__init__.py b/mxbluesky/devices/__init__.py index 97d9a197..5a8e42c7 100644 --- a/mxbluesky/devices/__init__.py +++ b/mxbluesky/devices/__init__.py @@ -1,4 +1,4 @@ -from auto_center import * -from generic import * -from top_align import * -from zebra import * +from .auto_center import * +from .generic import * +from .top_align import * +from .zebra import * diff --git a/utils/raster.py b/utils/raster.py index ed869336..fe7033fd 100644 --- a/utils/raster.py +++ b/utils/raster.py @@ -2,29 +2,39 @@ def calculate_matrix_index(k, M, N, pattern="horizontal"): - if pattern=="horizontal": + """ + Returns the row and column index in a raster based on the + index of the result array, shape of the raster and the + direction of collection + """ + if pattern == "horizontal": i = k // N - if i % 2 == 0: # odd row - j= k - (i*N) + if i % 2 == 0: # odd row + j = k - (i * N) else: - j = N - (k - (i*N)) - 1 + j = N - (k - (i * N)) - 1 return i, j elif pattern == "vertical": - j= k // M - if j % 2 == 0: # odd column - i = k - (j*M) + j = k // M + if j % 2 == 0: # odd column + i = k - (j * M) else: - i= M - (k - (j*M)) - 1 + i = M - (k - (j * M)) - 1 return i, j -def calculate_flattened_index(i, j, M, N, pattern='horizontal'): - if pattern == 'horizontal': +def calculate_flattened_index(i, j, M, N, pattern="horizontal"): + """ + Returns the index of the result array of a raster based on the + row and column index, shape of the raster and the + direction of collection + """ + if pattern == "horizontal": if i % 2 == 0: # odd row - return i * (N) + j + return i * (N) + j else: # even row return i * N + (N - 1 - j) - elif pattern == 'vertical': + elif pattern == "vertical": if j % 2 == 0: # Odd column return j * M + i else: # Even column @@ -34,29 +44,36 @@ def calculate_flattened_index(i, j, M, N, pattern='horizontal'): def create_snake_array(flattened, raster_type, M, N): + """ + Returns an MxN matrix of raster results given the + flattened result array, direction, and shape of the raster + """ # Reshape the list to a 2D array array_2d = np.array(flattened).reshape(M, N) - - if raster_type == 'horizontal': + if raster_type == "horizontal": # Reverse every even row for horizontal snaking array_2d[1::2] = np.fliplr(array_2d[1::2]) - elif raster_type == 'vertical': - # Reverse every odd column for vertical snaking + elif raster_type == "vertical": + # Reverse every even column for vertical snaking array_2d = array_2d.T - array_2d[:, 0::2] = np.flipud(array_2d[:, 0::2]) - + array_2d[:, 1::2] = np.flipud(array_2d[:, 1::2]) + return array_2d + def determine_raster_shape(raster_def): - if (raster_def["rowDefs"][0]["start"]["y"] - == raster_def["rowDefs"][0]["end"]["y"] + """ + Returns the shape and direction of a raster given + the raster definition + """ + if ( + raster_def["rowDefs"][0]["start"]["y"] == raster_def["rowDefs"][0]["end"]["y"] ): # this is a horizontal raster raster_dir = "horizontal" else: raster_dir = "vertical" - num_rows = len(raster_def["rowDefs"]) num_cols = raster_def["rowDefs"][0]["numsteps"] if raster_dir == "vertical": @@ -67,16 +84,14 @@ def determine_raster_shape(raster_def): def get_raster_max_col(raster_def, max_index): """ - Given the results of a snake raster return a matrix of the - desired metric. The numpy array returned will have M rows and N columns - regardless of how the data was collected + Returns the column of the raster corresponding to the given + index in the flattened array """ raster_dir, num_rows, num_cols = determine_raster_shape(raster_def) i, j = calculate_matrix_index(max_index, num_rows, num_cols, pattern=raster_dir) return j - def get_flattened_indices_of_max_col(raster_def, max_col): """ @@ -88,6 +103,8 @@ def get_flattened_indices_of_max_col(raster_def, max_col): indices = [] j = max_col for i in range(num_rows): - indices.append(calculate_flattened_index(i, j, num_rows, num_cols, pattern=raster_dir)) + indices.append( + calculate_flattened_index(i, j, num_rows, num_cols, pattern=raster_dir) + ) - return indices \ No newline at end of file + return indices From 80eecbb47bdb614d0a7f2ffe6b8d185cf1fbaea1 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 30 Jan 2024 12:14:50 -0500 Subject: [PATCH 16/33] Updating zebra sigs via TopAlign device - Added additional detailed logs for top align and auto raster --- mxbluesky/devices/top_align.py | 46 ++++++++++++++----------------- mxbluesky/plans/loop_detection.py | 8 +++--- mxbluesky/plans/top_view.py | 20 +++++++++----- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index 1f6de81d..676ebe3f 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -226,52 +226,46 @@ def __init__(self, *args, **kwargs): def _configure_device(self, *args, **kwargs): self.read_attrs = ["topcam", "zebra"] self.stage_sigs.clear() - # self.topcam.cam_mode.set("fine_face") - self.stage_sigs.update( [ ("topcam.cam.trigger_mode", 1), ("topcam.cam.image_mode", 2), ("topcam.cam.acquire", 1), ("gonio_o.velocity", 90), # slow down to get picture taken + ("zebra.pos_capt.source", "Enc4"), + ("zebra.armsel", 0), + ("zebra.pos_capt.gate.start", 0.5), # very important + ("zebra.pos_capt.gate.width", 4.5), + ("zebra.pos_capt.gate.step", 9), + ("zebra.pos_capt.gate.num_gates", 20), + ("zebra.pos_capt.pulse.start", 0), + ("zebra.pos_capt.pulse.width", 4), + ("zebra.pos_capt.pulse.step", 5), + ("zebra.pos_capt.pulse.delay", 0), + ("zebra.pos_capt.pulse.max_pulses", 1), + ("zebra.or3", 1), + ("zebra.or3loc", 30), + ] ) - self.zebra.stage_sigs.update( - [ - ("pos_capt.source", "Enc4"), - ("pos_capt.direction", 1), - ("armsel", 0), - ("pos_capt.gate.start", 0.5), # very important - ("pos_capt.gate.width", 4.5), - ("pos_capt.gate.step", 9), - ("pos_capt.gate.num_gates", 20), - ("pos_capt.pulse.start", 0), - ("pos_capt.pulse.width", 4), - ("pos_capt.pulse.step", 5), - ("pos_capt.pulse.delay", 0), - ("pos_capt.pulse.max_pulses", 1), - ("or3", 1), - ("or3loc", 30), - ] - ) + self.topcam.read_attrs = ["out9_buffer", "out10_buffer"] self.zebra.read_attrs = ["pos_capt.data.enc4"] def _update_stage_sigs(self, *args, **kwargs): if self.target_gov_state.get() == "TA": - self.zebra.stage_sigs.update( + self.stage_sigs.update( [ - ("pos_capt.direction", 0), # positive - ("pos_capt.gate.start", 0.1), + ("zebra.pos_capt.gate.start", 0.1), ] ) elif self.target_gov_state.get() == "SA": - self.zebra.stage_sigs.update( + self.stage_sigs.update( [ - ("pos_capt.direction", 1), # negative - ("pos_capt.gate.start", 180), + ("zebra.pos_capt.gate.start", 180), + ] ) diff --git a/mxbluesky/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py index bbcc7644..85d38005 100644 --- a/mxbluesky/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -14,7 +14,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): # face on attempt, most features, should work #yield from bps.abs_set(two_click_low.cam_mode, "two_click", wait=True) - + logger.info("Starting loop centering") two_click_low.cam_mode.set("two_click") yield from bp.count([two_click_low], 1) @@ -23,7 +23,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): scan_uid = yield from bp.count([loop_detector], 1) #box_coords_face: "list[int]" = db[scan_uid].table()['loop_detector_box'][1] box_coords_face: "list[int]" = loop_detector.box.get() - + logger.info("Got loop predictions") if len(box_coords_face) != 4: logger.exception("Exception during loop detection plan. Face on loop not found") # 640x512 @@ -44,7 +44,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): delta_x = mean_x * 2*two_click_low.pix_per_um.get() delta_cam_y = mean_y * 2*two_click_low.pix_per_um.get() - sample_detection["center_x"], sample_detection["center_y"] = delta_x, delta_cam_y + logger.info("Calculated delta") omega: float = gonio.o.user_readback.get() @@ -56,7 +56,7 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): yield from mvr_with_retry(gonio.gx, delta_x) yield from mvr_with_retry(gonio.py, -real_y) yield from mvr_with_retry(gonio.pz, -real_z) - + logger.info("Moving sample to center") # The sample has moved to the center of the beam (hopefully), need to update co-ordinates # orthogonal face, use loop model only if predicted width matches face on diff --git a/mxbluesky/plans/top_view.py b/mxbluesky/plans/top_view.py index 8f89b3ba..c53181ac 100644 --- a/mxbluesky/plans/top_view.py +++ b/mxbluesky/plans/top_view.py @@ -25,6 +25,7 @@ def cleanup_topcam(): yield from bps.abs_set(top_aligner_slow.topcam.cam.acquire, 1, wait=True) + yield from bps.abs_set(top_aligner_fast.zebra.pos_capt.direction, 0, wait=True) def inner_pseudo_fly_scan(*args, **kwargs): scan_uid = yield from bp.count(*args, **kwargs) @@ -76,7 +77,7 @@ def inner_pseudo_fly_scan(*args, **kwargs): @finalize_decorator(cleanup_topcam) def topview_optimized(): - + logger.info("Starting topview") try: # horizontal bump calculation, don't move just yet to avoid disturbing gov scan_uid = yield from bp.count([top_aligner_slow], 1) @@ -89,11 +90,14 @@ def topview_optimized(): # update work positions yield from set_TA_work_pos(delta_x=delta_x) + logger.info("Updated TA work pos, starting transition to TA") # SE -> TA yield from bps.abs_set(top_aligner_fast.target_gov_state, "TA", wait=True) + yield from bps.abs_set(top_aligner_fast.zebra.pos_capt.direction, 0, wait=True) yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, CamMode.COARSE_ALIGN.value) yield from bps.sleep(0.1) + logger.info("Starting 1st inner fly scan") try: delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( @@ -111,18 +115,19 @@ def topview_optimized(): delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) - + logger.info("Ended 1st inner fly scan, starting gonio move") yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) - + logger.info("Finished move, setting SA work pos") # update work positions yield from set_SA_work_pos(delta_y, delta_z) - + logger.info("Starting transition to SA") # TA -> SA yield from bps.abs_set(top_aligner_fast.target_gov_state, "SA", wait=True) + yield from bps.abs_set(top_aligner_fast.zebra.pos_capt.direction, 1, wait=True) yield from bps.abs_set(top_aligner_fast.topcam.cam_mode, CamMode.FINE_FACE.value) yield from bps.sleep(0.1) - + logger.info("Starting 2nd inner fly scan") try: delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] @@ -143,12 +148,13 @@ def topview_optimized(): delta_y, delta_z, omega_min = yield from inner_pseudo_fly_scan( [top_aligner_fast] ) - + logger.info("Finished 2nd inner fly scan, moving gonio") yield from mv_with_retry(top_aligner_fast.gonio_o, omega_min) yield from mvr_with_retry(top_aligner_fast.gonio_py, delta_y) yield from mvr_with_retry(top_aligner_fast.gonio_pz, -delta_z) - + logger.info("Finished gonio move setting SA work pos") set_SA_work_pos(delta_y, delta_z, gonio.py.user_readback.get(), gonio.pz.user_readback.get(), omega=gonio.o.user_readback.get()) + logger.info("Completed topview optimized") def set_TA_work_pos(delta_x=None): if delta_x: From 70e91c7f5efd48ce5a878d0b6a05f11e5642b0ff Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 30 Jan 2024 12:16:42 -0500 Subject: [PATCH 17/33] Added check for autoraster within gotoMaxRaster. Without check regular rasters are assumed to be autorasters --- daq_macros.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daq_macros.py b/daq_macros.py index 6b199abc..df81d1bf 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -2377,7 +2377,7 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): z = hotCoords["z"] logger.info("goto " + str(x) + " " + str(y) + " " + str(z)) - if "rasterRequest" in kwargs: + if "rasterRequest" in kwargs and autoRasterFlag: # Update the raster request with the location of the max raster image and co-ordinates. # NOTE: In the optimized autoraster, two 2D rasters are collected, face on and orthogonal. # Both rasters have the same number of columns, we save the column number of the hot cell (max_col) From 8d2de437fd3c769275546e4fc24126d276dfedcd Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 30 Jan 2024 12:27:39 -0500 Subject: [PATCH 18/33] Moved ZebraMXOr to zebra devices --- mxbluesky/devices/top_align.py | 8 +------- mxbluesky/devices/zebra.py | 4 ++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index 676ebe3f..6b6e4a17 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -11,7 +11,7 @@ from ophyd.status import SubscriptionStatus from pathlib import Path import requests -from .zebra import Zebra +from .zebra import ZebraMXOr from enum import Enum from collections import OrderedDict @@ -162,12 +162,6 @@ def trigger(self): return status -class ZebraMXOr(Zebra): - or3 = Cpt(EpicsSignal, "OR3_ENA:B3") - or3loc = Cpt(EpicsSignal, "OR3_INP4") - armsel = Cpt(EpicsSignal, "PC_ARM_SEL") - - class GovernorError(Exception): def __init__(self, message): super().__init__(self, message) diff --git a/mxbluesky/devices/zebra.py b/mxbluesky/devices/zebra.py index d1e5bf40..b27677c7 100644 --- a/mxbluesky/devices/zebra.py +++ b/mxbluesky/devices/zebra.py @@ -694,3 +694,7 @@ def describe_collect(self): } } +class ZebraMXOr(Zebra): + or3 = Cpt(EpicsSignal, "OR3_ENA:B3") + or3loc = Cpt(EpicsSignal, "OR3_INP4") + armsel = Cpt(EpicsSignal, "PC_ARM_SEL") \ No newline at end of file From 831f5d8bd3e7b58fa0c88ea2c46cd35f3a62427f Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 30 Jan 2024 12:48:07 -0500 Subject: [PATCH 19/33] Using configurable sizes for lowmag width and height --- mxbluesky/plans/loop_detection.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mxbluesky/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py index 85d38005..9ab8c0da 100644 --- a/mxbluesky/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -8,6 +8,7 @@ from start_bs import db, two_click_low, loop_detector, gonio from mxbluesky.plans.utils import mvr_with_retry, mv_with_retry +from daq_utils import lowMagPixX, lowMagPixY logger = getLogger() @@ -24,13 +25,16 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): #box_coords_face: "list[int]" = db[scan_uid].table()['loop_detector_box'][1] box_coords_face: "list[int]" = loop_detector.box.get() logger.info("Got loop predictions") + center_low_mag_x = int(lowMagPixX/2) + center_low_mag_y = int(lowMagPixY/2) if len(box_coords_face) != 4: logger.exception("Exception during loop detection plan. Face on loop not found") - # 640x512 - sample_detection["large_box_width"] = 430 * 2 * two_click_low.pix_per_um.get() - sample_detection["large_box_height"] = 340 * 2 * two_click_low.pix_per_um.get() - mean_x = 320 - mean_y = 256 + # If the loop is not found, raster as much of the low mag cam view as possible + # In the hopes of some diffraction + sample_detection["large_box_width"] = (lowMagPixX-10) * 2 * two_click_low.pix_per_um.get() + sample_detection["large_box_height"] = (lowMagPixY-10) * 2 * two_click_low.pix_per_um.get() + mean_x = center_low_mag_x + mean_y = center_low_mag_y box_coords_face = [105, 85, 535, 425] else: sample_detection["large_box_width"] = (box_coords_face[2] - box_coords_face[0]) * 2 * two_click_low.pix_per_um.get() @@ -39,9 +43,12 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): mean_x = (box_coords_face[0] + box_coords_face[2]) / 2 mean_y = (box_coords_face[1] + box_coords_face[3]) / 2 - mean_x = mean_x - 320 - mean_y = mean_y - 256 + # Calculate the distance in pixels the sample has to move + # such that the center of the sample aligns with the beam + mean_x = mean_x - center_low_mag_x + mean_y = mean_y - center_low_mag_y + # Calculate the delta in microns to move based on above calculation delta_x = mean_x * 2*two_click_low.pix_per_um.get() delta_cam_y = mean_y * 2*two_click_low.pix_per_um.get() logger.info("Calculated delta") From 61ed3283bc2753a289c1f5ec6db734dff8da47ab Mon Sep 17 00:00:00 2001 From: vshekar1 Date: Tue, 20 Feb 2024 11:17:24 -0500 Subject: [PATCH 20/33] Fixed top align device to allow margin of error in omega readback --- mxbluesky/devices/top_align.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index 6b6e4a17..46eb471d 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -258,7 +258,7 @@ def _update_stage_sigs(self, *args, **kwargs): elif self.target_gov_state.get() == "SA": self.stage_sigs.update( [ - ("zebra.pos_capt.gate.start", 180), + ("zebra.pos_capt.gate.start", 179.9), ] ) From 0b6f859b0f8d7db8bbc07319bfe3838b2402563d Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 5 Mar 2024 21:53:17 -0500 Subject: [PATCH 21/33] Adding on mount options --- daq_macros.py | 49 ++++++++++++++++++++++++++++++--------- gui/dialog/user_screen.py | 22 ++++++++++++++++-- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/daq_macros.py b/daq_macros.py index df81d1bf..f0879c3c 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -39,7 +39,7 @@ import bluesky.plans as bp from bluesky.preprocessors import finalize_wrapper, finalize_decorator from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX - +from config_params import ON_MOUNT_OPTION, OnMountAvailOptions from mxbluesky.plans import detect_loop, topview_optimized try: @@ -232,6 +232,42 @@ def changeImageCenterHighMag(x,y,czoom): def run_top_view_optimized(): RE(topview_optimized()) +def run_on_mount_option(sample_id): + option = OnMountAvailOptions(daq_utils.getBlConfig(ON_MOUNT_OPTION)) + request = {} + + if option == OnMountAvailOptions.DO_NOTHING: + return + + if (option == OnMountAvailOptions.CENTER_SAMPLE + or option == OnMountAvailOptions.AUTO_RASTER): + if daq_utils.beamline == "fmx": + # Run xrec for FMX, they don't have a top cam + retries = 3 + while retries: + success = loop_center_xrec() + if not success: + retries -= 1 + else: + retries = 0 + # Center using ML model + run_loop_center_plan() + + if option == OnMountAvailOptions.AUTO_RASTER: + # Set up a fake standard collection for autoRasterLoop + request = {"sample": sample_id, + "uid": -1, + "request_obj": { + "xbeam": getPvDesc('beamCenterX'), + "ybeam": getPvDesc('beamCenterY'), + "wavelength": daq_utils.energy2wave(beamline_lib.motorPosFromDescriptor("energy"), digits=6) + } + } + autoRasterLoop(request) + +def run_loop_center_plan(): + RE(loop_center_plan()) + def loop_center_plan(): global sample_detection if gov_robot.state.get() == 'M': @@ -247,16 +283,7 @@ def loop_center_plan(): def autoRasterLoop(currentRequest): global sample_detection, autoRasterFlag, max_col, face_on_max_coords, ortho_max_coords - if daq_utils.beamline == "fmx": - retries = 3 - while retries: - success = loop_center_xrec() - if not success: - retries -= 1 - else: - retries = 0 - - RE(loop_center_plan()) + if sample_detection["sample_detected"]: setTrans(getBlConfig("rasterDefaultTrans")) daq_lib.set_field("xrecRasterFlag","100") diff --git a/gui/dialog/user_screen.py b/gui/dialog/user_screen.py index 7322efcc..85100f90 100644 --- a/gui/dialog/user_screen.py +++ b/gui/dialog/user_screen.py @@ -4,7 +4,7 @@ from qt_epics.QtEpicsPVLabel import QtEpicsPVLabel from qtpy import QtCore, QtWidgets from qtpy.QtWidgets import QCheckBox - +from config_params import ON_MOUNT_OPTION import daq_utils if typing.TYPE_CHECKING: @@ -66,7 +66,21 @@ def __init__(self, parent: "ControlMain"): hBoxColParams3.addWidget(self.testRobotButton) hBoxColParams3.addWidget(self.recoverRobotButton) hBoxColParams3.addWidget(self.dryGripperButton) - robotGB.setLayout(hBoxColParams3) + + hBoxRobotParamsRow2 = QtWidgets.QHBoxLayout() + hBoxRobotParamsRow2.addWidget(QtWidgets.QLabel("After mount:")) + self.mountOptionDropdown = QtWidgets.QComboBox() + self.mountOptionDropdown.addItem("Do nothing", 0) + self.mountOptionDropdown.addItem("Center sample", 1) + self.mountOptionDropdown.addItem("Collect rasters", 2) + self.mountOptionDropdown.currentIndexChanged.connect(self.onMountOptionChanged) + self.mountOptionDropdown.setCurrentIndex(daq_utils.getBlConfig(ON_MOUNT_OPTION)) + hBoxRobotParamsRow2.addWidget(self.mountOptionDropdown) + + vBoxRobotParams = QtWidgets.QVBoxLayout() + vBoxRobotParams.addLayout(hBoxColParams3) + vBoxRobotParams.addLayout(hBoxRobotParamsRow2) + robotGB.setLayout(vBoxRobotParams) zebraGB = QtWidgets.QGroupBox() detGB = QtWidgets.QGroupBox() @@ -195,6 +209,10 @@ def __init__(self, parent: "ControlMain"): vBoxColParams1.addWidget(self.buttons) self.setLayout(vBoxColParams1) + + def onMountOptionChanged(self, index): + value = self.mountOptionDropdown.itemData(index) + daq_utils.setBlConfig(ON_MOUNT_OPTION, value) def setSlit1XCB(self): self.parent.send_to_server("setSlit1X", [self.slit1XMotor_ledit.text()]) From 4eaef68c33071c16e45b4c9e57a63f120df83beb Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 5 Mar 2024 21:53:52 -0500 Subject: [PATCH 22/33] Adding raster to scene if removed --- gui/control_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/control_main.py b/gui/control_main.py index 75f21f38..40b36f8c 100644 --- a/gui/control_main.py +++ b/gui/control_main.py @@ -1958,6 +1958,9 @@ def processSampMove(self, posRBV, motID): raster["graphicsItem"].setVisible(False) else: raster["graphicsItem"].setVisible(True) + if raster["graphicsItem"].scene() is None: + # Sometimes the item is removed from scene somewhere else + self.scene.addItem(raster["graphicsItem"]) newY = self.calculateNewYCoordPos(startYX, startYY) raster["graphicsItem"].setPos(raster["graphicsItem"].x(), newY) From 03246eb3559a1b9f64096ccca8444abf55e6d191 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 5 Mar 2024 21:59:04 -0500 Subject: [PATCH 23/33] Adding config params and daq_lib mount options --- config_params.py | 12 ++++++++++-- daq_lib.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config_params.py b/config_params.py index 54f5e891..1b06dadd 100644 --- a/config_params.py +++ b/config_params.py @@ -1,5 +1,6 @@ +import grp +import os from enum import Enum -import os, grp # BlConfig parameter variable names @@ -36,7 +37,7 @@ # GUI default configuration BEAM_CHECK = "beamCheck" UNMOUNT_COLD_CHECK = "unmountColdCheck" - +ON_MOUNT_OPTION = "onMountOption" # raster request status updates class RasterStatus(Enum): @@ -52,6 +53,13 @@ class RasterStatus(Enum): READY_FOR_SNAPSHOT = 3 READY_FOR_REPROCESS = 4 +class OnMountAvailOptions(Enum): + """ + This enumerates the options available to the user on mounting a sample + """ + DO_NOTHING = 0 # Only mounts sample + CENTER_SAMPLE = 1 # Mounts and centers sample + AUTO_RASTER = 2 # Mounts, centers and takes 2 orthogonal rasters HUTCH_TIMER_DELAY = 500 SAMPLE_TIMER_DELAY = 0 diff --git a/daq_lib.py b/daq_lib.py index f2465c9b..af67e2e3 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -284,6 +284,10 @@ def mountSample(sampID): if (detDist != saveDetDist): if (getBlConfig("HePath") == 0): beamline_lib.mvaDescriptor("detectorDist",saveDetDist) + if getBlConfig('robot_online') and getBlConfig("queueCollect") == 0: + # Only run mount options when the robot is online and queue collect is off + daq_macros.run_on_mount_option(sampID) + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') elif(mountStat == 2): return 2 else: From 3c15c05bf4bb7926a424a7bcb3795e29422fe6d7 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 5 Mar 2024 21:59:32 -0500 Subject: [PATCH 24/33] Small bug fixes for FMX --- mxbluesky/devices/auto_center.py | 15 +++++++--- mxbluesky/devices/top_align.py | 4 +-- mxbluesky/plans/loop_detection.py | 47 ++++++++++++++++--------------- mxbluesky/plans/top_view.py | 3 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/mxbluesky/devices/auto_center.py b/mxbluesky/devices/auto_center.py index 92951032..c8434076 100644 --- a/mxbluesky/devices/auto_center.py +++ b/mxbluesky/devices/auto_center.py @@ -6,7 +6,8 @@ ROIPlugin, TransformPlugin, StatsPlugin, ProcessPlugin, SingleTrigger, ProsilicaDetector) from pathlib import Path import requests - +from bluesky.utils import FailedStatus +import daq_utils import os @@ -130,13 +131,13 @@ class TwoClickLowMag(StandardProsilica): jpeg = Cpt( JPEGPluginWithFileStore, "JPEG1:", - write_path_template="/nsls2/data/amx/legacy/topcam", + write_path_template=f"/nsls2/data/{daq_utils.beamline}/legacy/topcam", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.read_attrs = ["cv1", "jpeg"] - self.jpeg.read_attrs = ['full_file_name'] + self.jpeg.read_attrs = ['full_file_name', 'file_write_mode', 'file_template'] self.cv1.read_attrs = ["outputs"] self.cv1.outputs.read_attrs = ["output1", "output2", "output3"] self.cam_mode.subscribe(self._update_stage_sigs, event_type="value") @@ -205,5 +206,11 @@ def stage(self, *args, **kwargs): self._update_stage_sigs(*args, **kwargs) super().stage(*args, **kwargs) - + def trigger(self): + try: + status = super().trigger() + status.wait(6) + except AttributeError: + raise FailedStatus + return status diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index 46eb471d..b9d2ff63 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -15,7 +15,7 @@ from enum import Enum from collections import OrderedDict - +import daq_utils class TopPlanLimit(Enum): """Prevent large goniometer movements during topview plan. Rotating gonio @@ -86,7 +86,7 @@ class TopAlignCam(StandardProsilica): tiff = Cpt( TIFFPluginWithFileStore, "TIFF1:", - write_path_template="/nsls2/data/amx/legacy/topcam", + write_path_template=f"/nsls2/data/{daq_utils.beamline}/legacy/topcam", ) cam_mode = Cpt(Signal, value=None, kind="config") pix_per_um = Cpt(Signal, value=0.164, doc="pixels per um") diff --git a/mxbluesky/plans/loop_detection.py b/mxbluesky/plans/loop_detection.py index 9ab8c0da..279cc66f 100644 --- a/mxbluesky/plans/loop_detection.py +++ b/mxbluesky/plans/loop_detection.py @@ -4,38 +4,41 @@ import numpy as np import bluesky.plan_stubs as bps import bluesky.plans as bp - - +from bluesky.utils import FailedStatus +from bluesky.preprocessors import finalize_decorator +import daq_utils from start_bs import db, two_click_low, loop_detector, gonio from mxbluesky.plans.utils import mvr_with_retry, mv_with_retry -from daq_utils import lowMagPixX, lowMagPixY logger = getLogger() +def cleanup_two_click_low(): + if daq_utils.beamline == "fmx": + logger.info("Cleaning up two_click_low") + yield from bps.abs_set(two_click_low.jpeg.file_write_mode, 2, wait=True) + yield from bps.abs_set(two_click_low.jpeg.file_template, "%s%s_%d.jpg", wait=True) + +@finalize_decorator(cleanup_two_click_low) def detect_loop(sample_detection: "Dict[str, float|int]"): # face on attempt, most features, should work - #yield from bps.abs_set(two_click_low.cam_mode, "two_click", wait=True) + yield from bps.abs_set(two_click_low.cam_mode, "two_click", wait=True) logger.info("Starting loop centering") - two_click_low.cam_mode.set("two_click") - - yield from bp.count([two_click_low], 1) + #two_click_low.cam_mode.set("two_click") + try: + yield from bp.count([two_click_low], 1) + except FailedStatus: + yield from bp.count([two_click_low], 1) + loop_detector.filename.set(two_click_low.jpeg.full_file_name.get()) scan_uid = yield from bp.count([loop_detector], 1) #box_coords_face: "list[int]" = db[scan_uid].table()['loop_detector_box'][1] box_coords_face: "list[int]" = loop_detector.box.get() logger.info("Got loop predictions") - center_low_mag_x = int(lowMagPixX/2) - center_low_mag_y = int(lowMagPixY/2) if len(box_coords_face) != 4: logger.exception("Exception during loop detection plan. Face on loop not found") - # If the loop is not found, raster as much of the low mag cam view as possible - # In the hopes of some diffraction - sample_detection["large_box_width"] = (lowMagPixX-10) * 2 * two_click_low.pix_per_um.get() - sample_detection["large_box_height"] = (lowMagPixY-10) * 2 * two_click_low.pix_per_um.get() - mean_x = center_low_mag_x - mean_y = center_low_mag_y - box_coords_face = [105, 85, 535, 425] + sample_detection["sample_detected"] = False + return else: sample_detection["large_box_width"] = (box_coords_face[2] - box_coords_face[0]) * 2 * two_click_low.pix_per_um.get() sample_detection["large_box_height"] = (box_coords_face[3] - box_coords_face[1]) * 2 * two_click_low.pix_per_um.get() @@ -43,12 +46,9 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): mean_x = (box_coords_face[0] + box_coords_face[2]) / 2 mean_y = (box_coords_face[1] + box_coords_face[3]) / 2 - # Calculate the distance in pixels the sample has to move - # such that the center of the sample aligns with the beam - mean_x = mean_x - center_low_mag_x - mean_y = mean_y - center_low_mag_y + mean_x = mean_x - 320 + mean_y = mean_y - 256 - # Calculate the delta in microns to move based on above calculation delta_x = mean_x * 2*two_click_low.pix_per_um.get() delta_cam_y = mean_y * 2*two_click_low.pix_per_um.get() logger.info("Calculated delta") @@ -70,7 +70,10 @@ def detect_loop(sample_detection: "Dict[str, float|int]"): # otherwise, threshold yield from bps.mv(gonio.o, sample_detection["face_on_omega"]+90) - scan_uid = yield from bp.count([two_click_low], 1) + try: + yield from bp.count([two_click_low], 1) + except FailedStatus: + yield from bp.count([two_click_low], 1) loop_detector.get_threshold.set(True) loop_detector.x_start.set(int(box_coords_face[0]-mean_x)) diff --git a/mxbluesky/plans/top_view.py b/mxbluesky/plans/top_view.py index c53181ac..31a17c36 100644 --- a/mxbluesky/plans/top_view.py +++ b/mxbluesky/plans/top_view.py @@ -84,10 +84,11 @@ def topview_optimized(): except FailedStatus: scan_uid = yield from bp.count([top_aligner_slow], 1) + logger.info(f"Finished top aligner slow scan {scan_uid}") x = db[scan_uid].table()[top_aligner_slow.topcam.cv1.outputs.output8.name][1] delta_x = ((top_aligner_slow.topcam.roi2.size.x.get() / 2) - x) / top_aligner_slow.topcam.pix_per_um.get() - + logger.info(f"Horizontal bump calc finished: {delta_x}") # update work positions yield from set_TA_work_pos(delta_x=delta_x) logger.info("Updated TA work pos, starting transition to TA") From 9c3c605dd2738e950dd4e46624faf5147060276d Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 8 Mar 2024 12:01:17 -0500 Subject: [PATCH 25/33] Small fixes to run the correct plans at the right time --- daq_lib.py | 9 +++++---- daq_macros.py | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/daq_lib.py b/daq_lib.py index af67e2e3..b090c29b 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -284,10 +284,10 @@ def mountSample(sampID): if (detDist != saveDetDist): if (getBlConfig("HePath") == 0): beamline_lib.mvaDescriptor("detectorDist",saveDetDist) - if getBlConfig('robot_online') and getBlConfig("queueCollect") == 0: - # Only run mount options when the robot is online and queue collect is off - daq_macros.run_on_mount_option(sampID) - gov_status = gov_lib.setGovRobot(gov_robot, 'SA') + if getBlConfig('robot_online') and getBlConfig("queueCollect") == 0: + # Only run mount options when the robot is online and queue collect is off + daq_macros.run_on_mount_option(sampID) + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') elif(mountStat == 2): return 2 else: @@ -540,6 +540,7 @@ def collectData(currentRequest): check_pause() else: logger.info("autoRaster") + daq_macros.run_loop_center_plan() if not (daq_macros.autoRasterLoop(currentRequest)): logger.info("could not center sample") db_lib.updatePriority(currentRequest["uid"],-1) diff --git a/daq_macros.py b/daq_macros.py index f0879c3c..45aacb1d 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -234,6 +234,7 @@ def run_top_view_optimized(): def run_on_mount_option(sample_id): option = OnMountAvailOptions(daq_utils.getBlConfig(ON_MOUNT_OPTION)) + logger.info(f"Running on mount option : {option}") request = {} if option == OnMountAvailOptions.DO_NOTHING: @@ -241,15 +242,6 @@ def run_on_mount_option(sample_id): if (option == OnMountAvailOptions.CENTER_SAMPLE or option == OnMountAvailOptions.AUTO_RASTER): - if daq_utils.beamline == "fmx": - # Run xrec for FMX, they don't have a top cam - retries = 3 - while retries: - success = loop_center_xrec() - if not success: - retries -= 1 - else: - retries = 0 # Center using ML model run_loop_center_plan() @@ -266,6 +258,15 @@ def run_on_mount_option(sample_id): autoRasterLoop(request) def run_loop_center_plan(): + if daq_utils.beamline == "fmx": + # Run xrec for FMX, they don't have a top cam + retries = 3 + while retries: + success = loop_center_xrec() + if not success: + retries -= 1 + else: + retries = 0 RE(loop_center_plan()) def loop_center_plan(): From 94f9eeb0a8e14b83f8ab02e8093a920e5ea24cee Mon Sep 17 00:00:00 2001 From: Shekar V Date: Wed, 20 Mar 2024 13:05:13 -0400 Subject: [PATCH 26/33] Removed setRobotGovState and replaced with gov_lib.setGovRobot() --- daq_lib.py | 6 ------ daq_macros.py | 4 ++-- embl_robot.py | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/daq_lib.py b/daq_lib.py index b090c29b..d29526dd 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -233,12 +233,6 @@ def getRobotConfig(): return getPvDesc("robotGovConfig",as_string=True) -def setRobotGovState(stateString, wait=True): - if (getRobotConfig() == "Robot"): - setPvDesc("robotGovGo",stateString, wait=wait) - else: - setPvDesc("humanGovGo",stateString, wait=wait) - def mountSample(sampID): global mountCounter diff --git a/daq_macros.py b/daq_macros.py index 45aacb1d..94696469 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -1521,7 +1521,7 @@ def snakeRasterNormal(rasterReqID,grain=""): if (daq_utils.beamline == "fmx"): setPvDesc("sampleProtect",0) setPvDesc("vectorGo", 0) #set to 0 to allow easier camonitoring vectorGo - daq_lib.setRobotGovState("DA") + gov_lib.setGovRobot(gov_robot, "DA") rasterRequest = db_lib.getRequestByID(rasterReqID) reqObj = rasterRequest["request_obj"] parentReqID = reqObj["parentReqID"] @@ -3646,7 +3646,7 @@ def zebraDaq(vector_program,angle_start,scanWidth,imgWidth,exposurePeriodPerImag logger.info("in Zebra Daq #1 " + str(time.time())) yield from bps.mv(eiger.fw_num_images_per_file, IMAGES_PER_FILE) - daq_lib.setRobotGovState("DA") + gov_lib.setGovRobot(gov_robot, "DA") yield from bps.mv(vector_program.expose, 1) if (imgWidth == 0): diff --git a/embl_robot.py b/embl_robot.py index b6ab9145..218cc71d 100644 --- a/embl_robot.py +++ b/embl_robot.py @@ -374,7 +374,7 @@ def preUnmount(self, gov_robot, puckPos,pinPos,sampID): #will somehow know where wait = False else: wait = True - daq_lib.setRobotGovState("SE", wait=wait) + gov_lib.setGovRobot(gov_robot, "SE") logger.info("Done setting SE") logger.info("unmounting " + str(puckPos) + " " + str(pinPos) + " " + str(sampID)) logger.info("absPos = " + str(absPos)) From fdf7306cd9bde3739dc4dcdd103f6ed1fcdc1f8b Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 5 Apr 2024 17:38:30 -0400 Subject: [PATCH 27/33] Refactored gotoMaxRaster to use numpy instead of loops and make it more readable --- daq_macros.py | 228 ++++++++++++++++++++++++++------------------------ 1 file changed, 118 insertions(+), 110 deletions(-) diff --git a/daq_macros.py b/daq_macros.py index 94696469..bb100353 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -2350,16 +2350,38 @@ def runRasterScan(currentRequest,rasterType="", width=0, height=0, step_size=10, time.sleep(1) #I think I really need this, not sure why RE(snakeRaster(rasterReqID)) -def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): +def get_score_vals(cellResults, scoreOption): + score_vals = np.zeros(len(cellResults)) + for i, res in enumerate(cellResults): + try: + score_vals[i] = float(res[scoreOption]) + except TypeError: + logger.debug(f"Option {scoreOption} not found for {res=}") + return score_vals + +def get_score_index(score_vals, scoreOption, indices=None): + if indices: + score_vals = score_vals[indices] + if scoreOption == "d_min": + # If value is -1 replace with inf so that it is not considered for np.min + score_vals = np.where(score_vals == -1, np.inf, score_vals) + scoreVal = np.min(score_vals) + max_index = np.argmin(score_vals) + else: + scoreVal = np.max(score_vals) + max_index = np.argmax(score_vals) + + return scoreVal, max_index + + +def gotoMaxRaster(rasterResult,multiColThreshold=None,**kwargs): global autoVectorCoarseCoords,autoVectorFlag, max_col, face_on_max_coords, ortho_max_coords requestID = rasterResult["request"] if (rasterResult["result_obj"]["rasterCellResults"]['resultObj'] == None): logger.info("no raster result!!\n") raise ValueError("raster result object is None") - return - ceiling = 0.0 - floor = 100000000.0 #for resolution where small number means high score + hotFile = "" scoreOption = "" logger.info("in gotomax") @@ -2373,32 +2395,24 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): else: scoreOption = "total_intensity" max_index = None - for i in range (0,len(cellResults)): - try: - scoreVal = float(cellResults[i][scoreOption]) - except TypeError: - scoreVal = 0.0 - if (multiColThreshold>-1): - logger.info("doing multicol") - if (scoreVal >= multiColThreshold): - hitFile = cellResults[i]["cellMapKey"] - hitCoords = rasterMap[hitFile] - parentReqID = rasterResult['result_obj']["parentReqID"] - if (parentReqID == -1): - addMultiRequestLocation(requestID,hitCoords,i) - else: - addMultiRequestLocation(parentReqID,hitCoords,i) - if (scoreOption == "d_min"): - if (scoreVal < floor and scoreVal != -1): - floor = scoreVal - hotFile = cellResults[i]["cellMapKey"] - else: - if (scoreVal > ceiling): - max_index = i - ceiling = scoreVal - hotFile = cellResults[i]["cellMapKey"] - if (hotFile != ""): - logger.info('raster score ceiling: %s floor: %s hotfile: %s' % (ceiling, floor, hotFile)) + score_vals = get_score_vals(cellResults, scoreOption) + score_val, max_index = get_score_index(score_vals, scoreOption) + + if multiColThreshold is not None: + logger.info("doing multicol") + for index in np.where(score_vals > multiColThreshold)[0]: + hitFile = cellResults[index]["cellMapKey"] + hitCoords = rasterMap[hitFile] + parent_req_id = rasterResult['result_obj']["parentReqID"] + if parent_req_id == -1: + addMultiRequestLocation(requestID, hitCoords, index) + else: + addMultiRequestLocation(parent_req_id, hitCoords, index) + + + if max_index: + hotFile = cellResults[max_index] + logger.info(f'raster score: {score_val=} at {hotFile=}') hotCoords = rasterMap[hotFile] x = hotCoords["x"] y = hotCoords["y"] @@ -2425,21 +2439,12 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): # max_col should be available for orthogonal rasters # Find maximum in col defined in max_col and then reset max_col indices = get_flattened_indices_of_max_col(rasterDef, max_col) - ceiling = 0 - # Loop through all the cells that belong to the column corresponding to max_col - for index in indices: - try: - scoreVal = float(cellResults[index][scoreOption]) - except TypeError: - scoreVal = 0.0 - if (scoreVal > ceiling): - max_index = index - ceiling = scoreVal - hotFile = cellResults[index]["cellMapKey"] - hotCoords = rasterMap[hotFile] - x = hotCoords["x"] - y = hotCoords["y"] - z = hotCoords["z"] + score_val, index = get_score_index(score_vals, scoreOption, indices) + hotFile = cellResults[index]["cellMapKey"] + hotCoords = rasterMap[hotFile] + x = hotCoords["x"] + y = hotCoords["y"] + z = hotCoords["z"] ortho_max_coords = (x, y, z) max_col = None @@ -2465,74 +2470,77 @@ def gotoMaxRaster(rasterResult,multiColThreshold=-1,**kwargs): else: beamline_lib.mvaDescriptor("sampleX",x,"sampleY",y,"sampleZ",z) if (autoVectorFlag): #if we found a hotspot, then look again at cellResults for coarse vector start and end - xminColumn = [] #these are the "line rasters" of the ends of threshold points determined by the first pass on the raster results - xmaxColumn = [] - vectorThreshold = 0.7*ceiling - xmax = -1000000 - xmin = 1000000 - ymin = 0 - ymax = 0 - zmin = 0 - zmax = 0 - for i in range (0,len(cellResults)): #first find the xmin and xmax of threshold (left and right ends of vector) - try: - scoreVal = float(cellResults[i][scoreOption]) - except TypeError: - scoreVal = 0.0 - if (scoreVal > vectorThreshold): - hotFile = cellResults[i]["cellMapKey"] - hotCoords = rasterMap[hotFile] - x = hotCoords["x"] - if (xxmax): - xmax = x - for i in range (0,len(cellResults)): #now grab the columns of cells on xmin and xmax, like line scan results on the ends - fileKey = cellResults[i]["cellMapKey"] - coords = rasterMap[fileKey] - x = coords["x"] - if (x == xmin): #cell is in left column - xEdgeCellResult = {"coords":coords,"processingResults":cellResults[i]} - xminColumn.append(xEdgeCellResult) - if (x == xmax): - xEdgeCellResult = {"coords":coords,"processingResults":cellResults[i]} - xmaxColumn.append(xEdgeCellResult) - maxIndex = -10000 - minIndex = 10000 - for i in range (0,len(xminColumn)): #find the midpoint of the left column that is in the threshold range - try: - scoreVal = float(xminColumn[i]["processingResults"][scoreOption]) - except TypeError: - scoreVal = 0.0 - if (scoreVal > vectorThreshold): - if (minIndex<0): #you only need the first one that beats the threshold - minIndex = i - if (i>maxIndex): - maxIndex = i - middleIndex = int((minIndex+maxIndex)/2) - xmin = xminColumn[middleIndex]["coords"]["x"] - ymin = xminColumn[middleIndex]["coords"]["y"] - zmin = xminColumn[middleIndex]["coords"]["z"] - for i in range (0,len(xmaxColumn)): #do same as above for right column - try: - scoreVal = float(xmaxColumn[i]["processingResults"][scoreOption]) - except TypeError: - scoreVal = 0.0 - if (scoreVal > vectorThreshold): - if (minIndex<0): - minIndex = i - if (i>maxIndex): - maxIndex = i - middleIndex = int((minIndex+maxIndex)/2) - xmax = xmaxColumn[middleIndex]["coords"]["x"] - ymax = xmaxColumn[middleIndex]["coords"]["y"] - zmax = xmaxColumn[middleIndex]["coords"]["z"] - - autoVectorCoarseCoords = {"start":{"x":xmin,"y":ymin,"z":zmin},"end":{"x":xmax,"y":ymax,"z":zmax}} + run_auto_vector(score_val, cellResults, scoreOption, rasterMap) else: raise ValueError("No max position found for gonio move") - + +def run_auto_vector(ceiling, cellResults, scoreOption, rasterMap): + xminColumn = [] #these are the "line rasters" of the ends of threshold points determined by the first pass on the raster results + xmaxColumn = [] + vectorThreshold = 0.7*ceiling + xmax = -1000000 + xmin = 1000000 + ymin = 0 + ymax = 0 + zmin = 0 + zmax = 0 + for i in range (0,len(cellResults)): #first find the xmin and xmax of threshold (left and right ends of vector) + try: + scoreVal = float(cellResults[i][scoreOption]) + except TypeError: + scoreVal = 0.0 + if (scoreVal > vectorThreshold): + hotFile = cellResults[i]["cellMapKey"] + hotCoords = rasterMap[hotFile] + x = hotCoords["x"] + if (xxmax): + xmax = x + for i in range (0,len(cellResults)): #now grab the columns of cells on xmin and xmax, like line scan results on the ends + fileKey = cellResults[i]["cellMapKey"] + coords = rasterMap[fileKey] + x = coords["x"] + if (x == xmin): #cell is in left column + xEdgeCellResult = {"coords":coords,"processingResults":cellResults[i]} + xminColumn.append(xEdgeCellResult) + if (x == xmax): + xEdgeCellResult = {"coords":coords,"processingResults":cellResults[i]} + xmaxColumn.append(xEdgeCellResult) + maxIndex = -10000 + minIndex = 10000 + for i in range (0,len(xminColumn)): #find the midpoint of the left column that is in the threshold range + try: + scoreVal = float(xminColumn[i]["processingResults"][scoreOption]) + except TypeError: + scoreVal = 0.0 + if (scoreVal > vectorThreshold): + if (minIndex<0): #you only need the first one that beats the threshold + minIndex = i + if (i>maxIndex): + maxIndex = i + middleIndex = int((minIndex+maxIndex)/2) + xmin = xminColumn[middleIndex]["coords"]["x"] + ymin = xminColumn[middleIndex]["coords"]["y"] + zmin = xminColumn[middleIndex]["coords"]["z"] + for i in range (0,len(xmaxColumn)): #do same as above for right column + try: + scoreVal = float(xmaxColumn[i]["processingResults"][scoreOption]) + except TypeError: + scoreVal = 0.0 + if (scoreVal > vectorThreshold): + if (minIndex<0): + minIndex = i + if (i>maxIndex): + maxIndex = i + middleIndex = int((minIndex+maxIndex)/2) + xmax = xmaxColumn[middleIndex]["coords"]["x"] + ymax = xmaxColumn[middleIndex]["coords"]["y"] + zmax = xmaxColumn[middleIndex]["coords"]["z"] + + autoVectorCoarseCoords = {"start":{"x":xmin,"y":ymin,"z":zmin},"end":{"x":xmax,"y":ymax,"z":zmax}} + def addMultiRequestLocation(parentReqID,hitCoords,locIndex): #rough proto of what to pass here for details like how to organize data parentRequest = db_lib.getRequestByID(parentReqID) sampleID = parentRequest["sample"] From 298be79f6fa4c0c806964cefa920f7bf952fb0c1 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 5 Apr 2024 20:26:50 -0400 Subject: [PATCH 28/33] If either raster has no spots, the collection stops --- daq_macros.py | 52 +++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/daq_macros.py b/daq_macros.py index bb100353..8cdf396e 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -304,7 +304,12 @@ def autoRasterLoop(currentRequest): runRasterScan(currentRequest, rasterType="Custom", width=sample_detection["large_box_width"], height=sample_detection["large_box_height"], step_size=step_size) - logger.info(f"AUTORASTER LOOP: {sample_detection['face_on_omega']=} ") + logger.info(f"AUTORASTER LOOP: {max_col} {face_on_max_coords}") + + if not face_on_max_coords: + autoRasterFlag = 0 + return 0 + RE(bps.mv(gonio.gx, sample_detection["center_x"], gonio.py, sample_detection["center_y"], gonio.pz, sample_detection["center_z"])) @@ -316,36 +321,39 @@ def autoRasterLoop(currentRequest): omega_rel=90) # Gonio should be at the hot cell for the ortho raster. # Now move to the hot cell of the face on raster, then start standard collection - if face_on_max_coords and ortho_max_coords: - logger.info(f"AUTORASTER LOOP: {face_on_max_coords=} {ortho_max_coords=}") - logger.info(f"AUTORASTER LOOP: {sample_detection=} {max_col=}") - _, y_f, z_f = face_on_max_coords - _, y_ort, z_ort = ortho_max_coords - y_center, z_center = sample_detection["center_y"], sample_detection["center_z"] + if not ortho_max_coords: + autoRasterFlag = 0 + return 0 + + logger.info(f"AUTORASTER LOOP: {face_on_max_coords=} {ortho_max_coords=}") + logger.info(f"AUTORASTER LOOP: {sample_detection=} {max_col=}") + _, y_f, z_f = face_on_max_coords + _, y_ort, z_ort = ortho_max_coords + y_center, z_center = sample_detection["center_y"], sample_detection["center_z"] - omega_face_on = sample_detection["face_on_omega"] - omega_ortho = (sample_detection["face_on_omega"] + 90) + omega_face_on = sample_detection["face_on_omega"] + omega_ortho = (sample_detection["face_on_omega"] + 90) - r_f = (y_f - y_center)*np.cos(np.deg2rad(omega_face_on)) + (z_f - z_center)*np.sin(np.deg2rad(omega_face_on)) - r_o = (y_ort - y_center)*np.cos(np.deg2rad(omega_ortho)) + (z_ort - z_center)*np.sin(np.deg2rad(omega_ortho)) + r_f = (y_f - y_center)*np.cos(np.deg2rad(omega_face_on)) + (z_f - z_center)*np.sin(np.deg2rad(omega_face_on)) + r_o = (y_ort - y_center)*np.cos(np.deg2rad(omega_ortho)) + (z_ort - z_center)*np.sin(np.deg2rad(omega_ortho)) - logger.info(f"AUTORASTER LOOP: {r_f=} {r_o=}") + logger.info(f"AUTORASTER LOOP: {r_f=} {r_o=}") - r = np.array([[r_f],[r_o]]) - A = np.matrix([[np.cos(np.deg2rad(omega_face_on)), np.sin(np.deg2rad(omega_face_on))],[np.cos(np.deg2rad(omega_ortho)), np.sin(np.deg2rad(omega_ortho))]]) - logger.info(f"AUTORASTER LOOP: {A=}") + r = np.array([[r_f],[r_o]]) + A = np.matrix([[np.cos(np.deg2rad(omega_face_on)), np.sin(np.deg2rad(omega_face_on))],[np.cos(np.deg2rad(omega_ortho)), np.sin(np.deg2rad(omega_ortho))]]) + logger.info(f"AUTORASTER LOOP: {A=}") - yz = np.linalg.inv(A)*r - delta_y, delta_z = yz[0,0], yz[1,0] - logger.info(f"AUTORASTER LOOP: {yz=}") + yz = np.linalg.inv(A)*r + delta_y, delta_z = yz[0,0], yz[1,0] + logger.info(f"AUTORASTER LOOP: {yz=}") - final_y = y_center + delta_y - final_z = z_center + delta_z + final_y = y_center + delta_y + final_z = z_center + delta_z - logger.info(f"AUTORASTER LOOP: {final_y=} {final_z=}") + logger.info(f"AUTORASTER LOOP: {final_y=} {final_z=}") - RE(bps.mv(gonio.py, final_y, gonio.pz, final_z)) + RE(bps.mv(gonio.py, final_y, gonio.pz, final_z)) autoRasterFlag = 0 return 1 From f24d989da94bd2d5ec7a560b9ae920ed901376ca Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 5 Apr 2024 21:33:28 -0400 Subject: [PATCH 29/33] Fixed snake array creation function --- utils/raster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/raster.py b/utils/raster.py index fe7033fd..97a6fd5f 100644 --- a/utils/raster.py +++ b/utils/raster.py @@ -49,13 +49,13 @@ def create_snake_array(flattened, raster_type, M, N): flattened result array, direction, and shape of the raster """ # Reshape the list to a 2D array - array_2d = np.array(flattened).reshape(M, N) - if raster_type == "horizontal": # Reverse every even row for horizontal snaking + array_2d = np.array(flattened).reshape(M, N) array_2d[1::2] = np.fliplr(array_2d[1::2]) elif raster_type == "vertical": # Reverse every even column for vertical snaking + array_2d = np.array(flattened).reshape(N, M) array_2d = array_2d.T array_2d[:, 1::2] = np.flipud(array_2d[:, 1::2]) From 818c091f92b0929a52b5d5a143e5b591944e2bc1 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 5 Apr 2024 22:42:12 -0400 Subject: [PATCH 30/33] run on mount option when nothing is mounted --- daq_lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daq_lib.py b/daq_lib.py index d29526dd..2743f383 100644 --- a/daq_lib.py +++ b/daq_lib.py @@ -295,6 +295,10 @@ def mountSample(sampID): mountStat = robot_lib.mountRobotSample(gov_robot, puckPos,pinPos,sampID,init=1) if (mountStat == 1): set_field("mounted_pin",sampID) + if getBlConfig('robot_online') and getBlConfig("queueCollect") == 0: + # Only run mount options when the robot is online and queue collect is off + daq_macros.run_on_mount_option(sampID) + gov_status = gov_lib.setGovRobot(gov_robot, 'SA') elif(mountStat == 2): return 2 else: From c4abd22dfab78354947b01722eea36c78529319a Mon Sep 17 00:00:00 2001 From: Shekar V Date: Fri, 12 Apr 2024 15:04:35 -0400 Subject: [PATCH 31/33] Split gotomaxRaster functions further --- daq_macros.py | 121 +++++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/daq_macros.py b/daq_macros.py index 8cdf396e..bb47ceb0 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -2359,6 +2359,9 @@ def runRasterScan(currentRequest,rasterType="", width=0, height=0, step_size=10, RE(snakeRaster(rasterReqID)) def get_score_vals(cellResults, scoreOption): + """ + Returns a numpy 1d-array that stores the selected scores as a flattened array + """ score_vals = np.zeros(len(cellResults)) for i, res in enumerate(cellResults): try: @@ -2367,7 +2370,12 @@ def get_score_vals(cellResults, scoreOption): logger.debug(f"Option {scoreOption} not found for {res=}") return score_vals -def get_score_index(score_vals, scoreOption, indices=None): +def get_score_index(score_vals: "np.ndarray", scoreOption, indices=None): + """ + Returns the maximum or minimum score value in the 1d array of scores and the corresponding index + If specific indices are provided the max/min score will be selected from those indices + + """ if indices: score_vals = score_vals[indices] if scoreOption == "d_min": @@ -2381,6 +2389,18 @@ def get_score_index(score_vals, scoreOption, indices=None): return scoreVal, max_index +def get_gonio_pos_from_raster_result(cell_results, raster_map, index): + """ + Returns the motor positions and the cell which corresponds to the index + given in the cell_results list + """ + hot_file = cell_results[index] + hot_coords = raster_map[hot_file] + x = hot_coords["x"] + y = hot_coords["y"] + z = hot_coords["z"] + logger.info("goto " + str(x) + " " + str(y) + " " + str(z)) + return hot_file, (x, y, z) def gotoMaxRaster(rasterResult,multiColThreshold=None,**kwargs): global autoVectorCoarseCoords,autoVectorFlag, max_col, face_on_max_coords, ortho_max_coords @@ -2389,10 +2409,8 @@ def gotoMaxRaster(rasterResult,multiColThreshold=None,**kwargs): if (rasterResult["result_obj"]["rasterCellResults"]['resultObj'] == None): logger.info("no raster result!!\n") raise ValueError("raster result object is None") - - hotFile = "" - scoreOption = "" logger.info("in gotomax") + cellResults = rasterResult["result_obj"]["rasterCellResults"]['resultObj'] rasterMap = rasterResult["result_obj"]["rasterCellMap"] rasterScoreFlag = int(db_lib.beamlineInfo(daq_utils.beamline,'rasterScoreFlag')["index"]) @@ -2419,57 +2437,7 @@ def gotoMaxRaster(rasterResult,multiColThreshold=None,**kwargs): if max_index: - hotFile = cellResults[max_index] - logger.info(f'raster score: {score_val=} at {hotFile=}') - hotCoords = rasterMap[hotFile] - x = hotCoords["x"] - y = hotCoords["y"] - z = hotCoords["z"] - logger.info("goto " + str(x) + " " + str(y) + " " + str(z)) - - if "rasterRequest" in kwargs and autoRasterFlag: - # Update the raster request with the location of the max raster image and co-ordinates. - # NOTE: In the optimized autoraster, two 2D rasters are collected, face on and orthogonal. - # Both rasters have the same number of columns, we save the column number of the hot cell (max_col) - # in the face on raster then look for the hot cell in the orthogonal raster in the same - # column. This is to emulate the original autoraster but leaves room for other ways of selecting - # points for standard collection - rasterDef = kwargs["rasterRequest"]["request_obj"]["rasterDef"] - - col = get_raster_max_col(rasterDef, max_index) - - - # Set the column index for the face on raster - if max_col is None: - max_col = col - face_on_max_coords = (x, y, z) - else: - # max_col should be available for orthogonal rasters - # Find maximum in col defined in max_col and then reset max_col - indices = get_flattened_indices_of_max_col(rasterDef, max_col) - score_val, index = get_score_index(score_vals, scoreOption, indices) - hotFile = cellResults[index]["cellMapKey"] - hotCoords = rasterMap[hotFile] - x = hotCoords["x"] - y = hotCoords["y"] - z = hotCoords["z"] - ortho_max_coords = (x, y, z) - max_col = None - - kwargs["rasterRequest"]["request_obj"]["max_raster"] = { - "file" : hotFile, - "coords": [x, y, z], - "index": max_index - } - if "omega" in kwargs: - kwargs["rasterRequest"]["request_obj"]["max_raster"]["omega"] = kwargs["omega"] - - db_lib.updateRequest(kwargs["rasterRequest"]) - - #Following lines for testing, remove for production! - req = db_lib.getRequestByID(kwargs["rasterRequest"]["uid"]) - logger.info(f'MAX RASTER INFO: {req["request_obj"]["max_raster"]["file"]} {req["request_obj"]["max_raster"]["coords"]}') - + x, y, z = run_auto_raster(max_index, score_vals, scoreOption, cellResults, rasterMap, **kwargs) if 'omega' in kwargs: beamline_lib.mvaDescriptor("sampleX",x, "sampleY",y, @@ -2483,6 +2451,49 @@ def gotoMaxRaster(rasterResult,multiColThreshold=None,**kwargs): else: raise ValueError("No max position found for gonio move") +def run_auto_raster(max_index, score_vals, scoreOption, cellResults, rasterMap, **kwargs): + global max_col, face_on_max_coords, ortho_max_coords + hotFile, (x, y, z) = get_gonio_pos_from_raster_result(cellResults, rasterMap, max_index) + if "rasterRequest" in kwargs and autoRasterFlag: + # Update the raster request with the location of the max raster image and co-ordinates. + # NOTE: In the optimized autoraster, two 2D rasters are collected, face on and orthogonal. + # Both rasters have the same number of columns, we save the column number of the hot cell (max_col) + # in the face on raster then look for the hot cell in the orthogonal raster in the same + # column. This is to emulate the original autoraster but leaves room for other ways of selecting + # points for standard collection + rasterDef = kwargs["rasterRequest"]["request_obj"]["rasterDef"] + + col = get_raster_max_col(rasterDef, max_index) + + + # Set the column index for the face on raster + if max_col is None: + max_col = col + face_on_max_coords = (x, y, z) + else: + # max_col should be available for orthogonal rasters + # Find maximum in col defined in max_col and then reset max_col + indices = get_flattened_indices_of_max_col(rasterDef, max_col) + score_val, max_index = get_score_index(score_vals, scoreOption, indices) + hotFile, (x, y, z) = get_gonio_pos_from_raster_result(cellResults, rasterMap, max_index) + ortho_max_coords = (x, y, z) + max_col = None + + kwargs["rasterRequest"]["request_obj"]["max_raster"] = { + "file" : hotFile, + "coords": [x, y, z], + "index": int(max_index) + } + if "omega" in kwargs: + kwargs["rasterRequest"]["request_obj"]["max_raster"]["omega"] = kwargs["omega"] + + db_lib.updateRequest(kwargs["rasterRequest"]) + + req = db_lib.getRequestByID(kwargs["rasterRequest"]["uid"]) + logger.info(f'MAX RASTER INFO: {req["request_obj"]["max_raster"]["file"]} {req["request_obj"]["max_raster"]["coords"]}') + return x, y, z + + def run_auto_vector(ceiling, cellResults, scoreOption, rasterMap): xminColumn = [] #these are the "line rasters" of the ends of threshold points determined by the first pass on the raster results xmaxColumn = [] From de6f6f83582ce83540c8fe21462b1f32022f2796 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Thu, 25 Apr 2024 12:13:07 -0400 Subject: [PATCH 32/33] bug fixes in gotomaxraster --- daq_macros.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/daq_macros.py b/daq_macros.py index bb47ceb0..ed1d3412 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -2387,6 +2387,8 @@ def get_score_index(score_vals: "np.ndarray", scoreOption, indices=None): scoreVal = np.max(score_vals) max_index = np.argmax(score_vals) + max_index = max_index if not indices else indices[max_index] + return scoreVal, max_index def get_gonio_pos_from_raster_result(cell_results, raster_map, index): @@ -2394,7 +2396,7 @@ def get_gonio_pos_from_raster_result(cell_results, raster_map, index): Returns the motor positions and the cell which corresponds to the index given in the cell_results list """ - hot_file = cell_results[index] + hot_file = cell_results[index]["cellMapKey"] hot_coords = raster_map[hot_file] x = hot_coords["x"] y = hot_coords["y"] @@ -2477,6 +2479,7 @@ def run_auto_raster(max_index, score_vals, scoreOption, cellResults, rasterMap, score_val, max_index = get_score_index(score_vals, scoreOption, indices) hotFile, (x, y, z) = get_gonio_pos_from_raster_result(cellResults, rasterMap, max_index) ortho_max_coords = (x, y, z) + logger.info(f"ORTHO DEBUG: {max_col=}\n {indices=}\n {max_index=}\n {ortho_max_coords=}") max_col = None kwargs["rasterRequest"]["request_obj"]["max_raster"] = { From 973088df62fabb72ba3969b1eb44207348c2a636 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Thu, 25 Apr 2024 12:40:26 -0400 Subject: [PATCH 33/33] Added prefixes based on beamline --- mxbluesky/devices/top_align.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/mxbluesky/devices/top_align.py b/mxbluesky/devices/top_align.py index b9d2ff63..c8c0e19d 100644 --- a/mxbluesky/devices/top_align.py +++ b/mxbluesky/devices/top_align.py @@ -17,6 +17,11 @@ from collections import OrderedDict import daq_utils +if daq_utils.beamline == 'amx': + device_prefix = "XF:17IDB-ES:AMX" +else: + device_prefix = "XF:17IDC-ES:FMX" + class TopPlanLimit(Enum): """Prevent large goniometer movements during topview plan. Rotating gonio with piezo stages extended can hit things like the collimator or beamstop. @@ -168,16 +173,16 @@ def __init__(self, message): class TopAlignerBase(Device): - topcam = Cpt(TopAlignCam, "XF:17IDB-ES:AMX{Cam:9}") - gonio_o = Cpt(EpicsMotor, "XF:17IDB-ES:AMX{Gon:1-Ax:O}Mtr", timeout=6) + topcam = Cpt(TopAlignCam, f"{device_prefix}{{Cam:9}}") + gonio_o = Cpt(EpicsMotor, f"{device_prefix}{{Gon:1-Ax:O}}Mtr", timeout=6) gonio_py = Cpt( - EpicsMotorSPMG, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Mtr", timeout=6 + EpicsMotorSPMG, f"{device_prefix}{{Gon:1-Ax:PY}}Mtr", timeout=6 ) gonio_pz = Cpt( - EpicsMotorSPMG, "XF:17IDB-ES:AMX{Gon:1-Ax:PZ}Mtr", timeout=6 + EpicsMotorSPMG, f"{device_prefix}{{Gon:1-Ax:PZ}}Mtr", timeout=6 ) - kill_py = Cpt(EpicsSignal, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Cmd:Kill-Cmd") - kill_pz = Cpt(EpicsSignal, "XF:17IDB-ES:AMX{Gon:1-Ax:PY}Cmd:Kill-Cmd") + kill_py = Cpt(EpicsSignal, f"{device_prefix}{{Gon:1-Ax:PY}}Cmd:Kill-Cmd") + kill_pz = Cpt(EpicsSignal, f"{device_prefix}{{Gon:1-Ax:PY}}Cmd:Kill-Cmd") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -205,7 +210,7 @@ def unstage(self, *args, **kwargs): class TopAlignerFast(TopAlignerBase): - zebra = Cpt(ZebraMXOr, "XF:17IDB-ES:AMX{Zeb:2}:") + zebra = Cpt(ZebraMXOr, f"{device_prefix}{{Zeb:2}}:") target_gov_state = Cpt( Signal, value=None, doc="target governor state used to trigger device" )