From 0062da5a6b91d45538590ae343ab8174997d1ac3 Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Thu, 12 Sep 2024 15:44:41 +0200 Subject: [PATCH] Refactored advanced scan. Added calculator function to convert energy/angle. --- README.md | 36 +++-- debye_bec/devices/mo1_bragg.py | 147 ++++++++++----------- debye_bec/devices/utils/__init__.py | 0 debye_bec/devices/utils/mo1_bragg_utils.py | 73 ++++++++++ debye_bec/scans/mono_bragg_scans.py | 6 +- 5 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 debye_bec/devices/utils/__init__.py create mode 100644 debye_bec/devices/utils/mo1_bragg_utils.py diff --git a/README.md b/README.md index 1c897e8..ee37dc1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Debye-specific plugins and configs for BEC ### Open visual studio code ``` +ssh x01da-bec-001 +cd /data/test/x01da-test-bec/bec_deployment code ``` ### Git @@ -15,29 +17,37 @@ git push origin feat/add_advanced_scan_modes git status ``` -### Start or restart BEC Server +### BEC Server ``` ssh x01da-bec-001 cd /data/test/x01da-test-bec/bec_deployment . /data/test/x01da-test-bec/bec_deployment/bec_venv/bin/activate -``` -Then -``` + bec-server start -``` -or -``` +bec-server restart +bec-server stop bec-server attach ``` -after change in code: -- ctrl-c + ctrl-c to stop scan server and device server module -- restart server modules +To restart individual server modules: +- ctrl-c + ctrl-c to stop for example scan server or device server module +- restart server module(s) -### Start BEC Client +### BEC Client ``` -ssh x01da-test-cons +ssh x01da-bec-001 cd /data/test/x01da-test-bec/bec_deployment -. /data/test/x01da-test-bec/bec_deployment/bec_venv/bin/activate bec ``` + +#### Useful commands in bec +Update Session with specific config: +``` +bec.config.update_session_with_file("debye_bec/debye_bec/device_configs/x01da_test_config.yaml") +``` + +Define folder and sample name for written files: +``` +bec.system_config.file_directory="test" +bec.system_config.file_suffix ="sampleA" +``` \ No newline at end of file diff --git a/debye_bec/devices/mo1_bragg.py b/debye_bec/devices/mo1_bragg.py index d98812f..1855701 100644 --- a/debye_bec/devices/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg.py @@ -12,8 +12,8 @@ import enum import threading import time import traceback -import numpy as np -from scipy.interpolate import BSpline +from typeguard import typechecked +from debye_bec.devices.utils.mo1_bragg_utils import compute_spline from dataclasses import dataclass from typing import Literal @@ -119,7 +119,7 @@ class Mo1BraggStatus(Device): class Mo1BraggEncoder(Device): """Mo1 Bragg PVs to communicate with the encoder""" - enc_reinit = Cpt(EpicsSignal, suffix="enc_reinit.PROC", kind="config") + enc_reinit = Cpt(EpicsSignal, suffix="enc_reinit", kind="config") enc_reinit_done = Cpt(EpicsSignalRO, suffix="enc_reinit_done_RBV", kind="config") @@ -131,7 +131,7 @@ class Mo1BraggCrystal(Device): xtal_enum = Cpt(EpicsSignalWithRBV, suffix="xtal_ENUM", kind="config") d_spacing_si111 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si111", kind="config") d_spacing_si311 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si311", kind="config") - set_offset = Cpt(EpicsSignal, suffix="set_offset.PROC", kind="config", put_complete=True) + set_offset = Cpt(EpicsSignal, suffix="set_offset", kind="config", put_complete=True) current_xtal = Cpt( EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True ) @@ -171,6 +171,11 @@ class Mo1BraggScanSettings(Device): a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=True) a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=True) +class Mo1BraggCalculator(Device): + calc_reset = Cpt(EpicsSignal, suffix="calc_reset", kind="config", put_complete=True) + calc_done = Cpt(EpicsSignalRO, suffix="calc_done_RBV", kind="config") + calc_energy = Cpt(EpicsSignalWithRBV, suffix="calc_energy", kind="config") + calc_angle = Cpt(EpicsSignalWithRBV, suffix="calc_angle", kind="config") class Mo1BraggScanControl(Device): """Mo1 Bragg PVs to control the scan after setting the parameters.""" @@ -179,15 +184,15 @@ class Mo1BraggScanControl(Device): scan_duration = Cpt( EpicsSignalWithRBV, suffix="scan_duration", kind="config", auto_monitor=True ) - scan_load = Cpt(EpicsSignal, suffix="scan_load.PROC", kind="config", put_complete=True) + scan_load = Cpt(EpicsSignal, suffix="scan_load", kind="config", put_complete=True) scan_msg = Cpt(EpicsSignalRO, suffix="scan_msg_ENUM_RBV", kind="config", auto_monitor=True) scan_start_infinite = Cpt( - EpicsSignal, suffix="scan_start_infinite.PROC", kind="config", put_complete=True + EpicsSignal, suffix="scan_start_infinite", kind="config", put_complete=True ) scan_start_timer = Cpt( - EpicsSignal, suffix="scan_start_timer.PROC", kind="config", put_complete=True + EpicsSignal, suffix="scan_start_timer", kind="config", put_complete=True ) - scan_stop = Cpt(EpicsSignal, suffix="scan_stop.PROC", kind="config", put_complete=True) + scan_stop = Cpt(EpicsSignal, suffix="scan_stop", kind="config", put_complete=True) scan_status = Cpt( EpicsSignalRO, suffix="scan_status_ENUM_RBV", kind="config", auto_monitor=True ) @@ -196,7 +201,7 @@ class Mo1BraggScanControl(Device): ) scan_done = Cpt(EpicsSignalRO, suffix="scan_done_RBV", kind="config", auto_monitor=True) scan_val_reset = Cpt( - EpicsSignal, suffix="scan_val_reset.PROC", kind="config", put_complete=True + EpicsSignal, suffix="scan_val_reset", kind="config", put_complete=True ) scan_progress = Cpt(EpicsSignalRO, suffix="scan_progress_RBV", kind="config", auto_monitor=True) scan_spectra_done = Cpt( @@ -241,6 +246,7 @@ class Mo1Bragg(Device, PositionerBase): crystal = Cpt(Mo1BraggCrystal, "") encoder = Cpt(Mo1BraggEncoder, "") scan_settings = Cpt(Mo1BraggScanSettings, "") + calculator = Cpt(Mo1BraggCalculator, "") scan_control = Cpt(Mo1BraggScanControl, "") status = Cpt(Mo1BraggStatus, "") @@ -277,8 +283,8 @@ class Mo1Bragg(Device, PositionerBase): ) # Execute motion - move_abs = Cpt(EpicsSignal, suffix="move_abs.PROC", kind="config", put_complete=True) - move_stop = Cpt(EpicsSignal, suffix="move_stop.PROC", kind="config", put_complete=True) + move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True) + move_stop = Cpt(EpicsSignal, suffix="move_stop", kind="config", put_complete=True) SUB_READBACK = "readback" _default_sub = SUB_READBACK @@ -528,85 +534,76 @@ class Mo1Bragg(Device, PositionerBase): self.scan_settings.s_scan_angle_hi.put(high) self.scan_settings.s_scan_scantime.put(scan_time) + @typechecked + def convert_angle_energy(self, mode:Literal["AngleToEnergy", "EnergyToAngle"], inp:float) -> float: + """Calculate energy to angle or vice versa + + Args: + mode (Literal["AngleToEnergy", "EnergyToAngle"]): Mode of calculation + input (float): Either angle or energy + + Returns: + output (float): Converted angle or energy + """ + # TODO the calc_reset should reset from the IOC itself, check EPICS implementation with Alvin/Xiaoqiang + self.calculator.calc_reset.put(0) + self.calculator.calc_reset.put(1) + + if not self.wait_for_signals( + signal_conditions=[(self.calculator.calc_done.get, 0)], + timeout=self.timeout_for_pvwait, + check_stopped=True, + ): + raise TimeoutError( + f"Timeout after {self.timeout_for_pvwait} while waiting for calc done," + ) + if mode == "AngleToEnergy": + self.calculator.calc_angle.put(inp) + elif mode == "EnergyToAngle": + self.calculator.calc_energy.put(inp) + + if not self.wait_for_signals( + signal_conditions=[(self.calculator.calc_done.get, 1)], + timeout=self.timeout_for_pvwait, + check_stopped=True, + ): + raise TimeoutError( + f"Timeout after {self.timeout_for_pvwait} while waiting for calc done," + ) + time.sleep(0.25) + if mode == "AngleToEnergy": + return self.calculator.calc_energy.get() + elif mode == "EnergyToAngle": + return self.calculator.calc_angle.get() + def set_advanced_xas_settings(self, low: float, high:float, scan_time: float, p_kink: float, e_kink: float) -> None: """Set Advanced XAS parameters for upcoming scan. + + Args: low (float): Low angle value of the scan in deg high (float): High angle value of the scan in deg scan_time (float): Time for a half oscillation in s p_kink (float): Position of kink in % e_kink (float): Energy of kink in eV - Args: """ - ## TODO Change to energy only, calculation done on ACS controller - ## ACS requires energy as input, outputs angle - ## + reset bit signal, + calc done bit signal - ## In the meantime, misuse s_scan_angle/energy PVs ## TODO Add fallback solution for automatic testing, otherwise test will fail ## because no monochromator will calculate the angle ## Unsure how to implement this + move_type = self.move_type.get() if move_type == MoveType.ENERGY: - self.scan_settings.s_scan_energy_lo.put(e_kink) - time.sleep(1) - e_kink_deg = self.scan_settings.s_scan_angle_hi.get() - self.scan_settings.s_scan_energy_lo.put(low) - self.scan_settings.s_scan_energy_hi.put(high) - time.sleep(1) - low_deg = self.scan_settings.s_scan_angle_lo.get() - high_deg = self.scan_settings.s_scan_angle_hi.get() + e_kink_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=e_kink) + # Angle and Energy are inverse proportional! + high_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=low) + low_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=high) + else: + raise Mo1BraggError("MoveType Angle not implemented for advanced scans, use Energy") - sf = 0.025 # safety factor to limit acceleration -> NEVER SET TO ZERO ! - n = 41 # number of samples to generate -> Always choose uneven number, otherwise peak value will not be included - degree = 3 # degree of spline, 3 works good - tc = 0.0062 # time to be compensated each spline in s - pc = 0.02 # angle to add at both limits, must be same values as used on ACS controller for simple scans + pos, vel, dt = compute_spline(low_deg=low_deg, high_deg=high_deg, p_kink =p_kink, e_kink_deg = e_kink_deg, scan_time=scan_time) - # increase motion range slightly so that xas trigger signals will occur at defined energy limits - low_deg = low_deg - pc - high_deg = high_deg + pc - - if p_kink < 0 or p_kink > 100: - raise Exception("Kink position not within range of [0..100%]") - - if e_kink_deg < low_deg or e_kink_deg > high_deg: - raise Exception("Kink energy not within selected energy range of scan") - - tc1 = sf / scan_time * tc - t_kink = (scan_time - tc - 2*(sf - tc1)) * p_kink/100 + (sf - tc1) - - t_input = [0, sf - tc1, t_kink , scan_time - tc - sf + tc1, scan_time - tc ] - p_input = [0, 0 , e_kink_deg - low_deg , high_deg - low_deg , high_deg - low_deg] - - cv = np.stack((t_input, p_input)).T # spline coefficients - max_param = len(cv) - degree - kv = np.clip(np.arange(len(cv)+degree+1)-degree,0,max_param) # knots - spl = BSpline(kv, cv, degree) # get spline function - p = spl(np.linspace(0,max_param,n)) - v = spl(np.linspace(0,max_param,n), 1) - a = spl(np.linspace(0,max_param,n), 2) - j = spl(np.linspace(0,max_param,n), 3) - - tim, pos = p.T - pos = pos + low_deg - vel = v[:,1]/v[:,0] - - acc = [] - for item in a: - acc.append(0) if item[1] == 0 else acc.append(item[1]/item[0]) - jerk = [] - for item in j: - jerk.append(0) if item[1] == 0 else jerk.append(item[1]/item[0]) - - dt = np.zeros(len(tim)) - for i in np.arange(len(tim)): - if i == 0: - dt[i] = 0 - else: - dt[i] = 1000*(tim[i]-tim[i-1]) - - self.scan_settings.a_scan_pos.put(pos) - self.scan_settings.a_scan_vel.put(vel) - self.scan_settings.a_scan_time.put(dt) + self.scan_settings.a_scan_pos.set(pos) + self.scan_settings.a_scan_vel.set(vel) + self.scan_settings.a_scan_time.set(dt) def set_xrd_settings( self, diff --git a/debye_bec/devices/utils/__init__.py b/debye_bec/devices/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/devices/utils/mo1_bragg_utils.py b/debye_bec/devices/utils/mo1_bragg_utils.py new file mode 100644 index 0000000..5f8deb9 --- /dev/null +++ b/debye_bec/devices/utils/mo1_bragg_utils.py @@ -0,0 +1,73 @@ +import numpy as np +from scipy.interpolate import BSpline + +################ Define Constants ############ +SAFETY_FACTOR = 0.025 # safety factor to limit acceleration -> NEVER SET TO ZERO ! +N_SAMPLES = 41 # number of samples to generate -> Always choose uneven number, otherwise peak value will not be included +DEGREE_SPLINE = 3 # DEGREE_SPLINE of spline, 3 works good +TIME_COMPENSATE_SPLINE = 0.0062 # time to be compensated each spline in s +POSITION_COMPONSATION = 0.02 # angle to add at both limits, must be same values as used on ACS controller for simple scans + + +class Mo1UtilsSplineError(Exception): + """ Exception for spline computation""" + + +def compute_spline(low_deg:float, high_deg:float, p_kink:float, e_kink_deg:float, scan_time:float) -> tuple[float, float, float]: + """ Spline computation for the advanced scan mode + + Args: + low_deg (float): Low angle value of the scan in deg + high_deg (float): High angle value of the scan in deg + scan_time (float): Time for a half oscillation in s + p_kink (float): Position of kink in % + e_kink_deg (float): Position of kink in degree + + Returns: + tuple[float,float,float] : Position, Velocity and delta T arrays for the spline + """ + + # increase motion range slightly so that xas trigger signals will occur at defined energy limits + low_deg = low_deg - POSITION_COMPONSATION + high_deg = high_deg + POSITION_COMPONSATION + + if p_kink < 0 or p_kink > 100: + raise Mo1UtilsSplineError(f"Kink position not within range of [0..100%] for p_kink: {p_kink}") + + if e_kink_deg < low_deg or e_kink_deg > high_deg: + raise Mo1UtilsSplineError(f"Kink energy not within selected energy range of scan, for e_kink_deg {e_kink_deg}, low_deg {low_deg} and high_deg {high_deg}.") + + tc1 = SAFETY_FACTOR / scan_time * TIME_COMPENSATE_SPLINE + t_kink = (scan_time - TIME_COMPENSATE_SPLINE - 2*(SAFETY_FACTOR - tc1)) * p_kink/100 + (SAFETY_FACTOR - tc1) + + t_input = [0, SAFETY_FACTOR - tc1, t_kink , scan_time - TIME_COMPENSATE_SPLINE - SAFETY_FACTOR + tc1, scan_time - TIME_COMPENSATE_SPLINE ] + p_input = [0, 0 , e_kink_deg - low_deg , high_deg - low_deg , high_deg - low_deg] + + cv = np.stack((t_input, p_input)).T # spline coefficients + max_param = len(cv) - DEGREE_SPLINE + kv = np.clip(np.arange(len(cv)+DEGREE_SPLINE+1)-DEGREE_SPLINE,0,max_param) # knots + spl = BSpline(kv, cv, DEGREE_SPLINE) # get spline function + p = spl(np.linspace(0,max_param,N_SAMPLES)) + v = spl(np.linspace(0,max_param,N_SAMPLES), 1) + a = spl(np.linspace(0,max_param,N_SAMPLES), 2) + j = spl(np.linspace(0,max_param,N_SAMPLES), 3) + + tim, pos = p.T + pos = pos + low_deg + vel = v[:,1]/v[:,0] + + acc = [] + for item in a: + acc.append(0) if item[1] == 0 else acc.append(item[1]/item[0]) + jerk = [] + for item in j: + jerk.append(0) if item[1] == 0 else jerk.append(item[1]/item[0]) + + dt = np.zeros(len(tim)) + for i in np.arange(len(tim)): + if i == 0: + dt[i] = 0 + else: + dt[i] = 1000*(tim[i]-tim[i-1]) + + return pos, vel, dt \ No newline at end of file diff --git a/debye_bec/scans/mono_bragg_scans.py b/debye_bec/scans/mono_bragg_scans.py index 0ae9384..b2bf87e 100644 --- a/debye_bec/scans/mono_bragg_scans.py +++ b/debye_bec/scans/mono_bragg_scans.py @@ -89,7 +89,7 @@ class XASSimpleScan(AsyncFlyScanBase): while True: # Readout monitored devices - yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary", point_id=self.point_id) # Check if complete call on Mo1 Bragg has been finished status = self.stubs.get_req_status( device=self.motor, RID=self.metadata["RID"], DIID=target_diid @@ -202,7 +202,7 @@ class XASAdvancedScan(XASSimpleScan): motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg". Examples: - >>> scans.xas_advanced_scan(start=9, stop=11, scan_time=0.5, scan_duration=10, p_kink=0.15, e_kink=1) + >>> scans.xas_advanced_scan(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500) """ super().__init__( start=start, @@ -269,7 +269,7 @@ class XASAdvancedScanWithXRD(XASAdvancedScan): motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg". Examples: - >>> scans.xas_advanced_scan_with_xrd(start=9, stop=11, scan_time=0.5, scan_duration=10, p_kink=0.15, e_kink=1, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000) + >>> scans.xas_advanced_scan_with_xrd(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000) """ super().__init__( start=start,