diff --git a/pxii_bec/device_configs/pxii-devices.yaml b/pxii_bec/device_configs/pxii-devices.yaml index 7fa50c9..33d9cd0 100644 --- a/pxii_bec/device_configs/pxii-devices.yaml +++ b/pxii_bec/device_configs/pxii-devices.yaml @@ -55,8 +55,8 @@ aerotech: deviceConfig: prefix: http://mx-x10sa-queue-01:5234 deviceTags: - - motors - aerotech + - motors enabled: true onFailure: buffer readoutPriority: baseline @@ -65,15 +65,15 @@ aerotech: mount: 0 work: 0 x: - in: -8.1e-06 + in: 0.05997465 out: -10 safe: -50 y: mount: -0.0 - work: -0.02600108 + work: -0.02999903 z: mount: 0 - work: -0.00900383 + work: -0.00900401 bcu_bpm1: description: BCU BPM Channel 1 deviceClass: ophyd.EpicsSignalRO @@ -330,7 +330,7 @@ bs_z: max_blout: 70 min: 13 safe: 45.1 - samp: 16.408 + samp: 16.80557617 type: continuous work_min: 20 bsf_f1_y: @@ -1322,6 +1322,40 @@ samcam_x: onFailure: buffer readOnly: true readoutPriority: monitored +samcam_xcurs: + description: Sample Camera X Cursor + deviceClass: ophyd.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: X10SA-ES-MS:Stats5:CursorX + deviceTags: + - scam + enabled: true + onFailure: buffer + readoutPriority: baseline +samcam_thresh: + description: Sample Camera Threshold + deviceClass: ophyd.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: X10SA-ES-MS:Stats5:CentroidThreshold + deviceTags: + - scam + enabled: true + onFailure: buffer + readoutPriority: baseline +samcam_xcursprof: + description: Sample Camera X Cursor Profile + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: X10SA-ES-MS:Stats5:ProfileCursorX_RBV + deviceTags: + - scam + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: monitored samcam_xsig: description: Sample Camera X Sigma deviceClass: ophyd.EpicsSignalRO @@ -1346,6 +1380,18 @@ samcam_y: onFailure: buffer readOnly: true readoutPriority: monitored +samcam_ycursprof: + description: Sample Camera Y Cursor Profile + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: X10SA-ES-MS:Stats5:ProfileCursorY_RBV + deviceTags: + - scam + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: monitored samcam_ysig: description: Sample Camera Y Sigma deviceClass: ophyd.EpicsSignalRO diff --git a/pxii_bec/macros/APscripts.py b/pxii_bec/macros/APscripts.py index c3f35aa..b4286ff 100644 --- a/pxii_bec/macros/APscripts.py +++ b/pxii_bec/macros/APscripts.py @@ -9,8 +9,8 @@ import numpy as np import math import matplotlib.pyplot as plt -import sys -import time, datetime +import sys +import time, datetime ################################ ### BASIC BL PROGRAMS @@ -95,11 +95,12 @@ def angle(e, *hkl): return a + ############## def resol(dstart, dend, nsteps): res = (Dstart - Dend) / nsteps print(f"resolution is: {res}") - return res + return res ##################### @@ -145,8 +146,8 @@ def rock(**kwargs): from lmfit.models import LinearModel, GaussianModel # LorentzianModel import matplotlib.pyplot as plt - ## create a dock area: - dock_area = bec.gui.new(name="Rock", geometry = [4000, 900, 700, 700]) + ## create a dock area: + dock_area = bec.gui.new(name="Rock", geometry=[4000, 900, 700, 700]) wr = dock_area.new(bec.gui.available_widgets.Waveform) # width of rocking curve of perfect xtal: at 20 keV: 14 urad == 0.0008 deg @@ -208,7 +209,7 @@ def rock(**kwargs): wr.plot(device_x=mot, device_y=det) ##set names/axes first ! s = scans.line_scan(mot, -dx, dx, steps=50, exp_time=time, relative=True) - + if ax == 0: data_x = s.scan.live_data.dcm_pitch.dcm_pitch.val else: @@ -244,15 +245,14 @@ def rock(**kwargs): center=center, sigma=(data_x[-1] - data_x[0]) / 20, slope=slope, - intercept=intercept + intercept=intercept, ) - - # maxy = max(data_y) - # indmax = np.argmax(data_y) - # xm = data_x[indmax] - # p = model.make_params(amplitude=maxy, center=xm) - + # maxy = max(data_y) + # indmax = np.argmax(data_y) + # xm = data_x[indmax] + # p = model.make_params(amplitude=maxy, center=xm) + g = model.fit(data_y, p, x=data_x) print(f'Center: {g.params["center"].value:.5f} FWHM: {g.params["fwhm"].value:.5f}') @@ -273,15 +273,15 @@ def rock(**kwargs): return - ################################ ### Helper PROGRAMS + ############################# ### just reconfigure beaml ############################# def loadmac(): - + bec.macros.load_all_user_macros() # d, planner = init_beamline_environment() @@ -291,10 +291,12 @@ def loadmac(): ############################# def plotset(y): import matplotlib.pyplot as plt + plt.ion() plt.figure() plt.plot(y) - + + ################################ ### FITTING PROGRAMS @@ -303,67 +305,71 @@ def plotset(y): ############################# -def justfit(data_x, data_y, model="gauss", ibg=0): +def justfit(data_x, data_y, tmodel="gauss", ibg=0): """ just fit a fct of Gauss, Voigt or Lorentzian type, Examples: - gfit, xmax = justfit(data_x, data_y, model = "gauss", ibg =1) : Gauss, lin BG - gfit, xmax = justfit(data_x, data_y, model = "voigt", ibg =2) : Voigt, quadrat BG - gfit, xmax = justfit(data_x, data_y, model = "lorentz", ibg =0) : Lorentzian, no BG + gfit, xmax = justfit(data_x, data_y, tmodel = "gauss", ibg =1) : Gauss, lin BG + gfit, xmax = justfit(data_x, data_y, tmodel = "voigt", ibg =2) : Voigt, quadrat BG + gfit, xmax = justfit(data_x, data_y, tmodel = "lorentz", ibg =0) : Lorentzian, no BG """ from lmfit.models import LinearModel, GaussianModel, VoigtModel, QuadraticModel, LorentzianModel import matplotlib.pyplot as plt - peak = GaussianModel(prefix = 'g_') - pre='g' - if model == "voigt": - peak = VoigtModel(prefix = 'v_') - pre='v' - if model == "lorentz": - peak = LorentzianModel(prefix = 'l_') - pre='l' - + peak = GaussianModel(prefix="g_") + pre = "g" + print("MODEL JUSTFIT = ", tmodel, peak) + if tmodel == "voigt": + peak = VoigtModel(prefix="v_") + pre = "v" + if tmodel == "lorentz": + peak = LorentzianModel(prefix="l_") + pre = "l" + + data_x = np.asarray(data_x) + data_y = np.asarray(data_y) model = peak - ypeak = data_y - pars = model.guess(data_y, x = data_x) - indmax = np.argmax(ypeak) - xm = data_x[indmax] + ypeak = data_y + pars = model.guess(data_y, x=data_x) + + indmax = np.argmax(ypeak) + xm = data_x[indmax] + if ibg: - background = LinearModel(prefix = 'bg_') + background = LinearModel(prefix="bg_") model = peak + background ### estimate initial params separately for both parts of the model # 1) BG - n = len(data_x) - edge= n//10 - idx=np.r_[0:edge, n-edge:n] # first and last part of the range - #m0, b0 = np.polyfit(data_x[idx], data_y[idx],1) - #ypeak= data_y - (m0*data_x + b0) # subtract linear part + n = len(data_x) + edge = n // 10 + idx = np.r_[0:edge, n - edge : n] # first and last part of the range + # m0, b0 = np.polyfit(data_x[idx], data_y[idx],1) + # ypeak= data_y - (m0*data_x + b0) # subtract linear part - pars = background.guess(data_y[idx], x = data_x[idx]) - - # 2) Function - BG - ypeak= data_y - (pars['bg_slope']*data_x + pars['bg_intercept'])# subtract linear part - pars.update( peak.guess(ypeak, x= data_x) ) - + pars = background.guess(data_y[idx], x=data_x[idx]) + + # 2) Function - BG + ypeak = data_y - (pars["bg_slope"] * data_x + pars["bg_intercept"]) # subtract linear part + pars.update(peak.guess(ypeak, x=data_x)) g = model.fit(data_y, pars, x=data_x) # diagnostics # print(f'Gfit: {g.params}') - #print(f'Center of {model} fit: {g.pars["center"].value:.5f}') - #print(f'Sigma: {g.pars["sigma"].value:.5f}') - #print(f'FWHM: {g.pars["fwhm"].value:.5f}') + # print(f'Center of {model} fit: {g.pars["center"].value:.5f}') + # print(f'Sigma: {g.pars["sigma"].value:.5f}') + # print(f'FWHM: {g.pars["fwhm"].value:.5f}') print(g.fit_report()) print(f"Position of maximum: {xm}") return g, xm -def fit_plothist(hindex: int, signal_name: str, model="gauss", ibg=0): +def fit_plothist(hindex: int, signal_name: str, tmodel="gauss", ibg=0): """ Get data for a completed scan from the BEC history, (gaussian, voigt, lorentzian + zero, lin, quadrat BG) fit, and plot all the results. @@ -373,7 +379,7 @@ def fit_plothist(hindex: int, signal_name: str, model="gauss", ibg=0): signal_name (str): the monitor Example: - gcen, xm , data_x, data_y = fit_plothist(-10, "lu_bpmsum",model="gauss", ibg=2) + gcen, xm , data_x, data_y = fit_plothist(-10, "lu_bpmsum", tmodel="gauss", ibg=2) """ @@ -382,19 +388,21 @@ def fit_plothist(hindex: int, signal_name: str, model="gauss", ibg=0): if hindex < 0: h = bec.history[hindex] - print('by scan number') + print("by scan number") else: h = bec.history.get_by_scan_number(hindex) - print('by scan number') + print("by scan number") md = h.metadata["bec"] - scanvar = list(md["args"].keys())[0] # string, returns the variable of the last performed scan + #scanvar = list(md["args"].keys())[0] # string, returns the variable of the last performed scan + # note: no longer, but + # md["positions"] will work # data = h.devices[device_name][signal].read()["value"] # data_x = h.devices.dcm_pitch.dcm_pitch.read()["value"] # data_y = h.devices.lu_bpmsum.lu_bpmsum.read()["value"] ## that should work in any case: - device_name = scanvar - #data_x = h.devices[device_name][device_name].read()["value"] + device_name = md["scan_report_devices"][0].decode() + # data_x = h.devices[device_name][device_name].read()["value"] data_xd = md["positions"] # last scan knows which device ... however, double array data_x = data_xd.flatten() @@ -402,22 +410,25 @@ def fit_plothist(hindex: int, signal_name: str, model="gauss", ibg=0): data_y = h.devices[signal_name][signal_name].read()["value"] print("dy = ", data_y) - prefix='v_' - if model == 'gauss': - prefix='g_' - - g, xm = justfit(data_x, data_y, model=model, ibg=ibg) + data_x = np.asarray(data_x) + data_y = np.asarray(data_y) + + prefix = "v_" + if tmodel == "gauss": + prefix = "g_" + + g, xm = justfit(data_x, data_y, tmodel=tmodel, ibg=ibg) plt.ion() plt.figure() plt.plot(data_x, data_y, "*") plt.plot(data_x, g.best_fit) - plt.xlabel(scanvar) + plt.xlabel("motor pos") plt.ylabel(signal_name) plt.show() # g.params - returncenvar = prefix+"center" + returncenvar = prefix + "center" gcen = g.params[returncenvar].value return gcen, xm, data_x, data_y @@ -428,7 +439,7 @@ def fit_plothist(hindex: int, signal_name: str, model="gauss", ibg=0): ######################### -def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclick=0): +def fit_plot(data_x, data_y, tmodel="gauss", ibg=1, silent=0, iplot=1, fitrange=0, fitclick=0): """ Get data for a completed scan, gaussian, voigt or lorentzian fit, default = gauss, plus linear background @@ -438,7 +449,7 @@ def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclic Args: datax , datay Example: - fit_plot(dx,dy, model='voigt', fitrange=0, fitclick=1) + fit_plot(dx,dy, tmodel='voigt', fitrange=0, fitclick=1) """ @@ -447,7 +458,9 @@ def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclic import numpy as np print("dy = ", data_y) - + print("MODEL = ", tmodel) + data_x = np.asarray(data_x) + data_y = np.asarray(data_y) ####### check the order of the data: lower --> upper ? if not, reverse all the indices! sz = len(data_x) if data_x[sz - 1] < data_x[0]: @@ -470,53 +483,52 @@ def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclic print("dy=", data_y) ##### - peak = GaussianModel(prefix = 'g_') - pre='g_' - if model == "voigt": - peak = VoigtModel(prefix = 'v_') - pre='v_' - if model == "lorentz": - peak = LorentzianModel(prefix = 'l_') - pre='l_' + peak = GaussianModel(prefix="g_") + pre = "g_" + if tmodel == "voigt": + peak = VoigtModel(prefix="v_") + pre = "v_" + if tmodel == "lorentz": + peak = LorentzianModel(prefix="l_") + pre = "l_" model = peak - ypeak = data_y - pars = model.guess(data_y, x = data_x) + ypeak = data_y + pars = model.guess(data_y, x=data_x) indmax = np.argmax(ypeak) - xm = data_x[indmax] - - if ibg: - background = LinearModel(prefix = 'bg_') - model = peak + background + xm = data_x[indmax] + if ibg: + background = LinearModel(prefix="bg_") + model = peak + background + ### estimate initial params separately for both parts of the model # 1) BG - n = len(data_x) - edge= n//10 - idx=np.r_[0:edge, n-edge:n] # first and last part of the range - #m0, b0 = np.polyfit(data_x[idx], data_y[idx],1) - #ypeak= data_y - (m0*data_x + b0) # subtract linear part + n = len(data_x) + edge = n // 10 + idx = np.r_[0:edge, n - edge : n] # first and last part of the range + # m0, b0 = np.polyfit(data_x[idx], data_y[idx],1) + # ypeak= data_y - (m0*data_x + b0) # subtract linear part - pars = background.guess(data_y[idx], x = data_x[idx]) - - # 2) Function - BG - ypeak= data_y - (pars['bg_slope']*data_x + pars['bg_intercept'])# subtract linear part - pars.update( peak.guess(ypeak, x= data_x) ) - + pars = background.guess(data_y[idx], x=data_x[idx]) + + # 2) Function - BG + ypeak = data_y - (pars["bg_slope"] * data_x + pars["bg_intercept"]) # subtract linear part + pars.update(peak.guess(ypeak, x=data_x)) g = model.fit(data_y, pars, x=data_x) ##### + if iplot: + plt.ion() + plt.figure() + plt.plot(data_x, data_y, "*") + plt.plot(data_x, g.best_fit) + plt.xlabel("x-axis") + plt.ylabel("signal") + plt.show() - plt.ion() - plt.figure() - plt.plot(data_x, data_y, "*") - plt.plot(data_x, g.best_fit) - plt.xlabel("x-axis") - plt.ylabel("signal") - plt.show() - - if fitclick: + if fitclick * iplot: print("Please define the x-range of the fit via click of graph") print("First click for start: ") x1 = plt.ginput(1) @@ -539,8 +551,8 @@ def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclic data_y = data_y[closest_low:closest_high] print("dx=", data_x) print("dy=", data_y) - - g, xm = justfit(data_x, data_y, model=model, ibg=ibg) + + g, xm = justfit(data_x, data_y, tmodel=tmodel, ibg=ibg) plt.figure() plt.plot(data_x, data_y, "*") @@ -550,25 +562,24 @@ def fit_plot(data_x, data_y, model="gauss", ibg=1, silent=0,fitrange=0, fitclic plt.ylabel("signal") plt.show() - # g.params - returncenvar = pre+"center" + returncenvar = pre + "center" gcen = g.params[returncenvar].value - print('gcen =', gcen) - if (not silent): + print("gcen =", gcen) + if not silent: print(g.fit_report()) # fit_id = pre+'_center' - - #center = g.pars[fit_id].value - return gcen, xm + + # center = g.pars[fit_id].value + return gcen, xm #################################### ### just SAVE DATA #################################### -def save_data(hindex: int, device_name: str, signal_name: str, isave = 1): +def save_data(hindex: int, device_name: str, signal_name: str, isave=1): """ Get data for a completed scan from the BEC history, and plot all the results, store in a CSV file @@ -576,13 +587,15 @@ def save_data(hindex: int, device_name: str, signal_name: str, isave = 1): Args: history_index (int): scan to fetch, e.g. -1 for the most recent scan device_name (str): the device for which to get the monitoring data, eg, device_name='dcm_pitch' - signal_name (str): the monitor, eg, signal_name='lu_bpmsum' + signal_name (str): the monitor, eg, signal_name='lu_bpmsum' """ h = bec.history[hindex] md = h.metadata["bec"] - print("Scan argument was ", md["args"]) + #print("Scan argument was ", md["args"]) + # or data_xd = md["positions"], data_x = data_xd.flatten() + # device_name = md["scan_report_devices"][0].decode() data_x = h.devices[device_name][device_name].read()["value"] data_y = h.devices[signal_name][signal_name].read()["value"] @@ -595,7 +608,7 @@ def save_data(hindex: int, device_name: str, signal_name: str, isave = 1): plt.ylabel(f"{signal_name} / AU") plt.show() - if isave: + if isave: ans = "n" ans = input("Store data in csv file? y/n ") if ans == "y": @@ -604,7 +617,7 @@ def save_data(hindex: int, device_name: str, signal_name: str, isave = 1): combined = np.column_stack((data_x, data_y)) filename = dirname + "Scan" + str(hindex) + device_name + ".txt" with open(filename, "w") as f: - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") return data_x, data_y @@ -673,7 +686,7 @@ def save_plot_gaps(hindex: int, device_name: str, signal_name: str): """ Get data for a completed scan from the BEC history, and plot all the results, store in a CSV file - For a certain gap size, scan through energy (Bragg angle) + For a certain gap size, scan through energy (Bragg angle) Args: history_index (int): scan to fetch, e.g. -1 for the most recent scan @@ -681,11 +694,12 @@ def save_plot_gaps(hindex: int, device_name: str, signal_name: str): signal_name (str): the monitor """ - from lmfit.models import LinearModel, GaussianModel import matplotlib.pyplot as plt h = bec.history[hindex] md = h.metadata["bec"] + #device_name = md["scan_report_devices"][0].decode() + # data = h.devices[device_name][signal].read()["value"] # data_x = h.devices.dcm_pitch.dcm_pitch.read()["value"] # data_y = h.devices.lu_bpmsum.lu_bpmsum.read()["value"] @@ -712,7 +726,7 @@ def save_plot_gaps(hindex: int, device_name: str, signal_name: str): combined = np.column_stack((en_vec, data_y)) filename = dirname + "EnScan" + str(hindex) + ".txt" with open(filename, "w") as f: - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") return @@ -750,10 +764,10 @@ def getdiodepos(diode="i1"): def read_mon(): - #from datetime import datetime + # from datetime import datetime e = getenergy() - #fesum = dev.fe_bpmsum.read()["fe_bpmsum"]["value"] + # fesum = dev.fe_bpmsum.read()["fe_bpmsum"]["value"] lusum = dev.lu_bpmsum.read()["lu_bpmsum"]["value"] sssum = dev.ss_bpmsum.read()["ss_bpmsum"]["value"] @@ -772,18 +786,17 @@ def read_mon(): vbpitch = dev.vfm_pitch.read()["vfm_pitch"]["value"] vbyaw = dev.vfm_yaw.read()["vfm_yaw"]["value"] vbroll = dev.vfm_roll.read()["vfm_roll"]["value"] - vfm_x= dev.vfm_x.read()["vfm_x"]["value"] - vfm_y= dev.vfm_y.read()["vfm_y"]["value"] - + vfm_x = dev.vfm_x.read()["vfm_x"]["value"] + vfm_y = dev.vfm_y.read()["vfm_y"]["value"] + # KB H hbu = dev.hfm_bu.read()["hfm_bu"]["value"] hbd = dev.hfm_bd.read()["hfm_bd"]["value"] hbpitch = dev.hfm_pitch.read()["hfm_pitch"]["value"] hbyaw = dev.hfm_yaw.read()["hfm_yaw"]["value"] hbroll = dev.hfm_roll.read()["hfm_roll"]["value"] - hfm_x= dev.hfm_x.read()["hfm_x"]["value"] - hfm_y= dev.hfm_y.read()["hfm_y"]["value"] - + hfm_x = dev.hfm_x.read()["hfm_x"]["value"] + hfm_y = dev.hfm_y.read()["hfm_y"]["value"] ## FE slits centre and size fe_sl_xcen = dev.fe_sl_xcen.read()["fe_sl_xcen"]["value"] @@ -828,19 +841,17 @@ def read_mon(): xeye_x_pos = dev.xi_x.read()["xi_x"]["value"] bcusum = 0 i1signal = 0 - #if xeye_x_pos < 0: + # if xeye_x_pos < 0: bcusum = dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"] i1signal = dev.i1.read()["i1"]["value"] print("Energy = ", e, " keV") - #print(f"fesum,lusum,sssum,bcusum,i1signal = {fesum,lusum,sssum,bcusum,i1signal}") + # print(f"fesum,lusum,sssum,bcusum,i1signal = {fesum,lusum,sssum,bcusum,i1signal}") print(f"lusum,sssum,bcusum,i1signal = {lusum,sssum,bcusum,i1signal}") print(f"bragg, pitch, perp, fpitch, froll, gap = {bragg, pitch,perp, fpitch, froll, gap}") # return e, fesum,lusum,sssum,bcusum,i1signal print("KB VERT") - print( - f"vbu, vbd, vbpitch,vbyaw,vbroll,vbvert = {vbu, vbd, vbpitch,vbyaw,vbroll,vfm_x, vfm_y}" - ) + print(f"vbu, vbd, vbpitch,vbyaw,vbroll,vbvert = {vbu, vbd, vbpitch,vbyaw,vbroll,vfm_x, vfm_y}") # return vbu, vbd, vbpitch,vbyaw,vbroll,vblat,vbvert print("KB HORI") print( @@ -855,41 +866,41 @@ def read_mon(): with open(filename, "w") as f: combined = np.column_stack((e, gap)) f.write("En and gap\n") - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("bragg, pitch,perp, fpitch, froll\n") combined = np.column_stack((bragg, pitch, perp, fpitch, froll)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("vbu, vbd, vbpitch,vbyaw,vbroll,vbvert\n") - combined = np.column_stack((vbu, vbd, vbpitch, vbyaw, vbroll, vfm_x, vfm_y)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + combined = np.column_stack((vbu, vbd, vbpitch, vbyaw, vbroll, vfm_x, vfm_y)) + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("hbu, hbd, hbpitch,hbyaw,hbroll,hblat,hbvert\n") - combined = np.column_stack((hbu, hbd, hbpitch, hbyaw, hbroll, hfm_x, hfm_y)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + combined = np.column_stack((hbu, hbd, hbpitch, hbyaw, hbroll, hfm_x, hfm_y)) + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("FE slits\n") combined = np.column_stack((fe_sl_xcen, fe_sl_xsize, fe_sl_ycen, fe_sl_ysize)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("BSF slits\n") combined = np.column_stack((bsf_sl_xcen, bsf_sl_xsize, bsf_sl_ycen, bsf_sl_ysize)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("SS slits\n") combined = np.column_stack((ss_sl_xcen, ss_sl_xsize, ss_sl_ycen, ss_sl_ysize)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") f.write("BCU slits\n") combined = np.column_stack((bcu_sl_xcen, bcu_sl_xsize, bcu_sl_ycen, bcu_sl_ysize)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") - #f.write("fesum,lusum,sssum,bcusum,i1signal\n") - #combined = np.column_stack((fesum, lusum, sssum, bcusum, i1signal)) + # f.write("fesum,lusum,sssum,bcusum,i1signal\n") + # combined = np.column_stack((fesum, lusum, sssum, bcusum, i1signal)) f.write("lusum,sssum,bcusum,i1signal\n") combined = np.column_stack((lusum, sssum, bcusum, i1signal)) - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.5f") return @@ -945,7 +956,7 @@ def colliscan(direction: str, range=0.3, nsteps=30, stime=0.5, centre=1): """ import sys - dock_area = bec.gui.new(name="ColliScan", geometry = [4000, 900, 700, 700]) + dock_area = bec.gui.new(name="ColliScan", geometry=[4000, 900, 700, 700]) wr = dock_area.new(bec.gui.available_widgets.Waveform) # check if i1 DIODE is IN @@ -1013,7 +1024,7 @@ def colliscan(direction: str, range=0.3, nsteps=30, stime=0.5, centre=1): ##################### ### SLITSCANS ##################### -def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0): +def slitscan(device_location: str, direction: str, range=1, nsteps=50, centre=0): """ Slit scan, and possible centre and plot all the results. @@ -1024,7 +1035,7 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 nsteps: default = 50 move to centre or not [0/1] - Example: slitscan("fe","h", 30, centre=1) + Example: slitscan("fe","h",1, 30, centre=1) """ @@ -1039,7 +1050,7 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 default_h = 1.0 ## 1.4 or 0.4 or ??? default_v = 1.0 ## 1.2 det = dev.lu_bpmsum - if direction in ["x","h"]: + if direction in ["x", "h"]: mot = dev.fe_sl_xcen size = dev.fe_sl_xsize s_closed = 0.1 @@ -1047,7 +1058,7 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 else: mot = dev.fe_sl_ycen size = dev.fe_sl_ysize - + s_closed = 0.1 s_open = default_v @@ -1120,7 +1131,7 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 print("Ok, cancelled") return - dock_area = bec.gui.new(name="SlitScan", geometry = [4000, 900, 700, 700]) + dock_area = bec.gui.new(name="SlitScan", geometry=[4000, 900, 700, 700]) wr = dock_area.new(bec.gui.available_widgets.Waveform) pos0 = mot.user_readback.get() @@ -1128,7 +1139,7 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 umv(size, s_closed) wr.plot(device_x=mot, device_y=det) - + wr.x_label = mot.name wr.y_label = det.name @@ -1142,9 +1153,9 @@ def slitscan(device_location: str, direction: str, range: 1, nsteps=50, centre=0 data_y = s.scan.live_data.lu_bpmsum.lu_bpmsum.val if mot.name == "fe_sl_xcen": - data_x = s.scan.live_data.fe_sxcen.fe_sxcen.val + data_x = s.scan.live_data.fe_sl_xcen.fe_sl_xcen.val if mot.name == "fe_sl_ycen": - data_x = s.scan.live_data.fe_sycen.fe_sycen.val + data_x = s.scan.live_data.fe_sl_ycen.fe_sl_ycen.val # BSF slits ================================ if mot.name == "bsf_sl_xcen": data_x = s.scan.live_data.bsf_sl_xcen.bsf_sl_xcen.val @@ -1295,7 +1306,7 @@ def bstatus(): ########################### -def detxeye_in(setdetzpos = 200, setdetypos = 213.6): +def detxeye_in(setdetzpos=200, setdetypos=213.6): """ moves in the Xray eye and the Diode below detector """ @@ -1304,14 +1315,14 @@ def detxeye_in(setdetzpos = 200, setdetypos = 213.6): setzoom = 0.1 setxrpos = 0.0 - #setdetzpos = 200 # 200 better? - #setdetypos = 213.6 + # setdetzpos = 200 # 200 better? + # setdetypos = 213.6 detzpos = dev.det_z.user_readback.get() detypos = dev.det_y.user_readback.get() print(f"detector position now: z = {detzpos}, y = {detypos}") - print(f"... moving to detector position: z = {setdetzpos}, y = {setdetypos}") + print(f"... moving to detector position: z = {setdetzpos}, y = {setdetypos}") ans = "n" ans = input("Is it ok to move detector in and up? y/n ") @@ -1326,14 +1337,14 @@ def detxeye_in(setdetzpos = 200, setdetypos = 213.6): print("moving X-ray eye below Eiger in") - #xrpos = dev.det_xi_x.user_readback.get() - #if abs(setxrpos - xrpos) > 1: + # xrpos = dev.det_xi_x.user_readback.get() + # if abs(setxrpos - xrpos) > 1: # umv(dev.det_xi_x, setxrpos) - #zoompos = dev.det_xi_zoom.user_readback.get() - #if abs(setzoom - zoompos) > 1: + # zoompos = dev.det_xi_zoom.user_readback.get() + # if abs(setzoom - zoompos) > 1: # umv(dev.det_xi_zoom, setzoom) - #focpos = dev.det_xi_focus.user_readback.get() - #if abs(setfoc - focpos) > 1: + # focpos = dev.det_xi_focus.user_readback.get() + # if abs(setfoc - focpos) > 1: # umv(dev.det_xi_focus, setfoc) @@ -1355,7 +1366,8 @@ def detxeye_out(): def measure_samcam(zoom=1000): - import sys + import sys + scinti_inpos = 36.4 # mm sc_rb = dev.diag_y.user_readback.get() if abs(scinti_inpos - sc_rb) > 0.3: @@ -1422,13 +1434,13 @@ def smoothplotgrad(dx, dy, reverse=0): plt.figure() plt.plot(dx, dy, "*") plt.plot(dx, gf) + plt.figure() - grf = np.gradient(gf) if reverse: grf = -grf plt.plot(dx, grf) - g, xm = justfit(dx, grf, model="gauss", ibg=0) + g, xm = justfit(dx, grf, tmodel="gauss", ibg=0) plt.plot(dx, g.best_fit) return @@ -1438,12 +1450,17 @@ def smoothplotgrad(dx, dy, reverse=0): ### enery scan program around a harmonic -def scan_eg(erange, nsteps=50, fit=True): +def scan_eg(erange, nsteps=50, iedge = 0, iresol = 0, fit=True): """ - Scan an energy range around a certain harmonic + Scan an energy range around a certain central energy, eg an edge or a harmonic scan_eg(erange, steps= 50, fit= True) # default scan_eg(erange, steps= 50, fit= False) # no fit wanted eg, erange = 12.4 *0.02 for a width of 2 % around 12.4 keV + FOR EN CAL check: + scan_eg(erange, steps= 50, iedge=1, fit= True) + eg, erange = 11.103 *0.005 for a width of 0.5 % around 11.103 keV + eg, erange = 11.103 *0.003 for a width of 0.3 % around 11.103 keV + """ import time from datetime import datetime @@ -1459,7 +1476,11 @@ def scan_eg(erange, nsteps=50, fit=True): e_start = e - erange e_end = e + erange - en_resol = (e_start - e_end) / nsteps + en_resol = 2* erange / nsteps + if iresol: + en_resol = float(input("Please enter resolution in keV ") ) + nsteps = round(2*erange/en_resol) + print(f"energy resolution is: {en_resol} keV") mot_scan = dev.dcm_bragg @@ -1480,6 +1501,8 @@ def scan_eg(erange, nsteps=50, fit=True): mot_perp_rb = mot_perp.user_readback.get() det = dev.lu_bpmsum + if iedge: + det = dev.bcu_bpmsum print(f"Position of bragg angle is {mot_scan_rb} mrad") print(f"Position of pitch angle is {mot_pitch_rb} mrad") @@ -1497,10 +1520,11 @@ def scan_eg(erange, nsteps=50, fit=True): s.scan.live_data.dcm_bragg.dcm_bragg.val ) # is a list and needs to be converted to array bpm_data = s.scan.live_data.lu_bpmsum.lu_bpmsum.val - + if iedge: + bpm_data = s.scan.live_data.bcu_bpmsum.bcu_bpmsum.val if fit: print("Bragg Angle") - bcen, xm = fit_plot(bragg_data, bpm_data, model="voigt", fitrange=1) + bcen, xm = fit_plot(bragg_data, bpm_data, tmodel="voigt", fitrange=1) print("") br_rad = np.array(bragg_data) / 1000.0 @@ -1508,8 +1532,14 @@ def scan_eg(erange, nsteps=50, fit=True): energy_data = 12.39842 / d0 / np.sin(br_rad) print("Energy") - ecen, xm = fit_plot(energy_data, bpm_data, model="voigt", fitclick=1) + ecen, xm = fit_plot(energy_data, bpm_data, tmodel="voigt", fitclick=1) + if iedge: + smoothplotgrad(bragg_data,bpm_data) + smoothplotgrad(energy_data,bpm_data) + + sete(e) # go back + ## save data to file ? ans = "n" @@ -1526,9 +1556,9 @@ def scan_eg(erange, nsteps=50, fit=True): tname = f"Scan_{timestamp}.txt" filename = dirname + tname with open(filename, "w") as f: - np.savetxt(f, combined, delimiter=",", fmt="%5f") + np.savetxt(f, combined, delimiter=",", fmt="%.9f") + - sete(e) # go back return @@ -1542,17 +1572,19 @@ def scan_eg(erange, nsteps=50, fit=True): ### open window/dock for long gap scan ################################################ # -### Note: all deprecated !!!! +### Note: all deprecated !!!! def scan_window(wname="Scan", fit=True): # Add a new dock with a Waveform to the BECDockArea - nam = "waveform_dock_"+wname - dock_area = bec.gui.new(name=wname, geometry = [4000, 900, 700, 700]) + nam = "waveform_dock_" + wname + dock_area = bec.gui.new(name=wname, geometry=[4000, 900, 700, 700]) - w1 = dock_area.new(bec.gui.available_widgets.Waveform) # CALL gui.available_widgets to see what is available + w1 = dock_area.new( + bec.gui.available_widgets.Waveform + ) # CALL gui.available_widgets to see what is available w2 = dock_area.new(bec.gui.available_widgets.ScanControl) # Add signals to the WaveformWidget - w1.plot(device_x = dev.id_gap, device_y = dev.lu_bpmsum) + w1.plot(device_x=dev.id_gap, device_y=dev.lu_bpmsum) ###w1.plot(device_x='id_gap', device_y='bpm') ###dock2 = dock_area.new(name="motor_dock", widget="MotorMap",relative_to=nam, position="right") @@ -1561,145 +1593,148 @@ def scan_window(wname="Scan", fit=True): gstart = 4.5 gend = 5.0 - resol = 0.003 # 3 um resol for gap scan - nsteps = round(gend-gstart)/resol - s = scans.line_scan(dev.id_gap, gstart, gend, steps = nsteps, exp_time = 0.1, relative = False) - - ### if done, remove - #w2.remove() + resol = 0.003 # 3 um resol for gap scan + nsteps = round(gend - gstart) / resol + s = scans.line_scan(dev.id_gap, gstart, gend, steps=nsteps, exp_time=0.1, relative=False) + + ### if done, remove + # w2.remove() + + ################################################ -### beam centre on detector with distance +### beam centre on detector with distance ################################################ -#/sls/x10sa/config/commissioning/Data +# /sls/x10sa/config/commissioning/Data + def readdist(filename): with open(filename, "r") as f: combined_data = np.loadtxt(f, delimiter=",") - + ind1 = 0 ind2 = 1 ind3 = 2 detdist = combined_data[:, ind1] bposx = combined_data[:, ind2] bposy = combined_data[:, ind3] - + plt.ion() plt.figure(1) - plt.plot(detdist,bposx, ".") + plt.plot(detdist, bposx, ".") plt.title("Bpos X vs Detectordist") plt.xlabel("dist / mm") plt.ylabel("bpos X in det pix ") - + plt.figure(2) - plt.plot(detdist,bposy, ".") + plt.plot(detdist, bposy, ".") plt.title("Bpos Y vs Detectordist") plt.xlabel("dist / mm") plt.ylabel("bpos Y in det pix ") plt.show() - return + return + ################################################ -### flux cal +### flux cal ################################################ + def read_av(signal, avlen): - #signal = dev.lu_bpmsum - + # signal = dev.lu_bpmsum + buf = [] - count =0 + count = 0 while count < avlen: - new_value = signal.read()[signal.name]['value'] - #new_value = dev.lu_bpmsum.read()['lu_bpmsum']["value"] + new_value = signal.read()[signal.name]["value"] + # new_value = dev.lu_bpmsum.read()['lu_bpmsum']["value"] buf.append(new_value) - count+=1 + count += 1 return sum(buf) / len(buf) - -def fitpoly(x,y, order): +def fitpoly(x, y, order): from scipy.optimize import curve_fit - + coeff = np.polyfit(x, y, order) polynomial = np.poly1d(coeff) print("Polynomial coefficients of the fit fct: ", order, coeff) x_fit = np.linspace(min(x), max(x), 100) y_fit = polynomial(x_fit) - + plt.ion() plt.figure() plt.title("X10SA Fl cal") plt.ylabel("fl/bcubpmsum") plt.title("E / keV") - + plt.plot(x, y, "*") plt.plot(x_fit, y_fit, color="blue") - def prepare_fluxmeas(): -# check all slits + # check all slits read_mon() -# + # print("preparing devices ...") ans = "n" ans = input("Is it safe to move det in and up and BS down? y/n ") if ans == "y": detxeye_in() bs_in = d["bs_y"].positions["in"] - #umv(dev.bs_y, bs_in +1) + # umv(dev.bs_y, bs_in +1) return -def fluxcal(start_en=13, stop_en=14, step =1): + +def fluxcal(start_en=13, stop_en=14, step=1): import time + # - tstart = time.perf_counter() # time logging - avlen= 10 - - dat_en=[] - dat_lubpm=[] - dat_ssbpm=[] - dat_bcubpm=[] - dat_xi=[] - dat_fluxi=[] - dat_quot_flbsum=[] - dat_i1=[] + tstart = time.perf_counter() # time logging + avlen = 10 + + dat_en = [] + dat_lubpm = [] + dat_ssbpm = [] + dat_bcubpm = [] + dat_xi = [] + dat_fluxi = [] + dat_quot_flbsum = [] + dat_i1 = [] diode_inpos = dev.diag_y.user_parameter["i1"] diode_outpos = dev.diag_y.user_parameter["out"] - for ei in range(start_en,stop_en+1, step): - bl_energy(ei*1000, plot = False) # in ev + for ei in range(start_en, stop_en + 1, step): + bl_energy(ei * 1000, plot=False) # in ev time.sleep(0.5) - #lusum = dev.lu_bpmsum.read()["lu_bpmsum"]["value"] - signal = dev.lu_bpmsum # "lu_bpmsum" + # lusum = dev.lu_bpmsum.read()["lu_bpmsum"]["value"] + signal = dev.lu_bpmsum # "lu_bpmsum" lusum = read_av(signal, avlen) - #sssum = dev.ss_bpmsum.read()["ss_bpmsum"]["value"] - signal = dev.ss_bpmsum # "ss_bpmsum" + # sssum = dev.ss_bpmsum.read()["ss_bpmsum"]["value"] + signal = dev.ss_bpmsum # "ss_bpmsum" sssum = read_av(signal, avlen) - - #bcusum = dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"] - signal = dev.bcu_bpmsum #"bcu_bpmsum" + + # bcusum = dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"] + signal = dev.bcu_bpmsum # "bcu_bpmsum" bcusum = read_av(signal, avlen) - - #xi = dev.xidiode.read()["xidiode"]["value"] - signal = dev.xidiode + # xi = dev.xidiode.read()["xidiode"]["value"] + signal = dev.xidiode xi = read_av(signal, avlen) - - fluxi = flux_x10sa(xi, ei, silent =1) - quot = fluxi/bcusum - - #i1diode = dev.i1.read()["i1"]["value"] - umv(dev.diag_y,diode_inpos) - signal = dev.i1 - i1diode = read_av(signal, avlen) - umv(dev.diag_y,diode_outpos) - - print(f'En {ei}, BCUSUM {bcusum}, xi {xi}, flux {fluxi}') + fluxi = flux_x10sa(xi, ei, silent=1) + quot = fluxi / bcusum + + # i1diode = dev.i1.read()["i1"]["value"] + umv(dev.diag_y, diode_inpos) + signal = dev.i1 + i1diode = read_av(signal, avlen) + umv(dev.diag_y, diode_outpos) + + print(f"En {ei}, BCUSUM {bcusum}, xi {xi}, flux {fluxi}") dat_en.append(ei) dat_lubpm.append(lusum) @@ -1709,27 +1744,28 @@ def fluxcal(start_en=13, stop_en=14, step =1): dat_fluxi.append(fluxi) dat_quot_flbsum.append(quot) dat_i1.append(i1diode) - - combined = np.column_stack((dat_en, dat_lubpm,dat_ssbpm,dat_bcubpm,dat_xi,dat_fluxi,dat_quot_flbsum, dat_i1)) - + + combined = np.column_stack( + (dat_en, dat_lubpm, dat_ssbpm, dat_bcubpm, dat_xi, dat_fluxi, dat_quot_flbsum, dat_i1) + ) + # open file for output timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") dirname = "/sls/x10sa/config/commissioning/Data/" filename = dirname + f"FLUX_x10sa_{timestamp}.txt" with open(filename, "w") as f: - np.savetxt(f, combined, delimiter=",", fmt="%.9f") # fmt for savetxt + np.savetxt(f, combined, delimiter=",", fmt="%.9f") # fmt for savetxt - - elapsed_time = time.perf_counter()-tstart - print(f"Runtime: {elapsed_time: .6f} seconds, i.e., {elapsed_time/60: .6f} min") - # CLOSE AND MOVE BACK ############ + elapsed_time = time.perf_counter() - tstart + print(f"Runtime: {elapsed_time: .6f} seconds, i.e., {elapsed_time/60: .6f} min") + # CLOSE AND MOVE BACK ############ # close shutter ! - + print("close the shutter !") - dev.bcu_shutter.put(0) - - # move beamstop back ! - #umv(dev.bs_y, bs_in) + dev.bcu_shutter.put(0) + + # move beamstop back ! + # umv(dev.bs_y, bs_in) fitpoly(dat_en, dat_quot_flbsum, 3) diff --git a/pxii_bec/macros/Undu_Helpers.py b/pxii_bec/macros/Undu_Helpers.py index 0302550..5c55b8e 100755 --- a/pxii_bec/macros/Undu_Helpers.py +++ b/pxii_bec/macros/Undu_Helpers.py @@ -45,8 +45,42 @@ def exponential_func2(x, a, b, c, d): ################################## - +# ##### Sept 2026 Data ###################################### def return_harmon(): + # from 3rd to 19th Energy range from 6 keV to 30 keV + # harmonics, Sept 2026 + import numpy as np + + nharm = 19 + h = [np.array([]) for _ in range(nharm + 1)] + h[0] = np.array([0]) + h[1] = np.array([0]) + h[2] = np.array([0]) + h[3] = np.array([0.095, -0.276, 4.154]) # 3rd harmon + h[4] = np.array([0.00692593, -0.131504, 1.45763, -0.864603]) + h[5] = np.array([0.00269444 , - 0.0616905 , 0.967925, - 0.295405]) + h[6] = np.array([ 0.00248148 , - 0.0767908 , 1.2178 , - 1.91516 ]) + h[7] = np.array([0.00110017 , - 0.0369968 , 0.77334 ,- 0.688198 ]) + h[8] = np.array([0.000643939 , - 0.0226645 , 0.570453 , - 0.0479048 ]) + h[9] = np.array([0.00021633 , -0.00619517 , 0.315392 , 0.971667 ]) + h[10] = np.array([ 0.000708236 , - 0.0357576 , 0.851686 - 2.48765 ]) + h[11] = np.array([ 0.000589549 , -0.0328631 , 0.837705 , - 2.90326 ]) + h[12] = np.array([ 0.00218182 , 0.1212 , 1.74739 ]) + h[13] = np.array([ 0.197702, 0.757869 ]) + h[14] = np.array([ 0.183733, 0.762778 ]) + h[15] = np.array([ 0.170381, 0.783155 ]) + h[16] = np.array([ 0.157321, 0.850464 ]) + h[17] = np.array([0.1466, 0.8906 ]) + h[18] = np.array([0.1362, 0.9538 ]) + h[19] = np.array([0.128, 0.985 ]) + + + fitpar_u19 = np.array([ 2.22322523, 0.50396248, -0.00783873]) # measured from U19 + + return h + +# ##### July 2025 Data ###################################### +def return_harmon_old(): # from 3rd to 21th Energy range from 6 keV to 30 keV # harmonics, July 2025 import numpy as np @@ -97,7 +131,7 @@ def plot_harmon(e_start, e_end, h_no, pr_out=False): plt.show() -def setu19(en, *harm_no, detune=0): +def setu19(en, *harm_no, detune=0, simul = 0, iexp = 0): """ set the U19 to the gaps defined in Jul2025, or the "theoretical" ones for higher harmonics @@ -106,10 +140,12 @@ def setu19(en, *harm_no, detune=0): en in keV, possibly select a special harmonics, or detune [0/1] to a value with a nicer beam shape but less flux """ - g0 = dev.id_gap.readback.get() - - fitpar_u19 = np.array([2.17078531, 0.519452, -0.00720255]) # measured from U19 + if not simul: + g0 = dev.id_gap.readback.get() + #fitpar_u19 = np.array([2.17078531, 0.519452, -0.00720255]) # measured from U19 + fitpar_u19 = np.array([ 2.22322523, 0.50396248, -0.00783873]) # measured from U19, Sept2026 + h_all = return_harmon() # dep on energy -> select harmonics if harm_no: @@ -120,18 +156,26 @@ def setu19(en, *harm_no, detune=0): print("Using the default optimised harmonics") if en <= 7: harm = 3 # h = h[3] - elif 7 < en <= 10: + elif 7 < en <= 10.5: harm = 5 - elif 10 < en <= 13: + elif 10.5 < en < 13: harm = 7 - elif 13 < en <= 16: + elif 13 <= en <= 16.5: harm = 9 - elif 16 < en <= 19: + elif 16.5 < en <= 19.5: harm = 11 - elif 19 < en <= 22: + elif 19.5 < en <= 22.5: harm = 13 + elif 22.5 < en <= 25.5: ## 22 + harm = 15 + elif 25.5 < en <= 28.5: ## 22 + harm = 17 + elif 28.5 < en <= 30: ## 22 + harm = 19 + + - if en <= 22: + if (en <= 30 and harm <=19): h = h_all[harm] print(f"harmonics is {harm}.") @@ -146,32 +190,33 @@ def setu19(en, *harm_no, detune=0): # 29: 15, 17, 19 # 30: 19 ## - if 23 < en <= 25: - n = 15 + if (iexp and harm_no): + harm_selected = int(harm_no[0]) + print("Selected harmonics is:", harm_selected) + n= harm_selected enorm = en / n g = exponential_func0(enorm, fitpar_u19[0], fitpar_u19[1], fitpar_u19[2]) - elif 25 < en <= 29: - n = 17 - enorm = en / n - g = exponential_func0(enorm, fitpar_u19[0], fitpar_u19[1], fitpar_u19[2]) - elif 29 < en: - n = 19 + elif (iexp): + n = harm # default for that energy range, as set before enorm = en / n + print('exp fct fit used') g = exponential_func0(enorm, fitpar_u19[0], fitpar_u19[1], fitpar_u19[2]) - print("Undulator has been ", g0, " mm") - if 4.5 <= g <= 20: - print("Moving Undulator gap to ", g, " mm") - else: - print("not a valid gap, do nothing") + + if not simul: + print("Undulator has been ", g0, " mm") + if 4.5 <= g <= 20: + print("Moving Undulator gap to ", g, " mm") + else: + print("not a valid gap, do nothing") - if detune: - g = g * 0.996 - print("moving to detuned gap value, slightly below max, about 0.15 % ") + if detune: + g = g * 0.996 + print("moving to detuned gap value, slightly below max, about 0.15 % ") # print("move disabled!!") - res = scans.umv(dev.id_gap, g, relative=False) + scans.umv(dev.id_gap, g, relative=False) - return + return round(g,4) ################################## @@ -192,7 +237,8 @@ def harmon_walk(estart=7.5, end_en=13): ################################## def gap_harm(e=12.4): - fitpar_u19 = np.array([2.17078531, 0.519452, -0.00720255]) + # old values fitpar_u19 = np.array([2.17078531, 0.519452, -0.00720255]) + fitpar_u19 = np.array([2.22322523, 0.50396248, -0.00783873]) fitpar_u17 = np.array([2.17078572, 0.46477267, -0.005766]) e = float(input("Please enter an energy between 6 and 30: ")) @@ -305,9 +351,26 @@ def long_gscan(estart=6, end_en=30.5, g_low=4.5, g_high=9.0, nsteps=1000, sett = return +def plottest(wname): + #wname="GapScan" + existing_docks = bec.gui.windows + if existing_docks.get(wname) is None: + dock_area = bec.gui.new(name=wname, geometry = [4000, 900, 700, 700]) + wr = dock_area.new(bec.gui.available_widgets.Waveform, object_name="Plot") + mot = dev.id_gap + det = dev.lu_bpmsum + else: + dock_area = open_docks[wname] + wr= dock_area.Plot + wr.plot(device_x=mot, device_y=det) ## names first ! + # wr.plot(x=mot.name,y=det.name) ### this comes later + wr.x_label = mot.name + wr.y_label = det.name + + ################################## -def gscan(centre=0, gomax=0, detune=0,sett=1): +def gscan(deltag = 0.05,centre=0, gomax=0, detune=0,sett=1, silent=0, iplot=1, isave=0): """ Scan the ID GAP and go to the max of lu_bpm intensity @@ -319,34 +382,58 @@ def gscan(centre=0, gomax=0, detune=0,sett=1): """ import time - dock_area = bec.gui.new(name="GapScan", geometry = [4000, 900, 700, 700]) - wr = dock_area.new(bec.gui.available_widgets.Waveform) + wname="GapScan" + mot = dev.id_gap - det = dev.lu_bpmsum + det = dev.lu_bpmsum + + existing_docks = bec.gui.windows + if existing_docks.get(wname) is None: + dock_area = bec.gui.new(name=wname, geometry = [4000, 900, 700, 700]) + wr = dock_area.new(bec.gui.available_widgets.Waveform, object_name="Plot") + else: + dock_area = existing_docks[wname] + wr= dock_area.Plot + wr.clear_all() wr.plot(device_x=mot, device_y=det) ## names first ! # wr.plot(x=mot.name,y=det.name) ### this comes later wr.x_label = mot.name wr.y_label = det.name g0 = dev.id_gap.readback.get() - deltag = 0.05 + #deltag = 0.05 steps = 30 resol = 2*deltag/steps + if resol < 0.002: + resol = 0.003 + steps = round(2*deltag/resol) + + start=-deltag + stop = deltag print(f"Gap resol is {resol} mm ") - - ds = scans.line_scan(dev.id_gap, -deltag, deltag, steps=steps, settling_time=sett,exp_time=0.5, relative=True) + if (g0-deltag < 4.5): + start=g0-4.5 + ds = scans.line_scan(dev.id_gap, start, stop, steps=steps, settling_time=sett,exp_time=0.1, relative=True) gap_data = ds.scan.live_data.id_gap.id_gap.val bpm_data = ds.scan.live_data.lu_bpmsum.lu_bpmsum.val gap_data=np.array(gap_data) bpm_data = np.array(bpm_data) - + + if isave: + en = round(getenergy()) + dirname = "/sls/x10sa/config/commissioning/Data/GapsFE_0p3/" + combined = np.column_stack((gap_data, bpm_data)) + filename = dirname + "gap" + str(en)+"_"+str(g0*100)[0:3]+ ".txt" + with open(filename, "w") as f: + np.savetxt(f, combined, delimiter=",", fmt="%5f") + maxy = max(bpm_data) indmax = np.argmax(bpm_data) - gm = gap_data[indmax] + gmax = gap_data[indmax] - gcen, xm = fit_plot(gap_data, bpm_data, model="voigt") + gcen, xm = fit_plot(gap_data, bpm_data, model="voigt", silent=silent, iplot=iplot) if gomax: gm = xm @@ -354,6 +441,8 @@ def gscan(centre=0, gomax=0, detune=0,sett=1): gm = gcen print("gap off by ", g0 - gm, " mm") + + print(f"gcen = {gcen}, gmax = {xm} or {gmax}") if detune: gm = gm * 0.996 print("moving to detuned gap value, slightly (0.15 %) below max") @@ -366,4 +455,31 @@ def gscan(centre=0, gomax=0, detune=0,sett=1): else: print("Fit too far off, try using option gomax=1") - return + return gm, xm + +################################## + +def bfield_x10sa(gap): + if gap < 4.5 or gap > 20: + print("no valid gap!") + return + + ### values for the U19 + l_u = 19 + a = 3.22482942 + b = -0.30100054 + c = 0.00808649 + + b0 = exponential_func0(gap, a,b,c ) + b0= round(b0,4) + # K = e/mc * l_u/2pi * B + k = round(0.0934 *l_u * b0,2) + + print(f'### X10SA and U19 ### ') + print(f'for a gap of {gap} mm : ') + print(f'B0 = {b0}') + print(f'K = {k}') + + return b0, k + +################################## diff --git a/pxii_bec/macros/beamline_planner.py b/pxii_bec/macros/beamline_planner.py index 96b22b6..6ec815c 100755 --- a/pxii_bec/macros/beamline_planner.py +++ b/pxii_bec/macros/beamline_planner.py @@ -2,6 +2,7 @@ from collections import defaultdict, deque from enum import Enum + # from pxii_bec.scripts import beamline_context as bl # from enums import BeamlineState, TemperatureMode @@ -74,7 +75,6 @@ class StateChangePlanner: state_name = BeamlineState(state_name) target = self._merged_state(state_name, modifier) - plan = self._plan(target) print("PLAN:") @@ -102,7 +102,7 @@ class StateChangePlanner: def available_states(self): """Return a list of available states""" return list(self.states.keys()) - + def state_devices(self): """Return the set of devices that participate in beamline state definitions""" @@ -111,14 +111,13 @@ class StateChangePlanner: for state in self.states.values(): devices.update(state.keys()) - + return sorted(devices) def get_positions(self): """Return current positions of devices that are used in state transitions""" - return {name: self.devices[name].pos - for name in self.state_devices()} + return {name: self.devices[name].pos for name in self.state_devices()} def print_positions(self): """Return current state of all devices""" @@ -131,7 +130,7 @@ class StateChangePlanner: """Return a dict of {device: (before, after)} pairs for devices that changed state""" after = self.get_positions() return {k: (before[k], after[k]) for k in before if before[k] != after[k]} - + def __get_closest_states(self): """ Return states ordered by number of mismatches. @@ -148,21 +147,9 @@ class StateChangePlanner: for dev, expected in config.items(): if not self.devices[dev].is_at(expected): - mismatches.append( - ( - dev, - self.devices[dev].pos, - expected, - ) - ) + mismatches.append((dev, self.devices[dev].pos, expected)) - results.append( - ( - state.name, - len(mismatches), - mismatches, - ) - ) + results.append((state.name, len(mismatches), mismatches)) return sorted(results, key=lambda x: x[1]) @@ -173,10 +160,7 @@ class StateChangePlanner: print(f"\n{state}: {count} mismatch(es)") for dev, actual, expected in mismatches: - print( - f" {dev:15s} " - f"{actual:10s} -> {expected}" - ) + print(f" {dev:15s} " f"{actual:10s} -> {expected}") def current_state(self): """Return all current matching BeamlineState and TemperatureMode combinations, diff --git a/pxii_bec/macros/build_devices.py b/pxii_bec/macros/build_devices.py index 434e955..f723a60 100755 --- a/pxii_bec/macros/build_devices.py +++ b/pxii_bec/macros/build_devices.py @@ -63,14 +63,7 @@ def build_devices(yaml_file): return state_devices -def update_position( - device, - position, - value, - axis=None, - force=False, - max_delta=0.5, -): +def update_position(device, position, value, axis=None, force=False, max_delta=0.5): """ Update a single saved position in memory. @@ -189,13 +182,7 @@ def update_positions(updates): return save_and_reload() -def save_current_position( - device, - position, - axis=None, - force=False, - max_delta=0.5, -): +def save_current_position(device, position, axis=None, force=False, max_delta=0.5): """ Save the current motor position as a named position. """ diff --git a/pxii_bec/macros/calculator.py b/pxii_bec/macros/calculator.py index c988c7e..65185f2 100755 --- a/pxii_bec/macros/calculator.py +++ b/pxii_bec/macros/calculator.py @@ -2,6 +2,7 @@ from dataclasses import dataclass import numpy as np + # from pxii_parameters import (EnergyDefaults, CamConversion) @@ -60,9 +61,7 @@ def convert_to_degrees(angle_mrad: float) -> float: return np.rad2deg(angle_mrad / 1000) -def create_conversion_result( - energy_ev: float, wavelength: float, bragg_angle_mrad: float -) -> dict: +def create_conversion_result(energy_ev: float, wavelength: float, bragg_angle_mrad: float) -> dict: """ Creates a dictionary containing converted values of energy and angles. @@ -226,9 +225,7 @@ def validate_energy(energy_ev): return energy_ev -def convert_from_bragg( - bragg_angle_mrad: float, temp=120, print_result: bool = False -) -> dict: +def convert_from_bragg(bragg_angle_mrad: float, temp=120, print_result: bool = False) -> dict: """ Convert the Bragg angle to wavelength and energy, returning the result as a dictionary. @@ -274,9 +271,7 @@ def convert_from_energy(energy_ev: float, temp=120, print_result: bool = False) """ energy_ev = validate_energy(energy_ev) wavelength = calculate_wavelength_from_energy(energy_ev) - bragg_angle_mrad = float( - calculate_bragg_angle_from_wavelength(wavelength, temp=temp) - ) + bragg_angle_mrad = float(calculate_bragg_angle_from_wavelength(wavelength, temp=temp)) result = create_conversion_result(energy_ev, wavelength, bragg_angle_mrad) if print_result: print_conversion_result(result) @@ -284,9 +279,7 @@ def convert_from_energy(energy_ev: float, temp=120, print_result: bool = False) def convert_from_wavelength( - wavelength: float, - temp: float = 120, - print_result: bool = False, + wavelength: float, temp: float = 120, print_result: bool = False ) -> dict: """ Convert a given wavelength value into corresponding energy, Bragg angle, and @@ -312,19 +305,14 @@ def convert_from_wavelength( outside of the permitted range, returns None. """ energy_ev = calculate_energy_from_wavelength(wavelength) - bragg_angle_mrad = float( - calculate_bragg_angle_from_wavelength(wavelength, temp=temp) - ) + bragg_angle_mrad = float(calculate_bragg_angle_from_wavelength(wavelength, temp=temp)) result = create_conversion_result(energy_ev, wavelength, bragg_angle_mrad) if print_result: print_conversion_result(result) return result -def calc_perp_position( - energy_ev: float, - print_result: bool = False, -) -> float: +def calc_perp_position(energy_ev: float, print_result: bool = False) -> float: """ Calculate the perpendicular motor position based on provided energy in electron-volts (eV). @@ -355,7 +343,7 @@ def calc_scam_microns(pixels, zoom=1000): """Convert pixels to microns for the sample camera""" return float(pixels / (CamConversion.a * np.exp(CamConversion.b * zoom))) + def calc_sscam_microns(pixels): """Convert pixels to microns for the ss camera""" - return pixels*20 - + return pixels * 20 diff --git a/pxii_bec/macros/calibrate_bpm.py b/pxii_bec/macros/calibrate_bpm.py index c0ca7c3..e0a2860 100644 --- a/pxii_bec/macros/calibrate_bpm.py +++ b/pxii_bec/macros/calibrate_bpm.py @@ -8,6 +8,8 @@ import numpy as np import pandas as pd import matplotlib.pyplot as plt +from pxii_bec.scripts import beamline_context as bl +from pxii_bec.macros.beamline_planner import BeamlineState # def compute_norm(n_readings=5): # """Average normalized BPM X/Y from repeated sets of four channels.""" @@ -35,7 +37,161 @@ import matplotlib.pyplot as plt # time.sleep(0.1) # return float(np.mean(xn_values)), float(np.mean(yn_values)) +def compute_norm_ss(n_readings=5): + """Average normalized BPM X/Y from repeated sets of four channels.""" + xn_values = [] + yn_values = [] + + for _ in range(n_readings): + bpm1 = dev.ss_bpm1.read()["ss_bpm1"]["value"] + bpm2 = dev.ss_bpm2.read()["ss_bpm2"]["value"] + bpm3 = dev.ss_bpm3.read()["ss_bpm3"]["value"] + bpm4 = dev.ss_bpm4.read()["ss_bpm4"]["value"] + + total = bpm1 + bpm2 + bpm3 + bpm4 + + if total == 0: + continue + + xn = ((bpm1 + bpm2) - (bpm3 + bpm4)) / total + yn = ((bpm1 + bpm3) - (bpm2 + bpm4)) / total + + xn_values.append(xn) + yn_values.append(yn) + + time.sleep(0.1) + + return float(np.mean(xn_values)), float(np.mean(yn_values)) + + +def compute_norm_lu(n_readings=5): + """Average normalized BPM X/Y from repeated sets of four channels.""" + + xn_values = [] + yn_values = [] + + for _ in range(n_readings): + bpm1 = dev.lu_bpm1.read()["lu_bpm1"]["value"] + bpm2 = dev.lu_bpm2.read()["lu_bpm2"]["value"] + bpm3 = dev.lu_bpm3.read()["lu_bpm3"]["value"] + bpm4 = dev.lu_bpm4.read()["lu_bpm4"]["value"] + + total = bpm1 + bpm2 + bpm3 + bpm4 + + if total == 0: + continue + + xn = ((bpm1 + bpm2) - (bpm3 + bpm4)) / total + yn = ((bpm1 + bpm3) - (bpm2 + bpm4)) / total + + xn_values.append(xn) + yn_values.append(yn) + + time.sleep(0.1) + + return float(np.mean(xn_values)), float(np.mean(yn_values)) + + +def run_calibration_ss(): + """Scan bpm in x and y and record normalised x and y positions""" + centred_x = 2.8625 + centred_y = -1.3874 + now = datetime.now() + fnow = filenow() + print(f"moving bpm to {centred_x}, {centred_y}") + umv(dev.ss_bpm_y, centred_y) + umv(dev.ss_bpm_x, centred_x) + + # # # Calibrate in X + # umv(dev.bsf_sl_xsize, 0.3) + # umv(dev.bsf_sl_xsize, 0.4) + # filename = bl.env.lut_dir / "bpm_calib" / f"ss_bpm_x_{fnow}.csv" + # xn_data = [] + # x_positions = np.linspace(centred_x - 0.3, centred_x + 0.3, 51) + # with open(filename, "w", encoding="utf-8") as f: + # f.write(f"Xpos,Xnorm,BPM1,BPM2,BPM3,BPM4\n") + # for x in x_positions: + # umv(dev.ss_bpm_x, x) + # print(f"Moving BPM X to {x} mm") + # time.sleep(0.2) + # xn, yn = compute_norm_ss() + # xn_data.append(xn) + # with open(filename, "a", encoding="utf-8") as f: + # f.write(f"{x},{xn}\n") + + # # Calibrate in Y + umv(dev.ss_bpm_x, centred_x) + umv(dev.bsf_sl_xsize, 0.3) + umv(dev.bsf_sl_ysize, 0.4) + filename = bl.env.lut_dir / "bpm_calib" / f"ss_bpm_y_{fnow}.csv" + y_positions = np.linspace(centred_y - 0.04, centred_y + 0.04, 41) + yn_data = [] + with open(filename, "w", encoding="utf-8") as f: + f.write(f"Ypos,Ynorm,BPM1,BPM2,BPM3,BPM4\n") + for y in y_positions: + umv(dev.ss_bpm_y, y) + print(f"Moving BPM Y to {y} mm") + time.sleep(0.2) + xn, yn = compute_norm_ss() + yn_data.append(yn) + with open(filename, "a", encoding="utf-8") as f: + f.write(f"{y},{yn}\n") + + umv(dev.ss_bpm_x, centred_x) + umv(dev.ss_bpm_y, centred_y) + umv(dev.bsf_sl_ysize, 3.0) + umv(dev.bsf_sl_xsize, 3.0) + +def run_calibration_lu(): + """Scan bpm in x and y and record normalised x and y positions""" + centred_x = 2.1816 + centred_y = 3.0175 + now = datetime.now() + fnow = filenow() + print(f"moving bpm to {centred_x}, {centred_y}") + umv(dev.lu_bpm_y, centred_y) + umv(dev.lu_bpm_x, centred_x) + + # # Calibrate in X + # umv(dev.bsf_sl_xsize, 0.3) + # umv(dev.bsf_sl_xsize, 0.4) + # filename = bl.env.lut_dir / "bpm_calib" / f"lu_bpm_x_{fnow}.csv" + # xn_data = [] + # x_positions = np.linspace(centred_x - 0.3, centred_x + 0.3, 51) + # with open(filename, "w", encoding="utf-8") as f: + # f.write(f"Xpos,Xnorm\n") + # for x in x_positions: + # umv(dev.lu_bpm_x, x) + # print(f"Moving BPM X to {x} mm") + # time.sleep(0.2) + # xn, yn = compute_norm_lu() + # xn_data.append(xn) + # with open(filename, "a", encoding="utf-8") as f: + # f.write(f"{x},{xn}\n") + + # # Calibrate in Y + umv(dev.lu_bpm_x, centred_x) + umv(dev.bsf_sl_xsize, 0.3) + umv(dev.bsf_sl_ysize, 0.4) + filename = bl.env.lut_dir / "bpm_calib" / f"lu_bpm_y_{fnow}.csv" + y_positions = np.linspace(centred_y - 0.04, centred_y + 0.04, 41) + yn_data = [] + with open(filename, "w", encoding="utf-8") as f: + f.write(f"Ypos,Ynorm,BPM1,BPM2,BPM3,BPM4\n") + for y in y_positions: + umv(dev.lu_bpm_y, y) + print(f"Moving BPM Y to {y} mm") + time.sleep(0.2) + xn, yn = compute_norm_lu() + yn_data.append(yn) + with open(filename, "a", encoding="utf-8") as f: + f.write(f"{y},{yn}\n") + + umv(dev.lu_bpm_x, centred_x) + umv(dev.lu_bpm_y, centred_y) + umv(dev.bsf_sl_ysize, 3.0) + umv(dev.bsf_sl_xsize, 3.0) def run_calibration(env): """Scan bpm in x and y and record normalised x and y positions""" @@ -86,27 +242,20 @@ def run_calibration(env): umv(dev.bcu_bpm_y, centred_y) umv(dev.ss_sl_ysize, 3.0) -def read_bpm_calibration_data( - env, - x_y, - rundate, - bpm_name="bcu_bpm", -): + +def read_bpm_calibration_data(env, x_y, rundate, bpm_name="bcu_bpm"): """Read BPM calibration data from CSV.""" filename = env.lut_dir / f"bpm_calib/{bpm_name}_{x_y}_{rundate}.csv" - data = np.loadtxt( - filename, - delimiter=",", - skiprows=1, - ) + data = np.loadtxt(filename, delimiter=",", skiprows=1) position = data[:, 0] bpm_norm = data[:, 1] return position, bpm_norm + # def fit_bpm_linear(env, filename, x_y, plot=True): # """ # Fit the linear region of a BPM calibration scan. @@ -200,31 +349,17 @@ def read_bpm_calibration_data( # plt.tight_layout() # plt.show() -# return result - - +# return result def polynomial_calibration( - env, - x_y, - rundate, - bpm_name="bcu_bpm", - degree=5, - signal_limit=0.9, - plot=True, + env, x_y, rundate, bpm_name="bcu_bpm", degree=5, signal_limit=0.9, plot=True ): # BPM centre during calibration run - bpm_centre = {"x": 0.441,"y": -1.5405,} + bpm_centre = {"x": 0.441, "y": -1.5405} - - xdata, ydata = read_bpm_calibration_data( - env, - x_y, - rundate=rundate, - bpm_name=bpm_name, - ) + xdata, ydata = read_bpm_calibration_data(env, x_y, rundate=rundate, bpm_name=bpm_name) # Keep useful BPM range mask = np.abs(ydata) < signal_limit @@ -262,26 +397,13 @@ def polynomial_calibration( print(f" crosshair_bpm_position = None") if plot: - signal_fit = np.linspace( - signal.min(), - signal.max(), - 500, - ) + signal_fit = np.linspace(signal.min(), signal.max(), 500) position_fit = np.polyval(coeffs, signal_fit) plt.figure(figsize=(7, 5)) - plt.scatter( - ydata, - xdata, - alpha=0.4, - label="Calibration data", - ) - plt.plot( - signal_fit, - position_fit, - label=f"{degree}th-order polynomial", - ) + plt.scatter(ydata, xdata, alpha=0.4, label="Calibration data") + plt.plot(signal_fit, position_fit, label=f"{degree}th-order polynomial") plt.xlabel(f"BPM {x_y} normalized signal") plt.ylabel(f"BPM {x_y} position (mm)") @@ -292,5 +414,3 @@ def polynomial_calibration( plt.show() return coeffs - - diff --git a/pxii_bec/macros/daily.py b/pxii_bec/macros/daily.py index 2826182..869f46c 100644 --- a/pxii_bec/macros/daily.py +++ b/pxii_bec/macros/daily.py @@ -1,458 +1,1137 @@ -""" Files needed for the daily setup routine for local contacts """ +"""Daily setup routine at for local contacts""" + +# ============================================================================= +# Imports / context +# ============================================================================= import time +import subprocess +import shlex +import csv +import base64 +import html as html_lib +import numpy as np +from datetime import datetime +from contextlib import contextmanager +from dataclasses import dataclass from pxii_bec.scripts import beamline_context as bl from pxii_bec.macros.beamline_planner import BeamlineState +import matplotlib.pyplot as plt +from matplotlib.patches import Ellipse -def daily_setup(energy=12400): - """ The daily setup workflow """ - - plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() - - messages = [] - - progress_state = { - "Set energy": "todo", - "Align beamstop": "todo", - "Measure beam position": "todo", - "Align collimator": "todo", - "Measure flux": "todo", - "Save new positions to BEC": "todo", - # "Align rotation axis": "todo", - } - - update_log(log, messages, "Starting daily beamline setup") - - clear_daily_plots - # # Step 1: set beamline energy - set_progress(progress, progress_state, "Set energy", "running") - daily_energy(energy,plot_widget=plot1) - set_progress(progress, progress_state, "Set energy", "done") - update_log(log, messages, f"Beamline energy is {get_current_energy():.5g} eV") - - # Step 2: align beamstop - clear_daily_plots() - set_progress(progress, progress_state, "Align beamstop", "running") - bs_result = daily_beamstop() - set_progress(progress, progress_state, "Align beamstop", "done") - update_log(log, messages, f"Beamstop centred: " - f"X centre: {bs_result['x_position']:.4g}, " - f"Y centre: {bs_result['y_position']:.4g}" - ) - save_current_position(dev.bs_x, 'in') - save_current_position(dev.bs_y, 'in') - set_progress(progress, progress_state, "Align beamstop", "done") - - # Step 3: measure beam position - set_progress(progress, progress_state, "Measure beam position", "running") - pos_result = daily_beampos() - update_log(log, messages, "Beam centres: ") - for zoom in CameraZoom: - update_log(log, messages, f"{zoom.label} zoom: ({pos_result[zoom.label]['x']:.4g}, {pos_result[zoom.label]['y']:.4g})") - set_progress(progress, progress_state, "Measure beam position", "done") - - # Step 4: Align collimator - # set_progress(progress, progress_state, "Align collimator", "running") - # coll_result = daily_coll() - # update_log(log, messages, f"Collimator alignment: " - # f"X centre: {coll_result['x_position']:.4g},\n" - # f" X width = {coll_result['x_width']:.4f} mm\n" - # f" Y centre = {coll_result['y_position']:.4f} mm\n" - # f" Y width = {coll_result['y_width']:.4f} mm\n" - # f" Y/X width ratio = {coll_result['y_width'] / coll_result['x_width']:.3f}" - # ) - update_log(log, messages, "Collimator alignment was skipped") - # save_current_position(dev.coll_x, 'in') - # save_current_position(dev.coll_y, 'in') - # set_progress(progress, progress_state, "Align collimator", "done") - - # Step 5: measure flux - set_progress(progress, progress_state, "Measure flux", "running") - flux1,flux2 = bpm2flux() - update_log(log, messages, f"Flux at 100% transmission is {flux2:.3g} ph/s") - set_progress(progress, progress_state, "Measure flux", "done") - - # Step 6: save new positions and reload - set_progress(progress, progress_state, "Save new positions to BEC", "running") - save_and_reload() - update_log(log, messages, f"Positions have been saved in BEC, please update in Aare") - set_progress(progress, progress_state, "Save new positions to BEC", "done") - - # Move back to sample alignment - bl.planner.move_to(BeamlineState.SAMPLE_ALIGNMENT) - -def select_daily_setup_gui_old(): - """Return widgets belonging to the Daily Setup GUI.""" - - window_name = "DailySetup" - open_windows = bec.gui.windows - - if open_windows.get(window_name) is None: - gui = bec.gui.new(window_name) - - plot1 = gui.new( - widget = "Waveform", - object_name = "Plot1" - ) - - plot2 = gui.new( - widget = "Waveform", - object_name = "Plot2" - ) - - results = gui.new( - widget = "TextBox", - object_name = "Results", - where = "bottom", - ) - - log = gui.new( - widget = "TextBox", - object_name = "Info", - where = "bottom", - ) - - progress = gui.new( - widget = "TextBox", - object_name = "Progress", - where = "left", - ) - - else: - gui = open_windows[window_name] - plot1 = gui.Plot1 - plot2 = gui.Plot2 - log = gui.Info - results = gui.Results - progress = gui.Progress - - return plot1, plot2, results, log, progress +@dataclass +class DailySetupContext: + plot1: object + plot2: object + plot3: object + plot4: object + log: object + progress: object + messages: list + progress_state: dict -def select_daily_setup_gui(): - """Return widgets belonging to the Daily Setup GUI.""" - - window_name = "DailySetup" - open_windows = bec.gui.windows - - if open_windows.get(window_name) is None: - gui = bec.gui.new(window_name) - - # Row 1 - plot1 = gui.new( - widget="Waveform", - object_name="Beamstop_X", - ) - - plot2 = gui.new( - widget="Waveform", - object_name="Beamstop_Y", - relative_to="Beamstop_X", - where="right", - ) - - # Row 2 - start a new full-width row - plot3 = gui.new( - widget="Waveform", - object_name="Collimator_X", - where="bottom", - ) - - plot4 = gui.new( - widget="Waveform", - object_name="Collimator_Y", - relative_to="Collimator_X", - where="right", - ) - - # Row 3 - start another full-width row - log = gui.new( - widget="TextBox", - object_name="Info", - where="bottom", - ) - - progress = gui.new( - widget="TextBox", - object_name="Progress", - relative_to="Info", - where="right", - ) - gui.set_layout_ratios( - horizontal=[1, 1], - vertical=[2, 2, 1], - ) - else: - gui = open_windows[window_name] - plot1 = gui.Beamstop_X - plot2 = gui.Beamstop_Y - plot3 = gui.Collimator_X - plot4 = gui.Collimator_Y - log = gui.Info - progress = gui.Progress - - return plot1, plot2, plot3, plot4, log, progress - -def clear_daily_plots(): - plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() - plot1.clear_all() - plot2.clear_all() - plot3.clear_all() - plot4.clear_all() +# ============================================================================= +# Main daily setup +# ============================================================================= - -def update_log(log_box, messages, message): - """Add new message to the daily setup log """ - messages.append(message) - log_box.set_plain_text("\n".join(messages)) - -def update_progress(progress_box, progress_state): - """ Sets up the status icons for the progress tracker""" - status = { - "todo": "\u25CB", - "running": "\u25B6", - "done": "\u2713", - "warning": "\u26A0", - "failed": "\u2717", - } - - - text = "\n".join( - f"{status[state]} {label}" - for label, state in progress_state.items() - ) - progress_box.set_plain_text(text) - -def set_progress(progress_box, progress_state, step, status): - """Change the status of one step and update the display.""" - progress_state[step] = status - update_progress(progress_box, progress_state) - -def plot_daily_setup_scan( - motor_name, - signal_name, - plot="x", - title=None, +def daily_setup( + energy=12400, + *, + pre_checks=True, + set_energy=True, + mono_scan=True, + move_beam=True, + align_beamstop=False, + # measure_beampos=False, + align_collimator=False, + measure_flux=True, + save_scilog=True, ): - """Plot live scan data in one of the Daily Setup waveform widgets.""" + """Run selected daily beamline setup tasks.""" + + # Check day of the week a and do the weekly and daily checks if a Wednesday + dt = datetime.now() + if dt.weekday() == 2: + scan_slits = True, + ge_scan = True, + check_xbpms = True, + + selected = { + "Pre checks": pre_checks, + "Scan slits": scan_slits, + "Check XBPM positions": check_xbpms, + "Ge scan": ge_scan, + "Set energy": set_energy, + "Move beam": move_beam, + "Measure flux": measure_flux, + } + + # If not a Wednesday, only do the daily checks + selected = { + "Pre checks": pre_checks, + "Set energy": set_energy, + "Move beam": move_beam, + "Align beamstop": align_beamstop, + # "Measure beam position": measure_beampos, + "Align collimator": align_collimator, + "Measure flux": measure_flux, + } + + results = {} + setup_start = time.perf_counter() + + ctx = get_daily_setup_context(selected) + + + for task, enabled in selected.items(): + ctx.progress_state[task] = "todo" if enabled else "skipped" + + update_progress(ctx.progress, ctx.progress_state) + update_log(ctx.log, ctx.messages, "Starting daily beamline setup") + + try: + # --------------------------------------------------------- + # Pre-flight checks + # --------------------------------------------------------- + if pre_checks: + with time_step(results, "Pre checks"): + prechecks_ok, results["Pre checks"] = run_pre_checks(ctx) + + # Machine checks are critical: stop daily setup + if not prechecks_ok: + update_log( + ctx.log, + ctx.messages, + f"Daily setup stopped because machine pre-checks failed." + ) + total_duration = time.perf_counter() - setup_start + + if save_scilog: + log_daily_setup_to_scilog( + bec, + results, + ctx.progress_state, + total_duration=total_duration, + messages=ctx.messages, + ) + return results + + # --------------------------------------------------------- + # Set energy + # --------------------------------------------------------- + if set_energy: + with time_step(results, "Set energy"): + results["Set energy"] = run_daily_energy( + energy, + mono_scan, + ctx, + ) + + # --------------------------------------------------------- + # Scan slits + # --------------------------------------------------------- + if scan_slits: + with time_step(results, "Scan slits"): + results["Scan slits"] = run_weekly_scanslits(ctx) + + # --------------------------------------------------------- + # Check XBPM positions + # --------------------------------------------------------- + if check_xbpms: + with time_step(results, "XBPM positions"): + results["Scan slits"] = run_weekly_check_xbpms(ctx) + + # --------------------------------------------------------- + # Ge K-edge scan + # --------------------------------------------------------- + if ge_scan: + with time_step(results, "Ge scan"): + results["Scan slits"] = run_weekly_ge_scan(ctx) + + # --------------------------------------------------------- + # Move beam + # --------------------------------------------------------- + if move_beam: + with time_step(results, "Move beam"): + results["Move beam"] = run_daily_movebeam(ctx) + + # --------------------------------------------------------- + # Align beamstop + # --------------------------------------------------------- + if align_beamstop: + with time_step(results, "Align beamstop"): + results["Align beamstop"] = run_daily_beamstop(ctx) + + # # --------------------------------------------------------- + # # Measure beam position + # # --------------------------------------------------------- + # if measure_beampos: + # with time_step(results, "Measure beam position"): + # results["Measure beam position"] = run_daily_beamposition(ctx) + + # --------------------------------------------------------- + # Align collimator + # --------------------------------------------------------- + if align_collimator: + with time_step(results, "Align collimator"): + results["Align collimator"] = run_daily_collimator(ctx) + + # --------------------------------------------------------- + # Measure flux + # --------------------------------------------------------- + if measure_flux: + with time_step(results, "Measure flux"): + results["Measure flux"] = run_daily_flux(ctx) + + # --------------------------------------------------------- + # Measure time taken + # --------------------------------------------------------- + + total_duration = time.perf_counter() - setup_start + update_log( + ctx.log, + ctx.messages, + f"Total duration: {format_duration(total_duration)}", + ) + + + # --------------------------------------------------------- + # Save daily diagnostic history + # --------------------------------------------------------- + log_daily_setup_to_csv(results) + + # --------------------------------------------------------- + # Save final beam image for HTML report and SciLog + # --------------------------------------------------------- + save_daily_beam_image(results) + + + # --------------------------------------------------------- + # Create and display final HTML report + # --------------------------------------------------------- + + html = create_daily_html_report( + results, + total_duration=total_duration, + messages=ctx.messages, + ) + + ctx.log.set_html_text(html) + + + # --------------------------------------------------------- + # SciLog + # --------------------------------------------------------- + if save_scilog: + log_daily_setup_to_scilog( + bec, + results, + ctx.progress_state, + total_duration=total_duration, + messages=ctx.messages, + ) + + return results + + finally: + print("done") + bl.planner.move_to(BeamlineState.ROBOT_SAMPLE_EXCHANGE) + + +def get_daily_setup_context(tasks=None): + """Get widgets and state used by daily setup tasks.""" plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() - if plot.lower() == "beamstop_x": - wf = plot1 - elif plot.lower() == "beamstop_y": - wf = plot2 - elif plot.lower() == "collimator_x": - wf = plot3 - elif plot.lower() == "collimator_y": - wf = plot4 - else: - raise ValueError("plot must be 'x' or 'y'") + # Clear text from previous run + log.set_plain_text("") + progress.set_plain_text("") - wf.clear_all() + # daily_report = f" bl.env." - if title is None: - title = f"{motor_name} scan" + if tasks is None: + tasks = [ + "Set energy", + "Move beam", + "Align beamstop", + # "Measure beam position", + "Align collimator", + "Measure flux", + "Save new positions to BEC", + ] - wf.title = title - wf.x_label = motor_name - wf.y_label = signal_name - - wf.plot( - device_x=motor_name, - device_y=signal_name, + return DailySetupContext( + plot1=plot1, + plot2=plot2, + plot3=plot3, + plot4=plot4, + log=log, + progress=progress, + messages=[], + progress_state={task: "todo" for task in tasks}, ) -def daily_energy(energy, plot_widget=None): - """ Set the beamline to the required energy """ - current_energy = get_current_energy() - if abs(energy - current_energy) > 1: - bl_energy(energy, window_name="DailySetup", plot_widget=plot_widget) - else: - gap_actual = dev.id_gap.position - gap_req = new_calc_gap(energy) - if abs(gap_actual-gap_req) > 0.05: - umv(dev.id_gap, gap_req) - mono_pitch_scan(window_name="DailySetup", plot_widget=plot_widget) -def set_zoom(zoom): - """Set sample camera zoom level.""" +# ============================================================================= +# Pre-checks +# ============================================================================= - if isinstance(zoom, str): - zoom = CameraZoom.from_label(zoom) - umv(dev.scam_zoom, zoom.position) - else: - umv(dev.scam_zoom, zoom) +def run_pre_checks(ctx): + """Run machine and experimental hutch pre-flight checks. -def daily_beampos(): - """Measure beam positions at different zoom levels""" - results = {} - # Ensure shutter and detector cover are closed before changing states - dev.bcu_shutter.put(0) - bl.d['det_cov'].move('close') - try: - # set transmission to 10% - dev.transm.put(0.1) + Machine checks are critical and stop the setup if they fail. + Experimental hutch checks are advisory and do not stop the setup. + """ - # move to beam visualisation - bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) + task = "Pre checks" - # open shutter - dev.bcu_shutter.put(1) + clear_daily_plots() + set_progress( + ctx.progress, + ctx.progress_state, + task, + "running", + ) - for zoom in CameraZoom: - print(F"Moving to {zoom.label}") - umv(dev.scam_zoom, zoom.position) - - # set autoexposure - auto_exposure(cam="samcam", max_iter=25) - time.sleep(0.2) - - # take readings - x = dev.samcam_x.read()['samcam_x']['value'] - y = dev.samcam_y.read()['samcam_y']['value'] - - # append results - results[zoom.label] = {'x': x, 'y': y} - - print(f" {zoom.label}: x = {x:.4g}, y = {y:.4g}") - return results - finally: - dev.bcu_shutter.put(0) - -def daily_coll(): - """Centre the collimator in X and Y using the I1 diode.""" - plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + # --------------------------------------------------------- + # Machine checks - critical / fail fast + # --------------------------------------------------------- + machine_result = machine_prechecks() result = { - "success": False, - "start_intensity": None, - "y_start_intensity": None, - "y_end_intensity": None, - "x_start_intensity": None, - "x_end_intensity": None, - "y_position": None, - "x_position": None, + "ok": machine_result["ok"], + "warning": False, + "machine": machine_result, + "experiment_hutch": None, } - dev.bcu_shutter.put(0) + if not machine_result["ok"]: + update_log( + ctx.log, + ctx.messages, + format_prechecks_for_gui(result), + ) + + set_progress( + ctx.progress, + ctx.progress_state, + task, + "failed", + ) + + return False, result + + # --------------------------------------------------------- + # Experimental hutch checks - advisory + # --------------------------------------------------------- + eh_result = EH_prechecks() + + result["experiment_hutch"] = eh_result + + # EH problems are warnings, not setup failures + result["ok"] = True + result["warning"] = not eh_result["ok"] + + update_log( + ctx.log, + ctx.messages, + format_prechecks_for_gui(result), + ) + + status = "warning" if result["warning"] else "done" + + set_progress( + ctx.progress, + ctx.progress_state, + task, + status, + ) + + # EH warnings do NOT stop daily setup + return True, result + + +def format_prechecks_for_gui(result): + """Format pre-flight check results for the daily setup GUI.""" + + lines = [] + + mach = result.get("machine") + + if mach is not None: + lines.append("") + + for check in mach["checks"]: + symbol = "✓" if check["ok"] else "✗" + lines.append(f"{symbol} {check['message']}") + + eh = result.get("experiment_hutch") + + if eh is not None: + lines.append("") + + for check in eh["checks"]: + symbol = "✓" if check["ok"] else "✗" + lines.append(f"{symbol} {check['message']}") + + return "\n".join(lines) + + +# -------------------------------------------------------------------------- +# Check machine current and shutter status +# -------------------------------------------------------------------------- +def machine_prechecks(): + """Run machine checks in dependency order. + Stop immediately if any machine check fails""" + + checks = [check_machine_current, + check_attended_mode, + check_fe_shutter, + check_hutch_shutter, + ] + + results = [] + + for check in checks: + result = check() + results.append(result) + + + if result["ok"]: + print(f"✓ {result['message']}") + else: + print(f"✗ {result['message']}") + + return{ + "ok": False, + "message": "Machine checks failed", + "checks": results, + } + + return { + "ok": True, + "message": "Machine checks passed", + "checks": results, + } + + +def check_machine_current(): + """Checks current is >30 mA""" + current = get_value(dev.sls_current) + + if current <= 30: + return { + "ok": False, + "message": f"Machine current too low {current:.1f} mA)", + "current": current, + } + + return { + "ok": True, + "message": f"Machine current OK ({current:.1f} mA)", + "current": current, + } + + + +def check_attended_mode(): + """Check beamline is in attended more""" + mode_pv = PreChecks.mode_pv + state = caget(mode_pv) + + if state == "offline": + return { + "ok": False, + "message": "Beamline is offline, please set to 'attended'", + } + + + return { + "ok": True, + "message": f"Beamline is not offline, (operation mode is {state})", + } + + + +def check_fe_shutter(): + """Check FE shutter is open""" + fe_shutter_pv = PreChecks.fe_shutter_pv + state = caget(fe_shutter_pv) + + if state != "Open": + return { + "ok": False, + "message": "FE shutter is not open'", + } + + return { + "ok": True, + "message": "FE shutter is open", + } + + + +def check_hutch_shutter(): + """Check hutch shutter is open""" + hutch_shutter_pv = PreChecks.hutch_shutter_pv + state = caget(hutch_shutter_pv) + + if state != "OPEN": + return { + "ok": False, + "message": "Hucth shutter is not open'", + } + + return { + "ok": True, + "message": "Hutch shutter is open", + } + + + + +# -------------------------------------------------------------------------- +# Check Experimental Hutch (EH) equipment +# -------------------------------------------------------------------------- +def EH_prechecks(): + """Check experimental hutch devices.""" + + checks = [ + check_xray_eye_out, + check_cryo_temp, + check_detector_height, + check_beamstop_position, + ] + + results = [] + + for check in checks: + result = check() + results.append(result) + + if result["ok"]: + print(f"✓ {result['message']}") + else: + print(f"✗ {result['message']}") + + all_ok = all(result["ok"] for result in results) + + return { + "ok": all_ok, + "message": ( + "Experiment hutch checks passed" + if all_ok + else "Experiment hutch checks require attention" + ), + "checks": results, + } + + +def check_xray_eye_out(): + ss_xi_pv = EHChecks.ss_xi_pv + ss_mot = EHChecks.ss_mot + + state = caget(ss_xi_pv) + try: + if state != "OUT": + umv(ss_mot, -7) + except Exception as exc: + return { + "ok": False, + "message": f"Moving X-ray eye failed: {exc}" + } + return { + "ok": True, + "message": "SS Xray eye is OUT" + } + + +def check_bcubpm(): + pass + +def check_beamstop_position(): + """Check that the beamstop blocks the direct beam. + + If the check fails, align the beamstop and verify again. + """ try: - if not bl.planner.is_state(BeamlineState.FLUX_MEASUREMENT): - bl.planner.move_to(BeamlineState.FLUX_MEASUREMENT) + if not bl.planner.is_state(BeamlineState.DET_DIODE): + bl.planner.move_to(BeamlineState.DET_DIODE) + bl.d["det_cov"].move("close") + dev.transm.put(EnergyDefaults.default_trans) dev.bcu_shutter.put(1) - dev.transm.put(0.1) + time.sleep(0.5) + + i2 = abs(dev.i2.read()["i2"]["value"]) + cutoff = Beamstop.i2_limit + + # Beamstop already OK + if i2 < cutoff: + return { + "ok": True, + "message": f"Beamstop blocking beam: I2 = {i2:.2e}", + "value": i2, + "realigned": False, + } + + print( + f"Beamstop check failed (I2={i2:.2e}). " + "Running beamstop alignment..." + ) + + # Close shutter before handing over to alignment routine + dev.bcu_shutter.put(0) + + alignment = daily_beamstop() + + if not alignment["ok"]: + return { + "ok": False, + "message": "Beamstop alignment failed", + "value": i2, + "realigned": True, + } + + # Verify after alignment + dev.bcu_shutter.put(1) time.sleep(0.2) - result["start_intensity"] = read_i1() - print(f"Starting intensity is {result['start_intensity']:.4g}") + i2_after = abs(dev.i2.read()["i2"]["value"]) - bl.d["coll_y"].move("in") + if i2_after < cutoff: + return { + "ok": True, + "message": ( + "Beamstop was misaligned and was successfully " + f"realigned (I2={i2_after:.2e})" + ), + "value": i2_after, + "realigned": True, + } - # Centre Y - result["y_start_intensity"] = read_i1() - print( - "Intensity with collimator in before Y scan: " - f"{result['y_start_intensity']:.4g}" - ) + return { + "ok": False, + "message": ( + "Beamstop still not blocking beam after alignment " + f"(I2={i2_after:.2e})" + ), + "value": i2_after, + "realigned": True, + } - y_result = find_feature( - dev.coll_y, - dev.i1, - -0.3, - 0.3, - 30, - feature="peak", - relative=True, - confirm=False, - plot_widget=plot4, - ) - - result["y_position"] = y_result["centre"] - result["y_end_intensity"] = read_i1() - result["y_width"] = y_result["width"] - - print( - "Intensity with collimator in after Y scan: " - f"{result['y_end_intensity']:.4g}" - ) - - # Centre X - result["x_start_intensity"] = read_i1() - - print( - "Intensity with collimator in before X scan: " - f"{result['x_start_intensity']:.4g}" - ) - - x_result = find_feature( - dev.coll_x, - dev.i1, - -0.3, - 0.3, - 30, - feature="peak", - relative=True, - confirm=False, - plot_widget=plot3, - ) - - result["x_position"] = x_result["centre"] - result["x_end_intensity"] = read_i1() - result["x_width"] = x_result["width"] - - print( - "Intensity with collimator in after X scan: " - f"{result['x_end_intensity']:.4g}" - ) - - result["success"] = True - - return result + except Exception as exc: + return { + "ok": False, + "message": f"Beamstop check failed: {exc}", + } finally: dev.bcu_shutter.put(0) +def check_cryo_temp(): + cryo_pv = EHChecks.cryo_pv + temp = caget(cryo_pv) + + if temp is None: + return { + "ok":False, + "message": "Could not read cryostream temperature" + } + + if temp >= 110: + return { + "ok": False, + "message": f"Cryostream too warm ({temp} K)" + } + + return { + "ok":True, + "message": f"Cryostream temperature: {temp} K", + } + +def check_detector_height(): + height = get_value(dev.det_y) + tol = 0.1 + if abs(height-BLSetup.detector_height) > tol: + return { + "ok":False, + "message": f"Detector not at operational height. Currently at {height:.1f} mm", + } + return { + "ok":True, + "message": f"Detector height: {height:.1f} mm" + } + +# ============================================================================= +# Energy +# ============================================================================= + + +def run_daily_energy(energy, mono_scan, ctx): + """Set beamline energy and log results.""" + + task = "Set energy" + + clear_daily_plots() + set_progress(ctx.progress, ctx.progress_state, task, "running") + + try: + current_energy = get_current_energy() + + if abs(energy - current_energy) > 1: + update_log( + ctx.log, + ctx.messages, + ( + f"\n▶ Changing energy from {current_energy:.0f} " + f"to {energy:.0f} ev.\n" + " Bragg movement is slow - this may take a few minutes. " + "Please wait....." + ), + ) + + result = daily_energy(energy, mono_scan, plot_widget=ctx.plot1) + + except Exception as exc: + result = { + "ok": False, + "message": f"Energy setup failed: {exc}", + } + + + update_log(ctx.log, ctx.messages, format_energy_for_gui(result)) + + status = "done" if result["ok"] else "failed" + + set_progress(ctx.progress, ctx.progress_state, task, status) + + return result + + +def daily_energy(energy, scan_mono, plot_widget=None): + """Set the beamline to the required energy and correct gap""" + + current_energy = get_current_energy() + + if abs(energy - current_energy) > 1: + bl_energy(energy, window_name="DailySetup", plot_widget=plot_widget) + + else: + gap_actual = dev.id_gap.position + gap_req = calc_gap(energy) + + if abs(gap_actual - gap_req) > 0.05: + if dev.id_gap.moving(): + return { + "ok": False, + "message": "Undulator gap is moving", + "energy": current_energy, + } + + umv(dev.id_gap, gap_req) + + if scan_mono: + mono_result = mono_pitch_scan(window_name="DailySetup", plot_widget=plot_widget) + + + new_energy = get_current_energy() + calib_pitch = get_dcm_motors_positions(energy)["dcm_pitch"] + pitch_offset = get_value(dev.dcm_pitch) - calib_pitch + if not mono_result["extended_scan"]: + return { + "ok": True, + "message": f"Beamline energy set to {new_energy:.0f} eV, " + f"pitch offset = {pitch_offset:.3f} mrad", + "energy": new_energy, + "pitch_offset": pitch_offset, + } + else: + return { + "ok": True, + "warning": True, + "message": ( + f"Warning - DCM pitch is not within expected tolerance" + f"The DCM may need to be re-calibrated. " + f"Pitch offset = {pitch_offset:.3f} mrad" + ), + "energy": new_energy, + "pitch_offset": pitch_offset, + } + + + +def format_energy_for_gui(result): + "Format energy change output for Daily GUI" + + icon = "✓" if result["ok"] else "✗" + + return f"\n{icon} {result['message']}" + + + +# ============================================================================= +# Move beam +# ============================================================================= + + +def run_daily_movebeam(ctx): + """Move beam to the scintillator crosshair""" + + task = "Move beam" + + set_progress(ctx.progress, ctx.progress_state, task, "running") + + try: + + result = daily_movebeam() + + except Exception as exc: + result = { + "ok": False, + "message": f"Align beam step failed: {exc}", + } + + if result.get("ok"): + status = "done" + + elif result.get("warning"): + status = "warning" + + else: + status = "failed" + + update_log(ctx.log, ctx.messages, format_movebeam_for_gui(result)) + + set_progress(ctx.progress, ctx.progress_state, task, status) + return result + + +def daily_movebeam(meas="scint", iterations=5): + """ + Move the beam onto the sample-camera crosshair using the VFM and HFM. + The sample crosshair parameters are defined in BeamMove class. + + The beam position is measured on the scintillator at 12.5X zoom. + VFM motor to move the beam vertically is defined in BeamMove class. + HFM motor to move beam horizontally is defined in BeamMove class. + + Corrections are repeated until the beam is within tolerance or the + maximum number of iterations is reached. + """ + + if meas != "scint": + raise ValueError(f"Unknown beam position measurement: {meas}") + + # ------------------------------------------------------------------ + # Setup + # ------------------------------------------------------------------ + + zoom = BeamMove.zoom + vfm_mot = BeamMove.vfm_mot + hfm_mot = BeamMove.hfm_mot + + x_target = BeamMove.scint_target_x + y_target = BeamMove.scint_target_y + tolerance = BeamMove.scint_tolerance + + # Safety limit for a single mirror motor correction + max_move = BeamMove.max_move + + # Starting mirror positions + vfm_start = get_value(vfm_mot) + hfm_start = get_value(hfm_mot) + + # Starting BCU BPM offsets + bpm_result = get_bcu_beampos() + bpm_x_start = bpm_result["bcu_x_um"] + bpm_y_start = bpm_result["bcu_y_um"] + + print( + "Moving beam to scintillator crosshair\n" + f"Target: X={x_target:.1f}, Y={y_target:.1f} px" + ) + + # ------------------------------------------------------------------ + # Initial measurement + # ------------------------------------------------------------------ + + prepare_scint_measurement(zoom=zoom) + try: + scint_result = read_scintXY() + + if not scint_result["ok"]: + print(f"✗ {scint_result['message']}") + return { + "ok": False, + "warning": True, + "message": scint_result["message"], + } + + x_start = scint_result["X_pixels"] + y_start = scint_result["Y_pixels"] + + # ------------------------------------------------------------------ + # Beam corrections + # ------------------------------------------------------------------ + + for i in range(iterations): + + x = scint_result["X_pixels"] + y = scint_result["Y_pixels"] + + diff_x = x_target - x + diff_y = y_target - y + + print( + f"\nCorrection {i + 1}: " + f"X={x:.2f}, Y={y:.2f} px " + f"(error X={diff_x:+.2f}, Y={diff_y:+.2f})" + ) + + # Already aligned + if abs(diff_x) <= tolerance and abs(diff_y) <= tolerance: + print("Beam is within tolerance.") + break + + # -------------------------------------------------------------- + # VFM: vertical beam position + # -------------------------------------------------------------- + + if abs(diff_y) > tolerance: + + current_vfm = get_value(vfm_mot) + vfm_change = diff_y / BeamMove.scint_y_slope + + if abs(vfm_change) > max_move: + message = ( + f"VFM correction {vfm_change:+.5f} mm exceeds " + f"maximum allowed move of {max_move:.3f} mm. " + "Manual check required." + ) + print(message) + + return { + "ok": False, + "warning": True, + "message": message, + "BeamX": None, + "BeamY": None, + } + + move_with_offset( + vfm_mot, + current_vfm + vfm_change, + ) + + # Remeasure because VFM movement may also affect X + scint_result = read_scintXY() + + if not scint_result["ok"]: + return { + "ok": False, + "warning": True, + "message": scint_result["message"], + } + + x = scint_result["X_pixels"] + y = scint_result["Y_pixels"] + + # -------------------------------------------------------------- + # HFM: horizontal beam position + # -------------------------------------------------------------- + + diff_x = x_target - x + + if abs(diff_x) > tolerance: + + current_hfm = get_value(hfm_mot) + hfm_change = diff_x / BeamMove.scint_x_slope + + if abs(hfm_change) > max_move: + message = ( + f"HFM correction {hfm_change:+.5f} mm exceeds " + f"maximum allowed move of {max_move:.3f} mm. " + "Manual check required." + ) + print(message) + + return { + "ok": False, + "warning": True, + "message": message, + "BeamX": None, + "BeamY": None, + } + + move_with_offset( + hfm_mot, + current_hfm + hfm_change, + ) + + # Measure again ready for next iteration + scint_result = read_scintXY() + + if not scint_result["ok"]: + return { + "ok": False, + "warning": True, + "message": scint_result["message"], + } + + # ------------------------------------------------------------------ + # Final verification + # ------------------------------------------------------------------ + + final_scint = read_scintXY(image=True) + + if not final_scint["ok"]: + return { + "ok": False, + "warning": True, + "message": final_scint["message"], + } + + final_x = final_scint["X_pixels"] + final_y = final_scint["Y_pixels"] + + diff_x = x_target - final_x + diff_y = y_target - final_y + + beamx = final_scint["X_diff_microns"] + beamy = final_scint["Y_diff_microns"] + + final_vfm = get_value(vfm_mot) + final_hfm = get_value(hfm_mot) + + #Final BCU BPM offsets + bpm_result = get_bcu_beampos() + bpm_x_end = bpm_result["bcu_x_um"] + bpm_y_end = bpm_result["bcu_y_um"] + + success = ( + abs(diff_x) <= tolerance + and abs(diff_y) <= tolerance + ) + + print( + f"\nStarting beam position: X={x_start:.2f}, Y={y_start:.2f} px\n" + f"Final beam position: X={final_x:.2f}, Y={final_y:.2f} px\n" + f"Final error: X={diff_x:+.2f}, Y={diff_y:+.2f} px\n" + f"VFM MOT: {vfm_start:.6f} -> {final_vfm:.6f}\n" + f"HFM MOT: {hfm_start:.6f} -> {final_hfm:.6f}" + ) + + if success: + message = ( + "Beam aligned\n" + f"Displacement: Beam X: {beamx:.1f} microns, " + f"Beam Y: {beamy:.1f} microns" + ) + warning = False + + else: + message = ( + f"Beam is not within ±{tolerance:.1f} px after " + f"{iterations} corrections." + ) + warning = True + + print(message) + + # ------------------------------------------------------------------ + # Results + # ------------------------------------------------------------------ + + return { + "ok": success, + "warning": warning, + "message": message, + "BeamX": beamx, + "BeamY": beamy, + "FinalX": final_x, + "FinalY": final_y, + "X FWHM": final_scint["X_FWHM"], + "Y FWHM": final_scint["Y_FWHM"], + "StartX": x_start, + "StartY": y_start, + "BPM_X_start": bpm_x_start, + "BPM_Y_start": bpm_y_start, + "Initial VFM MOT": vfm_start, + "Initial HFM MOT": hfm_start, + "Final VFM MOT": final_vfm, + "Final HFM MOT": final_hfm, + "BPM_X_end": bpm_x_end, + "BPM_Y_end": bpm_y_end, + "image": final_scint["image"], + } + finally: + finish_scint_measurement() + + + +def format_movebeam_for_gui(result): + "Format move beam output for Daily GUI" + + icon = "✓" if result["ok"] else "✗" + return f"\n{icon} {result['message']}" + + +# ============================================================================= +# Beamstop alignment +# ============================================================================= + + +def run_daily_beamstop(ctx): + """Align beamstop and save new positions.""" + + task = "Align beamstop" + + # clear_daily_plots() + set_progress(ctx.progress, ctx.progress_state, task, "running") + + try: + result = daily_beamstop() + + except Exception as exc: + result = { + "ok": False, + "message": f"Beamstop alignment failed: {exc}", + } + + + status = "done" if result["ok"] else "failed" + + update_log(ctx.log, ctx.messages, format_beamstop_for_gui(result)) + + # save_current_position(dev.bs_x, "in") + # save_current_position(dev.bs_y, "in") + + set_progress(ctx.progress, ctx.progress_state, task, status) + return result + + def daily_beamstop(): - """Centre the beamstop against the detector - cover, i2""" + """Centre the beamstop against the detector cover diode, i2""" plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() - result = { - # "success": False, - # "x_position": None, - "y_position": None, - "intensity after alignment": None - } + result = {} - dev.bcu_shutter.put(0) - dev.transm.put(0.1) + dev.transm.put(EnergyDefaults.default_trans) try: - # if not bl.planner.is_state(BeamlineState.BEAMSTOP_ALIGNMENT): - # bl.planner.move_to(BeamlineState.BEAMSTOP_ALIGNMENT) - # set_zoom('2x') - # auto_exposure('samcam', max_iter=20) - - # bl.d['bl_pos'].move('out') - # bl.d['det_cov'].move('close') - # dev.bcu_shutter.put(1) - if not bl.planner.is_state(BeamlineState.DET_DIODE): bl.planner.move_to(BeamlineState.DET_DIODE) - # set_zoom('2x') - # auto_exposure('samcam', max_iter=20) - # bl.d['bl_pos'].move('out') - bl.d['det_cov'].move('close') + bl.d["det_cov"].move("close") dev.bcu_shutter.put(1) - time.sleep(0.2) # Centre Y @@ -479,22 +1158,29 @@ def daily_beamstop(): 1.2, 30, feature="edge_offset", - offset = Beamstop.x_offset, + offset=Beamstop.x_offset, relative=True, confirm=False, window_name="DailySetup", plot_widget=plot1, ) - result["x_position"] = x_result["centre"] - result["y_position"] = y_result["centre"] + result["bs_x_pos"] = x_result["centre"] + result["bs_y_pos"] = y_result["centre"] result["intensity after alignment"] = read_i2() - print( - "Intensity after Y scan: " - f"{result['intensity after alignment']:.4g}\n" - f"Centred position is {result["x_position"]:.5g}, {result["y_position"]:.5g}" - ) + # message = ( + # "Intensity after Y scan: " + # f"{result['intensity after alignment']:.4g}\n" + # f"Centred position is {result["bs_x_pos"]:.5g}, {result["bs_y_pos"]:.5g}" + # ) + + message = f"BS_X: {result['bs_x_pos']:.3g} mm, " f"BS_Y: {result['bs_y_pos']:.3g} mm\n" + + print(message) + + result["ok"] = True + result["message"] = message return result @@ -502,4 +1188,1263 @@ def daily_beamstop(): dev.bcu_shutter.put(0) +def format_beamstop_for_gui(result): + "Format results for beamstop alignment" + icon = "✓" if result["ok"] else "✗" + return f"\n{icon} Beamstop aligned.\n{result['message']}" + + +# ============================================================================= +# Beam-position measurement +# ============================================================================= + + +def run_daily_beamposition(ctx): + """Measure the beam position at the scintillator at Aare zoom levels""" + + task = "Measure beam position" + set_progress(ctx.progress, ctx.progress_state, task, "running") + + beampos_result = daily_beamposition() + + update_log(ctx.log, ctx.messages, format_beam_position(beampos_result)) + + set_progress(ctx.progress, ctx.progress_state, task, "done") + + return beampos_result + + +def daily_beamposition(): + """Measure beam positions at different zoom levels""" + results = {} + + try: + # set transmission to default + dev.transm.put(EnergyDefaults.default_trans) + + # move to beam visualisation + if not bl.planner.is_state(BeamlineState.BEAM_VISUALISATION): + bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) + + # open shutter + dev.bcu_shutter.put(1) + + for zoom in CameraZoom: + print(f"Moving to {zoom.label}") + umv(dev.scam_zoom, zoom.position) + + # set autoexposure + auto_exposure(cam="samcam", max_iter=25) + time.sleep(0.2) + + # take readings + x = dev.samcam_x.read()["samcam_x"]["value"] + y = dev.samcam_y.read()["samcam_y"]["value"] + + # append results + results[zoom.label] = {"x": x, "y": y} + + print(f" {zoom.label}: x = {x:.4g}, y = {y:.4g}") + + finally: + dev.bcu_shutter.put(0) + + return results + + +def format_beamposition(pos_result): + """Format camera beam positions for display.""" + + lines = [f"{'Zoom':<8} {'X / px':>10} {'Y / px':>10}", "-" * 30] + + for zoom, pos in pos_result.items(): + lines.append(f"{zoom:<8} {pos['x']:>10.1f} {pos['y']:>10.1f}") + + return "\n".join(lines) + + +# ============================================================================= +# Collimator alignment +# ============================================================================= + + +def run_daily_collimator(ctx): + """Align the collimator and save positions""" + + task = "Align collimator" + set_progress(ctx.progress, ctx.progress_state, task, "running") + + try: + result = daily_collimator() + + except Exception as exc: + result = { + "ok": False, + "message": f"Collimator alignment step failed: {exc}" + } + + update_log(ctx.log, ctx.messages, format_collimator_for_gui(result)) + + status = "done" if result["ok"] else "failed" + + set_progress(ctx.progress, ctx.progress_state, task, status) + return result + + +def daily_collimator(): + """Centre the collimator in X and Y using the I1 diode.""" + plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + + result = {} + + try: + if not bl.planner.is_state(BeamlineState.FLUX_MEASUREMENT): + bl.planner.move_to(BeamlineState.FLUX_MEASUREMENT) + + dev.transm.put(EnergyDefaults.default_trans) + dev.bcu_shutter.put(1) + + time.sleep(0.2) + + get_value(dev.i1) + + result["intensity_coll_out"] = get_value(dev.i1) + print(f"Starting intensity is {result["intensity_coll_out"]:.3e}") + + bl.d["coll_y"].move("in") + + # Centre Y + result["start_intensity_coll_in"] = get_value(dev.i1) + print( + "Intensity with collimator in before scans: " f"{result['start_intensity_coll_in']:.3e}" + ) + + y_result = find_feature( + dev.coll_y, + dev.i1, + -0.3, + 0.3, + 30, + feature="peak", + relative=True, + confirm=False, + plot_widget=plot4, + ) + + result["coll_y_pos"] = y_result["centre"] + result["coll_y_width"] = y_result["width"] + + # Centre X + + x_result = find_feature( + dev.coll_x, + dev.i1, + -0.3, + 0.3, + 30, + feature="peak", + relative=True, + confirm=False, + plot_widget=plot3, + ) + + result["coll_x_pos"] = x_result["centre"] + result["final_intensity_coll_in"] = get_value(dev.i1) + result["coll_x_width"] = x_result["width"] + + print( + "Intensity with collimator in after scans: " f"{result['final_intensity_coll_in']:.3e}" + ) + + result["ok"] = True + + message = ( + f"Collimator X: {result['coll_x_pos']:.3f} mm, " + f"Collimator Y: {result['coll_y_pos']:.3f} mm" + ) + + result["message"] = message + + return result + + finally: + dev.bcu_shutter.put(0) + + +def format_collimator_for_gui(result): + """Format collimator result for the Daily Setup GUI.""" + icon = "✓" if result["ok"] else "✗" + return f"\n{icon} Collimator aligned\n{result['message']}" + + + +# ============================================================================= +# Flux measurement +# ============================================================================= + + +def run_daily_flux(ctx): + """Measure the beam intensity using BCU BPM.""" + + task = "Measure flux" + set_progress(ctx.progress, ctx.progress_state, task, "running") + + try: + flux, full_flux = bpm2flux() + + message = ( + f"Intensity at current transmission: " + f"{flux:.2e} ph/s\n" + f"Intensity at 100% transmission: " + f"{full_flux:.2e} ph/s" + ) + + result = {"ok": True, + "message": message, + "current_flux": flux, + "full_flux": full_flux, + } + + except Exception as exc: + result = { + "ok": False, + "message": f"(Flux measurement failed: {exc})", + } + + status = "done" if result["ok"] else "failed" + + update_log(ctx.log, ctx.messages, format_flux_for_gui(result)) + + set_progress(ctx.progress, ctx.progress_state, task, status) + return result + + +def format_flux_for_gui(result): + icon = "✓" if result["ok"] else "✗" + return f"\n{icon} Flux measured\n{result['message']}\n" + +# ============================================================================= +# Daily-setup GUI helpers +# ============================================================================= + + +def select_daily_setup_gui(): + """Return widgets belonging to the Daily Setup GUI.""" + + window_name = "DailySetup" + open_windows = bec.gui.windows + + if open_windows.get(window_name) is None: + gui = bec.gui.new(window_name) + + # Ensure the placeholder report exists before loading the profile + # filename = bl.env.daily_setup_dir / "daily_report.pdf" + # if not filename.exists(): + # create_empty_daily_report(filename) + + # Restore the standard Daily Setup layout + gui.load_profile("daily_setup") + # gui.Report.load_pdf(str(filename)) + # gui.Report.fit_to_page() + + else: + gui = open_windows[window_name] + + return ( + gui.Plot_1, + gui.Plot_2, + gui.Plot_3, + gui.Plot_4, + gui.Info, + gui.Progress, + ) + + +def clear_daily_plots(): + plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + plot1.clear_all() + plot2.clear_all() + plot3.clear_all() + plot4.clear_all() + + +def update_log(log_box, messages, message): + """Add new message to the daily setup log""" + messages.append(message) + log_box.set_plain_text("\n".join(messages)) + + +def update_progress(progress_box, progress_state): + """Sets up the status icons for the progress tracker""" + status = { + "todo": "\u25cb", + "running": "\u25b6", + "done": "\u2713", + "warning": "\u26a0", + "skipped": "–", + "failed": "\u2717", + } + + text = "\n".join(f"{status[state]} {label}" for label, state in progress_state.items()) + progress_box.set_plain_text(text) + + +def set_progress(progress_box, progress_state, step, status): + """Change the status of one step and update the display.""" + progress_state[step] = status + update_progress(progress_box, progress_state) + + +def plot_daily_setup_scan(motor_name, signal_name, plot="x", title=None): + """Plot live scan data in one of the Daily Setup waveform widgets.""" + + plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + + if plot.lower() == "plot_1": + wf = plot1 + elif plot.lower() == "plot_2": + wf = plot2 + elif plot.lower() == "plot_3": + wf = plot3 + elif plot.lower() == "plot_4": + wf = plot4 + else: + raise ValueError("plot must be 'x' or 'y'") + + wf.clear_all() + + if title is None: + title = f"{motor_name} scan" + + wf.title = title + wf.x_label = motor_name + wf.y_label = signal_name + + wf.plot(device_x=motor_name, device_y=signal_name) + + +# ============================================================================= +# SciLog +# ============================================================================= + + +def log_daily_setup_to_scilog( + bec, + results, + progress_state, + total_duration=None, + messages=None, +): + """Send compact daily setup summary to SciLog.""" + + msg = bec.messaging.scilog.new() + + date = datetime.now().strftime("%-d %B %Y") + + + msg.add_text(f"{bl.env.beamline_name} Daily Setup — {date}\n\n", bold=True) + + add_prechecks_to_scilog(msg, results) + add_setup_results_to_scilog(msg, results, progress_state) + + msg.add_text("\n") + + overall = get_daily_setup_status(progress_state) + + msg.add_text("Overall status: ", bold=True) + msg.add_text(f"{overall}\n") + + if total_duration is not None: + msg.add_text("Total duration: ", bold=True) + msg.add_text(f"{format_duration(total_duration)}\n") + + # Final beam image, if move_beam produced one + move_result = results.get("Move beam", {}) + image_file = move_result.get("image_file") + + if image_file is not None: + msg.add_text("\nFinal beam position\n", bold=True) + msg.add_attachment(str(image_file), width=600) + + # Preserve the same information shown in the GUI log + if messages: + msg.add_text("\nSetup information\n", bold=True) + for message in messages: + msg.add_text(f"{message}\n") + + msg.add_tags(["daily-setup", bl.env.beamline_name]) + msg.send() + + # Motor positions snapshot + log_daily_setup_positions(bec) + +def log_daily_setup_positions(bec): + """Record important motor positions after daily setup""" + + vfm_mot = BeamMove.vfm_mot + hfm_mot = BeamMove.hfm_mot + + bec.messaging.scilog.log_positions( + devices=[ + "dcm_bragg", + "dcm_froll", + "dcm_pitch", + "dcm_fpitch", + "vfm_y", + "vfm_pitch", + vfm_mot.name, + "vfm_bu", + "vfm_bd", + "hfm_x", + "hfm_pitch", + hfm_mot.name, + "hfm_bu", + "hfm_bd", + "coll_x", + "bs_x", + "bs_y", + ], + title=f"{bl.env.beamline_name} Daily Setup - Motor Positions", + tags=["daily_setup", bl.env.beamline_name, "positions"] + ) + +def add_prechecks_to_scilog(msg, results): + """Add pre-flight checks to the SciLog entry.""" + + if "Pre checks" not in results: + return + + result = results["Pre checks"] + + msg.add_text("Pre-flight\n", bold=True) + + machine = result.get("machine") + + if machine: + for check in machine["checks"]: + status = "OK" if check["ok"] else "FAILED" + msg.add_text( + f"{check['message']:<55} {status}\n" + ) + + eh = result.get("experiment_hutch") + + if eh: + for check in eh["checks"]: + status = "OK" if check["ok"] else "WARNING" + msg.add_text( + f"{check['message']:<55} {status}\n" + ) + + msg.add_text("\n") + +def add_setup_results_to_scilog(msg, results, progress_state): + """Add daily setup task results to SciLog.""" + + msg.add_text("Setup\n", bold=True) + + # --------------------------------------------------------- + # Energy + # --------------------------------------------------------- + if "Set energy" in results: + r = results["Set energy"] + + status = get_result_status("Set energy", r, progress_state) + + if r.get("energy") is not None: + detail = f"{r['energy'] / 1000:.3f} keV" + + if r.get("pitch_offset") is not None: + detail += ( + f", pitch offset = " + f"{r['pitch_offset']:.3f} mrad" + ) + + else: + detail = r.get("message", "") + + add_scilog_result( + msg, + "Set energy", + status, + detail, + ) + + # --------------------------------------------------------- + # Collimator + # --------------------------------------------------------- + if "Align collimator" in results: + r = results["Align collimator"] + + status = get_result_status( + "Align collimator", + r, + progress_state, + ) + + if r.get("ok"): + detail = ( + f"X={r['coll_x_pos']:.3f}, " + f"Y={r['coll_y_pos']:.3f} mm" + ) + else: + detail = r.get("message", "") + + add_scilog_result( + msg, + "Collimator alignment", + status, + detail, + ) + + # --------------------------------------------------------- + # Beamstop + # --------------------------------------------------------- + if "Align beamstop" in results: + r = results["Align beamstop"] + + status = get_result_status( + "Align beamstop", + r, + progress_state, + ) + + if r.get("ok"): + detail = ( + f"X={r['bs_x_pos']:.3f}, " + f"Y={r['bs_y_pos']:.3f} mm" + ) + else: + detail = r.get("message", "") + + add_scilog_result( + msg, + "Beamstop alignment", + status, + detail, + ) + + # --------------------------------------------------------- + # Move beam + # --------------------------------------------------------- + if "Move beam" in results: + r = results["Move beam"] + + status = get_result_status( + "Move beam", + r, + progress_state, + ) + + if r.get("ok"): + detail = ( + f"X={r['BeamX']:.1f}, " + f"Y={r['BeamY']:.1f} µm\n" + f"BPM X={r['BPM_X_end']:.2f} µm, " + f"BPM Y={r['BPM_Y_end']:.2f} µm" + ) + else: + detail = r.get("message", "manual check required") + + add_scilog_result( + msg, + "Move beam", + status, + detail, + ) + + # --------------------------------------------------------- + # Flux + # --------------------------------------------------------- + if "Measure flux" in results: + r = results["Measure flux"] + + status = get_result_status( + "Measure flux", + r, + progress_state, + ) + + if r.get("full_flux") is not None: + detail = f"{r['full_flux']:.2e} ph/s" + else: + detail = r.get("message", "") + + add_scilog_result( + msg, + "Flux", + status, + detail, + ) + +def add_scilog_result(msg, label, status, detail=""): + """Add one formatted setup-result line.""" + + line = f"{label:<24} {status:<8}" + + if detail: + line += f" {detail}" + + msg.add_text(line + "\n") + + +def get_result_status(task, result, progress_state): + """Return human-readable status for a SciLog result.""" + + state = progress_state.get(task) + + if state == "warning": + return "WARNING" + + if state == "failed": + return "FAILED" + + if result.get("ok"): + return "OK" + + return "FAILED" + + +def get_daily_setup_status(progress_state): + """Return overall daily-setup status.""" + + states = progress_state.values() + + if "failed" in states: + return "FAILED" + + if "warning" in states: + return "PASSED WITH WARNINGS" + + return "PASSED" + + +# ============================================================================= +# Save values to csv file +# ============================================================================= +def log_daily_setup_to_csv(results): + """Append key daily setup diagnostics to the beamline history CSV.""" + + vfm_mot = BeamMove.vfm_mot + hfm_mot = BeamMove.hfm_mot + + filename = bl.env.daily_setup_dir / "daily_beamline_health.csv" + + move_results = results.get("Move beam", {}) + energy_result = results.get("Set energy", {}) + flux_result = results.get("Measure flux", {}) + + row = { + "timestamp": datetime.now().isoformat(timespec="seconds"), + + # Energy / flux + "energy_eV": format_value(energy_result.get("energy"), ".0f"), + "pitch_offset": format_value( + energy_result.get("pitch_offset"), ".3f" + ), + "flux_ph_s": format_value( + flux_result.get("full_flux"), ".2e" + ), + + # Beam position before move_beam + "beam_start_x_px": format_value( + move_results.get("StartX"), ".0f" + ), + "beam_start_y_px": format_value( + move_results.get("StartY"), ".0f" + ), + + # Mirror positions before move_beam + "vfm_mot_start_mm": format_value( + move_results.get("Initial VFM MOT"), ".5f" + ), + "hfm_mot_start_mm": format_value( + move_results.get("Initial HFM MOT"), ".5f" + ), + + # Mirror positions after move_beam + "vfm_mot_final_mm": format_value( + move_results.get("Final VFM MOT"), ".5f" + ), + "hfm_mot_final_mm": format_value( + move_results.get("Final HFM MOT"), ".5f" + ), + + # Final beam displacement from crosshair + "beam_final_x_um": format_value( + move_results.get("BeamX"), ".1f" + ), + "beam_final_y_um": format_value( + move_results.get("BeamY"), ".1f" + ), + + # Final BPM beam displacement from crosshair + "beam_final_x_bpm_um": format_value( + move_results.get("BPM_X_end"), ".2f" + ), + "beam_final_y_bpm_um": format_value( + move_results.get("BPM_Y_end"), ".2f" + ), + + # Current mirror state after daily setup + "vfm_y_mm": format_value(get_value(dev.vfm_y), ".5f"), + "vfm_mot_mm": format_value(get_value(vfm_mot), ".5f"), + "vfm_pitch_mrad": format_value( + get_value(dev.vfm_pitch), ".5f" + ), + + "hfm_x_mm": format_value(get_value(dev.hfm_x), ".5f"), + "hfm_mot_mm": format_value(get_value(hfm_mot), ".5f"), + "hfm_pitch_mrad": format_value( + get_value(dev.hfm_pitch), ".5f" + ), + } + + file_exists = filename.exists() + + with open(filename, "a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=row.keys()) + + if not file_exists: + writer.writeheader() + + writer.writerow(row) + + print(f"✓ Daily beamline history saved to {filename}") + + +# ============================================================================= +# General helpers +# ============================================================================= +def format_value(value, fmt): + """Format a value for CSV file, return an empty string if unavailable""" + if value is None: + return "" + return format(value, fmt) + +def move_with_offset(motor, target, offset=0.002): + """Move motor to target using an overshoot to overcome deadband/backlash.""" + + current = get_value(motor) + dmove = target - current + + if abs(dmove) < 1e-9: + return current + + direction = np.sign(dmove) + overshoot = target + direction * abs(offset) + + umv(motor, overshoot) + umv(motor, target) + + final = get_value(motor) + + print(f"{motor.name}: requested {current:.6f} -> {target:.6f} " f"({dmove * 1000:+.3f} µm)") + print( + f"{motor.name}: actual {current:.6f} -> {final:.6f} " + f"({(final-current) * 1000:+.3f} µm), " + f"error {(final-target) * 1000:+.3f} µm" + ) + + return final + + +def set_zoom(zoom): + """Set sample camera zoom level.""" + + if isinstance(zoom, str): + zoom = CameraZoom.from_label(zoom) + umv(dev.scam_zoom, zoom.position) + else: + umv(dev.scam_zoom, zoom) + + +@contextmanager +def time_step(results, name): + """Time a daily setup step.""" + start = time.perf_counter() + + try: + yield + finally: + elapsed = time.perf_counter() - start + + if name not in results: + results[name] = {} + + results[name]["duration"] = elapsed + + +def format_duration(seconds): + if seconds < 60: + return f"{seconds:.1f} s" + + minutes, seconds = divmod(seconds, 60) + return f"{int(minutes)} min {seconds:.0f} s" + + + +def caget(pv): + """Read an EPICS PV using caget and return its value.""" + + try: + result = subprocess.run( + ["caget", "-t", pv], + capture_output=True, + text=True, + check=True, + timeout=2, + ) + + fields = shlex.split(result.stdout.strip()) + + # Find the PV name and take the value immediately after it + try: + pv_index = fields.index(pv) + value = fields[pv_index + 1] + except (ValueError, IndexError): + return None + + # Convert numeric values to float + try: + return float(value) + except ValueError: + return value + + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: + print(f"Could not read {pv}: {exc}") + return None + +def save_daily_beam_image(results): + """Save the final beam image with target crosshair to daily_setup_dir. + + The saved PNG is reused by both the HTML report and SciLog. + Returns the filename, or None if no beam image is available. + """ + + move_result = results.get("Move beam") + energy_result = results.get("Set energy") + + if not move_result: + return None + + image = move_result.get("image") + + if image is None: + return None + + # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = bl.env.daily_setup_dir / f"daily_beam_{energy_result['energy']:.0f}eV.png" + + fig, ax = plt.subplots(figsize=(8, 6)) + ax.imshow(image) + + x = BeamMove.scint_target_x + y = BeamMove.scint_target_y + length = 40 + gap = 8 + + # Target crosshair + ax.plot([x - length, x - gap], [y, y], color="red", linewidth=1.0) + ax.plot([x + gap, x + length], [y, y], color="red", linewidth=1.0) + ax.plot([x, x], [y - length, y - gap], color="red", linewidth=1.0) + ax.plot([x, x], [y + gap, y + length], color="red", linewidth=1.0) + + ax.axis("off") + + fig.savefig( + filename, + format="png", + bbox_inches="tight", + pad_inches=0, + dpi=150, + ) + plt.close(fig) + + move_result["image_file"] = filename + print(f"✓ Final beam image saved to {filename}") + + return filename + + +def image_file_to_data_uri(filename): + """Convert a PNG file to an embedded HTML data URI.""" + + with open(filename, "rb") as f: + encoded = base64.b64encode(f.read()).decode("ascii") + + return f"data:image/png;base64,{encoded}" + + +def html_status(result): + """Return a simple HTML status for a daily setup result.""" + + if not result: + return "– Not run" + + if result.get("warning"): + return "⚠ Warning" + + if result.get("ok"): + return "✓ OK" + + return "✗ Failed" + + +def html_value(value, fmt="", units=""): + """Safely format a value for the HTML report.""" + + if value is None: + return "–" + + try: + text = format(value, fmt) if fmt else str(value) + except (TypeError, ValueError): + text = str(value) + + return f"{text} {units}".strip() + + +def create_daily_html_report(results, total_duration=None, messages=None): + """Create the final Daily Setup HTML report.""" + + date = datetime.now().strftime("%d %B %Y %H:%M") + + precheck_result = results.get("Pre checks") + energy_result = results.get("Set energy") + move_result = results.get("Move beam") + beamstop_result = results.get("Align beamstop") + collimator_result = results.get("Align collimator") + flux_result = results.get("Measure flux") + + # -------------------------------------------------------------- + # Beam image + # -------------------------------------------------------------- + + image_src = None + + if move_result: + image_file = move_result.get("image_file") + + if image_file is not None: + try: + image_src = image_file_to_data_uri(image_file) + except Exception as exc: + print(f"Could not add beam image to HTML report: {exc}") + + if image_src: + beam_image_html = f""" +

Final beam position

+ +
+ Final beam position +
+ """ + else: + beam_image_html = """ +

Final beam position

+ +
+ Beam image not available +
+ """ + + # -------------------------------------------------------------- + # Detailed results + # -------------------------------------------------------------- + + result_rows = [] + + if energy_result: + result_rows.append( + ( + "Energy", + html_value(energy_result.get("energy"), ".0f", "eV"), + ) + ) + + result_rows.append( + ( + "DCM pitch offset", + html_value( + energy_result.get("pitch_offset"), + ".4f", + "mrad", + ), + ) + ) + + if flux_result: + result_rows.append( + ( + "Flux at 100% transmission", + html_value( + flux_result.get("full_flux"), + ".2e", + "ph/s", + ), + ) + ) + + if move_result: + result_rows.append( + ( + "Beam position", + ( + f"X = {html_value(move_result.get('BeamX'), '.1f', 'µm')}, " + f"Y = {html_value(move_result.get('BeamY'), '.1f', 'µm')}" + ), + ) + ) + + result_rows.append( + ( + "Approximate beam size", + ( + f"{html_value(move_result.get('X FWHM'), '.1f', 'µm')} × " + f"{html_value(move_result.get('Y FWHM'), '.1f', 'µm')}" + ), + ) + ) + + if beamstop_result: + result_rows.append( + ( + "Beamstop position", + ( + f"X = {html_value(beamstop_result.get('bs_x_pos'), '.3f', 'mm')}, " + f"Y = {html_value(beamstop_result.get('bs_y_pos'), '.3f', 'mm')}" + ), + ) + ) + + if collimator_result: + result_rows.append( + ( + "Collimator position", + ( + f"X = {html_value(collimator_result.get('coll_x_pos'), '.3f', 'mm')}, " + f"Y = {html_value(collimator_result.get('coll_y_pos'), '.3f', 'mm')}" + ), + ) + ) + + result_rows_html = "\n".join( + f""" + + {label} + {value} + + """ + for label, value in result_rows + ) + + if not result_rows_html: + result_rows_html = """ + + No detailed results available + + """ + + # -------------------------------------------------------------- + # Setup task summary + # -------------------------------------------------------------- + + task_rows = [] + + task_labels = [ + ("Pre checks", "Pre checks"), + ("Set energy", "Set energy"), + ("Move beam", "Move beam to crosshair"), + ("Align beamstop", "Align beamstop"), + ("Align collimator", "Align collimator"), + ("Measure flux", "Measure flux"), + ] + + for key, label in task_labels: + result = results.get(key) + + # Only show tasks that actually appear in results. + # Disabled tasks therefore disappear from the final report. + if result is not None: + duration = result.get("duration") + + duration_text = ( + format_duration(duration) + if duration is not None + else "–" + ) + + task_rows.append( + f""" + + {label} + {html_status(result)} + {duration_text} + + """ + ) + + task_rows_html = "\n".join(task_rows) + + total_duration_text = ( + format_duration(total_duration) + if total_duration is not None + else "–" + ) + + # -------------------------------------------------------------- + # Setup information / log messages + # -------------------------------------------------------------- + + if messages: + messages_text = "\n".join(messages) + + # Escape anything that could accidentally be interpreted as HTML + messages_text = html_lib.escape(messages_text) + + info_html = f""" +

Setup information

+ +
+ {messages_text} +
+ """ + else: + info_html = "" + + # -------------------------------------------------------------- + # HTML + # -------------------------------------------------------------- + + html = f""" + + + + + + + + + + +

{bl.env.beamline_name} Daily Setup

+

{date}

+ + +

Setup tasks

+ + + + + + + + + + {task_rows_html} + + + + + + + +
TaskStatusDuration
Total duration{total_duration_text}
+ + +

Results

+ + + {result_rows_html} +
+ + + {beam_image_html} + + {info_html} + + + + + """ + + return html \ No newline at end of file diff --git a/pxii_bec/macros/devices_manager.py b/pxii_bec/macros/devices_manager.py index 720caff..72ee82f 100755 --- a/pxii_bec/macros/devices_manager.py +++ b/pxii_bec/macros/devices_manager.py @@ -1,9 +1,9 @@ """ This module manages the initialization of devices.""" -#from guards import attach_guards -#from policies import attach_policies -#from build_devices import build_devices +# from guards import attach_guards +# from policies import attach_policies +# from build_devices import build_devices class DeviceManager: diff --git a/pxii_bec/macros/enScan.py b/pxii_bec/macros/enScan.py index e81b00d..47edfcc 100644 --- a/pxii_bec/macros/enScan.py +++ b/pxii_bec/macros/enScan.py @@ -11,7 +11,7 @@ def en_plothist(hindex: int, signal_name: str, isave=1): s=scans.line_scan(dev.dcm_bragg, 178.5, 179.5, steps=240, relative=False data_x, data_y = en_plothist(-1,"bcu_bpmsum") - scanned with + scanned with scans.line_scan(dev.aerotech.y,-0.05,0.05, steps=101, settling_time=0.5, relative=True) """ @@ -21,54 +21,56 @@ def en_plothist(hindex: int, signal_name: str, isave=1): if hindex < 0: h = bec.history[hindex] - print('by scan number') + print("by scan number") else: h = bec.history.get_by_scan_number(hindex) - print('by scan number') + print("by scan number") md = h.metadata["bec"] - sno = md['dataset_number'] + sno = md["dataset_number"] print(f"number of scan = {sno}") - scanvar = list(md["args"].keys())[0] # string, returns the variable of the last performed scan + #scanvar = list(md["args"].keys())[0] # string, returns the variable of the last performed scan # data = h.devices[device_name][signal].read()["value"] # data_x = h.devices.dcm_pitch.dcm_pitch.read()["value"] # data_y = h.devices.lu_bpmsum.lu_bpmsum.read()["value"] # data_x = h.devices[device_name][device_name].read()["value"] - # that works ... - #print(list(md['args'].keys())) - val = list(md['args'].keys()) - #val[0] = 'aerotech.x' - devicenam = val[0] - print('device = ', devicenam) + # that works ... + # print(list(md['args'].keys())) + ## val = list(md["args"].keys()) no longer !!!!! to be checked !!! + # val[0] = 'aerotech.x' + ##devicenam = val[0] + ##print("device = ", devicenam) + devicenam = md["scan_report_devices"][0].decode() + + data_xd = md["positions"] # last scan knows which device ... however, double array data_x = data_xd.flatten() print("dx = ", data_x) - #data_y = h.devices[signal_name][signal_name].read()["value"] + # data_y = h.devices[signal_name][signal_name].read()["value"] data_y1 = h.devices["bcu_bpmsum"]["bcu_bpmsum"].read()["value"] data_y2 = h.devices["ss_bpmsum"]["ss_bpmsum"].read()["value"] - - #print("dy = ", data_y) - data_y = data_y1/data_y2 - rev = 0 - #if val[0][9] == 'x': + # print("dy = ", data_y) + + data_y = data_y1 / data_y2 + rev = 0 + # if val[0][9] == 'x': # rev = 1 - smoothplotgrad(data_x, data_y, reverse = rev) - # g, xm = justfit(data_x, data_y, model=model, ibg=ibg) + smoothplotgrad(data_x, data_y, reverse=rev) + # g, xm = justfit(data_x, data_y, model=model, ibg=ibg) -# plt.ion() -# plt.figure() -# plt.plot(data_x, data_y, "*") -# plt.plot(data_x, g.best_fit) -# plt.xlabel(scanvar) -# plt.ylabel(signal_name) -# plt.show() + # plt.ion() + # plt.figure() + # plt.plot(data_x, data_y, "*") + # plt.plot(data_x, g.best_fit) + # plt.xlabel(scanvar) + # plt.ylabel(signal_name) + # plt.show() -# gcen = g.pars["center"].value + # gcen = g.pars["center"].value - - if isave: + if isave: ans = "n" ans = input("Store data in csv file? y/n ") if ans == "y": @@ -79,5 +81,4 @@ def en_plothist(hindex: int, signal_name: str, isave=1): with open(filename, "w") as f: np.savetxt(f, combined, delimiter=",", fmt="%.9e") - return data_x, data_y diff --git a/pxii_bec/macros/flux_macros.py b/pxii_bec/macros/flux_macros.py index 11984e5..155265b 100644 --- a/pxii_bec/macros/flux_macros.py +++ b/pxii_bec/macros/flux_macros.py @@ -256,7 +256,7 @@ def bpm2flux(energy=None, bpmsum=None, transm=None): """ calibration_min_energy = 6500 - calibration_max_energy = 21000 + # calibration_max_energy = 21000 if energy is None: energy = get_current_energy() @@ -266,14 +266,14 @@ def bpm2flux(energy=None, bpmsum=None, transm=None): # bpmsum = read_av(dev.bcu_bpmsum,10) # do not add to traffic jam if transm is None: - transm = dev.transm.read()["transm"]["value"] + transm = dev.transm.read(cached=True)["transm"]["value"] - if not calibration_min_energy <= energy <= calibration_max_energy: - raise ValueError( - f"Energy {energy:.0f} eV is outside the calibrated range " - f"{calibration_min_energy:.0f}–" - f"{calibration_max_energy:.0f} eV" - ) + # if not calibration_min_energy <= energy <= calibration_max_energy: + # raise ValueError( + # f"Energy {energy:.0f} eV is outside the calibrated range " + # f"{calibration_min_energy:.0f}–" + # f"{calibration_max_energy:.0f} eV" + # ) # coeffs = [ # 1.06436277e5, @@ -296,3 +296,17 @@ def bpm2flux(energy=None, bpmsum=None, transm=None): ) return float(flux), float(full_flux) + +def transmission_for_flux(target_flux): + """ + Calculate the transmission required to give target_flux + at the current energy. + + Transmission is returned as a fraction from 0 to 1. + """ + + _, full_flux = bpm2flux() + + transmission = target_flux / full_flux + + return float(np.clip(transmission, 0.0, 1.0)) diff --git a/pxii_bec/macros/katscripts.py b/pxii_bec/macros/katscripts.py index ff32868..7cb22e1 100755 --- a/pxii_bec/macros/katscripts.py +++ b/pxii_bec/macros/katscripts.py @@ -13,6 +13,7 @@ from pathlib import Path from pxii_bec.scripts import beamline_context as bl from pxii_bec.macros.beamline_planner import BeamlineState + def bragg_scan(): """Energy scans from 6 to 18 keV at various gap positions""" @@ -31,6 +32,7 @@ def bragg_scan(): ) gap += 0.5 + def scan_bpm(bpmname): """ Runs a grid scan of a BPM in x and y, and plots each channel @@ -188,34 +190,126 @@ def energy_checks(start_energy=8000, end_energy=20000): f.close() -def scan_fe(): - """Scan the FE slits in X and Y""" - print("Scanning FE slits in X") - umv(dev.fe_sl_xsize, 0.05, dev.fe_sl_ysize, 1) - go_to_peak(dev.fe_sl_xcen, dev.lu_bpmsum, -1.5, 1.5, 20, confirm=False, relative=True) - print("Scanning FE slits in Y") - umv(dev.fe_sl_xsize, 1, dev.fe_sl_ysize, 0.05) - go_to_peak(dev.fe_sl_ycen, dev.lu_bpmsum, -1.2, 1.2, 15, confirm=False, relative=True) - close_fe_slits(False) + +# def scan_gap(gomax = False, close = True, fine = False): +# """Scan the gap and monitor on lu_bpmsum or bcu_bpmsum""" +# close_fe_slits(close) +# current_gap = dev.id_gap.read()["id_gap"]["value"] +# if current_gap > 4.6: +# go_to_peak( +# dev.id_gap, dev.lu_bpmsum, -0.15, 0.15, 25, relative=True, gap=True, confirm=False, gomax=gomax +# ) +# else: +# go_to_peak( +# dev.id_gap, dev.lu_bpmsum, 4.5, 4.75, 25, relative=False, gap=True, confirm=False, gomax=gomax +# ) +# if fine: +# go_to_peak( +# dev.id_gap, dev.lu_bpmsum, -0.012, 0.012, 6, relative=True, gap=True, confirm=False, gomax=gomax +# ) +# close_fe_slits(False) -def scan_gap(gomax = False, close = True, fine = False): - """Scan the gap and monitor on lu_bpmsum or bcu_bpmsum""" +def scan_gap(gomax=False, close=True, fine=False, min_fwhm_steps=1.5): + """ + Scan the undulator gap and monitor the LU BPM sum. + + If gomax=True, go_to_peak() moves to the highest measured point + without applying peak-fit validation. + + Returns + ------- + dict + Results from the coarse and, if requested, fine scans. + `min_gap_reached` is True when the coarse-scan maximum is at + the physical minimum gap of 4.5 mm. + """ + + MIN_GAP = 4.5 + close_fe_slits(close) - current_gap = dev.id_gap.read()["id_gap"]["value"] - if current_gap > 4.6: - go_to_peak( - dev.id_gap, dev.lu_bpmsum, -0.15, 0.15, 25, relative=True, gap=True, confirm=False, gomax=gomax - ) - else: - go_to_peak( - dev.id_gap, dev.lu_bpmsum, 4.5, 4.75, 25, relative=False, gap=True, confirm=False, gomax=gomax - ) - if fine: - go_to_peak( - dev.id_gap, dev.lu_bpmsum, -0.012, 0.012, 6, relative=True, gap=True, confirm=False, gomax=gomax - ) - close_fe_slits(False) + + try: + current_gap = get_value(dev.id_gap) + + # -------------------------------------------------------- + # Coarse scan + # -------------------------------------------------------- + + if current_gap > 4.65: + coarse_result = go_to_peak( + dev.id_gap, + dev.lu_bpmsum, + -0.15, + 0.15, + 25, + relative=True, + gap=True, + confirm=False, + gomax=gomax, + min_fwhm_steps=min_fwhm_steps, + ) + + else: + coarse_result = go_to_peak( + dev.id_gap, + dev.lu_bpmsum, + MIN_GAP, + 4.75, + 25, + relative=False, + gap=True, + confirm=False, + gomax=gomax, + min_fwhm_steps=min_fwhm_steps, + ) + + # -------------------------------------------------------- + # Check for physical minimum-gap limit + # -------------------------------------------------------- + + min_gap_reached = False + + if gomax: + min_gap_reached = coarse_result["at_lower_boundary"] and np.isclose( + coarse_result["x_max"], MIN_GAP + ) + + if min_gap_reached: + print("\nMaximum occurs at the minimum gap of " f"{MIN_GAP:.2f} mm.") + print("The optimum for this harmonic may lie below " "the accessible gap.") + + # -------------------------------------------------------- + # Fine scan + # -------------------------------------------------------- + + fine_result = None + + if fine and not min_gap_reached: + fine_result = go_to_peak( + dev.id_gap, + dev.lu_bpmsum, + -0.012, + 0.012, + 6, + relative=True, + gap=True, + confirm=False, + gomax=gomax, + ) + + elif fine and min_gap_reached: + print("Skipping fine gap scan because the coarse maximum " "is at the minimum gap.") + + return { + "success": True, + "coarse": coarse_result, + "fine": fine_result, + "min_gap_reached": min_gap_reached, + } + + finally: + close_fe_slits(False) def record_pitch(): @@ -257,41 +351,294 @@ def beam_centre_from_ss(): return beampos -def generate_dcm_lut(env, planner, start_energy=6000, end_energy=30000, step=300, scint=True): - """Generate a lookup table for the dcm from start_energy to end_energy in steps of step""" - filename = env.lut_dir / f"dcm_calibration_{filenow()}.csv" - zoom = dev.scam_zoom.read()['scam_zoom']['value'] - with open(filename, "w", encoding="utf-8") as f: - f.write(f"energy,harmonic,gap,pitch,samcam_x,samcam_y,zoom={zoom}\n") +def generate_dcm_lut( + start_energy=6000, + end_energy=30000, + step=1000, + scan_id_gap=False, + scint=True, + prefix = "dcm_calibration", +): + + """Generate a DCM calibration lookup table. + + Parameters + ---------- + start_energy : int + Starting energy in eV. + end_energy : int + Final energy in eV. + step : int + Energy step in eV. + scan_id_gap : bool + If True, optimise both undulator gap and DCM pitch. + If False, only optimise the DCM pitch. + scint : bool + If True, record sample-camera beam position. + + Returns + ------- + list + List of failed scans with energy, scan type and failure reason. + """ + + filename = bl.env.lut_dir / f"{prefix}_{filenow(date_only=True)}.csv" + failure_filename = bl.env.lut_dir / f"{prefix}_fail_{filenow(date_only=True)}.csv" + + failures = [] + + zoom = get_value(dev.scam_zoom) + + # Create calibration file + if scan_id_gap: + with open(filename, "w", encoding="utf-8") as f: + if scint: + f.write(f"energy,harmonic,gap,pitch,lu_Xnorm,lu_Ynorm,ss_Xnorm,ss_Ynorm,bcu_x,bcu_y,samcam_x,samcam_y,zoom={zoom}\n") + else: + f.write("energy,harmonic,gap,pitch,lu_Xnorm,lu_Ynorm,ss_Xnorm,ss_Ynorm,bcu_x,bcu_y,samcam_x,samcam_y,zoom={zoom}\n") + else: + with open(filename, "w", encoding="utf-8") as f: + if scint: + f.write(f"energy,pitch,lu_Xnorm,lu_Ynorm,ss_Xnorm,ss_Ynorm,bcu_x,bcu_y,samcam_x,samcam_y,zoom={zoom}\n") + else: + f.write("energy,pitch,lu_Xnorm,lu_Ynorm,ss_Xnorm,ss_Ynorm,bcu_x,bcu_y\n") + + # Create failure log + with open(failure_filename, "w", encoding="utf-8") as f: + f.write("energy,scan,reason\n") + try: + for energy in range(start_energy, end_energy + 1, step): + + print("\n" + "=" * 60) + print(f"Moving to {energy:.0f} eV") + print("=" * 60) + + # ------------------------------------------------------------ + # Move to energy without automatically running mono pitch scan + # ------------------------------------------------------------ + + try: + bl_energy(energy, mono_scan=False) + + except Exception as exc: + reason = str(exc) + + print(f"Energy move failed at {energy:.0f} eV: {reason}") + + failure = {"energy": energy, "scan": "energy", "reason": reason} + + failures.append(failure) + + with open(failure_filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},energy,{reason}\n") + + # Cannot calibrate this energy if the energy move failed + continue + + # ------------------------------------------------------------ + # Undulator gap scan + # ------------------------------------------------------------ + + if scan_id_gap: + + try: + scan_gap(close=True, gomax=True, fine=True) + + except Exception as exc: + reason = str(exc) + + print(f"Gap scan failed at {energy:.0f} eV: " f"{reason}") + + failure = {"energy": energy, "scan": "gap", "reason": reason} + + failures.append(failure) + + with open(failure_filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},gap,{reason}\n") + + # Fall back to calculated gap + print(f"Setting calculated gap for " f"{energy:.0f} eV") + set_gap(energy) + + # ------------------------------------------------------------ + # DCM pitch scan + # ------------------------------------------------------------ + + try: + result = mono_pitch_scan() + + if not result["success"]: + + reason = "; ".join(result["problems"]) + + print(f"Mono pitch scan rejected at " f"{energy:.0f} eV:") + + for problem in result["problems"]: + print(f" - {problem}") + + failure = {"energy": energy, "scan": "mono_pitch", "reason": reason} + + failures.append(failure) + + with open(failure_filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},mono_pitch,{reason}\n") + + except Exception as exc: + reason = str(exc) + + print(f"Mono pitch scan failed at " f"{energy:.0f} eV: {reason}") + + failure = {"energy": energy, "scan": "mono_pitch_exception", "reason": reason} + + failures.append(failure) + + with open(failure_filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},mono_pitch_exception," f"{reason}\n") + + # ------------------------------------------------------------ + # Record calibration values + # ------------------------------------------------------------ + + res = get_bcu_beampos() + bcu_x = res['bcu_x_um'] + bcu_y = res['bcu_y_um'] + ss_res = compute_norm_ss() + ss_Xnorm = ss_res['ss_Xnorm'] + ss_Ynorm = ss_res['ss_Ynorm'] + lu_res = compute_norm_lu() + lu_Xnorm = lu_res['lu_Xnorm'] + lu_Ynorm = lu_res['lu_Ynorm'] + + + pitch = get_value(dev.dcm_pitch) + + if scan_id_gap: + gap_value = get_value(dev.id_gap) + harm = Gap() + h = harm.get_harmonic_by_energy(energy) + + if scint: + prepare_scint_measurement() + res = read_scintXY(image=False) + x = res["X_diff_microns"] + y = res["Y_diff_microns"] + finish_scint_measurement() + + with open(filename, "a", encoding="utf-8") as f: + + if scan_id_gap: + + if scint: + f.write( + f"{energy:.0f}," + f"{h}," + f"{gap_value:.5g}," + f"{pitch:.5g}," + f"{lu_Xnorm:.5g}," + f"{lu_Ynorm:.5g}," + f"{ss_Xnorm:.5g}," + f"{ss_Ynorm:.5g}," + f"{bcu_x:.2f}," + f"{bcu_y:.2f}," + f"{x:.4g}," + f"{y:.4g}\n" + ) + else: + f.write( + f"{energy:.0f},{h},{gap_value:.5g},{pitch:.5g}," + f"{ss_Xnorm:.5g},{ss_Ynorm:.5g},{bcu_x:.2f},{bcu_y:.2f}\n" + ) + + else: + + if scint: + f.write( + f"{energy:.0f},{pitch:.5g},{lu_Xnorm:.5g},{lu_Ynorm:.5g}," + f"{ss_Xnorm:.5g},{ss_Ynorm:.5g},{bcu_x:.2f},{bcu_y:.2f},{x:.4g},{y:.4g}\n" + ) + else: + f.write( + f"{energy:.0f}," f"{pitch:.5g},{lu_Xnorm:.5g},{lu_Ynorm:.5g}," + f"{ss_Xnorm:.5g},{ss_Ynorm:.5g},{bcu_x:.2f},{bcu_y:.2f}\n" + ) + + finally: + print("\nReturning beamline to safe conditions...") - for energy in range(start_energy, end_energy, step): - print(f"Moving to {energy: .0f}eV") - bl_energy(energy, mono_scan=False) try: - scan_gap(close=True, gomax=True, fine=True) + print("done") + # bl_energy(12400) except Exception as exc: - print(f"Gap scan failed at {energy:.0f} eV: {exc}") - set_gap(energy) # set fallback gap here + print(f"WARNING: Could not return to 12400 eV: {exc}") - mono_pitch_scan() - gap = dev.id_gap.read()["id_gap"]["value"] - harm = Gap() - h = harm.get_harmonic_by_energy(energy) - pitch = dev.dcm_pitch.read()["dcm_pitch"]["value"] - if scint: - x, y = read_samcam_scint(planner) - with open(filename, "a", encoding="utf-8") as f: - f.write( - f"{energy:.0f},{h},{gap:.5g},{pitch:.5g},{x:.4g},{y:.4g}\n" - ) - else: - with open(filename, "a", encoding="utf-8") as f: - f.write( - f"{energy:.0f},{h},{gap:.5g},{pitch:.5g}\n" - ) + # try: + # umv(dev.id_gap, 20) + # except Exception as exc: + # print(f"WARNING: Could not open undulator gap: {exc}") - bl_energy(12400) - umv(dev.id_gap, 20.0) + # ------------------------------------------------------------ + # Summary + # ------------------------------------------------------------ + + print("\n" + "=" * 60) + print("DCM calibration complete") + print("=" * 60) + + if failures: + + print(f"\n{len(failures)} scan failure(s):\n") + + for failure in failures: + print( + f"{failure['energy']:5.0f} eV " f"{failure['scan']:22s} " f"{failure['reason']}" + ) + + print(f"\nFailure log written to:\n" f"{failure_filename}") + + else: + print("\nNo scan failures.") + + print(f"\nCalibration data written to:\n" f"{filename}") + + return { + "filename": filename, + "failure_filename": failure_filename, + "failures": failures, + } + +def calibrate_dcm_pitch( + start_energy=6000, + end_energy=30000, + step=2000, + fit_order=3, +): + """Quickly recalibrate the DCM pitch lookup table.""" + + print("\nRunning DCM pitch calibration") + print( + f"{start_energy:.0f}–{end_energy:.0f} eV " + f"in {step:.0f} eV steps" + ) + + failures = generate_dcm_lut( + start_energy=start_energy, + end_energy=end_energy, + step=step, + scan_id_gap=False, + scint=False, + ) + + print("\nAnalysing DCM pitch calibration...") + + fit = pitch_analysis( + filedate=filenow(date_only=True), + fit_order=fit_order, + ) + + return { + "fit": fit, + "failures": failures, + } def write_lut(): @@ -311,7 +658,6 @@ def write_lut(): def measure_beamsize(cam="samcam"): - auto_exposure("samcam", max_iter=20) a = dev.samcam_xsig.read()["samcam_xsig"]["value"] b = dev.samcam_ysig.read()["samcam_ysig"]["value"] zoom = dev.scam_zoom.read()["scam_zoom"]["value"] @@ -339,68 +685,112 @@ def write_beamsize(): f.write(f"{lens_config},{sx:.4g},{sy:.4g},{i1_reading:.4g}\n") +def scan_ge(edge=11103.1, mode="coarse"): + """ + Scan across the Ge K absorption edge. -def scan_Ge(): - scanbefore = 12 # eV - scanafter = 12 # eV - stepsize = 0.2 # eV - # Edge sgould be at 11103.1 - actual_edge_Ge = 11103.1 # eV - actual_bragg = convert_from_energy(actual_edge_Ge)["bragg_angle_mrad"] - current_edge = 11095 - current_bragg = convert_from_energy(current_edge)["bragg_angle_mrad"] - print(f"Ge edge should be at a bragg angle of {actual_bragg:.5g} mrad\n") - print(f"Current estimate of edge is {current_bragg:.5g} mrad\n") - xdata = [] - ydata = [] - filename = f"{bl.env.lut_dir}/{filenow(date_only=True)}_ge_edge.csv" - print(filename) - with open(filename, 'w') as f: - f.write(f"Energy,Bragg,bcu_bpmsum\n") - + Parameters + ---------- + edge : float + Approximate edge energy in eV around which to centre the scan. - plot_live_data(dev.dcm_bragg,dev.bcu_bpmsum) - scanpoints = np.linspace(current_bragg - 0.5, current_bragg + 0.5, 100) - start_energy = convert_from_bragg(current_bragg+0.5)["energy_ev"] - bl_energy(start_energy) - dev.transm.put(1.0) # Take all the filters out - umv(dev.ss_f4_x, 6) # Put Ge filter in place + mode : {"coarse", "fine"} + coarse : ±100 eV, ~5 eV steps + fine : ±15 eV, ~0.5 eV steps + """ + + # Scan settings + if mode == "coarse": + scan_range = 100 + scanpoints = 41 + + elif mode == "fine": + scan_range = 15 + scanpoints = 61 + + else: + raise ValueError( + f"Unknown scan mode '{mode}'. Use 'coarse' or 'fine'." + ) + + # Energy range + start_eV = edge - scan_range + end_eV = edge + scan_range + + # Convert energy limits to Bragg positions + start_bragg = convert_from_energy(start_eV)["bragg_angle_mrad"] + end_bragg = convert_from_energy(end_eV)["bragg_angle_mrad"] + + bragg_points = np.linspace( + start_bragg, + end_bragg, + scanpoints, + ) + + energy_step = (end_eV - start_eV) / (scanpoints - 1) + + # Output file + filename = ( + bl.env.lut_dir + / f"{filenow(date_only=True)}_ge_edge_{mode}.csv" + ) + + with open(filename, "w") as f: + f.write("Energy,Bragg,ss_bpmsum,bcu_bpmsum,transmission\n") + + # Move to start + print() + print(f"Ge edge {mode} scan") + print("-------------------") + print(f"Centre: {edge:.1f} eV") + print(f"Range: {start_eV:.1f} - {end_eV:.1f} eV") + print(f"Points: {scanpoints}") + print(f"Approx step: {energy_step:.2f} eV") + print(f"Results: {filename}") + print() + + print(f"Moving energy to {start_eV:.1f} eV") + bl_energy(start_eV) + + # Take all the filters out and then put Ge foil in + dev.transm.put(1) + time.sleep(2) + umv(dev.ss_f4_x,6.0) + time.sleep(3) + + # Scan + print("Starting scan") + + for bragg in bragg_points: + + scans.umv( + dev.dcm_bragg, + bragg, + relative=False, + ) - for bragg in scanpoints: - scans.umv(dev.dcm_bragg, bragg, relative=False) energy = convert_from_bragg(bragg)["energy_ev"] + ss=get_value(dev.ss_bpmsum) inten = average_inten() - xdata.append(energy) - ydata.append(inten) - with open(filename, 'a') as f: - f.write(f"{energy:.2f},{bragg:.2f},{inten:.2f}\n") - data = { - "x_data": np.array(xdata), - "y_data": np.array(ydata), - "motor_name": "dcm_bragg", - "signal_name": "bcu_bpmsum", - "motor_device": dev.dcm_bragg, - "scan_number": "Current", - } - dev.transm.put(0.1) # set transmission back to 0.1 - # Define and fit model to scan data - fit_params = create_fit_parameters(deriv = True, - negative = False, - model = FitDefaults.MODEL, - baseline = FitDefaults.BASELINE) - fit_result = fit(data, fit_params) - - # Plot the fitted data if plot = True - - plot_fitted_data(data, fit_result) + i1 = get_value(dev.i1) + transmission = inten/ss + with open(filename, "a") as f: + f.write( + f"{energy:.2f},{bragg:.2f},{ss:.2f},{inten:.2f},{transmission:.6f}\n" + ) + + print() + print("Scan complete") + dev.transm.put(0.1) + print(f"Results written to {filename}") def cal_samcam_fixed_x(): # lat = dev.hfm_x.read()["hfm_x"]["value"] # pitch = dev.hfm_pitch.read()["hfm_pitch"]["value"] - fpitch = dev.dcm_fpitch.read()['dcm_fpitch']['value'] + fpitch = dev.dcm_fpitch.read()["dcm_fpitch"]["value"] scanpoints = np.linspace(3.75, 4.75, 7) zoom_high = 800 zoom_low = 200 @@ -631,15 +1021,21 @@ def correlate_fpitch(): plt.show() -def correlate_hfm_x(): +def correlate_hfm_xd(meas="bcu"): x_correlation = [] - lat = dev.hfm_x.read()["hfm_x"]["value"] - scanpoints = np.linspace(lat - 0.1, lat + 0.1, 10) + startx = get_value(dev.hfm_xd) + scanpoints = np.linspace(startx - 0.02, startx + 0.02, 10) for pos in scanpoints: - umv(dev.hfm_x, pos) - xpos, ypos = compute_pos() + umv(dev.hfm_xd, pos) + if meas == "bcu": + xpos, ypos = get_bcu_beampos() + elif meas == "scint": + xpos, ypos = read_scintXY(1000) + else: + print("define measurement device, bcu or scint") + return x_correlation.append(xpos) - umv(dev.hfm_x, lat) + umv(dev.hfm_xd, startx) x = np.array(scanpoints) y1 = np.array(x_correlation) @@ -656,45 +1052,61 @@ def correlate_hfm_x(): plt.plot( x_fit, xslope * x_fit + xint, label=f"y = {xslope:.5g}*x + ({xint:.5g})", color="lightcoral" ) - plt.xlabel("HFM lateral (mm)") - plt.ylabel("beam X (from BCU BPM)") + plt.xlabel("HFM Downstream X (mm)") + if meas == "bcu": + plt.ylabel("beam X (from BCU BPM)") + elif meas == "scint": + plt.ylabel("beam X (from Scintillator)") plt.legend() plt.grid(True) plt.tight_layout() plt.show() -# def correlate_vfm_y(): -# y_correlation = [] -# vert = dev.vfm_y.read()["vfm_y"]["value"] -# scanpoints = np.linspace(vert - 0.06, vert + 0.06, 10) -# for pos in scanpoints: -# umv(dev.vfm_y, pos) -# xpos, ypos = compute_pos() -# y_correlation.append(ypos) -# umv(dev.vfm_y, vert) +def correlate_vfm_ydr(meas="bcu"): + y_correlation = [] + starty = get_value(dev.vfm_ydr) + if meas == "bcu": + scanpoints = np.linspace(starty - 0.005, starty + 0.005, 10) + elif meas == "scint": + scanpoints = np.linspace(starty - 0.02, starty + 0.02, 10) -# x = np.array(scanpoints) -# y1 = np.array(y_correlation) + for pos in scanpoints: + umv(dev.vfm_ydr, pos) + if meas == "bcu": + xpos, ypos = get_bcu_beampos() + elif meas == "scint": + xpos, ypos = read_scintXY(1000) + else: + print("define measurement device, bcu or scint") + return + y_correlation.append(ypos) + umv(dev.vfm_ydr, starty) -# yslope, yint = np.polyfit(x, y1, 1) + x = np.array(scanpoints) + y1 = np.array(y_correlation) -# print(f"Fit for xpos is: y = {yslope}.x + {yint}") -# plt.figure(figsize=(7, 5)) -# # plt.scatter(x, y1, label='xpos') -# plt.scatter(x, y1, label="beam y position", color="rebeccapurple") + xslope, xint = np.polyfit(x, y1, 1) -# x_fit = np.linspace(min(x), max(x), 100) -# # plt.plot(x_fit, xslope*x_fit + xint, 'r--', label=f'y = {xslope}*x + {xint}') -# plt.plot( -# x_fit, yslope * x_fit + yint, label=f"y = {yslope:.5g}*x + ({yint:.5g})", color="lightcoral" -# ) -# plt.xlabel("VFM vertical (mm)") -# plt.ylabel("beam Y(from BCU BPM)") -# plt.legend() -# plt.grid(True) -# plt.tight_layout() -# plt.show() + print(f"Fit for ypos is: y = {xslope:.5g}.x + {xint:.5g}") + plt.figure(figsize=(7, 5)) + # plt.scatter(x, y1, label='xpos') + plt.scatter(x, y1, label="beam y position", color="rebeccapurple") + + x_fit = np.linspace(min(x), max(x), 100) + # plt.plot(x_fit, xslope*x_fit + xint, 'r--', label=f'y = {xslope}*x + {xint}') + plt.plot( + x_fit, xslope * x_fit + xint, label=f"y = {xslope:.5g}*x + ({xint:.5g})", color="lightcoral" + ) + plt.xlabel("VFM Downstream Ring Y (mm)") + if meas == "bcu": + plt.ylabel("beam Y (from BCU BPM)") + if meas == "scint": + plt.ylabel("beam Y (from Scintillator)") + plt.legend() + plt.grid(True) + plt.tight_layout() + plt.show() def correlate_hfm_pitch(): @@ -876,17 +1288,18 @@ def filters(pos="out"): if SE.scin.checkpos(): auto_exposure(cam="samcam", max_iter=20) -def filenow(date_only = False): + +def filenow(date_only=False): """Return current time formatted for filenames.""" if date_only: return datetime.now().strftime("%Y%m%d") else: - return datetime.now().strftime("%Y%m%d-%H%M") + return datetime.now().strftime("%Y%m%d-%H%M") def energy_vs_height(start_energy=11000, end_energy=25000, step=1000): """Correlate beam height with energy from start_energy to end_energy in steps of step""" - filedir, fnow = fnow(dir = "test") + filedir, fnow = fnow(dir="test") filename = f"{filedir}/{fnow}_energy-height-ss.csv" with open(filename, "w", encoding="utf-8") as f: # f.write("energy,bcu_norm_y,bcu_y,samcam_ypix,samcam_y\n") @@ -916,7 +1329,6 @@ def energy_vs_height(start_energy=11000, end_energy=25000, step=1000): # umv(dev.id_gap, 20.0) - def scan_in_x(xsize, xcen, mon, small, large, width, steps): umv(xsize, small) go_to_peak(xcen, mon, -(width * 0.5), (width * 0.5), steps=steps, relative=False) @@ -934,16 +1346,7 @@ def trans_fils(inc): x2 = dev.ss_f2_x.read()["ss_f2_x"]["value"] x3 = dev.ss_f3_x.read()["ss_f3_x"]["value"] x4 = dev.ss_f4_x.read()["ss_f4_x"]["value"] - umv( - dev.ss_f1_x, - x1 + inc, - dev.ss_f2_x, - x2 + inc, - dev.ss_f3_x, - x3 + inc, - dev.ss_f4_x, - x4 + inc, - ) + umv(dev.ss_f1_x, x1 + inc, dev.ss_f2_x, x2 + inc, dev.ss_f3_x, x3 + inc, dev.ss_f4_x, x4 + inc) def calc_from_centre(cam="samcam"): @@ -986,14 +1389,13 @@ def get_cam(low_zoom, high_zoom): # else: # print("Adjust X tilt in anticlockwise direction") if -15 <= ydiff <= 15: - print("Y is OK") + print("Y is OK") elif ydiff > 15: - print("Adjust Y tilt in anticlockwise direction") + print("Adjust Y tilt in anticlockwise direction") else: print("Adjust Y tilt in clockwise direction") - # def read_currents(): # """Read the individual bpm channels""" # bpm1 = dev.bcu_bpm1.read()["bcu_bpm1"]["value"] @@ -1013,17 +1415,19 @@ def get_cam(low_zoom, high_zoom): # yn = ((readings["bpm1"] + readings["bpm3"]) - (readings["bpm2"] + readings["bpm4"])) / total # return xn, yn -def average_inten(): - vals_inten = [] - for _ in range(5): - vals_inten.append(dev.bcu_bpmsum.read()['bcu_bpmsum']['value']) - time.sleep(0.1) - inten = np.mean(vals_inten) - return inten -def mirror_pitch(mirror = 'vfm'): - - if mirror == 'vfm': +def average_inten(): + vals_inten = [] + for _ in range(5): + vals_inten.append(dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"]) + time.sleep(0.1) + inten = np.mean(vals_inten) + return inten + + +def mirror_pitch(mirror="vfm"): + + if mirror == "vfm": pitch = dev.vfm_pitch.position mot = dev.vfm_pitch start = 2.94 @@ -1032,9 +1436,9 @@ def mirror_pitch(mirror = 'vfm'): mot = dev.hfm_pitch start = 3.3 print(f"Scanning {mirror}_pitch from 10-29 keV") - pitch_range = [start - 0.25, start - 0.2, start -0.15, start - 0.1, start -0.05] + pitch_range = [start - 0.25, start - 0.2, start - 0.15, start - 0.1, start - 0.05] - umv(mot,start) + umv(mot, start) results = [] now = datetime.now() @@ -1056,122 +1460,20 @@ def mirror_pitch(mirror = 'vfm'): vals_diode = [] for _ in range(5): - vals_diode.append(dev.xidiode.read()['xidiode']['value']) + vals_diode.append(dev.xidiode.read()["xidiode"]["value"]) time.sleep(0.1) diode = np.mean(vals_diode) - results.append([ - energy, - p, - diode, - ]) + results.append([energy, p, diode]) with open(csvfile, "a") as f: - f.write( - f"{energy:.2f},{p:.5f},{diode:.5g}\n" - ) - umv(mot,start) + f.write(f"{energy:.2f},{p:.5f},{diode:.5g}\n") + umv(mot, start) bl_energy(12400) results = np.array(results) -def read_scintXY(zoom=800): - """ Get the X and Y beam positions from the sample camera at - a sepcified zoom. Default zoom is 800""" - # Ensure shutter and detector cover are closed before changing states - dev.bcu_shutter.put(0) - bl.d['det_cov'].move('close') - try: - # set transmission to 10% - dev.transm.put(0.1) - # set camera zoom - umv(dev.scam_zoom, zoom) - # move to beam visualisation - bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) - # open shutter - dev.bcu_shutter.put(1) - # set autoexposure - auto_exposure(cam="samcam", max_iter=25) - time.sleep(0.2) - x = dev.samcam_x.read()['samcam_x']['value'] - y = dev.samcam_y.read()['samcam_y']['value'] - return x, y - finally: - dev.bcu_shutter.put(0) - -def find_best_roll(low = 6000, high = 20000): - # low_scanpoints = np.linspace(2.0, 7.0, 11) - low_scanpoints = np.linspace(2.5, 6.5, 9) - high_scanpoints = np.linspace(1.0, 9.0,9) - highs_y, lows_y = [], [] - filedir, _ = filenow(dir="test") - - # low energy - bl_energy(low) - # filename = f"{filedir}/roll_{low:.0f}.csv" - filename = env.lut_dir / f"roll_{low:.0f}.csv" - with open(filename, "w", encoding="utf-8") as f: - f.write("Roll, Y pos\n") - for i in low_scanpoints: - umv(dev.dcm_froll, i) - time.sleep(0.2) - _, low_y = read_samcam_scint() - print(f"Ypos at sample cam is {low_y}") - lows_y.append(low_y) - with open(filename, "a", encoding="utf-8") as f: - f.write(f"{i:.4g},{low_y:.4g} \n") - - # high energy - bl_energy(high) - # filename = f"{filedir}/roll_{high:.0f}.csv" - filename = env_lut_dir / f"roll_{high:.0f}.csv" - with open(filename, "w", encoding="utf-8") as f: - f.write("Roll, Y pos\n") - auto_exposure("samcam", max_iter=25) - for i in high_scanpoints: - umv(dev.dcm_froll, i) - time.sleep(0.2) - _, high_y = read_samcam_scint() - print(f"Ypos at sample cam is {high_y}") - highs_y.append(high_y) - with open(filename, "a", encoding="utf-8") as f: - f.write(f"{i:.4g},{high_y:.4g} \n") - - y_high = np.array(highs_y) - y_low = np.array(lows_y) - - - # # Fit linear models: y = m*x + b - m_high, b_high = np.polyfit(high_scanpoints, y_high, 1) - m_low, b_low = np.polyfit(low_scanpoints, y_low, 1) - - # # Intersection point - x_intersect = (b_low - b_high) / (m_high - m_low) - y_intersect = m_high * x_intersect + b_high - - print(f"\nIntersection at:") - print(f" DCM Roll = {x_intersect:.5f}") - print(f" samcam Y = {y_intersect:.2f}") - - # # --- Plot --- - plt.figure(figsize=(7,5)) - plt.scatter(high_scanpoints, y_high, color='red', label=f'High energy') - plt.scatter(low_scanpoints, y_low, color='blue', label=f'Low energy') - - x_fit = np.linspace(min(high_scanpoints), max(high_scanpoints), 100) - plt.plot(x_fit, m_high*x_fit + b_high, 'r--', label=f'High energy') - plt.plot(x_fit, m_low*x_fit + b_low, 'b--', label=f'Low energy') - - plt.scatter(x_intersect, y_intersect, color='green', s=80, zorder=5, label='Intersection') - - plt.xlabel('DCM fine roll') - plt.ylabel('samcam Y pos') - plt.title('Roll Calibration') - plt.legend() - plt.grid(True) - plt.tight_layout() - plt.show() def vfm_yaw_check(): slit_size = 0.02 @@ -1191,18 +1493,18 @@ def vfm_yaw_check(): # Point 1 print(f"Moving slits to position 1") umv(dev.ss_sl_ycen, slit_point1) - scans.line_scan(dev.vfm_x, mirror_start, mirror_end, steps = 20, relative=False) + scans.line_scan(dev.vfm_x, mirror_start, mirror_end, steps=20, relative=False) fit_result = fit_history(-1, "xidiode", deriv=True) - centre = fit_result['centre'] - print(f'Yaw = {yaw}, position = {slit_point1}, centre = {centre}') + centre = fit_result["centre"] + print(f"Yaw = {yaw}, position = {slit_point1}, centre = {centre}") point1.append(centre) # Point 2 print(f"Moving slits to position 2") umv(dev.ss_sl_ycen, slit_point2) - scans.line_scan(dev.vfm_x, mirror_start, mirror_end, steps = 20, relative=False) + scans.line_scan(dev.vfm_x, mirror_start, mirror_end, steps=20, relative=False) fit_result = fit_history(-1, "xidiode", deriv=True) - centre = fit_result['centre'] - print(f'Yaw = {yaw}, position = {slit_point2}, centre = {centre}') + centre = fit_result["centre"] + print(f"Yaw = {yaw}, position = {slit_point2}, centre = {centre}") point2.append(centre) print(f"Point 1 values are {point1}") print(f"Point 2 values are {point2}") @@ -1211,7 +1513,6 @@ def vfm_yaw_check(): y_point1 = np.array(point1) y_point2 = np.array(point2) - # # Fit linear models: y = m*x + b m_point1, b_point1 = np.polyfit(yaw_values, y_point1, 1) m_point2, b_point2 = np.polyfit(yaw_values, y_point2, 1) @@ -1225,38 +1526,42 @@ def vfm_yaw_check(): print(f" Y = {y_intersect:.2f}") # # --- Plot --- - plt.figure(figsize=(7,5)) - plt.scatter(yaw_values, y_point1, color='red', label=f'point1') - plt.scatter(yaw_values, y_point2, color='blue', label=f'point2') + plt.figure(figsize=(7, 5)) + plt.scatter(yaw_values, y_point1, color="red", label=f"point1") + plt.scatter(yaw_values, y_point2, color="blue", label=f"point2") x_fit = np.linspace(min(yaw_values), max(yaw_values), 100) - plt.plot(x_fit, m_point1*x_fit + b_point1, 'r--', label=f'point1') - plt.plot(x_fit, m_point2*x_fit + b_point2, 'b--', label=f'point2') + plt.plot(x_fit, m_point1 * x_fit + b_point1, "r--", label=f"point1") + plt.plot(x_fit, m_point2 * x_fit + b_point2, "b--", label=f"point2") - plt.scatter(x_intersect, y_intersect, color='green', s=80, zorder=5, label='Intersection') + plt.scatter(x_intersect, y_intersect, color="green", s=80, zorder=5, label="Intersection") - plt.xlabel('VFM yaw') - plt.ylabel('Edge') - plt.title('VFM Yaw Calibration') + plt.xlabel("VFM yaw") + plt.ylabel("Edge") + plt.title("VFM Yaw Calibration") plt.legend() plt.grid(True) plt.tight_layout() plt.show() -from dataclasses import dataclass -import time - - @dataclass class SlitAxis: name: str - centre_motor: object # e.g. dev.slit1_x - size_motor: object # e.g. dev.slit1_xsize - scan_func: callable # your existing peak scan function + centre_motor: object # e.g. dev.slit1_x + size_motor: object # e.g. dev.slit1_xsize scan_range: float = 2.0 - open_size: float = 2.0 - min_size: float = -0.4 - max_size: float = 5.0 + + +# @dataclass +# class SlitAxis: +# name: str +# centre_motor: object # e.g. dev.slit1_x +# size_motor: object # e.g. dev.slit1_xsize +# scan_func: callable # your existing peak scan function +# scan_range: float = 2.0 +# open_size: float = 2.0 +# min_size: float = -0.4 +# max_size: float = 5.0 @dataclass @@ -1265,125 +1570,205 @@ class Slit: x: SlitAxis y: SlitAxis +def centre_axis(axis, detector, slit_size=0.5): + """Measure the beam centre on one slit axis.""" -def read_intensity(detector, n=5, delay=0.1): - values = [] - for _ in range(n): - values.append(detector.read()[detector.name]["value"]) - time.sleep(delay) - return sum(values) / len(values) + original_size = axis.size_motor.position + start_centre = axis.centre_motor.position + try: + umv(axis.size_motor, slit_size) - -def reduce_to_transmission(axis, detector, baseline, target=0.15, step=0.05): - """ - Close slit size until intensity is around target * baseline. - """ - size = axis.size_motor.position - - while size > axis.min_size: - intensity = read_intensity(detector) - transmission = intensity / baseline - - print( - f"{axis.name}: size={size:.4f}, " - f"I={intensity:.3g}, transmission={transmission:.3f}" + go_to_peak( + axis.centre_motor, + detector, + start_centre - axis.scan_range, + start_centre + axis.scan_range, + 50, + confirm=False, ) - if transmission <= target: - return size, intensity - - size -= step - umv(axis.size_motor, size) - - raise RuntimeError(f"{axis.name}: reached minimum slit size without reaching target") - - -def open_to_transmission(axis, detector, baseline, target=0.99, step=0.05): - """ - Open slit until intensity is close to the original baseline. - """ - size = axis.size_motor.position - - while size < axis.max_size: - intensity = read_intensity(detector) - transmission = intensity / baseline + measured_centre = get_value(axis.centre_motor) print( - f"{axis.name}: size={size:.4f}, " - f"I={intensity:.3g}, transmission={transmission:.3f}" + f"{axis.name}: " + f"start={start_centre:.4f}, " + f"measured={measured_centre:.4f}, " + f"shift={measured_centre - start_centre:+.4f}" ) - if transmission >= target: - return size, intensity + return { + "start": start_centre, + "centre": measured_centre, + "shift": measured_centre - start_centre, + } - size += step - umv(axis.size_motor, size) - # final_size = axis.size.motor.position - # fudge = 0.5 - # umv(axis.size_motor,final_size + fudge) - - raise RuntimeError(f"{axis.name}: reached maximum slit size without restoring intensity") - -# def approx_centre_axis(axis, detector, slitsize=0.5): -# umv(axis.size_motor, slitsize) -# go_to_peak(axis.centre_motor, detector, -4, 4, 50, confirm=False) + finally: + umv(axis.size_motor, original_size) -def centre_axis(axis, detector, close_fraction=0.10, open_fraction=0.95): +# def read_intensity(detector, n=5, delay=0.1): +# values = [] +# for _ in range(n): +# values.append(detector.read()[detector.name]["value"]) +# time.sleep(delay) +# return sum(values) / len(values) + + +# def reduce_to_transmission(axis, detector, baseline, target=0.15, step=0.05): +# """ +# Close slit size until intensity is around target * baseline. +# """ +# size = axis.size_motor.position + +# while size > axis.min_size: +# intensity = read_intensity(detector) +# transmission = intensity / baseline + +# print( +# f"{axis.name}: size={size:.4f}, " f"I={intensity:.3g}, transmission={transmission:.3f}" +# ) + +# if transmission <= target: +# return size, intensity + +# size -= step +# umv(axis.size_motor, size) + +# raise RuntimeError(f"{axis.name}: reached minimum slit size without reaching target") + + +# def open_to_transmission(axis, detector, baseline, target=0.99, step=0.05): +# """ +# Open slit until intensity is close to the original baseline. +# """ +# size = axis.size_motor.position + +# while size < axis.max_size: +# intensity = read_intensity(detector) +# transmission = intensity / baseline + +# print( +# f"{axis.name}: size={size:.4f}, " f"I={intensity:.3g}, transmission={transmission:.3f}" +# ) + +# if transmission >= target: +# return size, intensity + +# size += step +# umv(axis.size_motor, size) +# # final_size = axis.size.motor.position +# # fudge = 0.5 +# # umv(axis.size_motor,final_size + fudge) + +# raise RuntimeError(f"{axis.name}: reached maximum slit size without restoring intensity") + + +# # def approx_centre_axis(axis, detector, slitsize=0.5): +# # umv(axis.size_motor, slitsize) +# # go_to_peak(axis.centre_motor, detector, -4, 4, 50, confirm=False) + + +# def centre_axis(axis, detector, close_fraction=0.10, open_fraction=0.95): +# print(f"\nAligning {axis.name}") + +# # approx_centre_axis(axis, detector, slitsize=0.5) + +# umv(axis.size_motor, axis.open_size) + +# baseline = read_intensity(detector) +# print(f"{axis.name}: baseline intensity = {baseline:.3g}") + +# # if baseline <= 0: +# # raise RuntimeError(f"{axis.name}: invalid baseline intensity") + +# reduce_to_transmission(axis, detector, baseline, target=close_fraction) + +# go_to_peak( +# axis.centre_motor, +# detector, +# axis.centre_motor.position - axis.scan_range, +# axis.centre_motor.position + axis.scan_range, +# 50, +# confirm=False, +# ) +# print(f"{axis.name}: moving centre to peak position") + +# open_to_transmission(axis, detector, baseline, target=open_fraction) + +# final_intensity = read_intensity(detector) +# print( +# f"{axis.name}: final intensity = {final_intensity:.3g}, " +# f"transmission = {final_intensity / baseline:.3f}" +# ) +# cen_pos = get_value(axis.centre_motor) +# return cen_pos + + +# def open_slits(size=3): +# slits = define_slits() +# for slit in slits: +# umv(slit.x.size_motor, size) +# umv(slit.y.size_motor, size) + + +# def centre_slit(slit, detector): +# print(f"\n=== Aligning {slit.name} ===") + +# centre_axis(slit.x, detector) +# centre_axis(slit.y, detector) + +# print(f"=== Finished {slit.name} ===") +def centre_axis(axis, detector, slit_size=0.5, final_size=3.0): + """Centre one slit axis using a fixed slit gap.""" + print(f"\nAligning {axis.name}") - # approx_centre_axis(axis, detector, slitsize=0.5) + # Set a known gap for the scan + umv(axis.size_motor, slit_size) - umv(axis.size_motor, axis.open_size) + start = axis.centre_motor.position - baseline = read_intensity(detector) - print(f"{axis.name}: baseline intensity = {baseline:.3g}") - - # if baseline <= 0: - # raise RuntimeError(f"{axis.name}: invalid baseline intensity") - - reduce_to_transmission( - axis, + go_to_peak( + axis.centre_motor, detector, - baseline, - target=close_fraction, + start - axis.scan_range, + start + axis.scan_range, + 50, + confirm=False, ) - go_to_peak(axis.centre_motor, detector, axis.centre_motor.position - axis.scan_range, - axis.centre_motor.position + axis.scan_range, - 50, - confirm=False) - print(f"{axis.name}: moving centre to peak position") + centre = get_value(axis.centre_motor) + print(f"{axis.name}: centre = {centre:.4f}") - open_to_transmission( - axis, - detector, - baseline, - target=open_fraction, - ) + # Open slit again + umv(axis.size_motor, final_size) - final_intensity = read_intensity(detector) - print( - f"{axis.name}: final intensity = {final_intensity:.3g}, " - f"transmission = {final_intensity / baseline:.3f}" - ) + return centre -def open_slits(size=3): - slits = define_slits() - for slit in slits: - umv(slit.x.size_motor, size) - umv(slit.y.size_motor, size) - - -def centre_slit(slit, detector): +def centre_slit(slit, detector, slit_size=0.5, final_size=3.0): print(f"\n=== Aligning {slit.name} ===") - centre_axis(slit.x, detector) - centre_axis(slit.y, detector) + x_centre = centre_axis( + slit.x, + detector, + slit_size=slit_size, + final_size=final_size, + ) + + y_centre = centre_axis( + slit.y, + detector, + slit_size=slit_size, + final_size=final_size, + ) print(f"=== Finished {slit.name} ===") + return { + "x": x_centre, + "y": y_centre, + } def centre_all_slits(slits, detector): for slit in slits: @@ -1393,6 +1778,7 @@ def centre_all_slits(slits, detector): print(f"WARNING: failed to align {slit.name}: {exc}") print("Continuing with next slit.") + def centre_one(name, detector): slits = define_slits() for slit in slits: @@ -1400,20 +1786,18 @@ def centre_one(name, detector): centre_slit(slit, detector) def define_slits(): - slits = [ + return [ Slit( name="bsf", x=SlitAxis( name="bsf_sl_x", centre_motor=dev.bsf_sl_xcen, size_motor=dev.bsf_sl_xsize, - scan_func=go_to_peak, ), y=SlitAxis( name="bsf_sl_y", centre_motor=dev.bsf_sl_ycen, size_motor=dev.bsf_sl_ysize, - scan_func=go_to_peak, ), ), Slit( @@ -1422,13 +1806,11 @@ def define_slits(): name="ss_sl_x", centre_motor=dev.ss_sl_xcen, size_motor=dev.ss_sl_xsize, - scan_func=go_to_peak, ), y=SlitAxis( name="ss_sl_y", centre_motor=dev.ss_sl_ycen, size_motor=dev.ss_sl_ysize, - scan_func=go_to_peak, ), ), Slit( @@ -1437,17 +1819,66 @@ def define_slits(): name="bcu_sl_x", centre_motor=dev.bcu_sl_xcen, size_motor=dev.bcu_sl_xsize, - scan_func=go_to_peak, ), y=SlitAxis( - name="ss_sl_y", + name="bcu_sl_y", centre_motor=dev.bcu_sl_ycen, size_motor=dev.bcu_sl_ysize, - scan_func=go_to_peak, ), ), - ] - return slits + ] + + +# def define_slits(): +# slits = [ +# Slit( +# name="bsf", +# x=SlitAxis( +# name="bsf_sl_x", +# centre_motor=dev.bsf_sl_xcen, +# size_motor=dev.bsf_sl_xsize, +# scan_func=go_to_peak, +# ), +# y=SlitAxis( +# name="bsf_sl_y", +# centre_motor=dev.bsf_sl_ycen, +# size_motor=dev.bsf_sl_ysize, +# scan_func=go_to_peak, +# ), +# ), +# Slit( +# name="ss", +# x=SlitAxis( +# name="ss_sl_x", +# centre_motor=dev.ss_sl_xcen, +# size_motor=dev.ss_sl_xsize, +# scan_func=go_to_peak, +# ), +# y=SlitAxis( +# name="ss_sl_y", +# centre_motor=dev.ss_sl_ycen, +# size_motor=dev.ss_sl_ysize, +# scan_func=go_to_peak, +# ), +# ), +# Slit( +# name="bcu", +# x=SlitAxis( +# name="bcu_sl_x", +# centre_motor=dev.bcu_sl_xcen, +# size_motor=dev.bcu_sl_xsize, +# scan_func=go_to_peak, +# ), +# y=SlitAxis( +# name="ss_sl_y", +# centre_motor=dev.bcu_sl_ycen, +# size_motor=dev.bcu_sl_ysize, +# scan_func=go_to_peak, +# ), +# ), +# ] +# return slits + def get_mirror_data(history_index: int): """Read data from the BEC history and return the X and Y data as arrays.""" @@ -1458,14 +1889,13 @@ def get_mirror_data(history_index: int): md = scan.metadata["bec"] x1_data = scan.devices[motor1][motor1].read()["value"] x2_data = scan.devices[motor2][motor2].read()["value"] - y1_data = scan.devices['microscope_stats']['microscope_stats_data_sigma_y'].read()['value'] - y2_data = scan.devices['microscope_stats']['microscope_stats_data_eccentricity'].read()['value'] + y1_data = scan.devices["microscope_stats"]["microscope_stats_data_sigma_y"].read()["value"] + y2_data = scan.devices["microscope_stats"]["microscope_stats_data_eccentricity"].read()["value"] np.savetxt("luts/x1_data", x1_data) np.savetxt("luts/x2_data", x2_data) np.savetxt("luts/y1_data", y1_data) np.savetxt("luts/y2_data", y2_data) - # plt.imshow(y1_data, cmap="hot") # plt.clf # return { @@ -1485,13 +1915,12 @@ def record_beampos_until(stop_time_str, interval_minutes=10): # Prepare the file filename = bl.env.lut_dir / "beampos" / f"beampos_{filenow()}.csv" - scam_zoom = dev.scam_zoom.read()['scam_zoom']['value'] + scam_zoom = dev.scam_zoom.read()["scam_zoom"]["value"] umv(dev.scam_zoom, 800) beam_size = "25 x 25" energy = get_current_energy() - # Write the header to the file with open(filename, "w", encoding="utf-8") as f: f.write(f"Beam size: {beam_size}, Energy: {energy:.0f} eV, Zoom: {scam_zoom}\n") @@ -1525,8 +1954,6 @@ def record_beampos_until(stop_time_str, interval_minutes=10): print(f"Finished — reached {end_time:%H:%M}.") bl.planner.move_to(BeamlineState.MANUAL_SAMPLE_EXCHANGE) - - def record_beam(filename): @@ -1538,7 +1965,7 @@ def record_beam(filename): umv(dev.bcu_bpm_x, BPM_X_Calibration.centred_position) umv(dev.bcu_bpm_y, BPM_Y_Calibration.centred_position) - try: + try: # Open shutter and record positions dev.bcu_shutter.put(1) auto_exposure(cam="samcam") @@ -1551,7 +1978,9 @@ def record_beam(filename): xpos_cam = dev.samcam_x.read()["samcam_x"]["value"] ypos_cam = dev.samcam_y.read()["samcam_y"]["value"] xpos_bpm, ypos_bpm = get_bcu_beampos() - print(f"Time is {fnow}, ScinX position is {xpos_cam:.5g}, ScinY position is {ypos_cam:.5g}") + print( + f"Time is {fnow}, ScinX position is {xpos_cam:.5g}, ScinY position is {ypos_cam:.5g}" + ) print(f"BPM X is: {xpos_bpm}, BPM Y is {ypos_bpm}") f.write( # f"{fnow}, {xpos: .5g}, {ypos: .4g}, {xpos_cam: .5g}, {ypos_cam: .4g}\n" @@ -1585,7 +2014,6 @@ def analyse_beampos(file): data["Scin_x"] = (data["Scin_x"] - ref_scinx) * calc_scam_microns(1) data["Scin_y"] = (data["Scin_y"] - ref_sciny) * calc_scam_microns(1) - # Extract Y-position columns (all columns except the first column) y_positions = data.iloc[:, 1:] # Exclude the first column @@ -1605,9 +2033,7 @@ def analyse_beampos(file): # Plot the line and scatter with the chosen color ax.plot(meas_time, y_positions[column], color=color, linewidth=0.5) # Line plot - ax.scatter( - meas_time, y_positions[column], color=color, label=column - ) # Scatter plot + ax.scatter(meas_time, y_positions[column], color=color, label=column) # Scatter plot # Set x-axis tick positions and labels tick_positions = range(0, len(meas_time), 7) # Adjust the step size as needed @@ -1624,14 +2050,14 @@ def analyse_beampos(file): # Show the resulting figure plt.show() + import time import numpy as np import matplotlib.pyplot as plt from epics import caget - -def scan_scin_focus(planner, type = 'fine'): +def scan_scin_focus(planner, type="fine"): dev.bcu_shutter.put(0) planner.move_to(BeamlineState.BEAM_VISUALISATION) dev.bcu_shutter.put(1) @@ -1640,10 +2066,9 @@ def scan_scin_focus(planner, type = 'fine'): min = 0.1 max = 9.9 - - if type == 'fine': + if type == "fine": steps = 100 - elif type == 'coarse': + elif type == "coarse": steps = 30 else: steps = 60 @@ -1672,7 +2097,7 @@ def bpm_errors(ch1, ch2, ch3, ch4): def find_bpm_centre(positions, errors): """Fit the BPM difference signal and return its zero crossing.""" - + slope, intercept = np.polyfit(positions, errors, 1) if abs(slope) < 1e-6: @@ -1682,19 +2107,20 @@ def find_bpm_centre(positions, errors): return centre, slope, intercept + def centre_bcu_bpm_x(): positions = [] errors = [] startx = dev.bcu_bpm_x.position starty = dev.bcu_bpm_y.position - scanpoints_x = np.linspace(startx-0.15, startx+0.15, 20) + scanpoints_x = np.linspace(startx - 0.15, startx + 0.15, 20) for x in scanpoints_x: umv(dev.bcu_bpm_x, x) - result = read_currents() - ch1 = float(result["bpm1"]) - ch2 = float(result["bpm2"]) - ch3 = float(result["bpm3"]) - ch4 = float(result["bpm4"]) + ch1 = get_value(dev.bcu_bpm1) + ch2 = get_value(dev.bcu_bpm2) + ch3 = get_value(dev.bcu_bpm3) + ch4 = get_value(dev.bcu_bpm4) + x_error, _ = bpm_errors(ch1, ch2, ch3, ch4) print(f"BPM_X = {float(x)}, Xerror = {x_error}") @@ -1702,23 +2128,23 @@ def centre_bcu_bpm_x(): positions.append(x) errors.append(x_error) - centre, slope, intercept = find_bpm_centre(positions,errors) + centre, slope, intercept = find_bpm_centre(positions, errors) print(f"BPM centre in X is at {centre} mm") umv(dev.bcu_bpm_x, centre) return centre + def centre_bcu_bpm_y(): positions = [] errors = [] starty = dev.bcu_bpm_y.position - scanpoints_y = np.linspace(starty-0.15, starty+0.15, 20) + scanpoints_y = np.linspace(starty - 0.15, starty + 0.15, 20) for y in scanpoints_y: umv(dev.bcu_bpm_y, y) - result = read_currents() - ch1 = float(result["bpm1"]) - ch2 = float(result["bpm2"]) - ch3 = float(result["bpm3"]) - ch4 = float(result["bpm4"]) + ch1 = get_value(dev.bcu_bpm1) + ch2 = get_value(dev.bcu_bpm2) + ch3 = get_value(dev.bcu_bpm3) + ch4 = get_value(dev.bcu_bpm4) _, y_error = bpm_errors(ch1, ch2, ch3, ch4) print(f"BPM_Y = {float(y)}, Yerror = {y_error}") @@ -1726,16 +2152,1108 @@ def centre_bcu_bpm_y(): positions.append(y) errors.append(y_error) - centre, slope, intercept = find_bpm_centre(positions,errors) + centre, slope, intercept = find_bpm_centre(positions, errors) print(f"BPM centre in Y is at {centre} mm") umv(dev.bcu_bpm_y, centre) return centre + + +def centre_ss_bpm_x(): + positions = [] + errors = [] + startx = dev.ss_bpm_x.position + starty = dev.ss_bpm_y.position + scanpoints_x = np.linspace(startx - 0.15, startx + 0.15, 20) + for x in scanpoints_x: + umv(dev.ss_bpm_x, x) + + ch1 = get_value(dev.ss_bpm1) + ch2 = get_value(dev.ss_bpm2) + ch3 = get_value(dev.ss_bpm3) + ch4 = get_value(dev.ss_bpm4) + x_error, _ = bpm_errors(ch1, ch2, ch3, ch4) + + print(f"BPM_X = {float(x)}, Xerror = {x_error}") + + positions.append(x) + errors.append(x_error) + + centre, slope, intercept = find_bpm_centre(positions, errors) + print(f"BPM centre in X is at {centre} mm") + umv(dev.ss_bpm_x, centre) + return centre + + +def centre_ss_bpm_y(): + positions = [] + errors = [] + starty = dev.ss_bpm_y.position + scanpoints_y = np.linspace(starty - 0.15, starty + 0.15, 20) + for y in scanpoints_y: + umv(dev.ss_bpm_y, y) + ch1 = get_value(dev.ss_bpm1) + ch2 = get_value(dev.ss_bpm2) + ch3 = get_value(dev.ss_bpm3) + ch4 = get_value(dev.ss_bpm4) + _, y_error = bpm_errors(ch1, ch2, ch3, ch4) + + print(f"BPM_Y = {float(y)}, Yerror = {y_error}") + + positions.append(y) + errors.append(y_error) + + centre, slope, intercept = find_bpm_centre(positions, errors) + print(f"BPM centre in Y is at {centre} mm") + umv(dev.ss_bpm_y, centre) + return centre + +def centre_lu_bpm_x(): + positions = [] + errors = [] + startx = dev.lu_bpm_x.position + starty = dev.lu_bpm_y.position + scanpoints_x = np.linspace(startx - 0.15, startx + 0.15, 20) + for x in scanpoints_x: + umv(dev.lu_bpm_x, x) + + ch1 = get_value(dev.lu_bpm1) + ch2 = get_value(dev.lu_bpm2) + ch3 = get_value(dev.lu_bpm3) + ch4 = get_value(dev.lu_bpm4) + x_error, _ = bpm_errors(ch1, ch2, ch3, ch4) + + print(f"BPM_X = {float(x)}, Xerror = {x_error}") + + positions.append(x) + errors.append(x_error) + + centre, slope, intercept = find_bpm_centre(positions, errors) + print(f"BPM centre in X is at {centre} mm") + umv(dev.lu_bpm_x, centre) + return centre + + +def centre_lu_bpm_y(): + positions = [] + errors = [] + starty = dev.lu_bpm_y.position + scanpoints_y = np.linspace(starty - 0.15, starty + 0.15, 20) + for y in scanpoints_y: + umv(dev.lu_bpm_y, y) + ch1 = get_value(dev.lu_bpm1) + ch2 = get_value(dev.lu_bpm2) + ch3 = get_value(dev.lu_bpm3) + ch4 = get_value(dev.lu_bpm4) + _, y_error = bpm_errors(ch1, ch2, ch3, ch4) + + print(f"BPM_Y = {float(y)}, Yerror = {y_error}") + + positions.append(y) + errors.append(y_error) + + centre, slope, intercept = find_bpm_centre(positions, errors) + print(f"BPM centre in Y is at {centre} mm") + umv(dev.lu_bpm_y, centre) + + return centre def read_i1(): - return dev.i1.read()['i1']['value'] + return dev.i1.read()["i1"]["value"] + def read_i2(): - return dev.i2.read()['i2']['value'] + return dev.i2.read()["i2"]["value"] + def quick_test(): bl.planner.closest_states() + + +import time +import numpy as np + + +def _read_signal(signal): + """Return the value from a BEC/ophyd signal.""" + data = signal.read() + key = next(iter(data)) + return data[key]["value"] + + +def _smooth_profile(profile, window=7): + """Simple moving-average smoothing.""" + profile = np.asarray(profile, dtype=float) + + if window <= 1: + return profile + + kernel = np.ones(window) / window + return np.convolve(profile, kernel, mode="same") + + +def find_dark_feature(profile, search_min, search_max, smooth_window=7, threshold_fraction=0.35): + """ + Find the centre and edges of a dark feature in a 1-D intensity profile. + + Parameters + ---------- + profile : array-like + Intensity profile. + + search_min, search_max : int + Pixel range in which to search. + + smooth_window : int + Width of moving-average smoothing. + + threshold_fraction : float + Fraction of the maximum dip depth used to define the feature edges. + + Returns + ------- + dict + centre, low_edge, high_edge, width, depth + """ + + profile = np.asarray(profile, dtype=float) + smooth = _smooth_profile(profile, smooth_window) + + x = np.arange(len(smooth)) + + mask = (x >= search_min) & (x <= search_max) + xx = x[mask] + yy = smooth[mask] + + if len(yy) == 0: + raise ValueError("Search range contains no profile points") + + # Estimate local background from the brighter part of the profile. + background = np.percentile(yy, 80) + + # How far each pixel lies below the background. + darkness = np.clip(background - yy, 0, None) + + peak_index = np.argmax(darkness) + peak_depth = darkness[peak_index] + + if peak_depth <= 0: + raise RuntimeError("No dark feature found") + + threshold = threshold_fraction * peak_depth + + # Start at deepest point and walk outwards until the dip disappears. + left = peak_index + while left > 0 and darkness[left] > threshold: + left -= 1 + + right = peak_index + while right < len(darkness) - 1 and darkness[right] > threshold: + right += 1 + + low_edge = xx[left] + high_edge = xx[right] + + # Darkness-weighted centre is more stable than a single minimum pixel. + feature_mask = np.arange(len(xx)) + feature_mask = (feature_mask >= left) & (feature_mask <= right) + + weights = darkness[feature_mask] + coords = xx[feature_mask] + + centre = np.average(coords, weights=weights) + + return { + "centre": float(centre), + "low_edge": float(low_edge), + "high_edge": float(high_edge), + "width": float(high_edge - low_edge), + "depth": float(peak_depth), + } + + +def measure_omega_runout( + omega_motor, + profile_x_signal, + profile_y_signal, + cursor_x_signal, + *, + angles=None, + x_search=(900, 1200), + y_search=(850, 1150), + settle=1.0, + cursor_settle=0.2, + smooth_window=7, + threshold_fraction=0.35, +): + """ + Measure apparent needle position while rotating omega. + + CursorY should already be positioned so that the X profile crosses + the needle. + + At each omega: + 1. Read ProfileCursorX + 2. Find needle X + 3. Move CursorX to that position + 4. Read ProfileCursorY + 5. Find needle Y + + Returns + ------- + list[dict] + One result dictionary per omega position. + """ + + if angles is None: + angles = np.arange(0, 361, 45) + + results = [] + + for angle in angles: + print(f"\nOmega = {angle:.1f}°") + + umv(omega_motor, (float(angle))) + time.sleep(settle) + + # ------------------------------------------------------ + # X position + # ------------------------------------------------------ + profile_x = _read_signal(profile_x_signal) + + x_result = find_dark_feature( + profile_x, + search_min=x_search[0], + search_max=x_search[1], + smooth_window=smooth_window, + threshold_fraction=threshold_fraction, + ) + + needle_x = x_result["centre"] + + # Put vertical cursor through the detected needle position. + cursor_x_signal.put(int(round(needle_x))) + + time.sleep(cursor_settle) + + # ------------------------------------------------------ + # Y position + # ------------------------------------------------------ + profile_y = _read_signal(profile_y_signal) + + y_result = find_dark_feature( + profile_y, + search_min=y_search[0], + search_max=y_search[1], + smooth_window=smooth_window, + threshold_fraction=threshold_fraction, + ) + + needle_y = y_result["centre"] + + result = { + "omega": float(angle), + "x": needle_x, + "x_width": x_result["width"], + "x_depth": x_result["depth"], + "y": needle_y, + "y_width": y_result["width"], + "y_depth": y_result["depth"], + } + + results.append(result) + + print(f" X = {needle_x:8.2f} px" f" width = {x_result['width']:6.1f}") + print(f" Y = {needle_y:8.2f} px" f" width = {y_result['width']:6.1f}") + + # ---------------------------------------------------------- + # Summary + # ---------------------------------------------------------- + + xs = np.asarray([r["x"] for r in results]) + ys = np.asarray([r["y"] for r in results]) + + print("\nOmega runout") + print("------------") + print(f"X peak-to-peak : {np.ptp(xs):.2f} px") + print(f"Y peak-to-peak : {np.ptp(ys):.2f} px") + + if len(results) > 1: + return_error = np.hypot(xs[-1] - xs[0], ys[-1] - ys[0]) + + print(f"0° -> 360° error: {return_error:.2f} px") + + return results + + +# def dcm_pitch_calibration(start_energy=6000, end_energy=31000, step=3000): +def dcm_pitch_calibration(): + """Quick calibration of DCM Pitch against energy""" + energies = [5500, 8000, 10000, 12000, 14000, 16000, 18000, + 20000, 22000, 24000, 27000, 30000] + filename = bl.env.lut_dir / f"dcm_pitch_{filenow(date_only=True)}.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write("energy,pitch\n") + # for energy in range(start_energy, end_energy, step): + try: + for energy in energies: + # energy_list = [29000,26000,23000,20000,17000,14000,11000,8000,6000] + # for energy in energy_list: + # bl_energy(energy, mono_scan=False) + # result = go_to_peak( + # dev.dcm_pitch, dev.lu_bpmsum, -0.1, 0.4, 40, min_fwhm_steps=1, confirm=False + # ) + # pitch_new = get_value(dev.dcm_pitch) + bl_energy(energy) + pitch_new = get_value(dev.dcm_pitch) + with open(filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},{pitch_new:.5g}\n") + finally: + bl_energy(12400) + + +def scan_roll(energy, start, stop, steps=9, settle=0.2, filename=None, overwrite=False): + """Measure scintillator Y position as a function of DCM fine roll.""" + + scanpoints = np.linspace(start, stop, steps) + print(f"Scanning roll from {start} to {stop}") + + if filename is None: + filename = bl.env.lut_dir / f"roll_{energy:.0f}.csv" + + if filename.exists() and not overwrite: + print(f"{filename.name} already exists.") + print("Use overwrite=True if you want to repeat this energy.") + return filename + + bl_energy(energy) + + try: + prepare_scint_measurement() + + with open(filename, "w", encoding="utf-8") as f: + f.write("Roll,Y pos\n") + + for roll in scanpoints: + umv(dev.dcm_froll, roll) + time.sleep(settle) + + results = read_scintXY() + if not results["ok"]: + raise RuntimeError( + f"Beam position measurement failed at " + f"{energy / 1000:.1f} keV, roll={roll:.3f}: " + f"{results['message']}" + ) + + print(f"{energy / 1000:.1f} keV | " f"roll = {roll:.3f} | " f"Y = {results["Y_pixels"]:.2f}") + + f.write(f"{roll:.6f},{results["Y_pixels"]}\n") + + finally: + finish_scint_measurement() + return filename + + + +def analyse_roll(low=6000, high=25000, low_file=None, high_file=None): + """Find the DCM fine-roll value where low/high-energy beam positions intersect.""" + + # low_file = bl.env.lut_dir / f"roll_{low:.0f}.csv" + # high_file = bl.env.lut_dir / f"roll_{high:.0f}.csv" + + low_data = np.loadtxt(low_file, delimiter=",", skiprows=1) + + high_data = np.loadtxt(high_file, delimiter=",", skiprows=1) + + low_roll = low_data[:, 0] + low_y = low_data[:, 1] + + high_roll = high_data[:, 0] + high_y = high_data[:, 1] + + # Linear fits: y = mx + b + m_low, b_low = np.polyfit(low_roll, low_y, 1) + m_high, b_high = np.polyfit(high_roll, high_y, 1) + + # Intersection + x_intersect = (b_low - b_high) / (m_high - m_low) + y_intersect = m_low * x_intersect + b_low + + print("\nIntersection:") + print(f" DCM Roll = {x_intersect:.5f}") + print(f" samcam Y = {y_intersect:.2f}") + + # Plot + plt.figure(figsize=(7, 5)) + + plt.scatter(low_roll, low_y, label=f"{low / 1000:.1f} keV") + + plt.scatter(high_roll, high_y, label=f"{high / 1000:.1f} keV") + + x_min = min(low_roll.min(), high_roll.min()) + x_max = max(low_roll.max(), high_roll.max()) + + x_fit = np.linspace(x_min, x_max, 200) + + plt.plot(x_fit, m_low * x_fit + b_low, "--", label=f"{low / 1000:.1f} keV fit") + + plt.plot(x_fit, m_high * x_fit + b_high, "--", label=f"{high / 1000:.1f} keV fit") + + plt.scatter(x_intersect, y_intersect, s=80, zorder=5, label="Intersection") + + plt.xlabel("DCM fine roll") + plt.ylabel("samcam Y pos") + plt.title("DCM Roll Calibration") + plt.legend() + plt.grid() + plt.tight_layout() + plt.show() + + return x_intersect + + +def find_best_roll( + low=6000, + high=25000, + low_range=(3.0, 5.0), + high_range=(1.0, 8.0), + steps=9, +): + """Collect missing roll scans and calculate optimum roll.""" + + filename_prefix = filenow(date_only=True) + low_file = bl.env.lut_dir / f"roll_{filename_prefix}_{low:.0f}.csv" + high_file = bl.env.lut_dir / f"roll_{filename_prefix}_{high:.0f}.csv" + + # Define the two scans + scans = [ + (low, low_range, low_file), + (high, high_range, high_file), + ] + + # Start with whichever energy is closest to the current energy + current_energy = get_current_energy() + + scans.sort( + key=lambda scan: abs(current_energy - scan[0]) + ) + + print(f"Current energy: {current_energy:.0f} eV") + print(f"Scanning {scans[0][0]:.0f} eV first") + + # Run scans in the chosen order + for energy, scan_range, filename in scans: + if not filename.exists(): + scan_roll( + energy, + *scan_range, + steps=steps, + filename=filename, + ) + else: + print(f"Using existing {filename.name}") + + return analyse_roll(low, high, low_file, high_file) + + +def test_hfm(): + target = current_hfm + correction + + print(f"HFM requested: {current_hfm:.6f} -> {target:.6f}") + umv(dev.hfm_xd, target) + + actual = get_value(dev.hfm_xd) + print( + f"HFM actual: {actual:.6f} " + f"(requested move {(target-current_hfm)*1000:+.3f} µm, " + f"actual move {(actual-current_hfm)*1000:+.3f} µm)" + ) + + +def test_hfm2(dmove, offset): + current = get_value(dev.hfm_xd) + target = current + dmove + print( + f"Current HFM = {current:.4f}, target position = {target:.4f}, difference = {target-current:.4f}" + ) + # Step 1, add the offset to dmove and add to current position + move_to_1 = current + dmove + offset + umv(dev.hfm_xd, move_to_1) + print(f"Moving to {move_to_1:.4f}, difference of {current-move_to_1:.4f}") + # Step 2, move back by the offset amount + move_to_2 = move_to_1 - offset + umv(dev.hfm_xd, move_to_2) + print(f"Moving to {move_to_2:.4f}, difference of {current-move_to_2:.4f}") + # Print actual move + new = get_value(dev.hfm_xd) + print(f"Requested move was {dmove:.4f}, actual move was {new-current:.4f}") + + +def test_hfm3(dmove, offset=0.002): + current = get_value(dev.hfm_xd) + target = current + dmove + + # Overshoot in the same direction as the requested move + signed_offset = np.sign(dmove) * abs(offset) + overshoot = target + signed_offset + + print( + f"Current HFM = {current:.6f}\n" + f"Target HFM = {target:.6f}\n" + f"Requested move = {dmove*1000:+.3f} µm" + ) + + umv(dev.hfm_xd, overshoot) + + after_overshoot = get_value(dev.hfm_xd) + print(f"Overshoot requested = {overshoot:.6f}, " f"actual = {after_overshoot:.6f}") + + umv(dev.hfm_xd, target) + + final = get_value(dev.hfm_xd) + + print( + f"Final requested = {target:.6f}, actual = {final:.6f}\n" + f"Requested net move = {dmove*1000:+.3f} µm\n" + f"Actual net move = {(final-current)*1000:+.3f} µm\n" + f"Position error = {(final-target)*1000:+.3f} µm" + ) + + +def test_vfm3(dmove, offset=0.002): + current = get_value(dev.vfm_ydr) + target = current + dmove + + direction = np.sign(dmove) + overshoot = target + direction * abs(offset) + + print( + f"Current VFM = {current:.6f}\n" + f"Target VFM = {target:.6f}\n" + f"Requested move = {dmove*1000:+.3f} µm" + ) + + umv(dev.vfm_ydr, overshoot) + + after_overshoot = get_value(dev.vfm_ydr) + print(f"Overshoot requested = {overshoot:.6f}, " f"actual = {after_overshoot:.6f}") + + umv(dev.vfm_ydr, target) + + final = get_value(dev.vfm_ydr) + + print( + f"Final requested = {target:.6f}, actual = {final:.6f}\n" + f"Requested net move = {dmove*1000:+.3f} µm\n" + f"Actual net move = {(final-current)*1000:+.3f} µm\n" + f"Position error = {(final-target)*1000:+.3f} µm" + ) + +def beamstop_check_energies(): + + bs_in = 0.904 + bs_out = 0 + + start_energy = 30000 + end_energy = 5000 + + filename = bl.env.lut_dir / f"bs_checks_{filenow(date_only=True)}.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write("Energy,I2_in,I2_out\n") + + try: + for energy in range (start_energy, end_energy, -2000): + print(f"Moving to energy {energy}") + bl_energy(energy) + dev.transm.put(0.1) + trans = get_value(dev.transm) + print(f"Transmission is {trans}") + dev.bcu_shutter.put(1) + time.sleep(1) + invalue = get_value(dev.i2) + print(f"Reading with beamstop in position is {invalue:.4g}") + # Check reading with beamstop out + umv(dev.bs_y, bs_out) + time.sleep(1) + outvalue = get_value(dev.i2) + dev.bcu_shutter.put(0) + print(f"Reading with beamstop out is {outvalue:.4g}") + umv(dev.bs_y, bs_in) + with open(filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},{invalue:.4g}, {outvalue:.4g}\n") + finally: + dev.bcu_shutter.put(0) + umv(dev.bs_y, bs_in) + bl_energy(12400) + +def check_perp(): + Xref = -0.0059957 + Yref = -0.000993 + + bl_energy(12000) + + start_perp = get_value(dev.dcm_perp) + + perps = [start_perp, start_perp-0.1, start_perp, start_perp+0.1, start_perp] + filename = bl.env.lut_dir / f"perp_check.csv" + with open(filename, 'w', encoding="utf-8") as f: + f.write("Delta perp, Pitch, Xnorm, Delta X") + + for perp in perps: + umv(dev.dcm_perp, perp) + delta_perp = perp - start_perp + mono_pitch_scan() + pitch = get_value(dev.dcm_pitch) + Xnorm, Ynorm = compute_norm_ss() + Xdelta = -0.247*(Xnorm-Xref) + Ydelta = 0.0420*(Ynorm-Yref) + with open(filename, 'a', encoding="utf-8") as f: + f.write(f"{delta_perp},{pitch:.5g},{Xnorm:.5g},{Xdelta:.5g}") + print( + f"Delta perp: {delta_perp}, " + f"Pitch: {pitch:.5g}, " + f"Xnorm: {Xnorm:.5g}, " + f"delta X: {Xdelta:.5g}" + ) + +def calibrate_dcm_perp(): + """ + Determine the DCM perp position required to maintain a fixed beam + position on the SS XBPM as a function of energy. + + At each energy: + 1. Move to the energy using the existing calibration. + 2. Optimise DCM pitch. + 3. Measure the beam position on the SS XBPM. + 4. Calculate a perp correction from the measured beam displacement. + 5. Apply the correction. + 6. Re-optimise pitch. + 7. Measure the final beam position. + 8. Save the results to CSV. + + The SS XBPM X calibration is: + beam_dx_mm = -0.247 * (Xnorm - Xref) + + From the 12 keV perp test: + beam_dx / perp_dx = 1.46 + """ + + # -------------------------------------------------------------- + # Calibration constants + # -------------------------------------------------------------- + + Xref = -0.0059957 + + # Beam displacement in mm per unit change in Xnorm + xnorm_to_mm = -0.247 + + # Measured response: + # beam displacement (mm) / perp displacement (mm) + beam_per_perp = 1.46 + + energies = [ + 6000, + 8000, + 10000, + 12000, + 16000, + 20000, + 24000, + 30000, + ] + + filename = bl.env.lut_dir / "dcm_perp_calibration.csv" + + # -------------------------------------------------------------- + # Create CSV + # -------------------------------------------------------------- + + with open(filename, "w", encoding="utf-8") as f: + f.write( + "energy_eV," + "nominal_perp_mm," + "initial_pitch_mrad," + "initial_xnorm," + "initial_beam_dx_mm," + "perp_correction_mm," + "corrected_perp_mm," + "final_pitch_mrad," + "final_xnorm," + "final_beam_dx_mm\n" + ) + + # -------------------------------------------------------------- + # Measure each energy + # -------------------------------------------------------------- + try: + for energy in energies: + + print() + print("=" * 60) + print(f"Energy: {energy / 1000:.1f} keV") + print("=" * 60) + + # Move energy using current calibration + bl_energy(energy) + + # This is the perp position predicted by the current + # beam_offset = 6 mm model + nominal_perp = get_value(dev.dcm_perp) + + # ---------------------------------------------------------- + # Initial pitch optimisation and BPM measurement + # ---------------------------------------------------------- + + mono_pitch_scan() + + initial_pitch = get_value(dev.dcm_pitch) + + Xnorm, Ynorm = compute_norm_ss() + + initial_dx = xnorm_to_mm * (Xnorm - Xref) + + print(f"Nominal perp: {nominal_perp:.6f} mm") + print(f"Initial pitch: {initial_pitch:.6f} mrad") + print(f"Initial Xnorm: {Xnorm:.6f}") + print(f"Initial beam dx: {initial_dx * 1000:+.1f} um") + + # ---------------------------------------------------------- + # Calculate required perp correction + # + # Positive perp produced positive beam dx in our test. + # Therefore correction has opposite sign to measured dx. + # ---------------------------------------------------------- + + perp_correction = -initial_dx / beam_per_perp + + corrected_perp = nominal_perp + perp_correction + + print(f"Perp correction: {perp_correction * 1000:+.1f} um") + print(f"Corrected perp: {corrected_perp:.6f} mm") + + # ---------------------------------------------------------- + # Apply correction and re-optimise pitch + # ---------------------------------------------------------- + + umv(dev.dcm_perp, corrected_perp) + + mono_pitch_scan() + + final_pitch = get_value(dev.dcm_pitch) + + final_Xnorm, final_Ynorm = compute_norm_ss() + + final_dx = xnorm_to_mm * (final_Xnorm - Xref) + + print(f"Final pitch: {final_pitch:.6f} mrad") + print(f"Final Xnorm: {final_Xnorm:.6f}") + print(f"Final beam dx: {final_dx * 1000:+.1f} um") + + # ---------------------------------------------------------- + # Save result + # ---------------------------------------------------------- + + with open(filename, "a", encoding="utf-8") as f: + f.write( + f"{energy}," + f"{nominal_perp:.8f}," + f"{initial_pitch:.8f}," + f"{Xnorm:.8f}," + f"{initial_dx:.8f}," + f"{perp_correction:.8f}," + f"{corrected_perp:.8f}," + f"{final_pitch:.8f}," + f"{final_Xnorm:.8f}," + f"{final_dx:.8f}\n" + ) + + print() + print(f"Calibration saved to:") + print(filename) + finally: + bl_energy(12400) +from datetime import datetime + + +def scan_dcm_exit_vs_energy(): + """ + Measure the DCM exit beam position as a function of energy. + + At each energy: + 1. Move to energy using the current DCM calibration. + 2. Optimise DCM pitch. + 3. Record DCM pitch and perp positions. + 4. Record SS XBPM Xnorm and Ynorm. + + No corrections are made to perp or beam position. + """ + + energies = [ + 6000, + 8000, + 10000, + 12000, + 13000, + 14000, + 15000, + 16000, + 18000, + 20000, + 22000, + 24000, + 27000, + 30000, + ] + + filename = ( + bl.env.lut_dir + / f"dcm_exit_scan_{datetime.now():%Y%m%d_%H%M%S}.csv" + ) + + # -------------------------------------------------------------- + # Create CSV + # -------------------------------------------------------------- + + with open(filename, "w", encoding="utf-8") as f: + f.write( + "energy_eV," + "perp_mm," + "pitch_mrad," + "xnorm_lu," + "ynorm_lu, " + "xnorm_ss, " + "ynorm_ss, " + "lu_bpmsum " + "ss_bpmsum\n" + ) + + # -------------------------------------------------------------- + # Energy scan + # -------------------------------------------------------------- + try: + for energy in energies: + + print() + print("=" * 60) + print(f"Energy: {energy / 1000:.1f} keV") + print("=" * 60) + + # Move to energy using current calibration + bl_energy(energy) + + + # Read actual DCM positions after optimisation + perp = get_value(dev.dcm_perp) + pitch = get_value(dev.dcm_pitch) + + # Read SS XBPM + Xnorm_ss, Ynorm_ss = compute_norm_ss() + Xnorm_lu, Ynorm_lu = compute_norm_lu() + lu_bpmsum = get_value(dev.lu_bpmsum) + ss_bpmsum = get_value(dev.ss_bpmsum) + + # ---------------------------------------------------------- + # Print result + # ---------------------------------------------------------- + + print( + f"Energy: {energy / 1000:5.1f} keV | " + f"Perp: {perp: .6f} mm | " + f"Pitch: {pitch: .6f} mrad | " + f"Xnorm_LU: {Xnorm_lu: .6f} | " + f"Ynorm_LU: {Ynorm_lu: .6f} | " + f"Xnorm_SS: {Xnorm_ss: .6f} | " + f"Ynorm_SS: {Ynorm_ss: .6f} | " + f"LU BPMsum: {lu_bpmsum:.5f} | " + f"SS BPMsum: {ss_bpmsum:.5f}" + ) + + # ---------------------------------------------------------- + # Save result + # ---------------------------------------------------------- + + with open(filename, "a", encoding="utf-8") as f: + f.write( + f"{energy}," + f"{perp:.8f}," + f"{pitch:.8f}," + f"{Xnorm_lu:.8f}," + f"{Ynorm_lu:.8f}," + f"{Xnorm_ss:.8f}," + f"{Ynorm_ss:.8f}," + f"{lu_bpmsum:.5f}," + f"{ss_bpmsum:.5f}\n" + ) + + print() + print("=" * 60) + print("DCM exit scan complete") + print(f"Results saved to: {filename}") + except Exception as exc: + print(f"\nDCM exit scan FAILED: {exc}") + raise + finally: + bl_energy(12400) + +def perp_again(): + energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] + perp_offsets = [-0.05, -0.025, 0.0, 0.025, 0.05, 0.0] + + filename = bl.env.lut_dir / f"perp_{filenow()}.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write("energy,dcm_pitch,dcm_perp,lu_bpmsum,lu_Xnorm,lu_Ynorm,bcu_bpmsum,bcu_x,bcu_y,samcam_x,samcam_y\n") + try: + for energy in energies: + bl_energy(energy) + + prepare_scint_measurement() + + perp_value = get_value(dev.dcm_perp) + + for offset in perp_offsets: + perp = perp_value + offset + + umv(dev.dcm_perp,perp) + mono_pitch_scan() + + dcm_pitch = get_value(dev.dcm_pitch) + dcm_perp = get_value(dev.dcm_perp) + + + lu_bpmsum = get_value(dev.lu_bpmsum) + lu = compute_norm_lu() + lu_Xnorm = lu["lu_Xnorm"] + lu_Ynorm = lu["lu_Ynorm"] + + bcu_bpmsum = get_value(dev.bcu_bpmsum) + bcu = get_bcu_beampos() + bcu_x = bcu["bcu_x_um"] + bcu_y = bcu["bcu_y_um"] + + scint = read_scintXY(image=False) + samcam_x = scint["X_diff_microns"] + samcam_y = scint["Y_diff_microns"] + + with open(filename, "a", encoding="utf-8") as f: + f.write( + f"{energy:.0f},{dcm_pitch:.5f},{dcm_perp:.5f},{lu_bpmsum:.1f},{lu_Xnorm:.3f},{lu_Ynorm:.3f}," + f"{bcu_bpmsum:.1f},{bcu_x:.2f},{bcu_y:.2f},{samcam_x:.2f},{samcam_y:.2f}\n" + ) + finally: + finish_scint_measurement() + bl_energy(12400) + +def perp_ss_xi(): + # energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] + energies = [25000] + perp_offsets = [-0.05, -0.025, 0.0, 0.025, 0.05, 0.0] + + filename = bl.env.lut_dir / f"perp_xi_{filenow()}.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write("energy,dcm_pitch,dcm_perp,lu_bpms,lu_Xnorm,lu_Ynorm,xi_cam_x,xi_cam_y\n") + try: + for energy in energies: + bl_energy(energy) + + prepare_ss_xi_measurement() + + perp_value = get_value(dev.dcm_perp) + + for offset in perp_offsets: + perp = perp_value + offset + + umv(dev.dcm_perp,perp) + mono_pitch_scan() + + dcm_pitch = get_value(dev.dcm_pitch) + dcm_perp = get_value(dev.dcm_perp) + + + lu_bpmsum = get_value(dev.lu_bpmsum) + lu = compute_norm_lu() + lu_Xnorm = lu["lu_Xnorm"] + lu_Ynorm = lu["lu_Ynorm"] + + + xi_read = read_ss_xicamXY() + xicam_x = xi_read["X_pixels"] + xicam_y = xi_read["Y_pixels"] + + with open(filename, "a", encoding="utf-8") as f: + f.write( + f"{energy:.0f},{dcm_pitch:.5f},{dcm_perp:.5f},{lu_bpmsum:.1f},{lu_Xnorm:.3f},{lu_Ynorm:.3f}," + f"{xicam_x:.1f},{xicam_y:.1f}\n" + ) + finally: + finish_ss_xi_measurement() + # bl_energy(12400) + +def gap_beam(): + # energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] + energies = [22000] + + filename = bl.env.lut_dir / f"gap_xi_{filenow()}.csv" + filename2 = bl.env.lut_dir / f"gap_scint_{filenow()}.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write("energy,gap,lu_bpmsum,lu_Xnorm,lu_Ynorm,xi_cam_x,xi_cam_y\n") + with open(filename2, "w", encoding="utf-8") as f: + f.write("energy,gap,lu_bpmsum,lu_Xnorm,lu_Ynorm,samcam_x,samcam_y\n") + try: + # for energy in energies: + # bl_energy(energy) + + # prepare_ss_xi_measurement() + + # scanpoints=np.linspace(4.535,4.555,20) + # for i in scanpoints: + # umv(dev.id_gap, i) + # time.sleep(0.2) + + # gap = get_value(dev.id_gap) + + # lu_bpmsum = get_value(dev.lu_bpmsum) + # lu = compute_norm_lu() + # lu_Xnorm = lu["lu_Xnorm"] + # lu_Ynorm = lu["lu_Ynorm"] + + # xi_read = read_ss_xicamXY() + # xicam_x = xi_read["X_pixels"] + # xicam_y = xi_read["Y_pixels"] + + # with open(filename, "a", encoding="utf-8") as f: + # f.write( + # f"{energy:.0f},{gap:.4f},{lu_bpmsum:.1f},{lu_Xnorm:.3f},{lu_Ynorm:.3f}," + # f"{xicam_x:.1f},{xicam_y:.1f}\n" + # ) + + for energy in energies: + bl_energy(energy) + finish_ss_xi_measurement() + prepare_scint_measurement() + + scanpoints=np.linspace(4.535,4.555,20) + for i in scanpoints: + umv(dev.id_gap, i) + time.sleep(0.2) + + gap = get_value(dev.id_gap) + + lu_bpmsum = get_value(dev.lu_bpmsum) + lu = compute_norm_lu() + lu_Xnorm = lu["lu_Xnorm"] + lu_Ynorm = lu["lu_Ynorm"] + + + scint_read = read_scintXY(image=False) + samcam_x = scint_read["X_diff_microns"] + samcam_y = scint_read["Y_diff_microns"] + + with open(filename2, "a", encoding="utf-8") as f: + f.write( + f"{energy:.0f},{gap:.4f},{lu_bpmsum:.1f},{lu_Xnorm:.3f},{lu_Ynorm:.3f}," + f"{samcam_x:.1f},{samcam_y:.1f}\n" + ) + finally: + finish_ss_xi_measurement() + finish_scint_measurement() + # bl_energy(12400) diff --git a/pxii_bec/macros/luts/roll_6000.csv b/pxii_bec/macros/luts/roll_6000.csv index 35bd5d2..cc5e249 100644 --- a/pxii_bec/macros/luts/roll_6000.csv +++ b/pxii_bec/macros/luts/roll_6000.csv @@ -1,12 +1,10 @@ Roll, Y pos -2,125.4 -2.5,194.1 -3,260.1 -3.5,325.8 -4,387.4 -4.5,446.9 -5,504.4 -5.5,558.2 -6,609.9 -6.5,658.1 -7,705.2 +4,802 +4.375,890.3 +4.75,994.2 +5.125,1096 +5.5,1190 +5.875,1276 +6.25,1359 +6.625,1443 +7,1524 diff --git a/pxii_bec/macros/mxcomm_analysis.py b/pxii_bec/macros/mxcomm_analysis.py index 7662d71..6bcb76f 100644 --- a/pxii_bec/macros/mxcomm_analysis.py +++ b/pxii_bec/macros/mxcomm_analysis.py @@ -101,11 +101,38 @@ def fit(data, fit_params): params["base_intercept"].set(value=y_min) params["base_slope"].set(value=0) + # # Add peak-specific parameters + # params.update(peak_model.guess(processed_data["y_to_fit"], x=processed_data["x_data"])) + + # # Perform the fitting + # lmfit_result = full_model.fit(processed_data["y_to_fit"], params, x=processed_data["x_data"]) + # Add peak-specific parameters - params.update(peak_model.guess(processed_data["y_to_fit"], x=processed_data["x_data"])) + x_data = np.asarray(processed_data["x_data"]) + + params.update( + peak_model.guess( + processed_data["y_to_fit"], + x=x_data, + ) + ) + + # Constrain peak width to something physically resolvable by the scan + step = np.median(np.diff(np.sort(x_data))) + scan_range = np.ptp(x_data) + + if "peak_sigma" in params: + params["peak_sigma"].set( + min=0.5 * step, + max=0.5 * scan_range, + ) # Perform the fitting - lmfit_result = full_model.fit(processed_data["y_to_fit"], params, x=processed_data["x_data"]) + lmfit_result = full_model.fit( + processed_data["y_to_fit"], + params, + x=x_data, + ) # Find the X that gives the max Y max_index = np.argmax(processed_data["y_to_fit"]) @@ -125,6 +152,7 @@ def fit(data, fit_params): centre_stderr = lmfit_result.params["peak_center"].stderr height_stderr = lmfit_result.params["peak_height"].stderr + snr = height / noise if noise > 0 else np.inf @@ -151,7 +179,7 @@ def fit(data, fit_params): } -def check_peak_fit(x_data, result): +def check_peak_fit(x_data, result, min_fwhm_steps=3): """Check whether a fitted peak is reliable Parameters: X data, results from the fit step Returns: success or problems""" @@ -166,13 +194,18 @@ def check_peak_fit(x_data, result): centre = result["centre"] fwhm = result["fwhm"] + + fwhm_steps = fwhm / step + problems = [] if not x_min + 0.1 * scan_range < centre < x_max - 0.1 * scan_range: problems.append("peak centre is too close to scan edge") - if fwhm < 3 * step: - problems.append("peak is narrower than 3 scan steps") + if fwhm < min_fwhm_steps * step: + problems.append( + f"Peak FWHM spans only {fwhm_steps:.2f} scan steps " f"(minimum {min_fwhm_steps})" + ) if fwhm > 0.7 * scan_range: problems.append("peak is too broad for scan range") @@ -184,8 +217,19 @@ def check_peak_fit(x_data, result): if centre_stderr is None: problems.append("centre uncertainty could not be estimated") - elif centre_stderr > 0.2 * fwhm: - problems.append(f"centre uncertainty is large " f"({centre_stderr:.3g})") + elif centre_stderr > 0.5 * step: + problems.append( + f"centre uncertainty is large " + f"({centre_stderr:.3g}, " + f"{centre_stderr / step:.2f} scan steps)" + ) + + print("FIT DEBUG") + print(f"centre : {centre}") + print(f"fwhm : {fwhm}") + print(f"step : {step}") + print(f"fwhm/step : {fwhm / step}") + print(f"centre error : {result.get('centre_stderr')}") return {"success": not problems, "problems": problems} diff --git a/pxii_bec/macros/mxcomm_data.py b/pxii_bec/macros/mxcomm_data.py index 73c1bb3..b01b6ba 100644 --- a/pxii_bec/macros/mxcomm_data.py +++ b/pxii_bec/macros/mxcomm_data.py @@ -81,17 +81,17 @@ def get_value(dev_name, details=False): root = "scam" live_root = "scam" name = "scam_contrast" - value = dev_name.read()[name]["value"] + value = dev_name.read(cached=True)[name]["value"] else: try: name = live_root = root = dev_name.name - value = dev_name.read()[name]["value"] + value = dev_name.read(cached=True)[name]["value"] except: print("non-standard device") root = dev_name.dotted_name name = dev_name.full_name live_root = dev_name.parent.name - value = dev_name.read()[name]["value"] + value = dev_name.read(cached=True)[name]["value"] if details: return {"value": value, "root": root, "name": name, "live_root": live_root} diff --git a/pxii_bec/macros/mxcomm_plot.py b/pxii_bec/macros/mxcomm_plot.py index ab9b7b5..384761c 100644 --- a/pxii_bec/macros/mxcomm_plot.py +++ b/pxii_bec/macros/mxcomm_plot.py @@ -76,7 +76,7 @@ def plot_fitted_data(data, fit_result, window_name="Fitting", plot_widget=None, f"FWHM = {fit_result['fwhm']:.5f}\n" f"Height = {fit_result['height']:.4g}\n" f"Model = {fit_result['model']}\n" - f"Chi sq = {fit_result['chi_sq']:.3g}\n" + f"Centre_StdErr = {fit_result['centre_stderr']:.3g}\n" f"SNR = {fit_result['snr']:.3g}" ) @@ -94,12 +94,8 @@ def plot_fitted_data(data, fit_result, window_name="Fitting", plot_widget=None, # Display the fitted curve as a line without symbols. wf.get_curve("Fit").set(symbol_size=0) -def plot_scan_data( - data, - window_name="Fitting", - plot_widget=None, - title=None, -): + +def plot_scan_data(data, window_name="Fitting", plot_widget=None, title=None): """Plot completed scan data without a fitted curve.""" if isinstance(plot_widget, str): @@ -120,8 +116,4 @@ def plot_scan_data( wf.x_label = data["motor_name"] wf.y_label = data["signal_name"] - wf.plot( - x=data["x_data"], - y=data["y_data"], - label="Data", - ) + wf.plot(x=data["x_data"], y=data["y_data"], label="Data") diff --git a/pxii_bec/macros/mxcomm_scan.py b/pxii_bec/macros/mxcomm_scan.py index ff38f04..8232dda 100755 --- a/pxii_bec/macros/mxcomm_scan.py +++ b/pxii_bec/macros/mxcomm_scan.py @@ -6,7 +6,6 @@ from dataclasses import dataclass import numpy as np - # from mxcomm_analysis import ( # create_fit_parameters, # get_data_from_history, @@ -26,6 +25,7 @@ import numpy as np # dev # ) + @dataclass(frozen=True) class ScanDefaults: """Default values for fitting routines""" @@ -100,9 +100,11 @@ def move_to_scan_position(motor_device, motor_name: str, position: float, data: scans.umv(motor_device, motor_position, relative=False) print(f"\n Moving {motor_name} to position {motor_position: .3f}") + def restore_position(motor_device, position): """Return a motor to its position before the scan.""" - scans.umv(motor_device, position, relative=False) + scans.umv(motor_device, position, relative=False) + def go_to_peak( motor_device, @@ -117,87 +119,152 @@ def go_to_peak( gomax: bool = False, negative: bool = False, gap: bool = False, + min_fwhm_steps: float = 3, window_name: str = "Fitting", plot_widget=None, ): """ - Go to the peak of a signal by scanning a motor within a specified range and - identifying the optimal position based on signal peak data. + Scan a motor and move either to the fitted peak centre or the + highest measured data point. - Parameters: - motor_device: The motor device to be scanned. - signal_device: The signal device to monitor during the scan. - start (float): The starting position of the scan. Ignored if `relative` is True. - stop (float): The ending position of the scan. Ignored if `relative` is True. - steps (int): The number of steps to divide the scan range into. - relative (bool, optional): If True, interpret `start` and `stop` as relative to - the current motor position. Defaults to RELATIVE_MODE constant. - plot (bool, optional): If True, plot the scan data and the fitted results. - Defaults to True. - settle (float, optional): The time in seconds to wait after each step for the - signal to stabilize. Defaults to DEFAULT_SETTLE_TIME constant. - confirm (bool, optional): If True, ask for user confirmation before starting - the scan. Defaults to True. + If gomax=False: + Fit the scan, validate the fit using check_peak_fit(), + and move to the fitted centre. - Raises: - Exception: Raises exceptions potentially raised by dependent functions or - operations such as plotting, fitting, or motor movement. + If gomax=True: + Skip peak-fit validation and move directly to the highest + measured signal value. Information about whether the maximum + occurred at a scan boundary is returned. - Returns: - fit parameters + Parameters + ---------- + motor_device + Motor device to scan. + signal_device + Signal device to monitor. + start : float + Scan start position. + stop : float + Scan end position. + steps : int + Number of scan steps. + relative : bool + If True, start and stop are relative to current motor position. + plot : bool + Plot scan data/results. + settle : float + Settling time at each scan point. + confirm : bool + Ask for confirmation before starting scan. + gomax : bool + If True, move to highest measured point rather than fitted centre. + negative : bool + Fit a negative-going peak. + gap : bool + Apply minimum-gap protection when using fitted peak position. + min_fwhm_steps : float + Minimum allowed fitted FWHM in scan steps. Only used when + gomax=False. + window_name : str + BEC GUI window name. + plot_widget + Optional explicit plot widget. + + Returns + ------- + dict + Result information. + + For a successful fitted peak: + success + problems + centre + fwhm + height + chi_sq + centre_stderr + snr + + For gomax=True: + success + problems + x_max + max_index + at_lower_boundary + at_upper_boundary + + For a rejected fit: + success=False + problems=list of fit rejection reasons """ + + # ------------------------------------------------------------ # Get motor and signal names + # ------------------------------------------------------------ + mres = get_value(motor_device, True) motor_name = mres["root"] + sres = get_value(signal_device, True) signal_name = sres["root"] + print(f"Signal name is {signal_name}") - # if plot: - # if plot_widget is not None: - # plot_live_data(plot_widget, motor_name, signal_name, title=f"{motor_name} scan") - # else: - # plot_live_data_bec(motor_name, signal_name, window_name=window_name) + # ------------------------------------------------------------ + # Set up live plotting + # ------------------------------------------------------------ if plot: plot_live_data( - motor_name, + motor_name, signal_name, window_name=window_name, - plot_widget=plot_widget, - title=f"{motor_name} scan") + plot_widget=plot_widget, + title=f"{motor_name} scan", + ) + + # ------------------------------------------------------------ + # Validate scan range and get starting position + # ------------------------------------------------------------ - # Validate and calculate step size step_size = calculate_step_size(start, stop, steps) - # Confirm the scan range starting_position = mres["value"] + print(f"Starting motor position = {starting_position}") if confirm: if relative: scan_start = starting_position + start scan_end = starting_position + stop + print( - f"\nScanning from {scan_start: .6g} to {scan_end: .6g} in " - f"{steps} steps of size {step_size}" + f"\nScanning from {scan_start:.6g} to {scan_end:.6g} " + f"in {steps} steps of size {step_size}" ) print(f"Relative mode = {relative}") + else: print( - f"\nScanning from {start: .5g} to {stop: .5g} in {steps} steps of size {step_size}" + f"\nScanning from {start:.5g} to {stop:.5g} " + f"in {steps} steps of size {step_size}" ) print(f"Relative mode = {relative}") + input("Press Enter to continue...") - # Perform the scan + # ------------------------------------------------------------ + # Perform scan + # ------------------------------------------------------------ + scan_result = scans.line_scan( motor_device, start, stop, steps=steps, relative=relative, settling_time=settle ) + motor_data = scan_result.scan.live_data[mres["live_root"]][mres["name"]].val - # # motor_data = scan_result.scan.live_data[motor_name][motor_name].val - # sres = get_value(signal_name, True) + signal_data = scan_result.scan.live_data[sres["root"]][sres["name"]].val + scan_number = "Current" data = { @@ -209,16 +276,75 @@ def go_to_peak( "scan_number": scan_number, } - # Define and fit model to scan data + # ============================================================ + # GOMAX MODE + # ============================================================ + # + # Do not fit or apply peak-shape validation. + # Simply use the highest measured point. + # + + if gomax: + + if negative: + max_index = int(np.argmin(data["y_data"])) + else: + max_index = int(np.argmax(data["y_data"])) + + x_max = float(data["x_data"][max_index]) + + at_lower_boundary = max_index == 0 + at_upper_boundary = max_index == len(data["x_data"]) - 1 + + print(f"Maximum measured signal at {x_max:.6g}") + + if at_lower_boundary: + print("Maximum is at the lower scan boundary") + + if at_upper_boundary: + print("Maximum is at the upper scan boundary") + + # Plot raw scan rather than trying to assess a fitted peak + if plot: + plot_scan_data( + data, + window_name=window_name, + plot_widget=plot_widget, + title=f"{motor_name} scan - maximum at {x_max:.6g}", + ) + + # Move to measured maximum + move_to_scan_position(data["motor_device"], data["motor_name"], x_max, data) + + return { + "success": True, + "problems": [], + "x_max": x_max, + "max_index": max_index, + "at_lower_boundary": at_lower_boundary, + "at_upper_boundary": at_upper_boundary, + } + + # ============================================================ + # FITTED PEAK MODE + # ============================================================ + fit_params = create_fit_parameters( - deriv=False, negative=False, model=FitDefaults.MODEL, baseline=FitDefaults.BASELINE + deriv=False, negative=negative, model=FitDefaults.MODEL, baseline=FitDefaults.BASELINE ) + fit_result = fit(data, fit_params) - check = check_peak_fit(data["x_data"], fit_result) + # ------------------------------------------------------------ + # Validate fitted peak + # ------------------------------------------------------------ + + check = check_peak_fit(data["x_data"], fit_result, min_fwhm_steps=min_fwhm_steps) if not check["success"]: + print("Peak fit rejected:") + for problem in check["problems"]: print(f" - {problem}") @@ -229,42 +355,45 @@ def go_to_peak( plot_widget=plot_widget, title=f"{motor_name} scan - FIT REJECTED", ) - print(f"Returning motor to start position, {starting_position:.5g}") - restore_position( - motor_device, - starting_position, - ) - return None + print(f"Returning motor to start position, " f"{starting_position:.5g}") + restore_position(motor_device, starting_position) + return {"success": False, "problems": check["problems"]} + + # ------------------------------------------------------------ + # Plot accepted fit + # ------------------------------------------------------------ - # Plot the fitted data if plot = True if plot: plot_fitted_data(data, fit_result, window_name=window_name, plot_widget=plot_widget) - # If gomax is set then move to the maximum value, rather than the fit centre - if gomax: - value = fit_result["x_max"] - print(f"Max position is at {value}") - move_to_scan_position(data["motor_device"], data["motor_name"], fit_result["x_max"], data) + # ------------------------------------------------------------ + # Move to fitted peak centre + # ------------------------------------------------------------ + + centre = fit_result["centre"] + + if gap and centre < 4.5: + print(f"Fitted gap position {centre:.5g} mm is below " "minimum gap of 4.5 mm") + umv(dev.id_gap, 4.5) + else: - if gap: - if fit_result["centre"] < 4.5: - umv(dev.id_gap, 4.5) - else: - move_to_scan_position( - data["motor_device"], data["motor_name"], fit_result["centre"], data - ) - else: - # Safely move the motor to the peak position - move_to_scan_position(data["motor_device"], data["motor_name"], fit_result["centre"], data) + move_to_scan_position(data["motor_device"], data["motor_name"], centre, data) + + # ------------------------------------------------------------ + # Return successful fit information + # ------------------------------------------------------------ return { - "centre": fit_result["centre"], + "success": True, + "problems": [], + "centre": centre, "fwhm": float(fit_result["fwhm"]), "height": float(fit_result["height"]), "chi_sq": float(fit_result["chi_sq"]), + "centre_stderr": float(fit_result["centre_stderr"]), "snr": float(fit_result["snr"]), } @@ -376,9 +505,9 @@ def find_feature( plot_live_data( motor_name, signal_name, - window_name = window_name, - plot_widget = plot_widget, - title = f"{motor_name} scans" + window_name=window_name, + plot_widget=plot_widget, + title=f"{motor_name} scans", ) # Perform scan scan_result = scans.line_scan( diff --git a/pxii_bec/macros/mxcomm_utils.py b/pxii_bec/macros/mxcomm_utils.py new file mode 100644 index 0000000..f38d6f6 --- /dev/null +++ b/pxii_bec/macros/mxcomm_utils.py @@ -0,0 +1,348 @@ +"""General reusable utilities for MX beamline commissioning and operation.""" + +import epics +import time +import numpy as np +import matplotlib.pyplot as plt +from pxii_bec.scripts import beamline_context as bl + +# ============================================================================= +# Camera utilities +# ============================================================================= +def predict_scint_transmission(target_flux=None): + """Predict transmission required for a scintillator measurement.""" + + if target_flux is None: + target_flux = BeamMove.scint_target_flux + + _, full_flux = bpm2flux() + + if not np.isfinite(full_flux) or full_flux <= 0: + raise ValueError( + f"Cannot predict scintillator transmission: " + f"invalid full flux ({full_flux:.3g} ph/s)" + ) + + transmission = target_flux / full_flux + + # Prevent an unreasonable attenuator setting + transmission = np.clip(transmission, 0.001, 1.0) + + return float(transmission) + + +def prepare_scint_measurement(autoexp=True, zoom=1000): + """Prepare for taking a reading of the beam from the scintillator""" + + # set transmission + transmission = predict_scint_transmission() + print( + f"Predicted scintillator transmission: " + f"{transmission:.2%}" + ) + dev.transm.put(transmission) + time.sleep(3) + + + # set camera zoom + if get_value(dev.scam_zoom) != zoom: + umv(dev.scam_zoom, zoom) + + # move to beam visualisation + if not bl.planner.is_state(BeamlineState.BEAM_VISUALISATION): + bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) + + # open shutter + dev.bcu_shutter.put(1) + time.sleep(0.5) + + # set autoexposure and check there is a reading in the image + if autoexp: + auto_exposure(cam="samcam", max_iter=25) + + return zoom + + + +def read_scintXY(image=False, measure_size=True): + """Get the X and Y beam positions from the sample camera at + a sepcified zoom. Default zoom is 1000""" + + # Target position on sample camera + x_target = BeamMove.scint_target_x + y_target = BeamMove.scint_target_y + zoom = get_value(dev.scam_zoom) + + results = {} + + + max_signal = get_value(dev.samcam_max) + results["max_signal"] = max_signal + thresh = get_value(dev.samcam_thresh) + + if max_signal < thresh: + return { + "ok": False, + "message": ( + f"Insufficient signal for centroid calculation " + f"(max signal = {max_signal:.0f}, threshold = {thresh:.0f})" + ), + "max_signal": max_signal, + } + + + x_px = dev.samcam_x.read()["samcam_x"]["value"] + y_px = dev.samcam_y.read()["samcam_y"]["value"] + results["X_pixels"] = x_px + results["Y_pixels"] = y_px + + results["ok"] = True + results["message"] = ( + f"Beam detected at X={x_px:.1f}, Y ={y_px:.1f} px" + ) + + # Calculate difference from target position + diff_x = x_target - x_px + diff_y = y_target - y_px + results["X_diff_px"] = diff_x + results["Y diff_px"] = diff_y + + # Convert to pixels + diff_x_mic = calc_scam_microns(diff_x, zoom) + diff_y_mic = calc_scam_microns(diff_y, zoom) + results["X_diff_microns"] = diff_x_mic + results["Y_diff_microns"] = diff_y_mic + + # Measure beamsize + if measure_size: + x_fwhm, y_fwhm = measure_beamsize() + results["X_FWHM"] = x_fwhm + results["Y_FWHM"] = y_fwhm + + print(f"Beam position: X = {results['X_pixels']:.1f} px, " + f"Y = {results['Y_pixels']:.1f} px\n" + f"Distance from crosshair: X = {results['X_diff_microns']:.1f} microns, " + f"Y = {results['Y_diff_microns']:.1f} microns\n") + + if image: + imagedata = get_cam_image(Cameras.sample) + results["image"] = imagedata + + + return results + +def finish_scint_measurement(): + dev.bcu_shutter.put(0) + + +def get_cam_image(prefix): + """Return the current RGB image from an EPICS areaDetector camera.""" + + width = int(epics.caget(f"{prefix}:image1:ArraySize1_RBV")) + height = int(epics.caget(f"{prefix}:image1:ArraySize2_RBV")) + + data = epics.caget(f"{prefix}:image1:ArrayData") + + if data is None: + raise RuntimeError(f"Could not read camera image from {prefix}") + + expected_size = width * height * 3 + + if data.size < expected_size: + raise RuntimeError( + f"Camera returned {data.size} bytes; " + f"expected {expected_size}" + ) + + return np.asarray( + data[:expected_size], + dtype=np.uint8, + ).reshape(height, width, 3) + +def save_cam_image( + image, + filename, + *, + crosshair=None, + beam_position=None, + crop=None, + title=None, +): + """Save a camera image, optionally with crosshair and beam position.""" + + fig, ax = plt.subplots(figsize=(4, 4)) + + ax.imshow(image) + + # Target crosshair + if crosshair is not None: + x, y = crosshair + ax.axvline(x, linewidth=1) + ax.axhline(y, linewidth=1) + + # Measured beam position + if beam_position is not None: + x, y = beam_position + ax.plot( + x, + y, + marker="+", + markersize=12, + markeredgewidth=2, + ) + + # Crop around crosshair + if crop is not None: + if crosshair is None: + raise ValueError( + "crosshair must be supplied when using crop" + ) + + x, y = crosshair + + ax.set_xlim(x - crop, x + crop) + + # Image coordinates have Y increasing downwards + ax.set_ylim(y + crop, y - crop) + + if title: + ax.set_title(title) + + ax.axis("off") + + fig.tight_layout() + fig.savefig(filename, dpi=150, bbox_inches="tight") + plt.close(fig) + + return filename + + + +def create_cam_report( + image, + pdf_filename, + *, + crosshair=None, + beam_position=None, + crop=None, + title="Camera snapshot", +): + """Create a one-page PDF containing an annotated camera image.""" + + fig, ax = plt.subplots(figsize=(6, 6)) + + ax.imshow(image) + + if crosshair is not None: + x, y = crosshair + ax.axvline(x, linewidth=1) + ax.axhline(y, linewidth=1) + + if beam_position is not None: + x, y = beam_position + ax.plot( + x, + y, + marker="+", + markersize=12, + markeredgewidth=2, + ) + + if crop is not None: + if crosshair is None: + raise ValueError( + "crosshair must be supplied when using crop" + ) + + x, y = crosshair + ax.set_xlim(x - crop, x + crop) + ax.set_ylim(y + crop, y - crop) + + ax.set_title(title) + ax.axis("off") + + fig.tight_layout() + + fig.savefig( + pdf_filename, + format="pdf", + bbox_inches="tight", + ) + + plt.close(fig) + + return pdf_filename + +# ============================================================================= +# Daily-setup PDF reports +# ============================================================================= + +def create_empty_daily_report(filename): + """Create the placeholder Daily Setup report.""" + + + fig, ax = plt.subplots(figsize=(8, 6)) + + ax.axis("off") + + ax.text( + 0.5, + 0.60, + "Daily Setup", + ha="center", + va="center", + fontsize=20, + weight="bold", + ) + + ax.text( + 0.5, + 0.48, + "Setup in progress.", + ha="center", + va="center", + fontsize=12, + ) + + ax.text( + 0.5, + 0.42, + "The report will appear here when complete.", + ha="center", + va="center", + fontsize=10, + ) + + fig.savefig( + filename, + format="pdf", + bbox_inches="tight", + ) + plt.close(fig) + + return filename + +def test_predicted_trans(): + filename = bl.env.lut_dir / f"predicted_trans.csv" + with open(filename, "w", encoding="utf-8") as f: + f.write(f"Energy,PredictedT,ExposureTime,max,auto_expT,max2\n") + + energies = [8000, 10000, 12400, 16000, 18000, 20000, 24000] + for energy in energies: + bl_energy(energy) + dev.transm.put(0.1) + time.sleep(3) + exp_time =0.005 + dev.samcam_exp.put(exp_time) + transmission = predict_scint_transmission() + dev.transm.put(transmission) + time.sleep(5) + max1 = get_value(dev.samcam_max) + auto_exposure() + aut_exp_time = get_value(dev.samcam_exp) + max2 = get_value(dev.samcam_max) + + with open(filename, "a", encoding="utf-8") as f: + f.write(f"{energy:.0f},{transmission:.3f},{exp_time:.4f},{max1:.0f},{aut_exp_time:.4f},{max2:.0f}\n") + + diff --git a/pxii_bec/macros/position_device.py b/pxii_bec/macros/position_device.py index eecdbac..eb963df 100755 --- a/pxii_bec/macros/position_device.py +++ b/pxii_bec/macros/position_device.py @@ -5,6 +5,7 @@ from typing import Callable, List, Dict, Optional, Union from bec_lib.logger import bec_logger from bec_lib.device import Device + def motor_resolver(bec_name): """Used to handle aerotech and smargon motors""" @@ -24,6 +25,7 @@ def motor_resolver(bec_name): raise ValueError(f"Cannot resolve motor for '{bec_name}'") + @dataclass class PositionDevice: """Generic device that moves between named or numeric positions""" @@ -53,14 +55,20 @@ class PositionDevice: if isinstance(target, str): name = target.lower() try: - bec_logger.logger.info(f"attempting to find position {name} from bec device {self.bec_name} userParameter.") + bec_logger.logger.info( + f"attempting to find position {name} from bec device {self.bec_name} userParameter." + ) bec_device: Device = dev[self.bec_name] position = bec_device.user_parameter[target] - bec_logger.logger.info(f"obtained position {position} - compare to cached value {self.positions.get(target)}") + bec_logger.logger.info( + f"obtained position {position} - compare to cached value {self.positions.get(target)}" + ) return position except Exception as e: - bec_logger.logger.debug(f"Failed to fetch user parameter for device {self.bec_name} and target {target} from bec, falling back to cached position {e}") + bec_logger.logger.debug( + f"Failed to fetch user parameter for device {self.bec_name} and target {target} from bec, falling back to cached position {e}" + ) if name not in self.positions: raise ValueError(f"Unknown position '{target}'") @@ -100,7 +108,6 @@ class PositionDevice: return scans.mv(self.mot_device, pos, relative=False) - # def set_position(self, target: Union[str, float]): # """Only to be used for testing purposes, bypasses guards""" # pos = self._resolve_target(target) @@ -129,5 +136,3 @@ class PositionDevice: pos = self._resolve_target(target) return abs(self.actual - pos) <= self.tol - - diff --git a/pxii_bec/macros/pxii_energy.py b/pxii_bec/macros/pxii_energy.py index 1ef07cd..c9c542e 100755 --- a/pxii_bec/macros/pxii_energy.py +++ b/pxii_bec/macros/pxii_energy.py @@ -25,7 +25,7 @@ def get_current_energy(): """ Returns the energy in eV from the current bragg angle. """ - current_bragg_angle = dev.dcm_bragg.user_readback.get() + current_bragg_angle = dev.dcm_bragg.user_readback.get(cached=True) current_energy = convert_from_bragg(current_bragg_angle, print_result=False)["energy_ev"] return current_energy @@ -88,8 +88,7 @@ def mono_pitch_scan(plot=True, window_name="Fitting", plot_widget=None): # Set fine pitch to middle of its' travel range scans.umv(EnergyDefaults.mono_fpitch, pos["dcm_fine_pitch"], relative=False) # Move DCM pitch to starting value - scans.umv(EnergyDefaults.mono_pitch, pos["dcm_pitch"], relative=False) - + scans.umv(EnergyDefaults.mono_pitch, pos["dcm_pitch"]-0.04, relative=False) # Go to peak using default parameters from EnergyDefaults if plot: @@ -104,9 +103,32 @@ def mono_pitch_scan(plot=True, window_name="Fitting", plot_widget=None): settle=0.01, plot=True, confirm=False, + min_fwhm_steps=1.1, window_name=window_name, - plot_widget=plot_widget + plot_widget=plot_widget, ) + result["extended_scan"] = False + + if not result["success"]: + extended_range = 2 * EnergyDefaults.pitch_scan["halfwidth"] + extended_steps = 2 * EnergyDefaults.pitch_scan["steps"] + print("Scanning monochromator pitch over a broader range.") + result = go_to_peak( + EnergyDefaults.mono_pitch, + EnergyDefaults.signals["sig1"], + -extended_range, + extended_range, + steps=extended_steps, + relative=True, + settle=0.01, + plot=True, + confirm=False, + min_fwhm_steps=1.1, + window_name=window_name, + plot_widget=plot_widget, + ) + result["extended_scan"] = True + else: print("Scanning monochromator pitch and moving to peak, without plotting.") result = go_to_peak( @@ -118,9 +140,28 @@ def mono_pitch_scan(plot=True, window_name="Fitting", plot_widget=None): relative=True, settle=0.01, plot=False, + min_fwhm_steps=1.1, confirm=False, ) + result["extended_scan"] = False + if not result["success"]: + extended_range = 2 * EnergyDefaults.pitch_scan["halfwidth"] + extended_steps = 2 * EnergyDefaults.pitch_scan["steps"] + print("Scanning monochromator pitch over a broader range.") + result2 = go_to_peak( + EnergyDefaults.mono_pitch, + EnergyDefaults.signals["sig1"], + -extended_range, + extended_range, + steps=extended_steps, + relative=True, + settle=0.01, + plot=True, + confirm=False, + min_fwhm_steps=1.1, + ) + result["extended_scan"] = True return result @@ -178,7 +219,13 @@ def get_dcm_motors_positions(energy_ev): perp = calc_perp_position(energy_ev, print_result=False) bragg_angle = convert_from_energy(energy_ev, print_result=False)["bragg_angle_mrad"] dcm_motor_values.update( - {"bragg_angle": bragg_angle, "perp": perp, "dcm_pitch": pitch, "dcm_roll": roll, "dcm_fine_pitch": fpitch} + { + "bragg_angle": bragg_angle, + "perp": perp, + "dcm_pitch": pitch, + "dcm_roll": roll, + "dcm_fine_pitch": fpitch, + } ) return dcm_motor_values @@ -202,7 +249,7 @@ def move_dcm_motors(energy_ev): # umv(EnergyDefaults.mono_pitch, dcm_pos["dcm_pitch"]) # umv(EnergyDefaults.mono_roll, dcm_pos["dcm_roll"]) # print("\n***DCM Roll movement is currently disabled ***\n") - scans.umv(EnergyDefaults.mono_fpitch,dcm_pos["dcm_fine_pitch"], relative=False ) + scans.umv(EnergyDefaults.mono_fpitch, dcm_pos["dcm_fine_pitch"], relative=False) scans.umv( EnergyDefaults.energy, dcm_pos["bragg_angle"], @@ -216,12 +263,9 @@ def move_dcm_motors(energy_ev): ) -def bl_energy(energy_ev, - move_gap=True, - mono_scan=True, - plot=True, - window_name="Fitting", - plot_widget=None): +def bl_energy( + energy_ev, move_gap=True, mono_scan=True, plot=True, window_name="Fitting", plot_widget=None +): """ Adjusts the beamline's energy to the specified energy in electron volts (eV). The function validates the target energy, checks the current energy, and makes @@ -248,26 +292,29 @@ def bl_energy(energy_ev, print(f"Energy change of {energy_diff:.2f} eV is too small, not changing energy.") return + transmission = get_value(dev.transm) + # Step 1: Move the gap if needed. move_gap_if_needed(energy_ev, move_gap) # Step 2: Move and set the DCM motors. move_dcm_motors(energy_ev) - # Step 3: Update the mirror stripe. + # Step 3: Reset the transmission after the energy change + print(f"Setting transmission to {transmission}") + dev.transm.put(transmission) + + # Step 4: Update the mirror stripe. set_mirror_stripe(energy_ev) - # Step 4: Perform DCM pitch scan and move to peak. + # Step 5: Perform DCM pitch scan and move to peak. if mono_scan: if plot: - result = mono_pitch_scan(plot=True, - window_name=window_name, - plot_widget=plot_widget) + result = mono_pitch_scan(plot=True, window_name=window_name, plot_widget=plot_widget) return result else: mono_pitch_scan(plot=False) - # def new_calc_gap(energy_ev): # """ @@ -301,7 +348,8 @@ def bl_energy(energy_ev, # # raise ValueError(msg) # return gap -def new_calc_gap(energy_ev, print_result=True): + +def calc_gap(energy_ev, print_result=True): """Calculate the undulator gap for the given energy.""" harmonic_values = Gap.get_harmonic_values(energy_ev) @@ -314,33 +362,26 @@ def new_calc_gap(energy_ev, print_result=True): if print_result: harmonic = Gap.get_harmonic_by_energy(energy_ev) - print( - f"Calculated gap is {gap:.3f} mm " - f"using {harmonic}" - ) + print(f"Calculated gap is {gap:.3f} mm " f"using {harmonic}") return gap + def set_gap(energy_ev, print_result=False): """Set the undulator gap.""" - gap = new_calc_gap(energy_ev) + gap = calc_gap(energy_ev) if gap is not None and Gap.minimum_gap <= gap <= 20: umv(dev.id_gap, gap) if print_result: harmonic = Gap.get_harmonic_by_energy(energy_ev) - print( - f"Setting gap to {gap:.3f} mm " - f"using {harmonic}" - ) + print(f"Setting gap to {gap:.3f} mm " f"using {harmonic}") else: - raise ValueError( - f"Cannot calculate a valid gap for {energy_ev:.0f} eV. " - f"Calculated gap: {gap}" - ) + raise ValueError( + f"Cannot calculate a valid gap for {energy_ev:.0f} eV. " f"Calculated gap: {gap}" + ) return gap - diff --git a/pxii_bec/macros/pxii_parameters.py b/pxii_bec/macros/pxii_parameters.py index a6a4e82..64ef12a 100755 --- a/pxii_bec/macros/pxii_parameters.py +++ b/pxii_bec/macros/pxii_parameters.py @@ -4,6 +4,7 @@ from dataclasses import dataclass import numpy as np from enum import Enum + @dataclass(frozen=True) class EnergyDefaults: """Parameters for PXII energy changes""" @@ -12,14 +13,14 @@ class EnergyDefaults: min_energy_ev = 4800 max_energy_ev = 30002 beam_offset = 6 + default_trans = 0.1 signals = {"sig1": dev.lu_bpmsum, "sig2": dev.ss_bpmsum, "sig3": dev.bcu_bpmsum} energy = dev.dcm_bragg mono_pitch = dev.dcm_pitch mono_fpitch = dev.dcm_fpitch mono_perp = dev.dcm_perp mono_roll = dev.dcm_froll - mono_roll_value = 4.75 - LUT_table = "luts/energy_lut.csv" + mono_roll_value = 4.36 stripe_thresholds = {"silicon": 9000, "rhodium": 20000, "platinum": 40000} pitch_scan = {"halfwidth": 0.075, "steps": 20} @@ -28,25 +29,11 @@ class EnergyDefaults: class Calibration: """Calibration parameters for PXII optics""" - # pitch = np.array([4.61823701e-14, -1.97330772e-09, 2.89694543e-05, -5.34468669e00]) - # pitch_calib = [-5.89030403e-19, - # 4.85665619e-14, - # -1.49364459e-09, - # 2.08652765e-05, - # -5.59843072e+00] - pitch_calib = [-4.09746242e-18, - 2.32320520e-13, - -4.95650128e-09, - 4.85168715e-05, - -5.67418203e+00] - + pitch_calib = [-7.60082220e-19, 6.09893342e-14, -1.81702221e-09, 2.45457520e-05, -5.61275367e00] + # roll = np.array([2.28291039e-03, -2.41928101e01]) -from dataclasses import dataclass -import numpy as np - - @dataclass(frozen=True) class Gap: """Fit parameters to calculate undulator gap from energy.""" @@ -56,15 +43,15 @@ class Gap: correction_factor = 0.9994 harmonics = { - "H3": np.array([9.57400000e-04, 1.62100000e-01]), - "H5": np.array([5.03757576e-04, 8.53992727e-01]), - "H7": np.array([3.57624242e-04, 8.74460000e-01]), - "H9": np.array([2.76218182e-04, 9.00565455e-01]), - "H11": np.array([2.25662626e-04, 9.04630909e-01]), - "H13": np.array([1.89440404e-04, 9.35049697e-01]), - "H15": np.array([1.64179798e-04, 9.35255758e-01]), - "H17": np.array([1.44460317e-04, 9.47441667e-01]), - "H19": np.array([1.26071429e-04, 1.04085357e00]), + "H3": np.array([9.37600000e-04, 2.98800000e-01]), + "H5": np.array([5.10102857e-04, 7.99666667e-01]), + "H7": np.array([3.58874286e-04, 8.60410476e-01]), + "H9": np.array([2.74520000e-04, 9.25540000e-01]), + "H11": np.array([2.24457143e-04, 9.25714286e-01]), + "H13": np.array([1.88834286e-04, 9.47505714e-01]), + "H15": np.array([1.63434286e-04, 9.52602857e-01]), + "H17": np.array([1.43320000e-04, 9.77500000e-01]), + "H19": np.array([1.27940000e-04, 9.85770000e-01]), } @classmethod @@ -75,7 +62,7 @@ class Gap: """ slope, intercept = cls.harmonics[harmonic] - # new_calc_gap() applies the correction factor afterwards, + # calc_gap() applies the correction factor afterwards, # so account for it here as well. raw_minimum_gap = cls.minimum_gap / cls.correction_factor @@ -124,9 +111,7 @@ class Gap: harmonic = cls.get_harmonic_by_energy(energy_ev) if harmonic is None: - raise ValueError( - f"{energy_ev:.0f} eV is below the minimum calibrated energy." - ) + raise ValueError(f"{energy_ev:.0f} eV is below the minimum calibrated energy.") slope, intercept = cls.harmonics[harmonic] @@ -141,8 +126,6 @@ class Gap: ) return harmonic, gap - - @dataclass(frozen=True) @@ -169,34 +152,69 @@ class Harmonics: @dataclass(frozen=True) class CamConversion: - """Convert pixels to microns for sam cam""" + """Convert pixels to microns for sample camera""" a = 0.5208 b = 0.002586 + # @dataclass(frozen=True) -class BPM_X_Calibration: - coeffs = (0.11079256, -0.02493623, -0.00799871, -0.00482772, 0.16569134, 0.44192518) +class BCU_BPM_X_Calibration: + coeffs = (0.11079256, -0.02493623, -0.00799871, -0.00482772, 0.16569134, 0.44192518) signal_limit = 0.90 # centred_position = 0.441 - centred_position = -0.009 # updated on 31st Aug26 - crosshair_signal = 0.000731 - crosshair_bpm_position = 0.444 - + centred_position = -0.009 # where the bpm was during calibration + crosshair_signal = -0.00339158 + crosshair_bpm_position = 0.0532 # @dataclass(frozen=True) -class BPM_Y_Calibration: +class BCU_BPM_Y_Calibration: coeffs = (-0.14645507, 0.010016, 0.08258468, -0.00547233, -0.02715706, -1.54419084) + signal_limit = 0.95 + centred_position = -1.540 # where the bpm was during calibration + crosshair_signal = -0.00392027 + crosshair_bpm_position = -1.548 + + +# @dataclass(frozen=True) +class SS_BPM_X_Calibration: + coeffs = (4.04882, -10.9093) signal_limit = 0.90 - centred_position = -1.540 - crosshair_signal = -0.01383 - crosshair_bpm_position = -1.540 + centred_position = 2.8625 # updated on 26th Sep26 + + +# @dataclass(frozen=True) +class SS_BPM_Y_Calibration: + coeffs = (-23.8286, -32.7051) + signal_limit = 0.90 + centred_position = -1.3876 +# @dataclass(frozen=True) + + +class LU_BPM_X_Calibration: + coeffs = (4.42860, -9.63986) + signal_limit = 0.90 + # centred_position = 0.441 + centred_position = 2.1816 # updated on 26th Sep26 + + +# @dataclass(frozen=True) +class LU_BPM_Y_Calibration: + coeffs = (-27.6544, 83.2683) + signal_limit = 0.8 + centred_position = 3.0175 # @dataclass(frozen=True) class Beamstop: - x_offset = -0.65 + x_offset = -0.55 + i2_limit = 5e-8 +@dataclass(frozen=True) +class Cameras: + sample = "X10SA-ES-MS" + ss_xray_eye = "X10SA-ES-SSCAM" + det_xray_eye = "X10SA-ES-XEYE" class CameraZoom(Enum): X1 = ("1x", 1) @@ -207,7 +225,7 @@ class CameraZoom(Enum): X12_5 = ("12.5x", 1000) def __init__(self, label, position): - self.label= label + self.label = label self.position = position @classmethod @@ -216,9 +234,7 @@ class CameraZoom(Enum): for zoom in cls: if abs(position - zoom.position) <= tolerance: return zoom - raise ValueError( - f"No camera zoom levle found near motor position {position}" - ) + raise ValueError(f"No camera zoom level found near motor position {position}") @classmethod def from_label(cls, label): @@ -228,7 +244,45 @@ class CameraZoom(Enum): return zoom raise ValueError(f"Unknown camera zoom: {label}") + +# @dataclass(frozen=True) +class PreChecks: + mode_pv = "AGEOP-BL:STATUS-X10SA" + fe_shutter_pv = "X10SA-FE-PSH1-EMLS-0010:OPEN" + hutch_shutter_pv = "X10SA-OP-PSH1-EMLS:STATE" +# @dataclass(frozen=True) +class EHChecks: + cryo_pv = "X10SA-ES-CS:TEMP_RBV" + ss_xi_pv = "X10SA-ES-SSXI:POS-SET" + ss_mot = dev.ss_xi_x + + +# @dataclass(frozen=True) +class BeamMove: + """Values used for moving beam to the crosshair using + the VFM DR motor and the HFM XD motors. Slope values apply to 12.5X + zoom when the scintillator is used for measurments""" + + # bcu_x_coeffs = (2519.3,-5184.1) + # bcu_y_coeffs = (1324.4,1108.7) + zoom = CameraZoom.X12_5.position + vfm_mot = dev.vfm_ydr + hfm_mot = dev.hfm_xd + max_move = 0.02 + scint_target_x = 993 + scint_target_y = 969 + scint_x_slope = -19450 + scint_y_slope = -37800 + scint_tolerance = 7 + scint_target_flux = 8e11 +# @dataclass(frozen=True) +class BLSetup: + """Values used for the weekly setup script""" + energy = 12400 + fe_xsize = 0.6 + fe_ysize = 0.6 + detector_height = 0 diff --git a/pxii_bec/macros/weekly.py b/pxii_bec/macros/weekly.py new file mode 100644 index 0000000..c93a696 --- /dev/null +++ b/pxii_bec/macros/weekly.py @@ -0,0 +1,347 @@ +# """Weekly beamline checks""" + +"""Weekly beamline checks.""" + +import csv +from dataclasses import dataclass +from datetime import datetime +from pxii_bec.scripts import beamline_context as bl +from pxii_bec.macros.beamline_planner import BeamlineState + + +# ============================================================================= +# Setup +# ============================================================================= + +def setup_weekly(): + """Put the beamline into standard conditions for weekly checks.""" + + print("\nSetting up standard weekly-check conditions...") + + # Set standard energy and run mono pitch scan + energy_result = daily_energy( + BLSetup.energy, + scan_mono=True, + plot_widget="Plot_1" + ) + + + return { + "ok": True, + "energy": BLSetup.energy, + "gap": get_value(dev.id_gap), + "energy_result": energy_result, + } + + +# ============================================================================= +# Slit scans +# ============================================================================= + +def weekly_fe_scan(window_name="DailySetup"): + """Scan the FE slits to find the centre values.""" + + print("\nScanning FE slits...") + + try: + results = scan_fe(window_name) + + return { + "ok": True, + "x": results["fe_xcen"], + "y": results["fe_ycen"], + } + + except Exception as exc: + reason = str(exc) + print(f"FE slit scan failed: {reason}") + + return { + "ok": False, + "error": reason, + } + +def scan_fe(window_name="Fitting", plot_widget=None): + """Scan the FE slits in X and Y""" + try: + print("Scanning FE slits in X") + umv(dev.fe_sl_xsize, 0.05, dev.fe_sl_ysize, 0.6) + go_to_peak(dev.fe_sl_xcen, dev.lu_bpmsum, -1, 1, 25, confirm=False, relative=True, window_name=window_name, plot_widget=1) + xcen = get_value(dev.fe_sl_xcen) + print("Scanning FE slits in Y") + umv(dev.fe_sl_xsize, 0.6, dev.fe_sl_ysize, 0.05) + go_to_peak(dev.fe_sl_ycen, dev.lu_bpmsum, -1, 1, 25, confirm=False, relative=True, window_name=window_name, plot_widget=2) + ycen = get_value(dev.fe_sl_ycen) + + return { + "fe_xcen": xcen, + "fe_ycen": ycen, + } + + finally: + umv(dev.fe_sl_xsize, WeeklySetup.fe_xsize) + umv(dev.fe_sl_ysize, WeeklySetup.fe_ysize) + + +def weekly_bsf_slits_scan(): + """Scan the BSF slits to find the centre values.""" + + print("\nScanning BSF slits...") + + try: + results = centre_one("bsf", dev.lu_bpmsum) + + return { + "ok": True, + "x": get_value(dev.bsf_sl_xcen), + "y": get_value(dev.bsf_sl_ycen), + # "result": results, + } + + except Exception as exc: + reason = str(exc) + print(f"BSF slit scan failed: {reason}") + + return { + "ok": False, + "error": reason, + } + + +def weekly_ss_slits_scan(): + """Scan the secondary slits to find the centre values.""" + + print("\nScanning secondary slits...") + + try: + results = centre_one("ss", dev.bcu_bpmsum) + + return { + "ok": True, + "x": get_value(dev.ss_sl_xcen), + "y": get_value(dev.ss_sl_ycen), + # "result": results, + } + + except Exception as exc: + reason = str(exc) + print(f"SS slit scan failed: {reason}") + + return { + "ok": False, + "error": reason, + } +# ============================================================================= +# BPM readings +# ============================================================================= +def measure_bpm_positions(): + lubpm = compute_norm_lu() + ssbpm = compute_norm_ss() + bcubpm = get_bcu_beampos() + + results = {} + + results["lu"]["X norm"] = lubpm["lu_Xnorm"] + results["lu"]["Y norm"] = lubpm["lu_Ynorm"] + results["ss"]["X norm"] = ssbpm["ss_Xnorm"] + results["ss"]["Y norm"] = ssbpm["ss_Ynorm"] + results["bcu"]["BCU Xnorm"] = bcubpm["bcu_Xnorm"] + results["bcu"]["Ynorm"] = bcubpm["bcu_Xnorm"] + results["bcu"]["bcu_x_um"] = bcubpm["bcu_x_um"] + results["bcu"]["bcu_y_um"] = bcubpm["bcu_y_um"] + + return results + +# ============================================================================= +# Save results +# ============================================================================= + +def save_weekly_results(results): + """Append weekly beamline diagnostics to the history CSV.""" + + filename = bl.env.daily_setup_dir / "weekly_beamline_health.csv" + + setup = results.get("setup", {}) + energy_result = setup.get("energy_result", {}) + + fe = results.get("FE", {}) + bsf = results.get("BSF", {}) + ss = results.get("SS", {}) + lubpm = results.get("lubpm", {}) + ssbpm = results.get("ssbpm", {}) + bcubpm = results.get("bcubpm", {}) + + + + row = { + "timestamp": datetime.now().isoformat(timespec="seconds"), + + # Energy / machine settings + "energy_eV": format_value( + energy_result.get("energy"), ".0f" + ), + + "id_gap_mm": format_value( + setup.get("gap"), ".3f" + ), + + "pitch_offset_mrad": format_value( + energy_result.get("pitch_offset"), ".4f" + ), + + + # Slit centres + "fe_x": format_value( + fe.get("x"), ".3f" + ), + + "fe_y": format_value( + fe.get("y"), ".3f" + ), + + "bsf_x": format_value( + bsf.get("x"),".3f" + ), + + "bsf_y": format_value( + bsf.get("y"), ".3f" + ), + + "ss_x": format_value( + ss.get("x"), ".3f" + ), + + "ss_y": format_value( + ss.get("y"),".3f" + ), + + "lu_Xnorm": format_value( + lubpm.get("lu_Xnorm"), ".4f" + ), + + "lu_Ynorm": format_value( + lubpm.get("lu_Ynorm"), ".4f" + ), + + "ss_Xnorm": format_value( + ssbpm.get("ss_Xnorm"), ".4f" + ), + + "ss_Ynorm": format_value( + ssbpm.get("ss_Ynorm"), ".4f" + ), + + "bcu_Xnorm": format_value( + bcubpm.get("bcu_Xnorm"), ".4f" + ), + + "bcu_Ynorm": format_value( + bcubpm.get("bcu_Ynorm"), ".4f" + ), + + "bcu_beamX": format_value( + bcubpm.get("bcu_x_um"), ".1f" + ), + + "bcu_beamY": format_value( + bcubpm.get("bcu_y_um"), ".1f" + ), + + } + + fieldnames = list(row.keys()) + + file_exists = filename.exists() + + with open(filename, "a", newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + if not file_exists: + writer.writeheader() + + writer.writerow(row) + + print(f"\nWeekly results saved to:") + print(filename) + + return filename +# ============================================================================= +# SciLog results +# ============================================================================= + +def send_week_to_scilog(): + filename = bl.env.daily_setup_dir / "weekly_beamline_health.csv" + msg = bec.messaging.scilog.new() + msg.add_text("Weekly PXII checks") + msg.add_attachment(filename) + msg.add_tags("weekly-check", "PXII") + msg.send() + +# ============================================================================= +# Main weekly check +# ============================================================================= + +def weekly_checks(): + """Run standard weekly beamline checks.""" + + print("\n========================================") + print("PXII WEEKLY CHECKS") + print("========================================") + + results = {} + + # ------------------------------------------------------------------ + # Standard conditions + # ----------------------------------SS-------------------------------- + + try: + results["setup"] = setup_weekly() + + except Exception as exc: + reason = str(exc) + + print(f"Weekly setup failed: {reason}") + + results["setup"] = { + "ok": False, + "error": reason, + } + + # Don't continue with slit scans if we couldn't establish + # the standard starting conditions. + save_weekly_results(results) + + return results + + # ------------------------------------------------------------------ + # BPM positions + # ------------------------------------------------------------------ + results["lubpm"] = compute_norm_lu() + results["ssbpm"] = compute_norm_ss() + results["bcubpm"] = get_bcu_beampos() + + + # ------------------------------------------------------------------ + # Slit scans + # ------------------------------------------------------------------ + results["FE"] = weekly_fe_scan() + results["BSF"] = weekly_bsf_slits_scan() + results["SS"] = weekly_ss_slits_scan() + + # ------------------------------------------------------------------ + # Save results + # ------------------------------------------------------------------ + + results["filename"] = save_weekly_results(results) + + # ------------------------------------------------------------------ + # Edge scan + # ------------------------------------------------------------------ + results["edge"] = scan_ge(11103, mode="coarse") + # ------------------------------------------------------------------ + # Save results to SciLog + # ------------------------------------------------------------------ + print("\nWeekly checks complete.") + + return results + diff --git a/pxii_bec/scripts/init_beamline.py b/pxii_bec/scripts/init_beamline.py index a698bae..39e25d7 100755 --- a/pxii_bec/scripts/init_beamline.py +++ b/pxii_bec/scripts/init_beamline.py @@ -14,16 +14,19 @@ class Environment: beamline_configs = { "x06sa": { + "name": "PXI", "package": "pxi_bec", "devices": "pxi-devices.yaml", "states": "pxi-beamline-states.yaml", }, "x10sa": { + "name": "PXII", "package": "pxii_bec", "devices": "pxii-devices.yaml", "states": "pxii-beamline-states.yaml", }, "x06da": { + "name": "PXIII", "package": "pxiii_bec", "devices": "pxiii-devices.yaml", "states": "pxiii-beamline-states.yaml", @@ -40,8 +43,9 @@ class Environment: ) cfg = self.beamline_configs[self.beamline] + self.beamline_name = cfg["name"] - # Import whichever deplyment BEV has already made available + # Import whichever deployment BEC has already made available package = importlib.import_module(cfg["package"]) self.bec_dir = Path(package.__file__).resolve().parent @@ -49,6 +53,10 @@ class Environment: self.device_config_dir = self.bec_dir / "device_configs" self.lut_dir = self.bec_dir / "macros" / "luts" + # Runtime files generated by daily setup + self.daily_setup_dir = self.bec_dir / "daily_setup_files" + self.daily_setup_dir.mkdir(exist_ok=True) + self.devices_file = self.device_config_dir / cfg["devices"] self.states_file = self.device_config_dir / cfg["states"]