diff --git a/pxiii_bec/bec_widgets/auto_updates.py b/pxiii_bec/bec_widgets/auto_updates.py index 573bcd8..543fde8 100644 --- a/pxiii_bec/bec_widgets/auto_updates.py +++ b/pxiii_bec/bec_widgets/auto_updates.py @@ -1,7 +1,16 @@ from bec_widgets.cli.auto_updates import AutoUpdates, ScanInfo +from bec_widgets.cli.rpc.rpc_base import RPCResponseTimeoutError class PlotUpdate(AutoUpdates): + create_default_dock = True + enabled = True + _scan_msg = None + + def do_update(self, msg): + """Save the original scan message for future use""" + self._scan_msg = msg + return super().do_update(msg) # def simple_line_scan(self, info: ScanInfo) -> None: # """ @@ -15,7 +24,42 @@ class PlotUpdate(AutoUpdates): # plt = self.figure.plot(dev_x, dev_y) # plt.set(title=f"PXIII: Scan {info.scan_number}", x_label=dev_x, y_label=dev_y) + def keyword_handler(self, info: ScanInfo) -> None: + """Simple keyword handler + + This simple keyword handler looks for the keyword 'datasource' in the scan arguments. + This allows the user to explictly specify the desired data source. Useful for alignment + scans. + """ + dev_x = info.scan_report_devices[0] + if "kwargs" in self._scan_msg.info: + dev_y = self._scan_msg.info["kwargs"].get("datasource", None) + else: + dev_y = None + if not dev_y: + return + + plt1 = self.get_default_figure() + try: + # This will throw RPCResponseTimeoutError + plt1.clear_all() + except RPCResponseTimeoutError: + pass + try: + # TODO: What about 2D scans? + # This will throw RPCResponseTimeoutError + plt1.plot(x_name=dev_x, y_name=dev_y) + except RPCResponseTimeoutError: + pass + try: + # This will throw RPCResponseTimeoutError + plt1.set(title=f"PXIII: Scan {info.scan_number}", x_label=dev_x, y_label=dev_y) + except RPCResponseTimeoutError: + pass + # plt1.add_dap(dev_x, dev_y, dap="LinearModel") + def handler(self, info: ScanInfo) -> None: + """Dock configuration handler""" # EXAMPLES: # if info.scan_name == "line_scan" and info.scan_report_devices: # self.simple_line_scan(info) @@ -24,3 +68,4 @@ class PlotUpdate(AutoUpdates): # self.run_grid_scan_update(info) # return super().handler(info) + self.keyword_handler(info) diff --git a/pxiii_bec/device_configs/x06da_device_config.yaml b/pxiii_bec/device_configs/x06da_device_config.yaml index d37444b..818b7d6 100644 --- a/pxiii_bec/device_configs/x06da_device_config.yaml +++ b/pxiii_bec/device_configs/x06da_device_config.yaml @@ -1,3 +1,40 @@ +sldi_cenx: + description: FE slit-diaphragm horizontal center + deviceClass: ophyd.EpicsMotor + deviceConfig: {prefix: 'X06DA-FE-SLDI:CENX'} + onFailure: buffer + enabled: true + readoutPriority: monitored + readOnly: false + softwareTrigger: false +sldi_sizex: + description: FE slit-diaphragm horizontal size + deviceClass: ophyd.EpicsMotor + deviceConfig: {prefix: 'X06DA-FE-SLDI:GAPX'} + onFailure: buffer + enabled: true + readoutPriority: monitored + readOnly: false + softwareTrigger: false +sldi_ceny: + description: FE slit-diaphragm vertical center + deviceClass: ophyd.EpicsMotor + deviceConfig: {prefix: 'X06DA-FE-SLDI:CENY'} + onFailure: buffer + enabled: true + readoutPriority: monitored + readOnly: false + softwareTrigger: false +sldi_sizey: + description: FE slit-diaphragm vertical size + deviceClass: ophyd.EpicsMotor + deviceConfig: {prefix: 'X06DA-FE-SLDI:GAPY'} + onFailure: buffer + enabled: true + readoutPriority: monitored + readOnly: false + softwareTrigger: false + slh_trxr: description: OP slit inner blade motion deviceClass: ophyd.EpicsMotor @@ -25,7 +62,7 @@ fi1_try: readoutPriority: monitored readOnly: false softwareTrigger: false -dccm_pitch1: +dccm_theta1: description: Monochromator pitch 1 deviceClass: ophyd.EpicsMotor deviceConfig: {prefix: 'X06DA-OP-DCCM:PITCH1'} @@ -34,15 +71,6 @@ dccm_pitch1: readoutPriority: monitored readOnly: false softwareTrigger: false -dccm_energy1: - description: Monochromator energy 1 - deviceClass: ophyd.EpicsMotor - deviceConfig: {prefix: 'X06DA-OP-DCCM:ENERGY1'} - onFailure: buffer - enabled: true - readoutPriority: monitored - readOnly: false - softwareTrigger: false dccm_diode: description: Diode between mono crystals deviceClass: ophyd.EpicsSignalRO @@ -52,7 +80,7 @@ dccm_diode: readoutPriority: monitored readOnly: true softwareTrigger: false -dccm_pitch2: +dccm_theta2: description: Monochromator pitch 2 deviceClass: ophyd.EpicsMotor deviceConfig: {prefix: 'X06DA-OP-DCCM:PITCH2'} @@ -61,15 +89,6 @@ dccm_pitch2: readoutPriority: monitored readOnly: false softwareTrigger: false -dccm_energy2: - description: Monochromator energy 2 - deviceClass: ophyd.EpicsMotor - deviceConfig: {prefix: 'X06DA-OP-DCCM:ENERGY2'} - onFailure: buffer - enabled: true - readoutPriority: monitored - readOnly: false - softwareTrigger: false dccm_xbpm: description: XBPM total intensity after monochromator deviceClass: ophyd.EpicsSignalRO @@ -80,18 +99,18 @@ dccm_xbpm: readOnly: true softwareTrigger: false dccm_energy: - description: Monochromator energy using ECMC virtual motors + description: Monochromator energy deviceClass: ophyd.EpicsMotor - deviceConfig: {prefix: 'X06DA-OP-DCCM:_ENERGY'} + deviceConfig: {prefix: 'X06DA-OP-DCCM:ENERGY'} onFailure: buffer enabled: true readoutPriority: monitored readOnly: false softwareTrigger: false -dccm_offset: - description: Monochromator energy offset for ECMC virtual motors +dccm_eoffset: + description: Monochromator energy offset deviceClass: ophyd.EpicsMotor - deviceConfig: {prefix: 'X06DA-OP-DCCM:_OFFSET'} + deviceConfig: {prefix: 'X06DA-OP-DCCM:EOFFSET'} onFailure: buffer enabled: true readoutPriority: monitored @@ -441,17 +460,17 @@ samstream: readoutPriority: async readOnly: false softwareTrigger: false -samimg: - description: Sample camera image from EPICS - deviceClass: pxiii_bec.devices.NDArrayPreview - deviceConfig: - prefix: 'X06DA-SAMCAM:image1:' - deviceTags: - - detector - enabled: true - readoutPriority: async - readOnly: false - softwareTrigger: false +# samimg: +# description: Sample camera image from EPICS +# deviceClass: pxiii_bec.devices.NDArrayPreview +# deviceConfig: +# prefix: 'X06DA-SAMCAM:image1:' +# deviceTags: +# - detector +# enabled: true +# readoutPriority: async +# readOnly: false +# softwareTrigger: false bstop_pneum: @@ -492,11 +511,11 @@ bstop_z: softwareTrigger: false bstop_pneum: description: Beamstop pneumatic - deviceClass: ophyd.EpicsSignal - deviceConfig: {read_pv: 'X06DA-ES-BS:GET-POS', write_pv: 'X06DA-ES-BS:SET-POS', put_complete: true} + deviceClass: pxiii_bec.devices.PneumaticValve + deviceConfig: {read_pv: 'X06DA-ES-BS:GET-POS', write_pv: 'X06DA-ES-BS:SET-POS', kind: 'config', auto_monitor: true, put_complete: true} onFailure: buffer enabled: true - readoutPriority: monitored + readoutPriority: baseline readOnly: false softwareTrigger: false bstop_diode: @@ -511,7 +530,7 @@ bstop_diode: frontlight: description: Microscope frontlight deviceClass: ophyd.EpicsSignal - deviceConfig: {read_pv: 'X06DA-ES-FL:SET-BRGHT', put_complete: true} + deviceConfig: {read_pv: 'X06DA-ES-FL:SET-BRGHT', kind: 'config', put_complete: true} onFailure: buffer enabled: true readoutPriority: monitored @@ -519,11 +538,11 @@ frontlight: softwareTrigger: false backlight: description: Backlight reflector - deviceClass: ophyd.EpicsSignal - deviceConfig: {read_pv: 'X06DA-ES-BL:GET-POS', write_pv: 'X06DA-ES-BL:SET-POS', put_complete: true} + deviceClass: pxiii_bec.devices.PneumaticValve + deviceConfig: {read_pv: 'X06DA-ES-BL:GET-POS', write_pv: 'X06DA-ES-BL:SET-POS', kind: 'config', auto_monitor: true, put_complete: true} onFailure: buffer enabled: true - readoutPriority: monitored + readoutPriority: baseline readOnly: false softwareTrigger: false det_y: @@ -595,7 +614,7 @@ abr: softwareTrigger: false shx: description: SmarGon X axis - deviceClass: pxiii_bec.devices.SmarGonAxis + deviceClass: pxiii_bec.devices.SmarGonAxisB deviceConfig: {prefix: 'SCS', low_limit: -2, high_limit: 2, sg_url: 'http://x06da-smargopolo.psi.ch:3000'} onFailure: buffer enabled: true @@ -604,7 +623,7 @@ shx: softwareTrigger: false shy: description: SmarGon Y axis - deviceClass: pxiii_bec.devices.SmarGonAxis + deviceClass: pxiii_bec.devices.SmarGonAxisB deviceConfig: {prefix: 'SCS', low_limit: -2, high_limit: 2, sg_url: 'http://x06da-smargopolo.psi.ch:3000'} onFailure: buffer enabled: true @@ -613,7 +632,7 @@ shy: softwareTrigger: false shz: description: SmarGon Z axis - deviceClass: pxiii_bec.devices.SmarGonAxis + deviceClass: pxiii_bec.devices.SmarGonAxisB deviceConfig: {prefix: 'SCS', low_limit: 10, high_limit: 22, sg_url: 'http://x06da-smargopolo.psi.ch:3000'} onFailure: buffer enabled: true @@ -622,7 +641,7 @@ shz: softwareTrigger: false chi: description: SmarGon CHI axis - deviceClass: pxiii_bec.devices.SmarGonAxis + deviceClass: pxiii_bec.devices.SmarGonAxisB deviceConfig: {prefix: 'SCS', low_limit: 0, high_limit: 40, sg_url: 'http://x06da-smargopolo.psi.ch:3000'} onFailure: buffer enabled: true @@ -631,7 +650,7 @@ chi: softwareTrigger: false phi: description: SmarGon PHI axis - deviceClass: pxiii_bec.devices.SmarGonAxis + deviceClass: pxiii_bec.devices.SmarGonAxisB deviceConfig: {prefix: 'SCS', sg_url: 'http://x06da-smargopolo.psi.ch:3000'} onFailure: buffer enabled: true diff --git a/pxiii_bec/devices/A3200.py b/pxiii_bec/devices/A3200.py index cf01e30..0303d8e 100644 --- a/pxiii_bec/devices/A3200.py +++ b/pxiii_bec/devices/A3200.py @@ -12,11 +12,11 @@ Standard bluesky interface: AerotechAbrStage.kickoff() AerotechAbrStage.stop() Additional bluesky functionality: - - + + Aerotech.is_homed() Aerotech.do_homing(wait=True) Aerotech.get_ready(ostart=None, orange=None, etime=None, wait=True) @@ -66,7 +66,6 @@ from bec_lib import bec_logger logger = bec_logger.logger - # pylint: disable=logging-fstring-interpolation class AerotechAbrMixin(CustomPrepare): """Configuration class for the Aerotech A3200 controller for the ABR stage""" @@ -84,7 +83,13 @@ class AerotechAbrMixin(CustomPrepare): scanargs = self.parent.scaninfo.scan_msg.info["kwargs"] scanname = self.parent.scaninfo.scan_msg.info["scan_name"] - if scanname in ("standardscan", "helicalscan"): + if scanname in ( + "standardscan", + "helicalscan", + "helicalscan1", + "helicalscan2", + "helicalscan3", + ): d["scan_command"] = AbrCmd.MEASURE_STANDARD d["var_1"] = scanargs["start"] d["var_2"] = scanargs["range"] @@ -93,8 +98,8 @@ class AerotechAbrMixin(CustomPrepare): d["var_5"] = 0 d["var_6"] = 0 d["var_7"] = 0 - d["var_8"] = 0 - d["var_9"] = 0 + # d["var_8"] = 0 + # d["var_9"] = 0 if scanname in ("verticallinescan", "vlinescan"): d["scan_command"] = AbrCmd.VERTICAL_LINE_SCAN d["var_1"] = scanargs["range"] / scanargs["steps"] @@ -104,8 +109,8 @@ class AerotechAbrMixin(CustomPrepare): d["var_5"] = 0 d["var_6"] = 0 d["var_7"] = 0 - d["var_8"] = 0 - d["var_9"] = 0 + # d["var_8"] = 0 + # d["var_9"] = 0 if scanname in ("screeningscan"): d["scan_command"] = AbrCmd.SCREENING d["var_1"] = scanargs["start"] @@ -115,8 +120,8 @@ class AerotechAbrMixin(CustomPrepare): d["var_5"] = scanargs["steps"] d["var_6"] = scanargs.get("delta", 0.5) d["var_7"] = 0 - d["var_8"] = 0 - d["var_9"] = 0 + # d["var_8"] = 0 + # d["var_9"] = 0 if scanname in ("rasterscan", "rastersimplescan"): d["scan_command"] = AbrCmd.RASTER_SCAN_SIMPLE d["var_1"] = scanargs["exp_time"] @@ -126,8 +131,8 @@ class AerotechAbrMixin(CustomPrepare): d["var_5"] = scanargs["steps_y"] d["var_6"] = 0 d["var_7"] = 0 - d["var_8"] = 0 - d["var_9"] = 0 + # d["var_8"] = 0 + # d["var_9"] = 0 # Reconfigure if got a valid scan config if len(d) > 0: @@ -157,7 +162,7 @@ class AerotechAbrStage(BECDeviceBase): """ custom_prepare_cls = AerotechAbrMixin - USER_ACCESS = ["reset", "kickoff", "bluekickoff", "complete", "set_axis_mode"] + USER_ACCESS = ["reset", "kickoff", "bluekickoff", "complete", "set_axis_mode", "arm", "disarm"] taskStop = Component(EpicsSignal, "-AERO:TSK-STOP", put_complete=True, kind=Kind.omitted) status = Component(EpicsSignal, "-AERO:STAT", put_complete=True, kind=Kind.omitted) @@ -295,9 +300,7 @@ class AerotechAbrStage(BECDeviceBase): return bool(value == 0) # Subscribe and wait for update - status = SubscriptionStatus( - self.scan_done, is_busy, timeout=timeout, settle_time=0.1 - ) + status = SubscriptionStatus(self.scan_done, is_busy, timeout=timeout, settle_time=0.1) status.wait() # return status @@ -321,9 +324,7 @@ class AerotechAbrStage(BECDeviceBase): # Subscribe and wait for update # status = SubscriptionStatus(self.task1, is_idle, timeout=timeout, settle_time=0.5) - status = SubscriptionStatus( - self.scan_done, is_idle, timeout=timeout, settle_time=0.5 - ) + status = SubscriptionStatus(self.scan_done, is_idle, timeout=timeout, settle_time=0.5) return status def reset(self, settle_time=0.1, wait_after_reload=1) -> None: diff --git a/pxiii_bec/devices/A3200utils.py b/pxiii_bec/devices/A3200utils.py index 4ddded2..30072dc 100644 --- a/pxiii_bec/devices/A3200utils.py +++ b/pxiii_bec/devices/A3200utils.py @@ -5,6 +5,7 @@ Created on Tue Jun 11 11:28:38 2024 @author: mohacsi_i """ import types +from collections import OrderedDict from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase from ophyd.status import Status, MoveStatus @@ -89,7 +90,7 @@ class A3200Axis(PVPositioner): vmax = Component(Signal, kind=Kind.config) offset = Component(EpicsSignal, "-OFF", put_complete=True, kind=Kind.config) - #pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments def __init__( self, prefix="", @@ -195,6 +196,18 @@ class A3200Axis(PVPositioner): raise return status + def describe(self): + """Workaround to schema expected by the BEC""" + d = super().describe() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + + def read(self) -> OrderedDict[str, dict]: + """Workaround to schema expected by the BEC""" + d = super().read() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + def move(self, position, wait=True, timeout=None, moved_cb=None, **kwargs) -> MoveStatus: """Exposes the ophyd move command through BEC abstraction""" return self.omove(position, wait=wait, timeout=timeout, moved_cb=moved_cb, **kwargs) @@ -242,5 +255,5 @@ class A3200Axis(PVPositioner): # Automatically start an axis if directly invoked if __name__ == "__main__": - omega = A3200Axis(prefix="X06DA-ES-DF1:OMEGA", base_pv='X06DA-ES', name="omega") + omega = A3200Axis(prefix="X06DA-ES-DF1:OMEGA", base_pv="X06DA-ES", name="omega") omega.wait_for_connection() diff --git a/pxiii_bec/devices/NDArrayPreview.py b/pxiii_bec/devices/NDArrayPreview.py index 94d2bc3..f76c578 100644 --- a/pxiii_bec/devices/NDArrayPreview.py +++ b/pxiii_bec/devices/NDArrayPreview.py @@ -11,7 +11,7 @@ Created on Wed Jan 29 2025 @author: mohacsi_i """ import numpy as np -from ophyd import Device, Component, EpicsSignal, Kind, Staged +from ophyd import Device, Component, EpicsSignal, EpicsSignalWithRBV, Kind, Staged from ophyd.areadetector import NDDerivedSignal @@ -20,6 +20,17 @@ from bec_lib import bec_logger logger = bec_logger.logger +class SilentNDDerivedSignal(NDDerivedSignal): + """Silent version of NDDerivedSignal, it does not spam the terminal on + every defective frame (shit happens, ok?).""" + + def _array_shape_callback(self, **kwargs): + try: + super()._array_shape_callback(**kwargs) + except RuntimeError: + pass + + class NDArrayPreview(Device): """Wrapper class around AreaDetector's NDStdArray plugins @@ -36,13 +47,16 @@ class NDArrayPreview(Device): _default_sub = SUB_MONITOR # Status attributes + min_callback_time = Component( + EpicsSignalWithRBV, "MinCallbackTime", kind=Kind.config, put_complete=True + ) array_size_x = Component(EpicsSignal, "ArraySize0_RBV", kind=Kind.config) array_size_y = Component(EpicsSignal, "ArraySize1_RBV", kind=Kind.config) array_size_z = Component(EpicsSignal, "ArraySize2_RBV", kind=Kind.config) ndimensions = Component(EpicsSignal, "NDimensions_RBV", kind=Kind.config) array_data = Component(EpicsSignal, "ArrayData", kind=Kind.omitted) shaped_image = Component( - NDDerivedSignal, + SilentNDDerivedSignal, derived_from="array_data", shape=("array_size_z", "array_size_y", "array_size_x"), num_dimensions="ndimensions", diff --git a/pxiii_bec/devices/PneumaticValve.py b/pxiii_bec/devices/PneumaticValve.py new file mode 100644 index 0000000..83761e5 --- /dev/null +++ b/pxiii_bec/devices/PneumaticValve.py @@ -0,0 +1,47 @@ +from ophyd import EpicsSignal +from ophyd.status import SubscriptionStatus + + +class PneumaticValve(EpicsSignal): + """Wrapper around EpicsSignal to wait until reaching target. Use the + status returned by set() to wait until movement is finished. Do NOT + use put if you want to wait, that's a low-level PV write op. + + NOTE: The SET and GET states do not match exactly + """ + + def set(self, value, *, timeout=5, settle_time=0.1): + """Overloaded setter that waits for target state + + NOTE: The SubscriptionStatus callback does not run in put() + """ + # Lazy hardcoded state lookup + target = 1 if value in (1, "Measure") else 2 + + # Define wait until an end state is reached + def on_target(*, value, **_): + return bool(value == target) + + # Subscribe a monitor in advance and wait for update + status = SubscriptionStatus(self, on_target, timeout=timeout, settle_time=0.1) + # Set value to start movement + super().set(value, settle_time=settle_time).wait() + # Return the monitor + return status + + def check_value(self, value): + """Input validation""" + if value not in (0, 1, "Measure", "Park"): + raise ValueError(f"Unsupported pneumatic valve target {value}") + return super().check_value(value) + + +if __name__ == "__main__": + pneum = PneumaticValve( + read_pv="X06DA-ES-BS:GET-POS", + write_pv="X06DA-ES-BS:SET-POS", + auto_monitor=True, + put_complete=True, + name="bspump", + ) + pneum.wait_for_connection() diff --git a/pxiii_bec/devices/SamCamDetector.py b/pxiii_bec/devices/SamCamDetector.py index 23b914e..75da5b1 100644 --- a/pxiii_bec/devices/SamCamDetector.py +++ b/pxiii_bec/devices/SamCamDetector.py @@ -13,7 +13,8 @@ Created on Thu Jan 30 2025 """ from ophyd import ADComponent from ophyd_devices.devices.areadetector.cam import GenICam -from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 + +# from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 from ophyd_devices.interfaces.base_classes.psi_detector_base import ( PSIDetectorBase, CustomDetectorMixin, @@ -25,6 +26,8 @@ logger = bec_logger.logger class SamCamSetup(CustomDetectorMixin): + """Simple camera mixin class, the SAMCAM is usually streaming""" + def on_stage(self): """Just make sure it's running continously""" self.parent.cam.acquire.put(1, wait=True) @@ -40,10 +43,10 @@ class SamCamDetector(PSIDetectorBase): """Sample camera device The SAMCAM continously streams images to the GUI and sample alignment - scripts via ZMQ. + scripts via ZMQ. """ custom_prepare_cls = SamCamSetup cam = ADComponent(GenICam, "cam1:") - image = ADComponent(ImagePlugin_V35, "image1:") + # image = ADComponent(ImagePlugin_V35, "image1:") diff --git a/pxiii_bec/devices/SmarGon.py b/pxiii_bec/devices/SmarGonA.py similarity index 82% rename from pxiii_bec/devices/SmarGon.py rename to pxiii_bec/devices/SmarGonA.py index 98d6bfa..aa902d4 100644 --- a/pxiii_bec/devices/SmarGon.py +++ b/pxiii_bec/devices/SmarGonA.py @@ -9,6 +9,8 @@ The SmarGon axes are interfaced as positioners. import time from threading import Thread, Lock import requests +from requests.adapters import HTTPAdapter, Retry +from collections import OrderedDict from ophyd import Component, Kind, Signal, PVPositioner from ophyd.status import SubscriptionStatus @@ -92,6 +94,7 @@ class SmarGonSignalRO(Signal): """Small helper class for read-only parameters PVs from SmarGon. Reads and optionally monitors a variable on the SmarGon. + """ def __init__(self, *args, read_addr="readbackSCS", auto_monitor=False, **kwargs): @@ -166,6 +169,12 @@ class SmarGonAxis(PVPositioner): self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url + # Fine-tune HTTP connection behavior + # NOTE: SmarGon has a few failed requests every one in a while + self._s = requests.Session() + retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504]) + self._s.mount("http://", HTTPAdapter(max_retries=retries)) + super().__init__( prefix=prefix, name=name, @@ -187,22 +196,33 @@ class SmarGonAxis(PVPositioner): """Move command that's masked by BEC""" return self.omove(position, wait, timeout, moved_cb) - def omove(self, position, wait=True, timeout=None, moved_cb=None): + def omove(self, position, wait=True, timeout=2.0, moved_cb=None): """Original move command without the BEC wrappers""" status = self.setpoint.set(position, settle_time=0.1) + status.wait() if not wait: return status - status.wait() - def on_target(*, value, **_): - distance = abs(value - position) - print(distance) + distance = abs(value - self.setpoint._value) + print(f"[self.name] Distance: {distance}") return bool(distance < self._tol) status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1) return status + def describe(self): + """Workaround to schema expected by the BEC""" + d = super().describe() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + + def read(self) -> OrderedDict[str, dict]: + """Workaround to schema expected by the BEC""" + d = super().read() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + def _pos_changed(self, timestamp=None, value=None, **kwargs): pass @@ -211,10 +231,16 @@ class SmarGonAxis(PVPositioner): cmd = f"{self.sg_url.get()}/{address}" try: with mutex: - r = requests.get(cmd, timeout=1, **kwargs) + r = self._s.get(cmd, timeout=1, **kwargs) except TimeoutError: - with mutex: - r = requests.get(cmd, timeout=1, **kwargs) + try: + time.sleep(0.05) + with mutex: + r = self._s.get(cmd, timeout=0.5, **kwargs) + except TimeoutError: + time.sleep(0.05) + with mutex: + r = self._s.get(cmd, timeout=0.5, **kwargs) if not r.ok: raise RuntimeError( f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}" @@ -226,10 +252,16 @@ class SmarGonAxis(PVPositioner): cmd = f"{self.sg_url.get()}/{address}" try: with mutex: - r = requests.put(cmd, timeout=1, **kwargs) + r = self._s.put(cmd, timeout=1, **kwargs) except TimeoutError: - with mutex: - r = requests.put(cmd, timeout=1, **kwargs) + try: + time.sleep(0.05) + with mutex: + r = self._s.put(cmd, timeout=0.5, **kwargs) + except TimeoutError: + time.sleep(0.05) + with mutex: + r = self._s.put(cmd, timeout=0.5, **kwargs) if not r.ok: raise RuntimeError( f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}" diff --git a/pxiii_bec/devices/SmarGonB.py b/pxiii_bec/devices/SmarGonB.py new file mode 100644 index 0000000..df50bfa --- /dev/null +++ b/pxiii_bec/devices/SmarGonB.py @@ -0,0 +1,249 @@ +""" +``SmarGon`` --- SmarGon control software +****************************************** + +The module provides an object to control the SmarGon goniometer axes at PX III. +The SmarGon axes are interfaced as positioners. +""" + +import time +import threading +from collections import OrderedDict +import requests +from requests.adapters import HTTPAdapter, Retry +from ophyd import Component, Kind, Signal, PVPositioner +from ophyd.status import SubscriptionStatus + +try: + from bec_lib import bec_logger + + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + + logger = logging.getLogger("SmarGon") + + +# SmarGon contoller can't really handle multiple connections +# Use this mutex to ensure one access at a time +mutex = threading.Lock() + + +class LimitedSmarGonSignal(Signal): + """SmarGonSignal (R/W) + + Small helper class to read/write parameters from SmarGon. As there is no + motion status readback from smargopolo, this should be substituted with + setting with 'settle_time'. + """ + + def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs): + self._limits = (low_limit, high_limit) + super().__init__(*args, **kwargs) + self.write_addr = write_addr + + @property + def limits(self): + return self._limits + + def check_value(self, value, **kwargs): + """Check if value falls within limits""" + lol = self.limits[0] + if lol is not None: + if value < lol: + raise ValueError(f"Target {value} outside of limits {self.limits}") + hil = self.limits[1] + if hil is not None: + if value > hil: + raise ValueError(f"Target {value} outside of limits {self.limits}") + + def put(self, value, *, timestamp=None, force=False, metadata=None, **kwargs): + """Overriden put to add communication with smargopolo""" + # Validate new value and get timestamp + if not force: + self.check_value(value) + if timestamp is None: + timestamp = time.time() + + # Perform the actual write to SmargoPolo + # pylint: disable=protected-access + r = self.parent._go_n_put(f"{self.write_addr}?{self.parent.name.upper()}={value}") + + # pylint: disable=attribute-defined-outside-init + old_value = self._readback + self._timestamp = timestamp + self._readback = r[self.parent.name.upper()] + self._value = r[self.parent.name.upper()] + + # Notify subscribers + self._run_subs( + sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp + ) + + +class SmarGonAxis(PVPositioner): + """SmarGon client deice + + This class controls the SmarGon goniometer via the REST interface. All + SmarGon axes share a common mutex to manage actual HW access. + """ + + USER_ACCESS = ["omove", "oldmove"] + + # Status attributes + sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False}) + + # Axis parameters + readback = Component(Signal, kind=Kind.hinted, metadata={"write_access": False}) + setpoint = Component(LimitedSmarGonSignal, kind=Kind.normal) + done = Component(Signal, value=1, kind=Kind.normal, metadata={"write_access": False}) + _tol = 0.001 + + # pylint: disable=too-many-arguments + def __init__( + self, + prefix="SCS", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + sg_url: str = "http://x06da-smargopolo.psi.ch:3000", + low_limit=None, + high_limit=None, + **kwargs, + ) -> None: + # self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}" + self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit + self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit + self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url + # Fine-tune HTTP connection behavior + # NOTE: SmarGon has a few failed requests every one in a while + self._s = requests.Session() + retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504]) + self._s.mount("http://", HTTPAdapter(max_retries=retries)) + + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + + def on_target(): + """Monitors the setpoint and readback and calculates the on_target flag""" + time.sleep(2) + while True: + # Read back target and setpoint values + # pylint: disable=protected-access + r = self._go_n_get("readbackSCS") + rb = r[self.name.upper()] + self.readback.set(rb, force=True).wait() + r = self._go_n_get("targetSCS") + sp = r[self.name.upper()] + self.setpoint._value = sp + # print(f"Readback: {rb}\tSetpoint: {sp}") + # Check if they're within tolerance + distance = abs(rb - sp) + done = 1 if distance < self._tol else 0 + self.done.put(done, force=True) + time.sleep(0.2) + + self._mon = threading.Thread(target=on_target, daemon=True) + self._mon.start() + + def omove(self, position, wait=True, timeout=None, moved_cb=None): + """Move command that's masked by BEC""" + self.done.put(0, force=True) + return self.move(position, wait, timeout, moved_cb) + + def oldmove(self, position, wait=True, timeout=2.0, moved_cb=None): + """Original move command without the BEC wrappers""" + status = self.setpoint.set(position, settle_time=0.1).wait() + if not wait: + return status + + def on_target(*, value, **_): + distance = abs(value - self.setpoint._value) + print(f"[self.name] Distance: {distance}") + return bool(distance < self._tol) + + status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1) + return status + + def describe(self): + """Workaround to schema expected by the BEC""" + d = super().describe() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + + def read(self) -> OrderedDict[str, dict]: + """Workaround to schema expected by the BEC""" + d = super().read() + d[str(self.name)] = d[f"{self.name}_readback"] + return d + + def _pos_changed(self, timestamp=None, value=None, **kwargs): + """Remove EPICS dependency""" + pass + + def _go_n_get(self, address, **kwargs): + """Helper function to connect to smargopolo""" + cmd = f"{self.sg_url.get()}/{address}" + try: + with mutex: + r = self._s.get(cmd, timeout=1, **kwargs) + except TimeoutError: + try: + time.sleep(0.05) + with mutex: + r = self._s.get(cmd, timeout=0.5, **kwargs) + except TimeoutError: + time.sleep(0.05) + with mutex: + r = self._s.get(cmd, timeout=0.5, **kwargs) + if not r.ok: + raise RuntimeError( + f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}" + ) + return r.json() + + def _go_n_put(self, address, **kwargs): + """Helper function to connect to smargopolo""" + cmd = f"{self.sg_url.get()}/{address}" + try: + with mutex: + r = self._s.put(cmd, timeout=1, **kwargs) + except TimeoutError: + try: + time.sleep(0.05) + with mutex: + r = self._s.put(cmd, timeout=0.5, **kwargs) + except TimeoutError: + time.sleep(0.05) + with mutex: + r = self._s.put(cmd, timeout=0.5, **kwargs) + if not r.ok: + raise RuntimeError( + f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}" + ) + return r.json() + + +if __name__ == "__main__": + shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000") + shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000") + shz = SmarGonAxis( + prefix="SCS", + name="shz", + low_limit=10, + high_limit=22, + sg_url="http://x06da-smargopolo.psi.ch:3000", + ) + shx.wait_for_connection() + shy.wait_for_connection() + shz.wait_for_connection() diff --git a/pxiii_bec/devices/StdDaqPreview.py b/pxiii_bec/devices/StdDaqPreview.py index 232b00e..afe4559 100644 --- a/pxiii_bec/devices/StdDaqPreview.py +++ b/pxiii_bec/devices/StdDaqPreview.py @@ -51,7 +51,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin): self._mon.join(timeout=1) # So also disconnect the socket try: - #pylint: disable=protected-access + # pylint: disable=protected-access self.parent._socket.disconnect(self.parent.url.get()) except zmq.error.ZMQError: # Might be already closed @@ -73,7 +73,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin): break # pylint: disable=no-member - #pylint: disable=protected-access + # pylint: disable=protected-access r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) # Length and throtling checks @@ -105,7 +105,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin): # self.parent.array_data.put(data, force=True) self.parent.shaped_image.put(image, force=True) - #pylint: disable=protected-access + # pylint: disable=protected-access self.parent._last_image = image self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) t_last = t_curr @@ -133,7 +133,7 @@ class StdDaqPreviewDetector(PSIDetectorBase): This was meant to provide live image stream directly from the StdDAQ but also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin. Note that the preview stream must be already throtled in order to cope with - the incoming data and the python class might throttle it further. + the incoming data and the python class might throttle it further. NOTE: As an explicit request, it does not record the image data. @@ -185,8 +185,8 @@ class StdDaqPreviewDetector(PSIDetectorBase): self._socket.connect(self.url.get()) def savemode(self, save=False): - """ Toggle save mode for the shaped image""" - #pylint: disable=protected-access + """Toggle save mode for the shaped image""" + # pylint: disable=protected-access if save: self.shaped_image._kind = Kind.normal else: diff --git a/pxiii_bec/devices/__init__.py b/pxiii_bec/devices/__init__.py index f853f9a..f68521a 100644 --- a/pxiii_bec/devices/__init__.py +++ b/pxiii_bec/devices/__init__.py @@ -6,7 +6,9 @@ Ophyd devices for the PX III beamline, including the MX specific Aerotech A3200 """ from .A3200 import AerotechAbrStage from .A3200utils import A3200Axis -from .SmarGon import SmarGonAxis +from .SmarGonA import SmarGonAxis as SmarGonAxisA +from .SmarGonB import SmarGonAxis as SmarGonAxisB from .StdDaqPreview import StdDaqPreviewDetector from .NDArrayPreview import NDArrayPreview from .SamCamDetector import SamCamDetector +from .PneumaticValve import PneumaticValve diff --git a/pxiii_bec/scans/__init__.py b/pxiii_bec/scans/__init__.py index c7c9821..87cbea4 100644 --- a/pxiii_bec/scans/__init__.py +++ b/pxiii_bec/scans/__init__.py @@ -4,4 +4,5 @@ from .mx_measurements import ( MeasureRasterSimple, MeasureScreening, MeasureHelical, + MeasureHelical2, ) diff --git a/pxiii_bec/scans/mx_measurements.py b/pxiii_bec/scans/mx_measurements.py index 2ff005f..b805eac 100644 --- a/pxiii_bec/scans/mx_measurements.py +++ b/pxiii_bec/scans/mx_measurements.py @@ -1,4 +1,4 @@ -""" MX measurements module +"""MX measurements module Scan primitives for standard BEC scans at the PX beamlines at SLS. Theese scans define the event model and can be called from higher levels. @@ -102,10 +102,6 @@ class AerotechFlyscanBase(AsyncFlyScanBase): # Call super yield from super().pre_scan() - # def stage(self): - # """ ToDo: Sot sure if we should call super() here as it'd stage the whole beamline""" - # return super().stage() - def scan_core(self): """The actual scan logic comes here.""" # Kick off the run @@ -275,18 +271,16 @@ class MeasureHelical(AerotechFlyscanBase): ready_rate : float, optional No clue what is this... (default=500) sg_start : (float, float, float, float, float) - Complete SmarGon coordinate in tuple form. + Complete SmarGon coordinate in tuple form. sg_end : (float, float, float, float, float) Complete SmarGon coordinate in tuple form. sg_steps : int - Number of steps with SmarGon. + Number of steps with SmarGon. """ scan_name = "helicalscan" required_kwargs = ["start", "range", "move_time", "sg_start", "sg_end", "sg_steps"] - - def pre_scan(self): """Mostly just checking if ABR stage is ok...""" @@ -305,7 +299,7 @@ class MeasureHelical(AerotechFlyscanBase): logger.info(f"StepSize:\t{self.smargon_step_size}") logger.info(f"StepTime:\t{self.smargon_step_time}") - # TODO: Move roughly to start position??? + # TODO: Move roughly to start position??? st0 = yield from self.stubs.send_rpc("shx", "omove", self.smargon_start[0]) st1 = yield from self.stubs.send_rpc("shy", "omove", self.smargon_start[1]) st2 = yield from self.stubs.send_rpc("shz", "omove", self.smargon_start[2]) @@ -342,8 +336,132 @@ class MeasureHelical(AerotechFlyscanBase): st3.wait() st4.wait() t_end = time.time() - t_elapsed = t_end-t_start - time.sleep(max(self.smargon_step_time-t_elapsed, 0)) + t_elapsed = t_end - t_start + time.sleep(max(self.smargon_step_time - t_elapsed, 0)) + + # Wait for scan task to finish + if self.abr_complete: + if self.abr_timeout is not None: + st = yield from self.stubs.send_rpc_and_wait("abr", "complete", self.abr_timeout) + st.wait() + else: + st = yield from self.stubs.send_rpc_and_wait("abr", "complete") + st.wait() + + +class MeasureHelical2(AerotechFlyscanBase): + """Helical scan using the OMEGA motor + + Measure an absolute continous line scan from `start` to `start` + `range` + during `move_time` on the Omega axis with PSO output. + + The scan itself is executed by the scan service running on the Aerotech + controller. Ophyd just configures, launches it and waits for completion. + + Example + ------- + >>> scans.standard_wedge(start=42, range=10, move_time=20) + + Parameters + ---------- + start : float + Scan start position of the axis. + range : float + Scan range of the axis. + move_time : float + Total travel time for the movement [s]. + ready_rate : float, optional + No clue what is this... (default=500) + sg_start : (float, float, float, float, float) + Complete SmarGon coordinate in tuple form. + sg_end : (float, float, float, float, float) + Complete SmarGon coordinate in tuple form. + sg_steps : int + Number of steps with SmarGon. + """ + + scan_name = "helicalscan2" + required_kwargs = ["start", "range", "move_time", "sg_start", "sg_end", "sg_steps"] + point_id = 0 + + # def __init__(self, *args, parameter: dict = None, **kwargs): + # """Just set num_pos=0 to avoid hanging and override defaults if explicitly set from + # parameters. + # """ + # self.num_pos = kwargs["sg_steps"] + # super().__init__(*args, parameter=parameter, **kwargs) + + def prepare_positions(self): + # Smargon has no velocity control + self.smargon_start = np.array(self.caller_kwargs.get("sg_start")) + self.smargon_end = np.array(self.caller_kwargs.get("sg_end")) + self.smargon_steps = self.caller_kwargs.get("sg_steps") + self.smargon_range = self.smargon_end - self.smargon_start + self.smargon_step_size = self.smargon_range / self.smargon_steps + self.smargon_step_time = self.caller_kwargs.get("move_time") / self.smargon_steps + + logger.info(f"Start:\t{self.smargon_start}") + logger.info(f"End:\t{self.smargon_end}") + logger.info(f"Steps:\t{self.smargon_steps}") + logger.info(f"Range:\t{self.smargon_range}") + logger.info(f"StepSize:\t{self.smargon_step_size}") + logger.info(f"StepTime:\t{self.smargon_step_time}") + + self.num_pos = self.smargon_steps + self.positions = np.linspace(self.smargon_start, self.smargon_end, self.smargon_steps) + self.start_pos = self.positions[0, :] + # Call super + yield from super().prepare_positions() + + # def update_scan_motors(self): + # """ Update step scan motors""" + # self.scan_motors = ['shx', 'shy', 'shz', 'chi', 'phi'] + + def pre_scan(self): + """Mostly just checking if ABR stage is ok...""" + # Move roughly to start position + st0 = yield from self.stubs.send_rpc("shx", "omove", self.start_pos[0]) + st1 = yield from self.stubs.send_rpc("shy", "omove", self.start_pos[1]) + st2 = yield from self.stubs.send_rpc("shz", "omove", self.start_pos[2]) + st3 = yield from self.stubs.send_rpc("chi", "omove", self.start_pos[3]) + st4 = yield from self.stubs.send_rpc("phi", "omove", self.start_pos[4]) + st0.wait() + st1.wait() + st2.wait() + st3.wait() + st4.wait() + + # print(f"\n\n{self.readout_priority}\n\n") + + # Call super + yield from super().pre_scan() + + def scan_core(self): + """The actual scan logic comes here.""" + # Kick off the run + yield from self.stubs.send_rpc_and_wait("abr", "kickoff") + logger.info("Measurement launched on the ABR stage...") + + logger.info("Performing SmarGon stepping...") + for _, sg_pos in enumerate(self.positions): + # sg_pos = self.smargon_start + ss * self.smargon_step_size + # Move to position but don't care + st0 = yield from self.stubs.send_rpc("shx", "omove", sg_pos[0]) + st1 = yield from self.stubs.send_rpc("shy", "omove", sg_pos[1]) + st2 = yield from self.stubs.send_rpc("shz", "omove", sg_pos[2]) + st3 = yield from self.stubs.send_rpc("chi", "omove", sg_pos[3]) + st4 = yield from self.stubs.send_rpc("phi", "omove", sg_pos[4]) + t_start = time.time() + st0.wait() + st1.wait() + st2.wait() + st3.wait() + st4.wait() + t_end = time.time() + t_elapsed = t_end - t_start + time.sleep(max(self.smargon_step_time - t_elapsed, 0)) + yield from self.stubs.read(group="monitored", point_id=self.point_id) + self.point_id += 1 # Wait for scan task to finish if self.abr_complete: diff --git a/pxiii_bec/scripts/beamlinescripts.py b/pxiii_bec/scripts/beamlinescripts.py new file mode 100644 index 0000000..32d92ff --- /dev/null +++ b/pxiii_bec/scripts/beamlinescripts.py @@ -0,0 +1,68 @@ +import bec +import bec_lib.devicemanager.DeviceContainer as dev + + +def rock(steps, exp_time, scan_start=None, scan_end=None, datasource=None, visual=True, **kwargs): + """Demo step scan with plotting + + This is a simple user-space demo step scan with the BEC. It be a + standard BEC scan, while still setting up the environment. + + Example: + -------- + ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm) + """ + # Dummy method to check beamline status + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") + + motor = dev.dccm_theta2 + if scan_start is None: + scan_start = -0.05 / dev.dccm_energy.user_readback.get() + if scan_end is None: + scan_end = 0.05 / dev.dccm_energy.user_readback.get() + + if visual: + # Get or create scan specific window + window = None + for _, val in bec.gui.windows.items(): + if val.title == "CurrentScan": + window = val.widget + window.clear_all() + if window is None: + window = bec.gui.new("CurrentScan") + + # Draw a simploe plot in the window + dock = window.add_dock(f"ScanDisplay {motor}") + plt1 = dock.add_widget("BECWaveformWidget") + plt1.plot(x_name=motor, y_name=datasource) + plt1.set_x_label(motor) + plt1.set_y_label(datasource) + plt1.add_dap(motor, datasource, dap="LinearModel") + window.show() + + print("Handing over to 'scans.line_scan'") + s = scans.line_scan( + motor, + scan_start, + scan_end, + steps=steps, + exp_time=exp_time, + datasource=datasource, + relative=True, + **kwargs, + ) + + if visual: + # If fitting via GUI + firt_par = plt1.get_dap_params() + else: + # Without GUI + firt_par = bec.dap.LinearModel.fit( + s, motor.name, motor.name, datasource.name, datasource.name + ) + + # TODO: Validate fitted position + # TODO: Move to fitted maximum + + return s, firt_par diff --git a/pxiii_bec/scripts/scanwrappers.py b/pxiii_bec/scripts/scanwrappers.py index 88314aa..a42c5cf 100644 --- a/pxiii_bec/scripts/scanwrappers.py +++ b/pxiii_bec/scripts/scanwrappers.py @@ -1,7 +1,10 @@ -from bec_widgets.cli.client_utils import BECGuiClient - +import bec +import bec_lib.devicemanager.DeviceContainer as dev +def bl_check_beam(): + """Check beamline status before scan""" + return True def ascan( @@ -10,43 +13,68 @@ def ascan( scan_end, steps, exp_time, - datasource, - **kwargs + datasource=None, + visual=True, + relative=False, + **kwargs, ): """Demo step scan with plotting - This is a small BEC user-space demo step scan. It tries to be a + This is a simple user-space demo step scan with the BEC. It be a standard BEC scan, while still setting up the environment. Example: -------- ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm) """ - # if not bl_check_beam(): - # raise RuntimeError("Beamline is not in ready state") + # Dummy method to check beamline status + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") - # # GUI setup - # # Get or create gui - # gui = BECGuiClient() - # gui.start() - # window = None - # for _, val in gui.windows.items(): - # if val.title == "Current scan": - # window = val.widget - # window.clear_all() - # if window is None: - # window = gui.new("Current scan") - - # dock = window.add_dock(f"ScanDisplay {motor}") - # plt1 = dock.add_widget('BECWaveformWidget') - # plt1.plot(x_name=motor, y_name=datasource) - # plt1.set_x_label(motor) - # plt1.set_y_label(datasource) + if visual: + # Get or create scan specific window + window = None + for _, val in bec.gui.windows.items(): + if val.title == "CurrentScan": + window = val.widget + window.clear_all() + if window is None: + window = bec.gui.new("CurrentScan") + # Draw a simploe plot in the window + dock = window.add_dock(f"ScanDisplay {motor}") + plt1 = dock.add_widget("BECWaveformWidget") + plt1.plot(x_name=motor, y_name=datasource) + plt1.set_x_label(motor) + plt1.set_y_label(datasource) + plt1.add_dap(motor, datasource, dap="LinearModel") + window.show() print("Handing over to 'scans.line_scan'") - if 'relative' in kwargs: - del kwargs['relative'] - scans.line_scan(motor, scan_start, scan_end, steps=steps, exp_time=exp_time, relative=False, **kwargs) + s = scans.line_scan( + motor, + scan_start, + scan_end, + steps=steps, + exp_time=exp_time, + datasource=datasource, + relative=relative, + **kwargs, + ) + if visual: + # Fitting via GUI + firt_par = plt1.get_dap_params() + else: + # Fitting without GUI + firt_par = bec.dap.LinearModel.fit( + s, motor.name, motor.name, datasource.name, datasource.name + ) + # # Some basic fit + # dkey = datasource.full_name + # NOTE: s.scan.data == bec.history[-1] + # datapoints = bec.history[-1].devices[dkey].read()[dkey]['value'] + # positions + + return s, firt_par