diff --git a/.gitignore b/.gitignore index db2b8df..7c31d29 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ pyiid/grad_test.txt old_files/ benchmarks/ extra/ -history.sqlite +history*.sqlite *.swp db/ diff --git a/startup/00-base.py b/startup/00-base.py index 755cf13..8eff41b 100644 --- a/startup/00-base.py +++ b/startup/00-base.py @@ -21,7 +21,7 @@ bec=True, magics=True, mpl=False, - # publish_documents_to_kafka=True + publish_documents_with_kafka=True ) from pathlib import Path diff --git a/startup/12-motors.py b/startup/12-motors.py index f05813c..bc45c5e 100644 --- a/startup/12-motors.py +++ b/startup/12-motors.py @@ -1,7 +1,8 @@ import ophyd from ophyd import (Device, Component as Cpt, EpicsSignal, EpicsSignalRO, EpicsMotor) -from nslsii.devices import TwoButtonShutter +from ophyd.device import DeviceStatus +from nslsii.devices import TwoButtonShutter as _TwoButtonShutter #import nslsii.devices Det_1_X = EpicsMotor('XF:28ID1B-ES{Det:1-Ax:X}Mtr', name='Det_1_X', labels=['positioners']) @@ -50,6 +51,33 @@ class FilterBank(Device): flt3 = Cpt(EpicsSignal, '3}Cmd:Opn-Cmd', string=True) flt4 = Cpt(EpicsSignal, '4}Cmd:Opn-Cmd', string=True) + +class TwoButtonShutter(_TwoButtonShutter): + def stop(self): + ... + def set(self, value): + if value == 0: + return super().set('Close') + #super().set('Close') + #status = DeviceStatus(self) + #return status + if value == 1: + return super().set('Open') + # super().set('Open') + # status = DeviceStatus(self) + # return status + + def read(self): #fix for whoever thought it was smart to use 'Not Open' instead of 'Close' - DO + ret = super().read() + val = ret['fb_two_button_shutters_flt1_status']['value'] + if val == 'Not Open': + ret['fb_two_button_shutters_flt1_status']['value'] = 'Close' + return ret + + # def read(self): + # ret = super().read() + # # FIX RET + # return ret class FilterBankTwoButtonShutter(Device): flt1 = Cpt(TwoButtonShutter, '1}') flt2 = Cpt(TwoButtonShutter, '2}') @@ -59,6 +87,10 @@ class FilterBankTwoButtonShutter(Device): fb = FilterBank('XF:28ID1B-OP{Fltr:', name='fb') fb_two_button_shutters = FilterBankTwoButtonShutter('XF:28ID1B-OP{Fltr:', name='fb_two_button_shutters') +#trying to make a temporary shutter - DO - 5/18/2022 +#fs = fb_two_button_shutters.flt4 +#if disable this, need to re-enable fs in 15-optics: line 105 + # Spinner Goniohead motors, add by HZ Spinnergo_X = EpicsMotor('XF:28ID1B-ES{Stg:Smpl-Ax:X}Mtr', name='Spinnergo_X', labels=['positioners']) Spinnergo_Y = EpicsMotor('XF:28ID1B-ES{Stg:Smpl-Ax:Y}Mtr', name='Spinnergo_Y', labels=['positioners']) @@ -78,7 +110,15 @@ class FilterBankTwoButtonShutter(Device): #45-degree shifting motor on M6-grid, for use with hot air blower / cryostream with angled sample bracket broadside45_shifter = EpicsMotor('XF:28ID1B-ES{Smpl:Array-Ax:Horiz}Mtr', name='broadside45_shifter') +Multi_X = EpicsMotor('XF:28ID1B-ES{Smpl:Array-Ax:Horiz}Mtr', name='Multi_X') #NOx BOx x/y sample position noxbox_x = EpicsMotor('XF:28ID1B-ES{NOx-Ax:X}Mtr', name='noxbox_x') noxbox_y = EpicsMotor('XF:28ID1B-ES{NOx-Ax:Y}Mtr', name='noxbox_y') + + +#Table X-tages +OT_stage_1_X = EpicsMotor('XF:28ID1-ES{Det-Ax:X1}Mtr', name='OT_stage_1_X', labels=['positioners']) +OT_stage_2_X = EpicsMotor('XF:28ID1-ES{Det-Ax:X2}Mtr', name='OT_stage_2_X', labels=['positioners']) +OT_stage_3_X = EpicsMotor('XF:28ID1-ES{Det-Ax:X3}Mtr', name='OT_stage_3_X', labels=['positioners']) +OT_stage_4_X = EpicsMotor('XF:28ID1-ES{Det-Ax:X4}Mtr', name='OT_stage_4_X', labels=['positioners']) diff --git a/startup/15-optics.py b/startup/15-optics.py index c3998e9..826d0f0 100644 --- a/startup/15-optics.py +++ b/startup/15-optics.py @@ -34,27 +34,25 @@ class SideBounceMono(Device): twist = Cpt(EpicsMotor, "Twist}Mtr") sbm = SideBounceMono("XF:28ID1A-OP{Mono:SBM-Ax:", name='sbm') -# Shutters: -#fs = EpicsSignal('XF:28ID1B-OP{PSh:1-Det:2}Cmd', name='fs') # fast shutter #temporary fast shutter -# class tempFSShutter: +#class tempFSShutter: # -# def set(self, value): -# if value == 0: -# return fb_two_button_shutters.flt4.set('Close') -# elif value == 1: -# return fb_two_button_shutters.flt4.set('Open') +# def set(self, value): +# if value == 0: +# return fb_two_button_shutters.flt1.set('Close') +# elif value == 1: +# return fb_two_button_shutters.flt1.set('Open') # -# def read(self): -# return fb_two_button_shutters.read() +# def read(self): +# return fb_two_button_shutters.read() # -# def describe(self): -# return fb_two_button_shutters.describe() +# def describe(self): +# return fb_two_button_shutters.describe() # -# def stop(self, success=False): -# return self.set('close') - -# fs = tempFSShutter() +# def stop(self, success=False): +# return self.set('close') +# +#fs = tempFSShutter() # Close the shutter on stop # fs.stop = lambda *args, **kwargs: fs.set(0) @@ -75,20 +73,20 @@ def __init__(self, *args, **kwargs): def set(self, val): # NOTE: temporary workaround until the fast shutter works. # - # def check_if_done(value, old_value, **kwargs): - # if ((val in ['Open', 1] and value == 0) or - # (val in ['Close', 0] and value == 1)): - # if self.st is not None: - # self.st._finished() - # self.st = None - # return True - # return False + def check_if_done(value, old_value, **kwargs): + if ((val in ['Open', 1] and value == 0) or + (val in ['Close', 0] and value == 1)): + if self.st is not None: + self.st._finished() + self.st = None + return True + return False self.cmd.set(self.setmap[val]) - # status = SubscriptionStatus(self.status, check_if_done,settle_time=self.settle_time.get()) - # return status + status = SubscriptionStatus(self.status, check_if_done,settle_time=self.settle_time.get()) + return status - ttime.sleep(1.0) # wait to set the value since the status PV does not capture the actual status - return NullStatus() + #ttime.sleep(1.0) # wait to set the value since the status PV does not capture the actual status + #return NullStatus() def get(self): return self.readmap[self.cmd.get()] @@ -101,9 +99,9 @@ def read(self): # def stop(self, success=False): # return self.set('Close') - +#temporary disable fast shutter while broken - DO 5/18/2022 fs = PDFFastShutter('XF:28ID1B-OP{PSh:1-Det:2}', name='fs') - +#if enable this, need to disable fs in 12-motors: line 80 class Mirror(Device): y_upstream = Cpt(EpicsMotor, 'YU}Mtr') diff --git a/startup/80-areadetector.py b/startup/80-areadetector.bak similarity index 100% rename from startup/80-areadetector.py rename to startup/80-areadetector.bak diff --git a/startup/80-areadetector2.py b/startup/80-areadetector2.py new file mode 100644 index 0000000..32f3526 --- /dev/null +++ b/startup/80-areadetector2.py @@ -0,0 +1,461 @@ +import time as ttime +from ophyd.areadetector import (PerkinElmerDetector, ImagePlugin, + TIFFPlugin, HDF5Plugin, + ProcessPlugin, ROIPlugin) +from ophyd.device import BlueskyInterface +from ophyd.areadetector.trigger_mixins import SingleTrigger, MultiTrigger +from ophyd.areadetector.filestore_mixins import (FileStoreIterativeWrite, + FileStoreHDF5IterativeWrite, + FileStoreTIFFSquashing, + FileStoreTIFF) +from ophyd import Signal, EpicsSignal, EpicsSignalRO # Tim test +from ophyd import Component as C, Device, DeviceStatus +from ophyd import StatusBase + +from nslsii.ad33 import StatsPluginV33 + +# from shutter import sh1 + +#shctl1 = EpicsSignal('XF:28IDC-ES:1{Det:PE1}cam1:ShutterMode', name='shctl1') +#shctl1 = EpicsMotor('XF:28IDC-ES:1{Sh2:Exp-Ax:5}Mtr', name='shctl1') + +# monkey patch for trailing slash problem +def _ensure_trailing_slash(path, path_semantics=None): + """ + 'a/b/c' -> 'a/b/c/' + EPICS adds the trailing slash itself if we do not, so in order for the + setpoint filepath to match the readback filepath, we need to add the + trailing slash ourselves. + """ + newpath = os.path.join(path, '') + if newpath[0] != '/' and newpath[-1] == '/': + # make it a windows slash + newpath = newpath[:-1] + return newpath + +ophyd.areadetector.filestore_mixins._ensure_trailing_slash = _ensure_trailing_slash + + +class PDFShutter(Device): + cmd = C(EpicsSignal, 'Cmd-Cmd') + close_sts = C(EpicsSignalRO, 'Sw:Cls1-Sts') + open_sts = C(EpicsSignalRO, 'Sw:Opn1-Sts') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._st = None + self._target = None + self.close_sts.subscribe(self._watcher_close, + self.close_sts.SUB_VALUE) + + self.open_sts.subscribe(self._watcher_open, + self.open_sts.SUB_VALUE) + + def set(self, value, *, wait=False, **kwargs): + if value not in ('Open', 'Close'): + raise ValueError( + "must be 'Open' or 'Close', not {!r}".format(value)) + if wait: + raise RuntimeError() + if self._st is not None: + raise RuntimeError() + self._target = value + self._st = st = DeviceStatus(self, timeout=None) + self.cmd.put(value) + + return st + + def _watcher_open(self, *, old_value=None, value=None, **kwargs): + print("in open watcher", old_value, value) + if self._target != 'Open': + return + if self._st is None: + return + + if new_value: + self._st._finished() + self._target = None + self._st = None + print("in open watcher") + + def _watcher_close(self, *, old_value=None, value=None, **kwargs): + print("in close watcher", old_value, value) + if self._target != 'Close': + return + + if self._st is None: + return + + if new_value: + self._st._finished() + self._target = None + self._st = None + + pass + + +class SavedImageSignal(Signal): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.stashed_datakey = {} + + def describe(self): + ret = super().describe() + ret[self.name].update(self.stashed_datakey) + return ret + + +def take_dark(cam, light_field, dark_field_name): + # close shutter + + # take the dark frame + cam.stage() + st = cam.trigger() + while not st.done: + ttime.sleep(.1) + ret = cam.read() + desc = cam.describe() + cam.unstage() + + # save the df uid + df = ret[light_field] + df_sig = getattr(cam, dark_field_name) + df_sig.put(**df) + # save the darkfrom description + df_sig.stashed_datakey = desc[light_field] + + +class XPDFileStoreTIFFSquashing(FileStoreTIFFSquashing): + def describe(self): + description = super().describe() + key = self.parent._image_name # should be the same as f"{self.parent.name}_image" + shape = list(description[key]["shape"]) + shape[0] = self.get_frames_per_point() + shape = tuple(shape) + description[key]["shape"] = shape + + cam_dtype = self.data_type.get(as_string=True) + type_map = { + "UInt8": "|u1", + "UInt16": ">>>>>> MNT: use new 94-load.py + +Initialize the XpdAcq objects for xpdacq >= 1.1.0. +This file will do the following changes to the name space: + + (1) create objects of `xrun`, `glbl` etc. using the UserInterface class in XpdAcq. + (2) import helper functions for users from XpdAcq + (3) change the home directory to `glbl['home']` or `glbl['base']` + (4) disable the logging of pyFAI + +XpdAcq < 1.1.0 uses the 94-load.bak file. +""" +import xpdacq + +xpdacq_version = tuple(map(int, xpdacq.__version__.split("."))) + +if xpdacq_version < (1, 1, 0): + import os + from xpdacq.xpdacq_conf import (glbl_dict, configure_device, _reload_glbl, _set_glbl, _load_beamline_config) -# configure experiment device being used in current version -if glbl_dict['is_simulation']: - from xpdacq.simulation import (xpd_pe1c, db, cs700, shctl1, - ring_current, fb) - pe1c = xpd_pe1c # alias - -configure_device(area_det=pe1c, shutter=fs, - temp_controller=eurotherm, #changed from None to eurotherm on 3/22/19 - DPO - db=db, - filter_bank=fb, - ring_current=ring_current) - -# cache previous glbl state -reload_glbl_dict = _reload_glbl() -from xpdacq.glbl import glbl - -# reload beamtime -from xpdacq.beamtimeSetup import (start_xpdacq, _start_beamtime, - _end_beamtime) - -bt = start_xpdacq() -if bt is not None: - print("INFO: Reload beamtime objects:\n{}\n".format(bt)) -if reload_glbl_dict is not None: - _set_glbl(glbl, reload_glbl_dict) - -# import necessary modules -from xpdacq.xpdacq import * -from xpdacq.beamtime import * -from xpdacq.utils import import_sample_info - -# instantiate xrun without beamtime, like bluesky setup -xrun = CustomizedRunEngine(None) -xrun.md['beamline_id'] = glbl['beamline_id'] -xrun.md['group'] = glbl['group'] -xrun.md['facility'] = glbl['facility'] -beamline_config = _load_beamline_config(glbl['blconfig_path']) -xrun.md['beamline_config'] = beamline_config - -# insert header to db, either simulated or real -xrun.subscribe(db.insert, 'all') - -if bt: - xrun.beamtime = bt - -HOME_DIR = glbl['home'] -BASE_DIR = glbl['base'] - -print('INFO: Initializing the XPD data acquisition environment\n') -if os.path.isdir(HOME_DIR): - os.chdir(HOME_DIR) + + # configure experiment device being used in current version + if glbl_dict['is_simulation']: + from xpdacq.simulation import (xpd_pe1c, db, cs700, shctl1, + ring_current, fb) + pe1c = xpd_pe1c # alias + + configure_device(area_det=pe1c, shutter=fs, + temp_controller=eurotherm, #changed from None to eurotherm on 3/22/19 - DPO + db=db, + filter_bank=fb, + ring_current=ring_current) + + # cache previous glbl state + reload_glbl_dict = _reload_glbl() + from xpdacq.glbl import glbl + + # reload beamtime + from xpdacq.beamtimeSetup import (start_xpdacq, _start_beamtime, + _end_beamtime) + + bt = start_xpdacq() + if bt is not None: + print("INFO: Reload beamtime objects:\n{}\n".format(bt)) + if reload_glbl_dict is not None: + _set_glbl(glbl, reload_glbl_dict) + + # import necessary modules + from xpdacq.xpdacq import * + from xpdacq.beamtime import * + from xpdacq.utils import import_sample_info + + # instantiate xrun without beamtime, like bluesky setup + xrun = CustomizedRunEngine(None) + xrun.md['beamline_id'] = glbl['beamline_id'] + xrun.md['group'] = glbl['group'] + xrun.md['facility'] = glbl['facility'] + beamline_config = _load_beamline_config(glbl['blconfig_path']) + xrun.md['beamline_config'] = beamline_config + + # insert header to db, either simulated or real + xrun.subscribe(db.insert, 'all') + + if bt: + xrun.beamtime = bt + + HOME_DIR = glbl['home'] + BASE_DIR = glbl['base'] + + print('INFO: Initializing the XPD data acquisition environment\n') + if os.path.isdir(HOME_DIR): + os.chdir(HOME_DIR) + else: + os.chdir(BASE_DIR) + + # See https://github.com/silx-kit/pyFAI/issues/1399#issuecomment-694185304 + import logging + logging.getLogger().addHandler(logging.NullHandler()) + + from xpdacq.calib import * + + # analysis functions, only at beamline + #from xpdan.data_reduction import * + + print('OK, ready to go. To continue, follow the steps in the xpdAcq') + print('documentation at http://xpdacq.github.io/xpdacq\n') + else: - os.chdir(BASE_DIR) + import os -from xpdacq.calib import * + # Disable interactive logging of pyFAI + # See https://github.com/silx-kit/pyFAI/issues/1399#issuecomment-694185304 + os.environ["PYFAI_NO_LOGGING"] = "1" -# We are adding this here because the previous -# line overwrites our logger config. This undoes the logger changes. -import logging -logging.getLogger().handlers.clear() + from xpdacq.utils import import_userScriptsEtc, import_sample_info + from xpdacq.beamtimeSetup import _start_beamtime, _end_beamtime + from xpdacq.beamtime import ScanPlan, Sample, ct, Tramp, Tlist, tseries + from xpdacq.ipysetup import UserInterface + # Do all setup in the constructor of UserInterface + # HOME directory will be changed to the one in glbl + ui = UserInterface( + area_dets=[pe1c, pe2c, pilatus1], + det_zs=[Det_1_Z.user_setpoint, Det_2_Z.user_setpoint], + shutter=fs, + temp_controller=eurotherm, + filter_bank=fb, + ring_current=ring_current, + db=db + ) + xrun = ui.xrun + glbl = ui.glbl + xpd_configuration = ui.xpd_configuration + run_calibration = ui.run_calibration + bt = ui.bt -# analysis functions, only at beamline -#from xpdan.data_reduction import * + # Remove the variables that won't be used + del UserInterface, ui -print('OK, ready to go. To continue, follow the steps in the xpdAcq') -print('documentation at http://xpdacq.github.io/xpdacq\n') +# remove the uselss names +del xpdacq_version diff --git a/startup/96-dan_functions.py b/startup/96-dan_functions.py index 99ecfd9..fcfca58 100644 --- a/startup/96-dan_functions.py +++ b/startup/96-dan_functions.py @@ -76,7 +76,7 @@ def show_me_db( dark_subtract=True, return_im=False, return_dark=False, - new_db=False, + new_db=True, suffix="_image", ): my_det_probably = db[my_id].start["detectors"][0] + suffix @@ -582,7 +582,8 @@ def get_total_counts(): def _motor_move_scan_shifter_pos(motor, xmin, xmax, numx): from epics import caget - #ensure shutter is closed + #ensure shutter is closedi + print ('closing shutter') RE(mv(fs,"Close")) I_list = np.zeros(numx) dx = (xmax - xmin) / numx @@ -809,3 +810,60 @@ def phase_parser(phase_str): del pe1c.tiff.stage_sigs[pe1c.proc.reset_filter] + +#for looking at data from Pilatus detector + +def set_Pilatus_parameters(num_images=1, exposure_time=0.1): + print ('setting number of images per collection to '+str(num_images)) + pilatus1.set_num_images(num_images) + print ('setting exposure time for a single image to '+str(exposure_time)) + pilatus1.set_exposure_time(exposure_time) + + +def show_me2(my_im, count_low=0, count_high=1, use_colorbar=False, use_cmap='viridis'): + #my_low = np.percentile(my_im, per_low) + #my_high = np.percentile(my_im, per_high) + plt.imshow(my_im, vmin=count_low, vmax=count_high, cmap= use_cmap) + if use_colorbar: + plt.colorbar() + +def show_me_db2( + my_id, + count_low=1, + count_high=99, + use_colorbar=False, + dark_subtract=False, + return_im=False, + return_dark=False, + new_db = True, + use_cmap='viridis', + suffix="_image", +): + my_det_probably = db[my_id].start["detectors"][0] + suffix + if new_db: + my_im = (db[my_id].table(fill=True)[my_det_probably][1][0]).astype(float) + else: + my_im = (db[my_id].table(fill=True)[my_det_probably][1]).astype(float) + + if len(my_im) == 0: + print("issue... passing") + pass + if dark_subtract: + if "sc_dk_field_uid" in db[my_id].start.keys(): + my_dark_id = db[my_id].start["sc_dk_field_uid"] + if new_db: + dark_im = (db[my_dark_id].table(fill=True)[my_det_probably][1][0]).astype(float) + else: + dark_im = (db[my_dark_id].table(fill=True)[my_det_probably][1]).astype(float) + + my_im = my_im - dark_im + else: + print("this run has no associated dark") + if return_im: + return my_im + if return_dark: + return dark_im + + #if all else fails, plot! + show_me2(my_im, count_low=count_low, count_high=count_high, use_colorbar=use_colorbar, use_cmap=use_cmap) + diff --git a/startup/97-MA_functions.py b/startup/97-MA_functions.py index 21f30ba..03a0987 100644 --- a/startup/97-MA_functions.py +++ b/startup/97-MA_functions.py @@ -251,9 +251,9 @@ def lastimage(n): I = light_img - dk_img imshow(I, vmax = (I.sum()/(2048*2048)), cmap = 'jet' ) - imsave("/nsls2/xf28id1/xpdacq_data/user_data/tiff_base/" + "dark_sub_image" + ".tiff", light_img - dk_img) - imsave("/nsls2/xf28id1/xpdacq_data/user_data/tiff_base/" + "dark_image" + ".tiff", dk_img) - imsave("/nsls2/xf28id1/xpdacq_data/user_data/tiff_base/" + "light_image" + ".tiff", light_img) + imsave("/nsls2/data/pdf/legacy/processed/xpdacq_data/MA_01_27_2023/" + "dark_sub_image" + ".tiff", light_img - dk_img) + imsave("/nsls2/data/pdf/legacy/processed/xpdacq_data/MA_01_27_2023/" + "dark_image" + ".tiff", dk_img) + imsave("/nsls2/data/pdf/legacy/processed/xpdacq_data/MA_01_27_2023/" + "light_image" + ".tiff", light_img) #---------------------------------HAB T setpoint threshold-------------------------------------------- @@ -316,4 +316,4 @@ def HAB_Tset(t, threshold, settle_time): T_now = caget("Readback PV") time.sleep(0.5) time.sleep(settle_time) -''' \ No newline at end of file +''' diff --git a/startup/98-jog_scans.py b/startup/98-jog_scans.py index c6f8ff9..6622b14 100644 --- a/startup/98-jog_scans.py +++ b/startup/98-jog_scans.py @@ -157,6 +157,7 @@ def per_shot(dets): def jog(exposure_s, motor, start, stop): """ pass total exposure time (in seconds), motor name (i.e. Grid_Y), start and stop positions for the motor.""" + #yield from rocking_ct([pilatus], exposure_s, motor, start, stop) yield from rocking_ct([pe1c], exposure_s, motor, start, stop) diff --git a/startup/999-two-detector.py b/startup/999-two-detector.py new file mode 100644 index 0000000..1fb4976 --- /dev/null +++ b/startup/999-two-detector.py @@ -0,0 +1,172 @@ +import typing as T +from dataclasses import dataclass +from functools import partial +import itertools as its + +import bluesky.plan_stubs as bps +import bluesky.plans as bp +import bluesky.preprocessors as bpp +import numpy as np +from bluesky.utils import Msg +from ophyd import Device, Signal + +Plan = T.Generator[Msg, T.Any, T.Any] +Motor = T.Union[Device, Signal] +Number = T.Union[float, int] +Detector = Device +OtherDetectors = T.List[Detector] +Metadata = T.Optional[T.Dict[str, T.Any]] + + +def _take_reading( + dets: OtherDetectors, + static: Detector, + moving: Detector, + motor: Motor, + pos_in: Number, + pos_out: Number, +) -> Plan: + return bpp.pchain( + bps.mv(motor, pos_out), + bp.count(dets + [static, motor]), + bps.mv(motor, pos_in), + bp.count(dets + [moving, motor]), + ) + + +@dataclass +class TwoDetectors: + """Helper to compose two-detector plans. + + Attributes + ---------- + static: Detector + Detector not moving. + moving: Detector + Detector moving in and out of beam. + motor: Motor + Axis motor of the moving detector stage. + pos_in: Number + Position that moving detector is in the beam. + pos_out: Number + Position that moving detector is out of the beam. + """ + + static: Detector + moving: Detector + motor: Motor + pos_in: Number + pos_out: Number + + def take_reading(self, dets: OtherDetectors) -> Plan: + """Take a reading of two detectors. + + It contains two bluesky runs. First, count the static dectector after the moving detector moves out. Second, count the moving detector after the detector moves in. It will count `dets` together with the moving or static detector and the moving motor. Please do not add moving, static detector or the moving motor in the `dets`. + + Parameters + ---------- + dets : OtherDetectors + Other detector to take a reading. + """ + return _take_reading( + dets, self.static, self.moving, self.motor, self.pos_in, self.pos_out + ) + + def _one_shot(self, detectors: OtherDetectors) -> Plan: + return bps.one_shot(detectors, take_reading=self.take_reading) + + def _one_nd_scan( + self, + detectors: OtherDetectors, + step: T.Dict[Motor, Number], + pos_cahce: T.Dict[Motor, T.Optional[Number]], + ) -> Plan: + return bps.one_nd_step( + detectors, step, pos_cahce, take_reading=self.take_reading + ) + + def _outer_scan( + self, + detectors: OtherDetectors, + motors: T.List[Motor], + lists: T.List[T.Sequence[Number]], + ) -> Plan: + m = len(motors) + pos_cache = dict(zip(motors, [None] * m)) + for positions in its.product(*lists): + step = dict(zip(motors, positions)) + yield from self._one_nd_scan(detectors, step, pos_cache) + return + + def count( + self, detectors: OtherDetectors, num: int = 1, delay: Number = None + ) -> Plan: + """Take one or more readings from detectors. + + Parameters + ---------- + detectors : OtherDetectors + list of 'readable' objects + num : integer, optional + number of readings to take; default is 1 + + If None, capture data until canceled + delay : iterable or scalar, optional + Time delay in seconds between successive readings; default is 0. + + Notes + ----- + If ``delay`` is an iterable, it must have at least ``num - 1`` entries or + the plan will raise a ``ValueError`` during iteration. + """ + return bps.repeat(partial(self._one_shot, detectors), num, delay) + + def list_grid_scan(self, detectors: OtherDetectors, *args) -> Plan: + """Scan over a mesh; each motor is on an independent trajectory. + + Parameters + ---------- + detectors: OtherDetectors + list of 'readable' objects + args: list + patterned like (``motor1, position_list1,`` + ``motor2, position_list2,`` + ``motor3, position_list3,`` + ``...,`` + ``motorN, position_listN``) + + The first motor is the "slowest", the outer loop. ``position_list``'s + are lists of positions, all lists must have the same length. Motors + can be any 'settable' object (motor, temp controller, etc.). + """ + motors = args[0::2] + positions = args[1::2] + return self._outer_scan(detectors, motors, positions) + + def grid_scan(self, detectors: OtherDetectors, *args) -> Plan: + """Scan over a mesh; each motor is on an independent trajectory. + + Parameters + ---------- + detectors: OtherDetectors + list of 'readable' objects + ``*args`` + patterned like (``motor1, start1, stop1, num1,`` + ``motor2, start2, stop2, num2,`` + ``motor3, start3, stop3, num3,`` ... + ``motorN, startN, stopN, numN``) + + The first motor is the "slowest", the outer loop. For all motors + except the first motor, there is a "snake" argument: a boolean + indicating whether to following snake-like, winding trajectory or a + simple left-to-right trajectory. + """ + motors = args[0::4] + starts = args[1::4] + stops = args[2::4] + nums = args[3::4] + positions = [ + np.linspace(start, stop, num) + for start, stop, num in zip(starts, stops, nums) + ] + return self._outer_scan(detectors, motors, positions)