From d4fafdc178b0d27122ff9d4caff50974f396f7d6 Mon Sep 17 00:00:00 2001 From: x10sa Date: Thu, 8 Oct 2026 12:09:16 +0200 Subject: [PATCH 1/6] updates, bug fixes etc --- pxii_bec/device_configs/pxii-devices.yaml | 56 +- pxii_bec/macros/APscripts.py | 548 ++-- pxii_bec/macros/Undu_Helpers.py | 198 +- pxii_bec/macros/beamline_planner.py | 32 +- pxii_bec/macros/build_devices.py | 17 +- pxii_bec/macros/calculator.py | 30 +- pxii_bec/macros/calibrate_bpm.py | 214 +- pxii_bec/macros/daily.py | 2761 ++++++++++++++++++--- pxii_bec/macros/devices_manager.py | 6 +- pxii_bec/macros/enScan.py | 61 +- pxii_bec/macros/flux_macros.py | 30 +- pxii_bec/macros/katscripts.py | 2396 ++++++++++++++---- pxii_bec/macros/luts/roll_6000.csv | 20 +- pxii_bec/macros/mxcomm_analysis.py | 58 +- pxii_bec/macros/mxcomm_data.py | 6 +- pxii_bec/macros/mxcomm_plot.py | 16 +- pxii_bec/macros/mxcomm_scan.py | 265 +- pxii_bec/macros/mxcomm_utils.py | 348 +++ pxii_bec/macros/position_device.py | 17 +- pxii_bec/macros/pxii_energy.py | 107 +- pxii_bec/macros/pxii_parameters.py | 152 +- pxii_bec/macros/weekly.py | 347 +++ pxii_bec/scripts/init_beamline.py | 10 +- 23 files changed, 6208 insertions(+), 1487 deletions(-) create mode 100644 pxii_bec/macros/mxcomm_utils.py create mode 100644 pxii_bec/macros/weekly.py 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"] -- 2.54.0 From db244b493659fab7fa460f1b2c91d4a4a859bf74 Mon Sep 17 00:00:00 2001 From: x10sa Date: Thu, 8 Oct 2026 12:09:42 +0200 Subject: [PATCH 2/6] updates, bug fixes etc --- pxii_bec/macros/analyse_luts.py | 583 ++++++++++++++++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 pxii_bec/macros/analyse_luts.py diff --git a/pxii_bec/macros/analyse_luts.py b/pxii_bec/macros/analyse_luts.py new file mode 100644 index 0000000..dfaf726 --- /dev/null +++ b/pxii_bec/macros/analyse_luts.py @@ -0,0 +1,583 @@ +"""Analyse LUT data to determine fits for DCM and undulator parameters.""" + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from scipy.signal import savgol_filter + +from pxii_bec.scripts import beamline_context as bl + + + +def get_data(filename): + """Open the raw DCM calibration data file.""" + + filename = bl.env.lut_dir / f"{filename}.csv" + + return pd.read_csv(filename) + + +def format_polynomial(fit): + """Return polynomial coefficients as a readable equation string.""" + + terms = [] + order = len(fit) - 1 + + for i, coeff in enumerate(fit): + power = order - i + + if power > 1: + term = f"{coeff:+.3e} x^{power}" + elif power == 1: + term = f"{coeff:+.3e} x" + else: + term = f"{coeff:+.4f}" + + terms.append(term) + + return " ".join(terms).lstrip("+") + + +def plot_lut( + ax, + arr, + fit_order, + xname=None, + yname=None, + title=None, + show_equation=True, +): + """ + Fit LUT data and draw the data and fit onto an existing axes. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axes on which to draw the plot. + arr : numpy.ndarray + Two-column array containing x and y data. + fit_order : int + Polynomial order to fit. + xname : str, optional + X-axis label. + yname : str, optional + Y-axis label. + title : str, optional + Plot title. + show_equation : bool, optional + Display the fitted polynomial on the plot. + + Returns + ------- + numpy.ndarray + Polynomial coefficients. + """ + + x = arr[:, 0] + y = arr[:, 1] + + fit = np.polyfit(x, y, fit_order) + p = np.poly1d(fit) + + x_range = np.linspace(x.min(), x.max(), 100) + + ax.scatter( + x, + y, + label="Data", + color="rebeccapurple", + ) + + ax.plot( + x_range, + p(x_range), + label="Polynomial fit", + color="lightcoral", + ) + + if xname is not None: + ax.set_xlabel(xname) + + if yname is not None: + ax.set_ylabel(yname) + + if title is not None: + ax.set_title(title) + + if show_equation: + equation = format_polynomial(fit) + + ax.text( + 0.02, + 0.02, + equation, + transform=ax.transAxes, + fontsize=9, + verticalalignment="bottom", + bbox=dict( + boxstyle="round", + facecolor="whitesmoke", + edgecolor="lightgray", + alpha=0.8, + ), + ) + + return fit + + +def lut_plot( + arr, + xname, + yname, + title, + fit_order, + save=False, +): + """ + Create a standalone LUT plot. + + This is useful for calibration plots such as DCM pitch where only + one fit is required. + """ + + savefile = bl.env.lut_dir / f"{title}.png" + + fig, ax = plt.subplots() + + fit = plot_lut( + ax, + arr, + fit_order, + xname=xname, + yname=yname, + title=title, + show_equation=True, + ) + + ax.legend(fontsize=9) + + fig.tight_layout() + + if save: + fig.savefig(savefile, bbox_inches="tight") + plt.close(fig) + else: + plt.show() + + print(np.poly1d(fit)) + + return fit + + +def pitch_analysis(filename, fit_order=3, save=False): + """Fit and plot the DCM pitch calibration.""" + + df = get_data(filename) + + pitch = df[["energy", "pitch"]] + + fit = lut_plot( + pitch.to_numpy(), + xname="energy", + yname="pitch", + title="DCM_Pitch_Calibration", + fit_order=fit_order, + save=save, + ) + + return fit + +def ge_edge_analysis(filename, save=False): + """ + Analyse a Ge K-edge scan. + + Finds the approximate absorption edge from the largest negative + derivative of the BCU BPM sum signal. + + Parameters + ---------- + filename : str + CSV filename without the .csv extension. + save : bool, optional + Save the plots to the LUT directory instead of displaying them. + + Returns + ------- + dict + Measured edge energy, expected edge energy, energy offset, + and derivative data. + """ + + # Known Ge K absorption edge + expected_edge = 11103.1 # eV + + df = get_data(filename) + + energy = df["Energy"].to_numpy() + signal1 = df["bcu_bpmsum"].to_numpy() + signal = df["transmission"].to_numpy() + + # ---------------------------------------------------------- + # Calculate derivative + # ---------------------------------------------------------- + # smooth_signal = savgol_filter( + # signal, + # window_length=11, + # polyorder=2, + # ) + # derivative = np.gradient(smooth_signal, energy) + + derivative = np.gradient(signal, energy) + + # Ge absorption causes the BPM signal to fall, so locate + # the strongest negative derivative. + edge_idx = np.argmin(derivative) + + measured_edge = energy[edge_idx] + edge_derivative = derivative[edge_idx] + + energy_offset = expected_edge - measured_edge + + # ---------------------------------------------------------- + # Print results + # ---------------------------------------------------------- + + print() + print("Ge K-edge analysis") + print("------------------") + print(f"Expected edge: {expected_edge:.2f} eV") + print(f"Measured edge: {measured_edge:.2f} eV") + print(f"Energy offset: {energy_offset:+.2f} eV") + print() + + # ---------------------------------------------------------- + # Plot BPM signal + # ---------------------------------------------------------- + + fig1, ax1 = plt.subplots() + + ax1.plot( + energy, + signal, + "o-", + markersize=4, + label="Data", + ) + + # ax1.plot( + # energy, + # smooth_signal, + # "-", + # label="Smoothed", + # ) + + ax1.axvline( + measured_edge, + linestyle="--", + label=f"Measured edge: {measured_edge:.2f} eV", + ) + if energy.min() <= expected_edge <= energy.max(): + ax1.axvline( + expected_edge, + linestyle=":", + label=f"Expected edge: {expected_edge:.1f} eV", + ) + + ax1.set_xlabel("Energy (eV)") + ax1.set_ylabel("Transmission (BCU BPM / SS BPM)") + ax1.set_title("Ge K-edge scan") + ax1.legend() + + fig1.tight_layout() + + # ---------------------------------------------------------- + # Plot derivative + # ---------------------------------------------------------- + + fig2, ax2 = plt.subplots() + + ax2.plot( + energy, + derivative, + "o-", + markersize=4, + ) + + ax2.plot( + measured_edge, + edge_derivative, + "o", + markersize=8, + label=f"Edge: {measured_edge:.2f} eV", + ) + + ax2.axvline( + measured_edge, + linestyle="--", + ) + + ax2.set_xlabel("Energy (eV)") + ax2.set_ylabel("d(Transmission) / dE") + ax2.set_title("Ge K-edge derivative") + ax2.legend() + + fig2.tight_layout() + + # ---------------------------------------------------------- + # Save or display + # ---------------------------------------------------------- + + if save: + signal_file = bl.env.lut_dir / f"{filename}_signal.png" + derivative_file = bl.env.lut_dir / f"{filename}_derivative.png" + + fig1.savefig(signal_file, bbox_inches="tight", dpi=150) + fig2.savefig(derivative_file, bbox_inches="tight", dpi=150) + + plt.close(fig1) + plt.close(fig2) + + print(f"Saved: {signal_file}") + print(f"Saved: {derivative_file}") + + else: + plt.show() + + return { + "expected_edge": expected_edge, + "measured_edge": measured_edge, + "energy_offset": energy_offset, + "derivative": derivative, + } + +def perp_analysis(filename): + """Select the DCM perpendicular translation calibration data.""" + + df = get_data(filename) + sel = df[['energy','dcm_perp', 'xi_cam_x']] + savefile = bl.env.lut_dir / "perp_actual_calc.csv" + with open(savefile, 'w', encoding='utf-8') as f: + f.write("energy,calc_perp,actual_perp\n") + + energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] + for energy in energies: + + data = sel[sel['energy']== energy] + print(data) + + target = 149.4 # value of xi_cam_x at 12.4 keV + + x = data["dcm_perp"].to_numpy() + y = data["xi_cam_x"].to_numpy() + + # Linear fit: gap = slope * energy + intercept + slope, intercept = np.polyfit(x, y, 1) + + # perp_value = intercept/-slope + perp_value = (target - intercept)/slope + calc_per = calc_perp_position(energy) + + + with open(savefile, 'a', encoding='utf-8') as f: + f.write(f"{energy},{calc_per:.4f},{perp_value:.4f}\n") + + # print( + # f"Energy = {energy} eV, " + # f"Calculated perp = {calc_per:.4f}, " + # f"Actual perp = {perp_value:.4f}" + # ) + + + # fig, ax = plt.subplots(figsize = (5,7)) + # ax.scatter(x,y) + # ax.set_title(f"{energy}") + + # # Generate fitted line only over the measured range + # x_fit = np.linspace(x.min(), x.max(), 100) + # y_fit = slope * x_fit + intercept + + # # # Plot fit first and get its automatically assigned colour + # line, = ax.plot( + # x_fit, + # y_fit, + # label=energy, + # ) + # plt.show() + + # return results + + # conv_factor = 12365.75 / (2 * 3.13481) + + # df["costheta"] = np.cos(conv_factor / df["energy"]) + + # perp = df[["costheta", "bsccam_x"]] + + # return ( + # perp.to_numpy(), + # "costheta", + # "bsccam_x", + # ) + + +def roll_analysis(): + """Select the DCM roll calibration data.""" + + df = get_data() + + roll = df[["energy", "roll"]] + + # Select the useful linear range + roll = roll[ + (roll["roll"] > 0.1) + & (roll["roll"] < 9.6) + ] + + return ( + roll.to_numpy(), + "energy", + "roll", + ) + + +def gap_analysis(save=False): + """ + Fit and plot the undulator gap calibration for all harmonics. + + Produces one plot containing all harmonic data and linear fits. + Prints fit coefficients in a format that can be copied directly + into pxii_parameters.py. + """ + + df = get_data() + gap = df[["energy", "gap", "harmonic"]] + + fig, ax = plt.subplots(figsize=(10, 7)) + + results = {} + HARMONICS = [ + "H3", + "H5", + "H7", + "H9", + "H11", + "H13", + "H15", + "H17", + "H19", + ] + for harmonic in HARMONICS: + + data = gap[gap["harmonic"] == harmonic] + + if data.empty: + print(f"{harmonic}: no data") + continue + + x = data["energy"].to_numpy() + y = data["gap"].to_numpy() + + # Linear fit: gap = slope * energy + intercept + slope, intercept = np.polyfit(x, y, 1) + + # Store results + min_gap_energy = (4.5 - intercept) / slope + + results[harmonic] = { + "slope": slope, + "intercept": intercept, + "min_gap_energy": min_gap_energy, + } + + # Generate fitted line only over the measured range + x_fit = np.linspace(x.min(), x.max(), 100) + y_fit = slope * x_fit + intercept + + # Plot fit first and get its automatically assigned colour + line, = ax.plot( + x_fit, + y_fit, + label=harmonic, + ) + + # Plot measured points using the same colour + ax.scatter( + x, + y, + color=line.get_color(), + s=35, + zorder=3, + ) + + ax.set_xlabel("energy (eV)") + ax.set_ylabel("gap (mm)") + ax.set_title("Undulator Gap Calibration") + + ax.legend( + title="Harmonic", + fontsize=9, + ) + + fig.tight_layout() + + if save: + savefile = bl.env.lut_dir / "Undulator_Gap_Calibration.png" + + fig.savefig( + savefile, + bbox_inches="tight", + dpi=150, + ) + + plt.close(fig) + + else: + plt.show() + + # ---------------------------------------------------------- + # Print fit summary + # ---------------------------------------------------------- + + print() + print("Linear fits: gap = slope * energy + intercept") + print() + print( + f"{'Harmonic':<10}" + f"{'Slope':>16}" + f"{'Intercept':>16}" + f"{'Energy at 4.5 mm':>20}" + ) + + print("-" * 62) + + for harmonic, result in results.items(): + + print( + f"{harmonic:<10}" + f"{result['slope']:>16.8e}" + f"{result['intercept']:>16.8e}" + f"{result['min_gap_energy']:>16.0f} eV" + ) + + # ---------------------------------------------------------- + # Print copy/paste-ready parameters + # ---------------------------------------------------------- + + print() + print("Copy/paste into pxii_parameters:") + print() + print("harmonics = {") + + for harmonic, result in results.items(): + + print( + f' "{harmonic}": ' + f'np.array([' + f'{result["slope"]:.8e}, ' + f'{result["intercept"]:.8e}' + f']),' + ) + + print("}") + + return results + -- 2.54.0 From 6f5e45de7147389f70d8548c6811bbbbd14f27b8 Mon Sep 17 00:00:00 2001 From: x10sa Date: Sun, 11 Oct 2026 12:11:44 +0200 Subject: [PATCH 3/6] refactor daily and weekly setup and update PXII calibration routines --- pxii_bec/bec_widgets/profiles/DailySetup.ini | 605 +++++++++++++++ pxii_bec/device_configs/pxii-devices.yaml | 24 +- pxii_bec/macros/analyse_luts.py | 72 +- pxii_bec/macros/bcu_bpm_positions.py | 30 +- pxii_bec/macros/calibrate_bpm.py | 36 +- pxii_bec/macros/create_daily_setup_gui.py | 29 + pxii_bec/macros/daily.py | 227 +++--- pxii_bec/macros/katscripts.py | 745 +++++++++---------- pxii_bec/macros/luts/roll_6000.csv | 20 +- pxii_bec/macros/mxcomm_utils.py | 143 +--- pxii_bec/macros/pxii_energy.py | 205 +++-- pxii_bec/macros/pxii_parameters.py | 142 ++-- pxii_bec/macros/pxii_setup.py | 353 +++++++++ pxii_bec/macros/weekly.py | 390 +++------- 14 files changed, 1914 insertions(+), 1107 deletions(-) create mode 100644 pxii_bec/bec_widgets/profiles/DailySetup.ini create mode 100644 pxii_bec/macros/create_daily_setup_gui.py create mode 100644 pxii_bec/macros/pxii_setup.py diff --git a/pxii_bec/bec_widgets/profiles/DailySetup.ini b/pxii_bec/bec_widgets/profiles/DailySetup.ini new file mode 100644 index 0000000..0e7b421 --- /dev/null +++ b/pxii_bec/bec_widgets/profiles/DailySetup.ini @@ -0,0 +1,605 @@ +[BECDockArea] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=false +baseSize=@Size(0 0) +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 23 1142 746) +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +maximumSize=@Size(16777215 16777215) +minimumSize=@Size(0 0) +mode=creator +mouseTracking=false +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +tabletTracking=false +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle=BEC - DailySetup +workspace_is_locked=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Info.dockWidgetScrollArea.qt_scrollarea_viewport.Info] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +baseSize=@Size(0 0) +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 408 667) +html_text="\n\n


" +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +maximumSize=@Size(16777215 16777215) +minimumSize=@Size(0 0) +mouseTracking=false +plain_text= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +tabletTracking=false +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Progress.dockWidgetScrollArea.qt_scrollarea_viewport.Progress] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +baseSize=@Size(0 0) +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 201 667) +html_text="\n\n


" +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +maximumSize=@Size(16777215 16777215) +minimumSize=@Size(0 0) +mouseTracking=false +plain_text= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +tabletTracking=false +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_1.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_1] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 242 194) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_2.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_2] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 243 194) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_3.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_3] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 242 193) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_4.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_4] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 243 193) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_5.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_5] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 242 194) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_6.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_6] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=true +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0) +geometry=@Rect(0 0 243 194) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0) +invert_x=false +invert_y=false +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0) +legend_label_size=9 +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +signal_x= +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +whatsThis= +windowFilePath= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0) +windowModified=false +windowOpacity=1 +windowTitle= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECDockArea.dockSettingsAction] +autoRepeat=true +checkable=false +checked=false +enabled=true +iconText=Dock settings +iconVisibleInMenu=true +menuRole=@Variant(\0\0\0\x7f\0\0\0\x12QAction::MenuRole\0\0\0\0\x1) +priority=@Variant(\0\0\0\x7f\0\0\0\x12QAction::Priority\0\0\0\0\x80) +shortcut= +shortcutContext=@Variant(\0\0\0\x7f\0\0\0\x14Qt::ShortcutContext\0\0\0\0\x1) +shortcutVisibleInContextMenu=true +statusTip= +text=Dock settings +toolTip=Dock settings +whatsThis= + +[Perspectives] +1\Name=BEC - DailySetup +1\State="@ByteArray(\0\0\x3\xd3x\xda\xa5\x93_K\xc3\x30\x14\xc5\xdf\xfd\x14\x97\xbc\xcf\xaei6'\xb4\x1d\xa3\x32\xf0\xc5?l\xd3G\xa9\xed\xb5\x4\xdb\x44\x92l\xa8\xf8\xe1M\xdaQDS\xa1\xec)\xc9\xb9'\xbf{r!\xf1\xf2\xbd\xa9\xe1\x80Js)\x12\x12\x9eO\t\xa0(d\xc9\x45\x95\x90\xddv=Y\x90\x65\x1a\xdf\x9bUy\xc8\x45\x81\xe5\x95,^mm\xf3\xa1\r6\xf0\xd0_$\xb0\xd3\xa8\xfa\xb3\xc5\x64R\x98\x9c\v\xab\xb8r\x1a\xf7gX\xd7\x32\x37m\x83\xa9\xd5\x37o57\xc6\xca\xb7\x8a\xa3\xb5\x98\x16\xf0\xe5\0{a\x12\xc2\xacg\xa5\x30\x87m\xfe\xdc\xa2 \xdb+\x85\xaet\xa7\x64\xa5Pk\xebx\xe4\x65\x85\x6n\xf2\x6\x7f\xe8\x90\xd5Rc\xe9\x1a\x5i\x1c\x38\xcc \xecZ\xbc\xc8\xdf\xa0V\xf3\x42\xfc\xa9'}\xea\xe8\x9f\xd4\xb5\x34O\xe1\x9f\xcc\x9d:&q{%\xf2\x82\xa2\xf1\xa0\x99\x17\x34\x1bx>\xff\x44\x9dRz\t\x94.\xc0\xadq\xd0iv=\x8e\xe6\xf4!Qo$:\xfem\xcc\vb\xe3\x41s/h~\xd2\x90:\xd7\xf4\x2X\xc8\x80\x32\xeb\x64~g\xd0\x7f!\xbb\x1f\xf8\x92\xe9\xd9\x37,-7\xa6)" +size=1 + +[mainWindow] +DockingState="@ByteArray(\0\0\x3\xd3x\xda\xa5\x93_K\xc3\x30\x14\xc5\xdf\xfd\x14\x97\xbc\xcf\xaei6'\xb4\x1d\xa3\x32\xf0\xc5?l\xd3G\xa9\xed\xb5\x4\xdb\x44\x92l\xa8\xf8\xe1M\xdaQDS\xa1\xec)\xc9\xb9'\xbf{r!\xf1\xf2\xbd\xa9\xe1\x80Js)\x12\x12\x9eO\t\xa0(d\xc9\x45\x95\x90\xddv=Y\x90\x65\x1a\xdf\x9bUy\xc8\x45\x81\xe5\x95,^mm\xf3\xa1\r6\xf0\xd0_$\xb0\xd3\xa8\xfa\xb3\xc5\x64R\x98\x9c\v\xab\xb8r\x1a\xf7gX\xd7\x32\x37m\x83\xa9\xd5\x37o57\xc6\xca\xb7\x8a\xa3\xb5\x98\x16\xf0\xe5\0{a\x12\xc2\xacg\xa5\x30\x87m\xfe\xdc\xa2 \xdb+\x85\xaet\xa7\x64\xa5Pk\xebx\xe4\x65\x85\x6n\xf2\x6\x7f\xe8\x90\xd5Rc\xe9\x1a\x5i\x1c\x38\xcc \xecZ\xbc\xc8\xdf\xa0V\xf3\x42\xfc\xa9'}\xea\xe8\x9f\xd4\xb5\x34O\xe1\x9f\xcc\x9d:&q{%\xf2\x82\xa2\xf1\xa0\x99\x17\x34\x1bx>\xff\x44\x9dRz\t\x94.\xc0\xadq\xd0iv=\x8e\xe6\xf4!Qo$:\xfem\xcc\vb\xe3\x41s/h~\xd2\x90:\xd7\xf4\x2X\xc8\x80\x32\xeb\x64~g\xd0\x7f!\xbb\x1f\xf8\x92\xe9\xd9\x37,-7\xa6)" +Geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\x17\0\0\x4u\0\0\x3\0\0\0\0\0\0\0\0\0\xff\xff\xff\xff\xff\xff\xff\xff\0\0\0\0\0\0\0\0\xf\0\0\0\0\0\0\0\0\x17\0\0\x4u\0\0\x3\0) +State=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\xf\x80\x4\x95\x4\0\0\0\0\0\0\0\x43\0\x94.) + +[manifest] +widgets\1\closable=true +widgets\1\floatable=true +widgets\1\floating=false +widgets\1\floating_abs_h=0 +widgets\1\floating_abs_w=0 +widgets\1\floating_abs_x=0 +widgets\1\floating_abs_y=0 +widgets\1\floating_rel_h=0 +widgets\1\floating_rel_w=0 +widgets\1\floating_rel_x=0 +widgets\1\floating_rel_y=0 +widgets\1\floating_screen= +widgets\1\movable=true +widgets\1\object_name=Info +widgets\1\widget_class=TextBox +widgets\2\closable=true +widgets\2\floatable=true +widgets\2\floating=false +widgets\2\floating_abs_h=0 +widgets\2\floating_abs_w=0 +widgets\2\floating_abs_x=0 +widgets\2\floating_abs_y=0 +widgets\2\floating_rel_h=0 +widgets\2\floating_rel_w=0 +widgets\2\floating_rel_x=0 +widgets\2\floating_rel_y=0 +widgets\2\floating_screen= +widgets\2\movable=true +widgets\2\object_name=Plot_1 +widgets\2\widget_class=Waveform +widgets\3\closable=true +widgets\3\floatable=true +widgets\3\floating=false +widgets\3\floating_abs_h=0 +widgets\3\floating_abs_w=0 +widgets\3\floating_abs_x=0 +widgets\3\floating_abs_y=0 +widgets\3\floating_rel_h=0 +widgets\3\floating_rel_w=0 +widgets\3\floating_rel_x=0 +widgets\3\floating_rel_y=0 +widgets\3\floating_screen= +widgets\3\movable=true +widgets\3\object_name=Plot_2 +widgets\3\widget_class=Waveform +widgets\4\closable=true +widgets\4\floatable=true +widgets\4\floating=false +widgets\4\floating_abs_h=0 +widgets\4\floating_abs_w=0 +widgets\4\floating_abs_x=0 +widgets\4\floating_abs_y=0 +widgets\4\floating_rel_h=0 +widgets\4\floating_rel_w=0 +widgets\4\floating_rel_x=0 +widgets\4\floating_rel_y=0 +widgets\4\floating_screen= +widgets\4\movable=true +widgets\4\object_name=Plot_3 +widgets\4\widget_class=Waveform +widgets\5\closable=true +widgets\5\floatable=true +widgets\5\floating=false +widgets\5\floating_abs_h=0 +widgets\5\floating_abs_w=0 +widgets\5\floating_abs_x=0 +widgets\5\floating_abs_y=0 +widgets\5\floating_rel_h=0 +widgets\5\floating_rel_w=0 +widgets\5\floating_rel_x=0 +widgets\5\floating_rel_y=0 +widgets\5\floating_screen= +widgets\5\movable=true +widgets\5\object_name=Plot_4 +widgets\5\widget_class=Waveform +widgets\6\closable=true +widgets\6\floatable=true +widgets\6\floating=false +widgets\6\floating_abs_h=0 +widgets\6\floating_abs_w=0 +widgets\6\floating_abs_x=0 +widgets\6\floating_abs_y=0 +widgets\6\floating_rel_h=0 +widgets\6\floating_rel_w=0 +widgets\6\floating_rel_x=0 +widgets\6\floating_rel_y=0 +widgets\6\floating_screen= +widgets\6\movable=true +widgets\6\object_name=Plot_5 +widgets\6\widget_class=Waveform +widgets\7\closable=true +widgets\7\floatable=true +widgets\7\floating=false +widgets\7\floating_abs_h=0 +widgets\7\floating_abs_w=0 +widgets\7\floating_abs_x=0 +widgets\7\floating_abs_y=0 +widgets\7\floating_rel_h=0 +widgets\7\floating_rel_w=0 +widgets\7\floating_rel_x=0 +widgets\7\floating_rel_y=0 +widgets\7\floating_screen= +widgets\7\movable=true +widgets\7\object_name=Plot_6 +widgets\7\widget_class=Waveform +widgets\8\closable=true +widgets\8\floatable=true +widgets\8\floating=false +widgets\8\floating_abs_h=0 +widgets\8\floating_abs_w=0 +widgets\8\floating_abs_x=0 +widgets\8\floating_abs_y=0 +widgets\8\floating_rel_h=0 +widgets\8\floating_rel_w=0 +widgets\8\floating_rel_x=0 +widgets\8\floating_rel_y=0 +widgets\8\floating_screen= +widgets\8\movable=true +widgets\8\object_name=Progress +widgets\8\widget_class=TextBox +widgets\size=8 + +[profile] +created_at=2026-10-10T16:30:18Z +quick_select=true +screenshot="@ByteArray(\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\x4v\0\0\x2\xea\b\x6\0\0\0PE\xd3\x82\0\0\0\tpHYs\0\0\xe\xc4\0\0\xe\xc4\x1\x95+\xe\x1b\0\0 \0IDATx\x9c\xec\xddwT\x14W\x1b\xc0\xe1\xdfR\xa5\vbA\xb0\xb7\xa8\xd8\xc1\xde{\x8b\x5\x5{Kl\xb1\x63\x8b\xbd\xf7\xde{\x8d\r{\x8f\xc6h\xa2\x31\x89\x1a\x89\xbd\x97\xd8\xc5^@@`\xd9\xfd\xfe\xe0\x63#\xa1,e\x97\x65\xe1}\xce\xf1\x1c\x99\x99\x9d{\xe1\xce;\xe5\xdd{\xef(BB#\xd4\b!\x84\x10\x42\b!\x84\x10\x42\b\xa3\x63\x62\xe8\n\b!\x84\x10\x42\b!\x84\x10\x42\x88\xe4\x91\xc4\x8e\x10\x42\b!\x84\x10\x42\b!\x84\x91\x92\xc4\x8e\x10\x42\b!\x84\x10\x42\b!\x84\x91\x92\xc4N\x1a\xf2\xe4\xf9k\x9e<\x7fm\xe8j\b\x91hr\xcc\x1a'i\xb7\x8cK\xda^\x88\xb8Il\b!\xd2\"97\x89\xc4\x92\xc4\x8e\x10\x42\b!\x84\x10\x42\b!\x84\x91\x92\xc4\x8e\x10\x42\b!\x84\x10\x42\b!\x84\x91\x32\x33t\x5\x84\x10\x42\b!\x84\x30\x16\xc1\xc1\x9f\t\xfc\x14\x8c\x32Re\xe8\xaa\b\x91,\x16\x16\xe6\x64\xb2\xb4\xc0\xc1\xce\xda\xd0U\x11\x42\xe8\x88$v\x84\x10\x42\b!\x84H\x84\x80W\xef\xb0\xb2\x30'\xb7[V\xccL\xa5\xe3\xbb\x30Naa\x11|\f\f\xe5\xe9\xf3\x37\xb8\xe5t6tu\x84H\x91'\xcf_\x93+gVCW\xc3\xe0$\xb1#\x84\x10\x42\b!\x84\x16\xc1\xc1\x9f\xc9\x64i\x8eKvGCWE\x88\x14\xb1\xb4\x34'[VsL\xcd 8$\f\x1bkKCWI\b\x91\x42\xf2U\x83\x10\x42\b!\x84\x10Z\x4~\n&\x9b\xb3\x83\xa1\xab!2\x80;\xff\x4\xa4J9\xe\xf6\x36|\f\fN\x95\xb2\x84\x10\xfa%\x89\x1d!\x84\x10\x42\b!\xb4PF\xaa\x64\xf8\x95HW\xccLM\x89TE\x1a\xba\x1a\x42\b\x1d\x90\xab\x93\x10\x42\b!\x84\x10\x42\b\x91\xce\xa8\xd5j\xd4ju\xaa}N\x18\x8e\xcc\xb1#\x84\x10\x42\b!\x84\x10\"N~\xdbwq\xde\xff\x42\xa2\xb7\xafT\xd1\x93\xd6\xadZ\xea\xb1\x46\"1\xd4j5s\xe7-B\xadV3t\xc8@T*\x15\xc3G\x8c\xc5\xbbuK*V\xf0\x8c\xb1\xed\xd9s\xe7\xd9\xb9k/3\xa6M\xc2\xcc\xcc\x94\x99\xb3\xe6\x11\x16\x1e\xc6\xb8\x31#Q(\x14:\xad\xd3\xc2\xc5\xcby\xf2\xe4i\x8c\xe5\xb9r\xb9\x31\xb0\xffw:-+\xa3\x91\xc4\x8e\x10\x42\b!\x84\x10\x42\x88\x38\x9d\xf7\xbf\xc0\xa1\xc3G(T\xb0\x80\xd6m?}\xfa\xc4\xae\xdd{\t\r\xf9L\xa7N\xed\xf4R\x9f;\xb7\xefr\xe2\xe4)\xbe\xeb\xd5]/\xfb\xd7\xe6\xd1\xa3'l\xdf\xb1\x8b\x97\xaf^S\xbcXQ\xda\xb7\xf3&S\xa6L\x6\xa9Kb,Y\xb6\x12\xb5Z\x8d\xef\xa0\xfe\xec\xda\xbd\x17O\x8f\xb2\xb1\x12;\xf\x1f>b\xd7\xee\xbdL\x9d<\x8ey\xf3\x17\xb1|\xe5\x1a\xfa~\xd7S\xe7uY\xb4\x64\x5\xf3\x17,\xa6X\xd1\xaf\x30\x35\x35\x5@\xa5R\xb1k\xf7\xde\xa8:\xe\xec\xab\xf3\x32S\x9bR\x19\x89J\x15\x89\x85\x85\x45\xaa\x96+\x89\x1d!\x84\x10\x42\b!\x84\x10\xf1*T\xb0\0\a\xf6\xed\xe0\xed\xdb\xb7\xf1ncoo\x8f\xa9\xa9\x19\x43\x86\x8e`\xcc\xf8I\0:O\xee\xdc\xb9}\x97\x36\x1d\xba\xe2\x9c\x35\v]:u\xc0\xda\xdaJ\xa7\xfb\xd7\xe6\xda\xf5\x1b\xb4\xf2\xee@&KK\xdc\xdc\\\xd9\xb7\xff\0\xbb\xf7\xec\x63\xef\x1e?,\xcc\xcdS\xb5.\xda(\x14\n\x86\f\x1e\0\xc0\xe2\xa5+\x88P*\xb5~f\xce\xdc\x85\xac^\xbb\x81~}z1t\xc8\xc0\x14\xf5\xa0\xd9\xb5{/k\xd6m\xd4\xfc\xacV\xc3\xad[\xb7\x19\xd0\xaf\x37\x43\x6\xf\x8c\xb1\xed\xbc\xf9\x8bX\xb0p\tG~:\x86\xc9\x17\x65v\xeb\xda\x99\x36\xde^\xc9\xae\x43j\x89\x88P\xb2r\xf5Zv\xed\xda\xcb\xc3G\x8fY\xbbj\x19u\xea\xd4L\xd5:\x18l\x8e\x9d\xc7\x8f\x1f\x93'\x7fQ\x1e?~\x9c\xac\xf5\t9p\xf0 \r\x1a\x36Ni\x15\xc5\xffU\xaeR\x83\xa3G\x8f&{\xbd\xd0\x8f\x62\xee\x65\xf8\xe5\x97_\f]\x8d\x34\xc9\xd0\xc7\xac\x9c\x83\x92N\xae\tB\b!D\xda\xf6\xf6\xed[<*T\x8f\xf7\xdf\x8fG~\xc2\xcc\xcc\x94\xb9sf\xd0\xa2YS\xc6\x8c\x9f\xc4\xae\xdd{uV\xfe\xed[w\xf0i\xdf\x5\xe7\xacY\xf0\xdb\xbc>\xd5\x93:\0\xb3\xe7.$\xa7K\xe~?\xf5\x33\x87\xf\xee\x66\xcb\xa6\xf5\\\xbb~\x83\xbd\xfb\xe\xa6z]\x12\x43\xa9\x8c\xa4_\xdf^\xf4\xec\xde\x8dU\xab\xd7i\xdd~\xf5\xda\r\xf4\xec\xde\x8d\xfe\xfdz\xa3T\xa6lb\xedw\xef\xdes\xfb\xf6]J\x95p\xa7T\twJ\x97t\xc7wP\x7f\x6\xfb\xe\x88\xb5\xad\xef\xa0\xfe\xf8\xe\xeaO\x99\x92%4\xdb\xdf\xbe}7\xc1\x44\x62Z\xf1\xe8\xd1\x13Zx\xb5\x61\xdf\xbe\x3\x14(\x90\x9f\n\xe5=R=\xa9\x3:\xee\xb1\x13\x12\x12\x42Q\xf7rX\x98\x9bq\xed\x8a?\x96\x96\x96\xba\xdc\xbd\xc6\xaaU\xab\x99:c\x1e\0\x36\xd6\x99\xa8X\xbe\x1cm\xdaxS\xaf^=LLd>\xe8\xc4\xe8\xdb\xb7?\x87\x8e\x1c\xe7\xa7\x1f\xf7\xf2\xd5W_\xe9\xa5\x8cs\xe7\xce\xe1\xd3\xae\xab\xe6\xe7\x12\xc5\x8bP\xa6tI\xbc\xbcZR\xa6L\x99\x14\xef\xff\xc8\xd1\xa3l\xdb\xb6\x9d\x35\xabW\xea\xb4\xab[dd$G\x8f\xfe\xc4o\xbf\xfd\xc6\xd9s\xe7\xb1\xb3\xb3\xa3|y\xf:\xb4oG\x81\x2\xda\xbb\xa0\n\xfdH\x8d\x63v\xf1\xe2%\xdc\xbd{\x8f\x45\x8b\x16\xc4X~\xfa\xf4\xeft\xec\xd2\x83\a\xf7\xae\xc7y\x8e\t\xfd\xfc\x99o\xbe\xe9N\x8f\xee\xdfP\xbbvm\xbd\xd4\xcd\x18\xa5\xd7k\xc2\xf4\xe9\x33\xc8\x94)\x13\xbe\xbe\x83t\xbeo!\x84\xf1\xf2\x1d<\x8c-~\xfb\0\x30\x35\x35\xa1\xa2giJ\x97*\x81\x8fOk\x8a\x16M\xfcuk\xc5\x8aU\\\xb9v\x9d\xa5\x8b\x17\xa0P(\xa8\xdf\xe0k\xda\xf8x\xf1\xed\xb7\xdd\xb4~622\x92\x13'~e\xc7\xce\xdd\xf8\xff}\x99,N\x99)W\xae\x34\xadZ\xb5\xa4\x42yO\xad\x9fW\xa9T\x4\x6\x6\x62mm\x9d\xea\xc3\btI\xa9\x8c\xa4g\xaf>\xd4\xa8^\x95.]:\x19\xba:\x19Ntr\xe7\xf2\x95\xab\x9c\x39{^'\xf3\xed\xdc\xbau\x9b\xb6\x1d\xba\xf1\xfe\xfd{\x9a\x34i\xc0\xa6\xcd\xdb\x62m\x93={v\xdc\xdd\x8b\xe1^\xbc\x98\xde\xe6j\xb9{\xe7\x1eM\x1a\x37\xc0\xce\xde\xe\x80\x8a\x15<\xc9\x91=;\xf7\xee\xde\xd3Ky)5b\xd4\xb8$'\xd7V\xadY\xcf\xaa\x35\xebi\xdd\xaa%sgOKQ\xf9\x66\x66\x66\xcc\x9c\x31\x45\xebv\n\x85\x82\x41\x3\xfa\xc4X\xb6'\x8d&\xcb\xbe\x14\x11\xa1\xa4O\xbf\x41\xe4\xca\xe5\xc6\xa2\x85s\x18\x39j<\x5\v\xe4\x37H]t\x9a\xd8\x39{\xf6,\0\xe1\x11J.\\\xb8H\xa5J\x15u\xb9\xfb\x18<\xcb\x95\x64\xfc\xb8\x31(\x95J\xee\xdc\xb9\xc3\x90\x61\xa3\x18\x35\xe2-\xed\xdb\xb7O\xf2\xbe\x8e\x1c=\xca\x92%\xcb\x38|\xe8\x80\x1ej\x9a\xf6\x4\x4\x4p\xe8\xc8q\0N\x9e<\xa5\xb7\x87\xe4h;\xb6m\xc4\xda\xda\x8aw\xef\xdes\xe9\xf2%Z\xf9td\xec\xe8\x61t\xeb\xda\x35\x45\xfbu\xb0\xb7'_\xde<\x9a\xf1\x99\xba\x10\x12\x12\x82\xef\xe0!\xbcy\xf3\x96\x8e\x1d\xdb\xe3\xe5\xd5\x92\xb7o\xdfr\xef\xfe}T*\x95N\xca\xa8Q\xb3\xe\xe3\xc6\x8e\xa2N\x9d::\xd9_F\x90\xda\xc7\xac\x36\xd3\xa6M\xc7\xd2\xd2\x92!C\x6\x3Q\xaf\v\xcd\x9f//vv\xf6\x6\xadWZc\xac\xd7\x4mr\xb8\xe4\xc0\xd2\x88\x1fx\x84\x10\xfaS\xbfNU\xfa\xf6\xe9\x45\xa4J\xc5\x8b\x80\x17\x9c\xfa\xed\x34\x35\xea|\xcd\xfc\x39\x93\xe9\xd0\xbem\xa2\xf6\xa1V\x83Z\x95\xbc\xb7\xd1,_\xb1\x8aM\x9b\xb7\x33h\xe0wt\xee\xd4\x9e\xe0\xe0\x10n\xde\xba\xc5\xc7\xf\x1f\x13\xf5\xf9\xc0\xc0@\n\x17\xf3\x64\xff\xeeMz=g\v\xe3\x64jj\x8a\xab\xabk\xbc\xebml\xac\x35\xff\x37\x33\x33\xc5\xd6\xd6Vge\x9f\xfc\xedw\xde\xbf\x7f\xf\xc0\xa1\x43q\xf4\xbaV\xab\t\f\nB\xa5RQ\xb7N-\xa6M\x9d@\xf6l\xd9tV~4WW\x17\xfc\xff\xbeHdd$\xa6\xa6\xa6\xdcx'O\xe\b\b`\xff\xa1\x9f\x18\x32\xc4\x17WWW\xfa\r\x18\xc2\xe0\xc1\xbe\x9aL\xadJ\xa5\xe2\xd0\xa1\x43l\xdf\xbe\x93O\xc1\xc1\xe4\xcd\x13\x33\x3\xa8m\xbd\x36\xae\xae\xae\x84\x84|&22\xf6X@\xb5Z\xcd\xf1\xe3\xc7\xd9\xb6m;\xf7\xffy@\xedZ5\xe8\xdd\xbb\x17\xd9\xb3g\xe7\xcf?\xff\xa4O\xff!\0\xe4\xc9_\x94V-\x9a\x30o\xde\x9cX\xfb\xd0\xf7\x41\x91ZT*\x15~~;\xf8\xaewOJ\x97.\x8d\xef\xd0\xd1\\\xbbv\x8d\xd2\xa5Kk\xb6\xb9t\xe9\x12\xabV\xad\xe6\xd6\xed\xbb\xd4\xadS\x8bg\x1\xaf\x62\xec\x43\xdbzm\xaaW\xaf\x46\x5\x8fR\xfc\xfe\xfb\x1f\x9a\xc4\xce\xe5\xcb\x97Y\xb9r\x15\x97\xaf^\xa7^\x9dZ\xf4\xed\xdb\a\v\v\v*U\xa9\xc9\xe2\x85s4\xbd[\x2\x3\x3\xf1\xacX\x8d=;\xb7r\xff\x9f\x7fX\xbat9?\x1d\xfd\x11\x80\xf0\xf0p\xb6o\xdf\xce\xfe\x3\x87x\xf9\xf2\x35\xf5\xea\xd6\xe2\xfb\xef\x87\x63ii\x19\xe7\xfe\xb3\x66\xcd\x1a\xa3^AAA\xcc\x99\xbf\x94\x3{\xfd\x12\f\xc4\xb0\xb0\x30\xb6n\xdd\xca\xc1\x43?\x12\x16\x16\x46\xf3\x66_\xd3\xa9SG\xac\xac\xa2\xc6\xf9\x16)V\x8a\x81\xfd{s\xf3\xc6-.\\\xbaL\x83\xfau\xe9\xdf\xaf/\x8e\x8e\x8e\xb4i\xdb\x9e\xb7\xef>\xd2\xbdW\xd4\xd8\xd2#\x87vS\xacX1\x8a\xb9\x97\x61\xfa\xd4\t\xfc\xf6\xdb\xef\xfc}\xf1\x12\x9b\x36\xae\xc7\xcd\xcd\x35\xde\xe3\x36\x31\xe4\x98%\xd1\xeb\x93\x62\xd9\xf2\xe5\xac\xdd\xb0\x35\xea\xff+\xd7\x33k\xc6\x44\xda\xf8\xf8P\xcc\xbd\fK\x16\xcd\x8ds(VPP\x10\xeb\xd6\xaf\xe7\xc8\x91\x63\0T\xae\\\x11\xdf\x41\x3\xb1\xb3\xb3\x8b\xb5mzi\xb7\xb4|MX\xbbv\x1d\x97/_\xd1\f\xb9\xfb\xf8\xf1#%\xcbT\xd4\xc4\xa3J\xa5\xe2\xe0\xc1\x83\xf8m\xdfIHH\bu\xeb\xd4\x66\xf9\xca\xb5\x1c\xfdq?\xb9s\xe7\x66\xdc\xb8\xf1\xd8;\xd8\x33t\xc8\x10\xee\xdf\xbfO+\xefv\f\x1c\xd0\x87_~\xf9\x95\x17/^\xd1\xae\xad\x37\x9d;w\xc6\xcc\xcc\f\x95J\xc5\x9e={\xf0\xdb\xbe\x93\x80\x17/\xa9R\xa9\"=z|K\xa1\x42\xb1\x1f\xee\xd2K\xdb\v!\xfe\xa5P(h\xdb\xc6\x87\xcd[\xb6\xb3s\xd7\x1e\x46\x8f\xfa\x9ek\xd7\xae\xb3\x63\xe7.\xce\x9e\xf5\xe7\x63`\x10\r\xea\xd7\xa2o\x9f\xef\xc8\x9e=\xaaw\xc1\x9c\x39\xf3\xb9z\xfd\x6\x1b\xd7\xaf\x8e\xb5\xbf.\xddz\x90\xc5\xc9\x91ysgi\x96\x1d:\xf4#\xa3\xc6N\xe2\xc7\x83{\x88\x8cT\xf1\xee\xfd\xbb\x4\xeb\xf4\xe4\xc9S\x96,]\xce\xaf'O\xf3U\x91\x42\xf4\xed\xd3\x8b\n\x15\xca\x13\x18\x18\x44\xe1\x62Q\xf\x80\xcd[E\r_:\xb8o+\xf6vv\xd4\xa8\xf3\x35\xe7\xcf\x9c O\x9e\xdc\0\x1c\x38x\x88\xee\xbd|y\xf5\xfc\xae\xa6\xce\x11\xca\blml8\xf4\xe3O\xd8X[\xd3\xa5s\a\x9a\x35k\xaauH\xcc\xdb\xb7oY\xb3v=G\x7f:\xc1\xdbw\xef\xa9T\xc1\x83\x31\xa3G\xe0\xec\x9c\x85}\xfb\xf\xf0\xe3\x8f?q\xf9\xea\r\n\x17\xcaO\xb7.\x9dh\xd2\xa4\x11\x81\x81\x81\x94(]\x89\x39\x33'\xe1\xed\xddJ\xb3\xaf\x89\x13\xa7\xf0\xfe\xc3\a\x16\xcc\x9f\x13\x63\b\xdb\xcb\x97/)W\xbe&sgO\xe6\xd4o\xa7\xf9\xeb\xaf\v\xd4\xacQ\x85\xc1\xbe\x3\xc9\x99\xd3\x45\xf3\xf9\xbf\xff\xbe\xc0\x8a\x95k\xb8q\xf3\x36\xf5\xeb\xd5&<<\x1c+++\xc6\x8c\x1e\x91\xe0\xef\x90\xda\xde\x7f\xf8\xc8\x90\x61\xa3\xf9\xf8\x31(\xce\xf5\xe\xev\xcc\x9d=\x15\xc7\xcc\xe\xa9\\3\xfd\x1a\x34\xb0\x1fj\x14\xcc\x9b\xbf\b Vr'S\xa6L4i\xdc\x10WWW\xbcZ\xb7\x63\xeb\xb6\xedt\xe9\xdc\x41\xa7u\xa8Y\xb3:[7\xaf\x63\xfd\xc6-<|\xfc\x98\x46\x8d\xea\x33h`_\xec\xed\xa3zm\xbfz\xf5\x9a\xd6m:\xb0x\xe1\\J\x95,\xa1\xd3\xb2\xd3\x83?\xfe<\xcb\x36\xbf\x9d\xacY\xb9\x84z\xf5\xa2\x9e\xef\x8e\xfd|\x9c\x1e\xbd\xfa\xd3\xb4I#\xaaT\xd6\xed\x97\x80...\\\xba|\x85m~;c,o\xdf\xce\a\x9f\x14N\xca\xacTF\xf2\xf0\xd1\x63J\xba\x17\xd3,k\xd2\xa4\x11\xabV\xafg\xec\xf8\xc9L\x9e\x34.\xc6\x84\xda\x7f_\xb8H\x89\x12\xeez\x9b\x64[g\x89\x9d\x93'OQ\xad\x8a'y\xf2\xe4\xc1\xd1\xd1\x91\x88\b%\x7f\x9d?O\xedZ\xb5\0\xd8\xb3g\xfk\xd6n`\xc4\xf7\x43\xb1\xb6\xb6\x66\xc0\xc0!1>\xafm}|\xd4j5o\xde\xbc\x61\xdf\xbe\x3t\xed\xd2\x1e\xf3\x38\xfeP\a\xf\x1d\x62\xd9\xb2\x95\x8c\x1c\x31\f\x1b\x1b\x1b\xb6o\xdfI\xb7o\xba\xb3k\xe7vJ\x95*E\x9f^\xddX\xb6r=\xa7~9\x12\xef\x1c\x10\xfa<(R\xd3\x8d\x1b\x37\xb8v\xe3\xe\x15+V\xc4\xd1\xd1\x91\xba\xb5\xabr\xe2\xc4/\x9a\x87\xe4\x9b\x37o\xd2\xa6}\x17\xa6M\x1eO\x8f\x1e\xdd\x39\x7f\xde\x1f\x80\xe8\xe\xc1\xda\xd6'\x86\x42\xa1\xc0\xbd\x84;w\xee\x44\xdd\f\xdc\xb9s\x87n\xdf\xf6\x62\xd6\x8c\xa9t\xef\xfe-{\xf6\xec\x65\xf0\x90\xa1lX\xbf\x8e\xce\x1d\xdb\xf2\xe3\x8fG5\x89\x9d\x33g\xce\xe0^\xac\b\xc5\x8a\x15\xe3\xfe?\xff\xc4\xd8\xef\x9c\x39sy\xf2\xe4\t\xc3\x87\r!2R\xc5\xa3G\xf\xb1\xb4\xb4Lp\xff_\xf6\xbax\xf2\xe4\t\x80\xd6\x87\xa7\xa9S\xa7\xf1\xee\xdd{F\x8d\xfc\x9e\x90\x90\x10\x16,\\\xcc\xb3g\xcf\x98\x38q\x2\0\x9f?\x87\x33s\xf6\"\xb6nZC\xeb\xd6\xadX\xbat\x19s\xe7\xce\x63\xca\x94\xc9\xcc\x9d\x33\x8b*\xd5\xeb\xe1;\xa0\x37-Z4'{\x8e\x1c\0\x4\x87|f\x80\xef\b\xe6\xcf\x99J\xd3\xa6\x8d\xc9\x99\xd3%\xc1\xe3\xd6\xda\xda:\x81\x1a\x46\x91\x63\x96\x44\xadO*\x1foov\xec\xdc\x83\xad\x8d\rK\x16/\xc0\xc1\x41\xfbM\xd3\xfc\x5\v\tx\xfe\x9c\x19\xd3\xa7\xa0V\xaby\xfa\xf4Y\xbc\xdd\x93\xd3K\xbb\xa5\xe5k\x82\x36\xbbw\xef\x66\xed\xba\x8d\x31\xca\xe\xe\xf9\x1c\xef\xf6\xef?\x4q\xfa\xb7\xdf\x19\x38\xa0?\x1\x1/\x18\x39z\x8a~\xf5\x15;\xb7\xad\xc5\xbb\xdd\xb7\x8c\x1c\xde\x9fzu\xeb\x90;w.\x9e={\x9e\xa8\xdfs\xfe\xa2\xd5\f\xea\xdf\x83Q#\x86r\xfb\xce]z\xf7\x1b\x8a\xc2\x44\x41\xb3\xaf\x9b&\xf8\xb9\xc1\x43G\xa0P(\x98\x30n$j\xb5\x9a;w\xef\xe1\xe0\xe0\x80\x42\x61\xc2\xc5\x8b\x97\xa9W\xaf\xe\xdf~\xdb\x95[7o\xd1\xad\xc7\0\xcd\x30\xb1.\x9d|\xd8\x7f\xe0\x90&\xb1\x13\x1a\x1a\xca\x16\xbf\xdd\xac\\6?\xcer\xc2#\x94\xf4\x1f\x34\x92\x35+\xe7\xd3\xa8\x61}\xd6\xad\xfb\x81~\xfd}\xd9\xb1}\vff\xa6\\\xb8p\x91\xa6-\xda\x31~\xccP:vh\xcb\xb9\xbf\xce\xb3t\xc5\x6\xbav\xf2N\xd4\xef\x9f\x9a\x1c\x33;\xd0\xbfoO\xbc\xdbv%00fr\xc7\xc1\xde\x9e\x9d\xdb\x37\xa4JR',,\x8c}\a\xe\x3\xd0\xa2Y\x13\xbd\xcd\xa5\xf7%\xdf\x81}Q(\x14\x1c\xfc\xb6n\xd4\xdb\xa4\xc5_\x1a\x34\xa0\xf=\xbbwK0i\xe2\xe4\xe4\xc8\xdbw\t\xf7^K\xaa\x87\x8f\x1es\xe8\xf0\x11\xae^\xbd\xce\xf3\xe7\x1\xe4\xc9\x9d\x9b%\x8b\xe7\xf2\xee\xdd;\x9a\x36\xf3\xc6\xc4T\xc1\xcb\x97\xaf)]\xba${\xf6\xec\xe7\xe7\x63\xbf\xa0\x8c\x8c\xc4o\xeb\x46\n\x15\x34\xcc\x84\xbaiM&++BBBQ\xab\xd5\x9a\x63\x45\xadV\x13\x1c\x1c\x8a\x95\x95\xf6/\xae\x93\xc3\xc4\xc4\x84\xa9\x93\xc7\xa3P\0\n\x5S&\x8e\xd3\xc9}\x95\x99\x99)y\xf3\xe4\xe6\xea\xb5\x1b\x14\xfb\x62\x82\xfc\xbcyrs`\xef\xe\xe\x1e\xfa\x91\x8b\x97\xaf\xf0\x31\xf0#\xa8\xd5\xe4\xc9\x9dK\xa7s\xc3\xc6\xaa\x8f.vr\xe3\xc6M\0\xf6\xec\xff\x91=\xfb\x7f\xd4,\xf7\xbfp\x95w\xff\xf\xa8\xd7o\xde\x93\xe3\xff\xbd\x13\x80\x18\xdf\xa2~\xf8\xf0!\xc1\xf5\xf1\x39\xb4\x7f\a\xe6\xe6\xe6\xb8\xb8\xb8\xc4\xfb\xad\xf9\x87\xf\x1f\bx\xf9&\xc6\xbe\xad\xad\xad)V\xb4\b/^\xbeL\xe4o\x18\x45_\aEj\t\t\t\xe1\xc0\xe1\xa8\xa1 \x8d\x9a\xb6\x8a\xb1.zb\xd3\x9b\x37o\xe1\xe3\xd3:\xc6:W\x97\x7f'\x1e\xd3\xb6>1\xd4j5\xd7\xaf\xdf\xc0\xab\x65\v\0\xae_\xbf\xc1\x39\xff\xcbl\xf1\xdb\x13\x63\xbb\xf0\xf0p\x8a\x17/N\xb9\x32\xee\xfc\xf9\xe7\x19<==8v\xfc$\x93'\xc7\x1e\x7f\x19\xdd\xdb\xe6\xbf\xc3\xab\xb4\xed\xffK\xb9sGu/\xbe{\xf7\x1e\x39s\xc6=\x1Z\xf4\xcd\xcd\x97\xe5\xb8\xb9\xb9\x1\xf0\xe6\xcd\x1b\\\\\\\xb0\xb1\xce\x14\xe3\xf8\xcd\x92\xc5\x89\xe0\x90\xcf\xa8T\xaax\x8f\x17\x1b\xebL\x94\xf8\xe2\x81OW\xc7\xad\x1c\xb3I;f3e\xb2\xe4\xd3\xa7O\xb1\x96+\x95\x11\x64uvL\xf6\xcd\xca\xd8\xb1\x63\xd8\xbbw/m\xdau\xc4%Gv\xda\xb7kK\xe3\xc6\x8d\xe2=\xb9\x1b{\xbb\xa5\xe5k\x82\x36\xc9-\xfb\xcb\x43#{\x8e\xec\xdc\xbb{\x1f\0\xafV\xad\xb0\xb0\xb0`\xf4\xe8\xb1\xbcy\xfb\x8eN\x9d\xda\xd3\xca\xcb+\xde\a\x5\x63o{!D\xdc^\xbdz\x8d\xa7GTR_\xadVs\xf2\xe4)\x8e\x9f\xf8\x95\xabW\xafs\xf6|\xd4\x37\xb8\x61\x61\xf1\xf7\f\x8c\x66kk\xcb\x37]\xdbs\xe8\xf0\x11\x1a\x37j\xc8o\xa7OS\xad\x8a\a\x85\xff?\xbc\xd3\xcc\xcc\x8c\x1a\x35\xaaS\xa3\x46u\x1e|@\xa1P\xf0\xfcy@\x8c\xf5y\xf3\xe6\xc1\xca\xca\x8a\xb1#\a1x\xd8\x38\xfe\xfa\xcb\x9fz\xf5\xeaP\xb3\x46uM\xcf\xe4\x17/^r\xf8\xf0\x8f\x9c\xa5;\t%uBCC\xb9t\xf9*-\x9b'\xfc\xa5Lb\xfd}\xe1\"c\xc7M\xe6\xfa\x8d\x9b\xd8\xd9\xdbQ\xaa\x84;E\x8a\x14\xa4^\xdd\xa8/Zmmm)^\xbc\x18\xc7~>\xce\xfe\xbd;(]\xaa\x4O\x9f>\xa3N\xfd\xa6\x14+\xfa\x15\xd9\xb3%\x9c\xb0\xd5\xa7\xb3\xe7\xce\xf3\xf0\xe1\xa3\x18\xcb<<\xca\xc5\x9aO\xe7\xde\xfd\a\xf8\xfb\xff\x1d\x63Y\xde\xbcy\x12\x35?OR\xd4\xa9Y\x83\x39s\x17\x30w\xde\"|\a\xf5\x43\xad\x86\xf9\v\x16\xf3\xf6\xdd[j\xd7\xac\xae\xd3\xb2\xbe\x64\x62\x62\xc2\x94I\xe3\x35\xff\xd7\x95\xd6\xad[\xb2\x66\xcd\xbaX=\xd6,,,h\xe5\xd5\x82V^-\b\x8f\x88\xa0I\xd3\x96\xb4n\x9d\xf2\xb7\xc3%D'\x89\x9d\x63\xc7\x8e\xd1\xe2\xeb\x86,\\\xf8o\xd7\xc7\xa0\xa0 <*T\xe5\xcc\x99\xb3\x34l\xd8\0;[k\x1e=z\xa4\x99S\xe5\xcby\xf\x1c\x1c\x1c\x12\\\x1f\x97\xfau\xabk&\xcaL\x88\x83\x83\x3\x36\xd6\x99\x62\xec;((\x88\x33g\xfd\xe9\xd9\xe3[\xcdv\xea\x44Nf\xab\xaf\x83\"5\x9c;w\xe\a{[\xce\xfey*\xc6\x45\xafG\xcf^\x9a\x89M\xf3\xe7\xcf\xa7\x19\"\x15\xed\xcb\xf9H\xb4\xadO\x8c\x33g\xce\xf0\xcf\x83\x87\xd4\xae\x1d\x35$\xa3`\xc1\x2\x14.\\\x88)S&\xc7\xb9}\xc7\xe\xed\xd8\xb7\x7f?!\xa1!tl\xef\x1d\xe7\x30\x86\xe8\x87\xb8\xd7\xaf_\xc7\x1aJ\xa5m\xff\xd1\xb2\x64\xc9\x42\xe7\xe\xde\xcc\x9e\x33\x8f\xd2\xa5K\xc5\xf9`\x18\xfdM\xda\xb3g\xcf\xc8\x95+\x17\0w\xef\x46\xfd=\\\x92p!Sk\x19\a\x94\xd8\xe3\x36\x31\xe4\x98M\xfc\x31[\xa0`A\x16/]\xc5\xc7\x8f\x1f\x63\xb4\xff\xad\xdb\xb7\xa9Y\xbdj\x8c\xc4\x8eZ[#~!{\xf6l\xf4\xee\xdd\x8b\x9e={p\xe3\xc6M|\xdav\xc4\xc6\xc6:\xc1\x9eW\xc6\xdcni\xf9\x9a\0`aa\xce\xd3g\xcf\xe2\\\x97\x9c\xb2\x13\x62\x95)\x13\xde\xde\xdex{{\xf3\xfcy\0\xc3\x86\r\xe7\xed\xdb\xb7\f\x1d\x12\xff\xd0\x32\x63n{!DlJ\xa5\x92?\xcf\x9c\xa3\xb5Ws\0\x36l\xdc\xc4\xe6\xcd~\x8c\x1f?\x8a\xefz\xf7\xe0\xfa\xf5\x9bt\xea\xd6G\xeb\xbd\x41\xb4&M\x1aS\xa7~\v&\x8e\x1f\xcb\x8e\x9d{\xe8\xd8\xbem\x9c_<\xe4\xcb\x97\x97\xde\xbd{bjf\xc6\xe8q\xd3\x19\x39\x62\x38\x8e\x8e\x99)^\xb4 \x9b\x36\xc6\x9c\xbf'S&\xed\xc3\x45\xd4Z\x6\x31\xff\xf7\v3\x13\xd3\xa8\xf3\x97\xb9\xb9\x5\xfe\xfe\xfex\xb7\x8by\xff\xf2\xe7oG)X\xb0\0\xfd\xfb\xf7\xa5U+/~=y\x8au\xeb\x7f`\xd4\xe8Il\xdb\xba\x8e\xec\xd9\xb2\xd1\xbc\x65\x1b\xda\xb6\xf1\x62\xd4\xa8\xe1\x38\x39:R\xda\xa3:\xb7n\xdd\xfe\xff~\xcd\xe9\xd2\xb9\x1d\a\xe\x1c\xa6I\xe3\x46l\xdd\xbe\x87\xed\xdb\x36h\xfd=\xa2}\xf9'S\xa9T\x84G(Q\xa5\xe0|o(\xa5J\xba\xb3kGTO\x99\x92%\x8a\xa7Z\xb9Y\x9c\x9c\xc8\xec\xe0\xc0\x87\x8fQo\\\xcb\x9c\x39\x33Y\xe2\x19j\x9c\x9a\xc2\xc2\xc2\x18\x35\x66\"\x81\x81\x81\x31\xe6_J\xaeg\xcf\x9e\xd1\xb6]\x17J\x95*\xc9\xaa\x15\x8b\xa8U\xb3\x46\xac\xb9\x38-,,\xf0n\xdd\x9cS\xbf\x9d\xd6$\xd6\xdc\xdc\\q\xcd\x99\x83\xa6M\x1b\x19\xb4\xd7\xd7\xce]{c\xbd\xee|\xe6\xf4\xc9\xb1\x12;\xfe\xfe\x7f\xf3\xfd\xc8\xb1\x31\x96\xb5n\xd5R\xe7\x89\x9d\x42\x85\v2q\xfcX&L\x9a\xca\x9au\x1bQ\xab\xd5\xa8T*&M\x18K\xa1\xc2\x5uZ\xd6\x7f\xe9\xe3\x9e\xaaW\x8fo9z\xf4g\xc6\x8d\x9f\xcc\xa4\x89\x63\x63\rGT*#\x19>|4\x96\x96\x99\xe8\xd9\xfd\x1b\x9d\x97\xff\xa5\x14'vBCC\xf9\x61\xf3\x36\x66\xcf\x9c\x1a\x63\xb9\x9d\x9d\x1d\xde^\xcd\x38t\xe8\x10M\x9a\x34\x66\x88o?\xa6M\x9f\x85\xa9\xa9)\xce\xce\xce\x8c\x1b\xffo\xaf\vSS\xd3\x4\xd7'U\x16\xa7,\x4\xbcx\xcd\xfd\xfb\xf7\xc9\x9d;7\xc3\x87\xe\x62\xca\xd4\x99\x98\x9a\x9a\x92={v\xe\x1d:L\xb9\xb2%)_>*\xe3\xef\xec\xec\xcc\xc3\xc7\xcf\x39w\xee/\x9c\x9c\x9c(T(\xe1\x83\xcaXo\xb4\xf\x1c\x38\x44\x87v\xde\xb1\xbe\xc9h\xda\xa4\x31#GO`\xf0`_\xbc\xbd[\xe3\xdd\xb6\x33y\xf3\xe6\xa1\x44\x89\x12\\\xbdv-\xc6\xb6\xda\xd6\xc7\xe5\xe6\xcd\x9bXYY\xf1\xf1\xe3G\xae\\\xb9\xca\xb2\x15kX\xbax>\xce\xceQI\x92\xf6\xed\xdb\xd1\xa4\x99\x37_}\xf5\x15\x95*U$88\x98\x37o\xdeh\x1ezk\xd5\xaa\x85\xef\xd0\xd1\x1c\xff\xe5w\xf6\xec\xdc\x12g\x19\x85\v\x17\xa6r\xc5r\xcc\x98\x31\x8b\xde\xbd{\xa2R\xa9\xb8w\xff>-[\xb4\xd0\xba\xff/\xf9\xfa\xe\xa2{\x8f^4k\xee\x45\x9f>\xbd\xc8\x9b'/\x9f>\x5q\xf7\xee=\xea\xd5\xabK\xc1\x82\x5i\xd2\xb0\x36\xe3\xc6M\xe0\xbb\xefzabb\xc2\xb2\x65+\x18\xd4\xbf\x97\xe6\xf7\xd1\xa6x\xb1\"\x9c>}\x9a\x2\x5\xf2\xe3\xe8\xe8\x18g\xa2\xca\xd4\xd4T\xebq\xeb^\xac\b\xf7\xff\xf9\awww\xb2iy\x9d\xa3\x1c\xb3\x89;f\xabV\xa9\x42\x89\xe2_1h\xd0`:w\xee\x88\x9d\x9d\x1d\x37n\xdc`\xe9\xb2\xd5\xec\xdd\xbd]\xb3\x9d\x8bK\xe\xe\x1e\xfa\x91\xdb\xb7occc\xa3\xe9\xb5\x15\xed\xbf\xe7\xa0\x3\a\xe\x92-[V\xb2g\xcf\xce\xc7\x8f\x1f\x89\x88Pbc\xa3\xfd\x15\xa0\xc6\xd8n\xc6pMpwwg\xcc\xf8il\xdb\xe6G\xd1\xa2_\xb1o\xdf\xfe\x98\x65\xf\xee\xaf\xb3\xb2\xff\xfc\xf3O\x2\x3\x3\xc9\x9d;7\x91\x91\x91\x84\x85\x87\xe3\x90\x88\x1b\xadi\xdc\xa4%?\x1f;A\x99\x32\xa5y\xf0\xe8\x19]:w\xc2\xc9\xc9\x11\x80\xc8H\x15\xc7N\xfc\xae\xf9\\\xbdzu\x98\x30y\xe{\xf6\xec\xc3%G6J\x96H^o\x15\v\v\v\xaaT*\xc7\xef\xbf\xffI\xbd/^\xbb\xbcg\xffQ\x6\xf4M\xda\x17j\x86\x90Z\xbdt\xbe\x64ii\xc9\xba\xb5\xcb\x99\x33w!\n\x85\x82\xa1\x43\x6\xa6\xca\x1c;\xbf\xffq\x86+Wc\xdf\xcb\xa9\xd5j^\xbdz\xcd\xc9\x93\xbf\xf1\xec\xf9s\xa6M\x9d\x44\xe1\x42\x5R\\\xde\xe9\xdf\xcf\x10\xa1T\xb2q\xfd\xaa\x18\xafs\xff\xd2\xbbw\xef\x98\x31s.aaa\xfc\xb0i+m\xdb\xb4\xe6\xe4\xa9\xdf\xf8\xe7\xc1#\x96\xafXC\xedZ5\xc8\xa7\xc7W\x87'd\xd6\x8c\xc9\xcc\x98\x36\x89Y\xb3\xe7\xb1j\xcd\xfa\x44}\xa6g\xf7n\f\x1f\x36\x18\x13\x13\xfd\f\xab\xeb\xd0\xde\x87\x1a\xd5\xabp\xf6\xdc_(\x14\n*V\xf0\x34\xda\x17\x44\x98\x9b\x9b\xb1t\xf1|\xfa\xf6\xf7\xa5i\xb3Vt\xef\xfe\r\xee\xc5\x8b\x62\x66\x62\xca\xb5\x1b\x37X\xbe\x62\x35\x99\x32Y\xb1l\xc9\x2\x9d\xbf\xa1\xed\xbfR\xbc\xf7s\xe7\xce\x11\xfa\x39\x8c\n\x15*\xc4ZW\xaf^]:w\xebM@@\0\x9d:u\xc2\xdc\xdc\x82\x45\x8b\x97\x62gkK+\xaf\x96\xbcx\xf9\xefk\b\xb5\xadO\n\xf\x8fr\xd4\xaaQ\x95\xbe\xfd\x6\xb2|\xd9\xe2\xa8\x37\x16Y[\xb1\x62\xe5j>~\f\xa4\x41\x83\xba\xacX\xbeTs\xe1\xf1\xf4\xf4`\xd4\xf7\xbe\f\x1e:\x1c\xf7\xe2\x45Y\xb6t\x89^\xc7\xbf\x19\xc2\xcb\x97/\xa3\x86\x45\xc4\x91\x18\xa9X\xb1\x12\xc1!\x9f\x35\x13\x9bn\\\xb7\x82\xd5k\xd6\xf1\xc3\xa6\xad\x34i\xdc\x90zu\xaai\xb6-W\xae\\\x82\xeb\xe3\xd2\xca\xa7\x13\xa6\xa6&T\xf0,C\xd9\xb2\xa5\x39rx_\x8c\xe0uww\xe7\xd0\xfe\x1d\xac[\xb7\x81%KW\x90-\x9b\x33\r\x1b\xd6\xa7V\xadZ(\x14\n\x1c\x1d\x1d\xa9\xe0Q\x8as\xfe\x97)]\xbaT\x9c\x65\x98\xac\x65]P\0\0 \0IDAT\x9b\x9b\xb3x\xd1\x2V\xac\\I\x9f\xbe\x3\xb0\xb5\xb5\xa1n\x9d\xda\x84\x85\x85i\xdd\xff\x97\x9c\x9c\x9c\xd8\xba\x65\x13\xbbw\xef\xe6\xd4\xc9S\xcc\xf6_\x80k\xce\x1cT\xadZ\x19SSSLLL\x98\x31\x63:k\xd7\xae\x63\xf4\x98\xf1\x38\x66v\xa0\x45\xcb\xe6x\xb7n\x1dg\xbd\xe2\x32n\xecX\xa6M\x9bN\xaf\xde}\x19;f\x94\x66L\xf9\x7fi;n\a\f\xe8\xc7\xe8\x31\xe3\xf9\x14\xf4\t_\xdf\x41\x89.\xdfX\x18\xe2\x98\xb5\xb0\xb0`\xf9\xf2\xa5l\xda\xb4\x99\x85\x8b\x96\x12\x18\x18H\x95\xca\x15\x39\xb8\x7f\x17\xf9\xf2\xfd\xfb\xcd\x86\x8f\x8f\xf\xf\x1e<\xa4\xdb\x37=\xe9\xd2\xb9\x3\xbdz\xc5|5\xed\x7f\xcf\x41\x61\x61\x9fY\xb9j57n\xde!_\xde\xdc\xcc\x9e\x39\x85\xf2\xe5u\xfb\rHZa\f\xd7\x84\x32\x65\xca\xb0p\xde\fV\xafY\x8b\xa5\xa5%\r\xea\xd7#\xab\xb3\xe3\xbf\x65w\xec\x88\xb9\x99\xb9\xa6l\xf\x8fr\\\xbez+Y\xd7\x5\xb5\x1a\xe\x1f>\xc2\xdf\x17.accM\xf3\x66M\xe9\xd8\xb1\x63\xb2~\xf!D\xda\xf7\xe2\xc5K\xce\xfdu\x1e\xb5J\xc5\xabW\xaf\x39\xfd\xfb\x1f\xfc|\xfc\x14\xabV,$s\xe6\xa8\x44J\t\xf7\x62\xf8m\xdfM\x81\x2\xf9\xb1\xb6\xb2\xc2\xa7}\xf7\x4\xf7\x99\x35k\x16\xae]\xbf\xc1\x8d\x1b\x37)V\xac(\n\x85\x82\xce\x9d\xda\xd3\x7f\xd0Hz|\xd3\x1eG\xc7\xa8\xf3\xd7\xf3\xe7\x1\xb4m\xdf\x5\xaf\x96_S\xa0@~l\xacmx\xf0\xf0!3g/d\xec\xe8\x61\x98\x9b\x9b\xd3\xa2\x45\x33V\xad\xd9\xc0w}\x6\xd0\xaf_o\x1c\x1c\x1c\xb8q\xfd\x6\x9e\x9e\x1e\xe4\xcf\x9f\xf{{{\n\x15\xc8\xcd\xde}\a\xb0\xb3\xb7'\xabs\x16\\]sR\xd1\xb3\x14\x8b\x16/#$8\x98\xf0\xf0\bF\x8d\x9b\x1e\xa3\x8e\x96\x96Q\xf7&#G\x8d\xa5I\xe3\x46\xbcx\xf9\x92\xe9\x33\x17\xe0\xb7\x65m\x82\xbf\x9bR\xa9\x64\xec\xb8\x89T\xa9\\\t\x17\x97\x1c\xbcz\xf5\x9a\x1b\xb7\xee\xe1;\xa8 \xae\xaeQ\xc3\xe2\x97.[A\xed\xda\x35\x35\xbdo\xbf\x1c\x36V\xa8`A\xaaU\xf1\xc0w\xd8\x38\xa6O\x1e\x95\xa2\xfb\xf7\x1\xfd\xfb\xd0\xbaM7\x1c\x32;P\xbe\xbc'\xd7\xafG\xcd\x11\xa7\xfc\"\xc9%b*W\xb6\f\xdb\xb6lH\xd5\x32\x7f;\xfd;\x9b\x36o\x8bs\x9dK\x8e\xec\x94,Q\x82\xd5+\x96P\xb8H\xec\xb7O&G\xe0\xff\xe7/\x1a=fB\xacu\x1e\xe5\xca\xd2\xb1\x63[lmm)Y\xa2\x4%K\x96\x64\xfc\xc4\xa9\x8c\x9b\x30\x5\x80\x6\xf5\xeb\x61\x66\x66j\xd0\xb7\x93\x99\x98\x98\x30g\xee|V\xadYO\xcf\x1e\xdf\xb0j\xf5\xba\x4\xb7\xef\xd9\xbd\x1b\xab\xd6\xac\xc7\xdc\xdc\x9c\x61\x43\xf5\xf7|\xe1\xe6\xe6Jk7\xfd\xeMJ-y\xf2\xe4\x62\xefn?V\xadY\xc7\xf2\xe5\xabx\xf8\xe8\x31\x10\x35\xd7\x8e\xb7\xb7\x17=\xbb\x7f\xa3\xf7\xa4\xe\x80\"$4\"\xb9/\x87\x11:\xf6\xe4\xf9k\0r\xe5\x8c=O\x8c\x10i\x91\x1c\xb3\xc6I\xda-i.^\xbcH\xdb\xe]\xb9v\xd9?Yo\xd9JK\xa4\xed\x85\x88[bb\xe3\xc9\xf3\xd7\x14\xce\x1f\xff\xb0k\xdf\xc1\xc3\xd8\xe2\xb7\xf\x88\x9a;\xafz\xb5\x8a\x94+S\x9a\xf6\xed\xdb\xe2\xec\x9c\x45\xb3\xdd\xdb\xb7o\x99\x33w\x1G\x8e\x9e\xa0h\xd1\x42\xf8x{1\xec\xfbq\x9c|D\xf9\xcau9\xb8w\x8b\x66\xb2\xe0\xe0\xe0`N\xfdv\x9a\x63\xc7\x8es\xf9\xcau>~\f\xc4\xa3\\i\xda\xb5\xf5\xa6\x66\xcd\x1a\x9a/\xb3\xde\xbe}\xcb\x8a\x95k\xf8\xe5\x97S\x84\x84\x86R\xa5r\x5\xbe\xeb\xdd\x83\x2\x5\xa2z7\\\xbf~\x83I\x93\xa7\xf3\xecy\0#\x86\xf\xa6i\xd3\xc6\xde\xadP(\x14\x1c\xfd\xe9\x18\v\x16.\xe5\xe3\xc7@\x9a}\xdd\x88\xc8\xc8\xa8\xa9\x13\xbe|\xfd\xb8\x9f\xdf\xe\x6\f\x1e\xcd\xb9?~&_\xbe\xbc\x9a\xe5\xff}\xddy\x89\x32U\xf9\xf1\x80\x1f\x1e\x1e\xe5\0x\xf0\xe0\x1\x15\xaa\xd4\xe7\xd4\x89\x83\x14\xfd\xff\xc4\xa7\xbf\xfez\x92\x35k7\xf0<\xe0%M\x1a\xd5\xe7\xf8/'\xa9]\xab:\xc3\x87%\xee\xcd\x8c\xbar\xe7\x9f\x80\x4\x8f\x35]\x97\xa5\xed\x9a\x30\x64\xd8(\xe\x1d>B\xa1\x82I\xeb\x1s\xf7\xde}\x9a\x36i\xc4\xdc\xd9\xd3\xb4o\x9c\x46\xed?x\x98\x1f~\xd8\x1a\xe7:\x1b[\x1bV.[\x88\x95\xd5\xbf\x43\x19\x7f\xfb\xedw:u\xed\xc1\xd4I\xe3\xe9\xd8\x31\xe5sW\xa5\xe4\xba\xadV\xab\x99=g\x1K\x97\xaf\xa2_\x9f^\f\x1a\xd8\x8f\x82\x45J0s\xfa\x64\xda\xb6\x89\xf9\xa5\xb4\xdf\xf6]|?r,\xb7o\\d\xc9\xd2\x95,^\xba\x82~}z1t\xc8\xc0\x64\xcf\x31\xb9j\xf5:\xa6N\x9fM\t\xf7\xe4\r\x11\xbcz\xed:\xdf\xf\x1fL\x9f\xde=\x12\xdc\xee\xc9\xf3\xd7i\xea\xbe\x46\xa9\x8c\x1a\xd2if\x96\xba\x1d\x45$\xb1\x93\x86\xc8\r\xb7\x30\x36r\xcc\x1a'i\xb7\x84\xbd|\xf9\x8a\x13\xbf\x9c\xa0H\xe1\"\x98\x98(X\xb2t\x19\xd5\xaaV\xa5k\xd7.\x86\xaeZ\x8aI\xdb\v\x11\x37]$vR\xd3\xde\xbd\xfb\x99:}\xeg\xfe\xf8%M$\x9c\xff\x9b\x8cJ\xfT*\x15u\xea\x35\xa1\x7f\xdf^xy\xb5H\xd5\xb2\xd3Zb\xc7o\xfb.\xce\xfb_H\xd6\xfe==\xca\xc6J\"\xa4gA\x81\x41L\x98<\x9d\xfe\xfdz\x93\x37O\xee\x14\xef/\xa5\x89\x9d\xb9\xf3\x16\x1\x30\x64\xf0\0T*\x15\xc3G\x8c\xc5\xbbu\xec\xb9s\xce\x9e;\xcf\xce]{\x99\x31m\x12\x66\x66\xa6\x31>\x97\xdc\xc4\xce\xc9\x93\xbfq\xf0\xf0\xd1\x64}6Z\x93\x46\r\xa8]\xbb\x46\x82\xdb\xa4\xb5\xc4\x8e\xa1\xe8\xbfO\x90\x10\x42\baD\xc2\xc3\xc3\xb9z\xe5*K\x97\xad\xc4\xd5%\a\xde\xde\xad\xf0\xf2\xf2\x32t\xb5\x84\x10\x2\x88zX\xfb\x61\xd3V\xbatn\x97&\x92:\xe9\xc5\xa7O\x9fX\xbf\xe1\a\x8a\x14.L\xe6\xcc\xe\x9c<\xf5\x1b*\x95\x8az\xf5\xea\x18\xbaj\x6\xd7\xb6M\xeb\f\x95\x9cI\t;{\xbb\x34\xd3\x43I\xa1P0d\xf0\0\xcd\xffMMM\xe3\xad[\xc5\n\x9e\x31\x92=_~.\xb9j\xd6\xacNM=\xbe\xe9J\xc4$\x89\x1d!\x84\x10\xe2\v\xb9r\xb9\x31}z\xda\xb8)\x13\x42\x88\xff\xbaq\xf3&\x7f\x9c\xbd\xc0\xf4i\x93\f]\x95t%<<\x82\xa0\xa0OL\x9f\x39\x97\xc8\xc8H\xea\xd5\xad\xc5\xce\x1d\x9b\xb1\xb3\xb3\x33t\xd5\x84H\xb6\xe4&fR\x92\xd0\x11\x86!\x89\x1d!\x84\x10\x42\b!\x8c\x44\xf1\x62\xc5\x12|\xc3\x94!\f\x1d\xeak\xe8*\xa4\x98\x93\x93#\xa3\x46\xeg\xd4\xc8\xe1\x86\xae\x8a\x10\x42$\x99\xbcOU\b!\x84\x10\x42\b!\x84\x10\xc2HIbG\b!\x84\x10\x42\b-,,\xcc\t\v\x93W_\x8b\xf4\xe3sX\x4\x96\x16\x32O\x93\x10\xe9\x81$v\x84\x10\x42\b!\x84\xd0\xc2\xca\xd2\x82\x8f\x41!\x86\xae\x86\xc8\0R\xeb\x8dXaa\x11Xf\x92\xc4\x8e\x10\xe9\x81$v\x84\x10\x42\b!\x84\xd0\xc2\xde\xce\x9a\x8f\x81\xa1\xbc}\x1f\x88\x32\x32\xd2\xd0\xd5\x11\"\xd9>\x87\x45\xf0\x31(\x84\x97o>\xe2`kc\xe8\xea\b\x91\"\xf2\xaa\xf3(2y\xb2\x10\x42\b!\x84\x10\x89\xe0\x96\xd3\x99\xe0\xd0\xcf\x4\x11\xa9R\x11\x11\xae$R\xa5\x32T\x9d\x84HS\xcc\xcd\xcd\xb0\x30\x37\xc3\xca\xd2\x12++\x8b\x64\xed\x43\x62K\b!\x84\xae\xc8uI\x88\x8cK\xe2_\x88\x8c-\xbes\x80\"$4B\x1d\x16\x1e\xc1\xab\x37\x1f\xc8\xe2h\x8f\x89\x42\x81\xb9\x85\x19\xa6&\xd2\x99G\b\x80\x88\b%\xe1\x11J\xc2\"\"P*#\xc9\x92\xd9\xeSS\xd3\x44}VbK\b!\x84\xae\xc9uI\x88\x8cK\xe2_\x88\x8c-\xbes\x80\"$4B\xfd\xe4\xf9kr\xe5\xccj\xe8:\n\x91\xe6\x85\x84~\xe6\xed\xfb\xa0\x44\xc7\x8b\xc4\x96\x10\x42\b}\x92\xeb\x92\x10\x19\x97\xc4\xbf\x10\x19\xdb\x97\xe7\0\xc5\xd3\x80w\xeaL\x96\x16X[Y\x1a\xba^B\x18\x85\x90\xd0\x30\xc2\xc2\xc3qt\xb0Kp\xbbw\x1f\x82\x90\xd8\x12\x42\b\xa1or]\x12\"\xe3\x92\xf8\x17\"c\x8b>\a\x98\x44\xaaT\x98(\x14\x86\xae\x8f\x10\x46\xc3\x44\x1\xcaH\xb5\xd6\xed$\xb6\x84\x10\x42\xa4\x6\xb9.\t\x91qI\xfc\v\x91\xb1\x45\x9f\x3L\"\xc2\x95\x98[\x98i\xff\x84\x10\x2\0s\vs\"\xc2#\xb4n'\xb1%\x84\x10\"5\xc8uI\x88\x8cK\xe2_\x88\x8c-\xfa\x1c`\x12\xa9R\xc9\xa4YB$\x81\xa9\x89I\xa2\xde \xb1%\x84\x10\"5\xc8uI\x88\x8cK\xe2_\x88\x8c-\xfa\x1c \xd1-\x84\x10\x42\b!\x84\x10\x42\ba\xa4R\xdc\x1fo\xd4\xd8\t\xdc\xbeu7I\x9f)\xf2U!\xa6M\x9e\x90\xd2\xa2\x85H\xd7$\xb6\x84\x10\x42\b!\x84\x10\x42h\x93\xe2\xc4\xce\xed[w)X \x1f\x1e\x1e\x1e\x89\xda\xde\xdf\xdf?\xc9\xf\xab\"yf\xcd\x9e\xcf\xb9\xbf\xfc\x1\xa8P\xde\x83\xe1\xc3|\r\\\xa3\x7f\x8d\x1a\x33\x9e\xb3g\xffJ\xd4\xb6n\xb9r\xf1\xc3\xfaUz\xaeQ\xda#\xb1%\x84Ho\xe4\xba$D\xc6%\xf1/D\xc6&\xe7\0\xfd\xd2\xc9\fZ\x1e\x1e\x1ex\xb7n\x91\xe8\xed\xef\xdd\x7f\x90\xa8\xedn\xdd\xba\x45\x83\xc6-5?\x17\xc8\x97\x8b\xea\xd5\xaa\xf0\xcd\x37\xdd\xc8\x9d;w\x92\xeb\x99\xd1\x1c\xff\xf5$\xb7o\xdd\x1 (\xf8S\x9a\n\x9e\x80\x17/\xb9\xff\xcf\x43\x43W#\xcd\xd3Wl\x1\xa8T*\xf2\x15,\xce\x82y\xd3i\xd9\"\xf1\x65\xfc\xfd\xf7\x5\x96,YJ\xc0\x8b\x97t\xed\xd2\x91\xb6m\xdb&\xfa\xb3\x42\x88\x8cM\xaeK\x19\xcb\xcbW\xaf\x30\x33\x35%K\x96,\x89\xda~\xf8\xf0\x11l\xdf\xb5\x1f\x80L\x99,\xa8\\\xc1\x83\x66\xcd\xbf\xa6i\x93&\x98\x9b\x9b\xf3\xf2\xe5+\xcaW\xaa\xc1\xde][([\xb6l\xa2\xf6\xa9R\xa9\b\n\n\xc2\xca\xca\n\v\v\x8b\x44\xd7=00\x90\xcd[\xb6\xb0l\xf9\x1aN\xfc|\x84\xec\xd9\xb3%\xfa\xb3\"n\x12\xff\x19KR\xe2?\xfa\x9e\x34ZVgG*W,O\xc7\x8e\xed)_\xbe<\0\xa7N\x9d\xa2s\xb7\xde\xdc\xb8\xea\x8f\x8d\x8dM\xa2\xea\x10\x1e\x1eNhh(vvv\x98$r~\xa1\xdb\xb7o\xb3}\xc7N\xce\x9d\xfb\v\x85\xc2\x84\xc6\x8d\xea\xd3\xa9S'\xec\xec\x12~\x9d\xbc\xd0N\xce\x1\xfa\xa5\xd3\xa9\xd1\xf3\x17rOp\xfd?w\xaf%k\xbfsgM\xc6\xcd\xcd\x8d\xf\x1f>\xb0}\xfbN\x1a\x37m\xc9\x91\xc3\xfb\xc9\x95\xcb-Y\xfbK-\xcf\x9e=\xe3\xd6\xad\xbb\xd4\xa9S3\xce\xf5'N\x9c\xe4\xab\xaf\n\xe1\xea\xea\xaa\x93\xf2\xe\x1e\xfe\x91\v\x17.\xf1M\xd7.\\\xb8t\x89\a_<\xe4?\xb8\xff\x80\xdd{\xf6Q\xde\xd3\x93\xb5\xeb\x37P\xae\\\x19\xben\xd2X'\xe5&\xc5\xa8\x31\xe3\xf1\xf1n\r@\x87\xemh\xdc\xb0\x41\x82\xdb_\xb8x\x99}\xfb\xf6\xa7\xa8\xcc\xd4n\a}\xd0Wl%\xd5\xb3g\xcfh\xdf\xa9\x1b\x13\xc6\x8e\xa4`\xc1\x2X[[\xeb\xa5\fco/!D\x14\xb9.\xc5-#\x9c\xe7\x82\x2\x83\xe8\xdc\xb5'\xf5\xea\xd4\x62\xe8\x90\x81\x89\xfe\\\x9d\x9aU\xe8\xd5\xab\a\x11\x11\x11\xdc\xbcu\x8b\x11\xa3\xc6s\xff\xfe}\x86\xe\x19\x92\xbcz\x4\x5Q\xb2LEvl\xdb@\x85\n\x15\xb4n\x1f\x1a\x1a\xca\xa1\x43\x87\x98=w\x1/_\xbdKV\x99\x90\x31\xdaX\x1b\x89\xff\xb8\x65\x84\x63#\xb9\xf1\xdf\xef\xbbo\xa9^\xbd\x1a!!!\x9c>\xfd;\xdem\xbb\xb0~\xcdRj\xd7\xae\x9d\xacz\\\xbcx\x11\x9fv]\xb9r\xf1,\xe\xe\xeZ\xb7\x7f\xf0\xe0\x1-[\xb5\xa5O\xef\xee\f\x1a\xd8\x9f\xbbw\xef\xf2\xc3\xa6m\xfc\xf3\xcf\x43\x66\xcf\x9e\x89\"\t\xaf\x8b\xcf\b\xed\xac\x8d\x9c\x3\xe2\xa6\xaf\x63\x43\xa7\x89\x9d;7/\xebrw\x1a\xa5J\x95\xa2P\xa1\x42\0\xd4\xa8Q\x83\x66\xcd\xbd\xd8\xb6m\x1b\xc3\x87\xf\xd3Ky\xbar\xeb\xd6]\xfa\xf4\xf7\x65\xe1\xfcY4lP/\xc6\xba\xa3?\xfd\xcc@\xdf\xe1,[<_'\x1\x1d\x12\x12\xca\x94)\xb3x\xf1\xf2%\xeb\xd6o\x8a\xb5><\"\x82\xc1\x43Gj~>r\xe4g\xea\xd4\xaa\x85\xb5\xb5U\x8a\xcbN\x8a\xb3g\xff\xa2N\xadZ\0\x14\xc8\x9f\x9f\xaaU*%\xb8}\xd8\xe7\xb0\x14\aOj\xb6\x83\xbe\xe8+\xb6\x92\xea\xde\xbd\xfb|\xfe\x1cN\x8b\x16\xcd\xb1\xb2\xd2\xcf\xb1\x93\x1e\xdaK\b!\xd7\xa5\x84\xa4\xf7\xf3\\xD\x4\xbd\xfa\f [Vg\x6\xe\xe8\x9b\xa4\xcf\xe6\xc8\x91]\x93\x80\xa9Z\xb5*\xd9\xb2\x66\x65\x80\xef\b\xda\xb5m\x8b\x99\x99\xb9>\xaa\x1b\xc3\xb5k\xd7\xf0\xdb\xbe\x93\x19\xd3&\xe3\xe0\x90\x19/\xef\xe\xc9\xdaOzocm$\xfe\xe3\x97\xde\x8f\x8d\x94\xc4\x7f\xc1\x42\x5\x35\xf1_\xabV-\xc2\xc3\xc3\x99\x36}V\xb2\x13;I\x95'O\x1e~9~\x84\x1c\x39r\0P\xaf^=\n\x15*D\xf7^\x3\x18>|(\xd9\xb2%\xbe\xe7^zogm\xe4\x1c\x10?}\x1d\x1b:}+V\xd3\xe6\xad\x12\xfc\xa7\vVVV\xd4\xaaY\x9d\xab\xd7\xa2z(,X\xb0\x90\x39s\xe7\xb2i\xf3\x66\x9a\x35o\xc9\xa8Q\xa3\x1P\xab\xd5\x1c?~\x9c\x1e={Q\xa3\x66\x1d\xc6\x8c\x19\xcb\xd3\xa7OuR\x87\xc4\xaaS\xa7&\v\xe7\xcf\x62\xa0\xefp\x8e\xfe\xf4\xb3\x66yt\x83-\x9c?+\xdeL]R\xadZ\xbd\x96\x17/_\xc6Z\x9e\x39sf2\xc7\x91\xa1~\xf1\xf2%+W\xaf\xd3I\xd9i]j\xb6\x83\xbe\xa4\x46l\xbd|\xf9\x8a\x42\x45J\xb0k\xf7n\x6\r\x1aL\x95j5\x19\x39r\x14\x1\x1\x1\0\x1c\x38x\x90\xce\xddz\x1\xf0U\xf1\xb2\xe4\xc9_\x94\xcb\x97\xa3\x12No\xde\xbc\x61\xfa\xf4\x19\xd4\xad\xd7\x90\xe\x1d:\xb1{\xf7\x1eT\x89x\xf5\x66\\\xd2\x43{\t!\xe4\xba\x94\x90\xf4r\x9e\v\x8f\x88\xe0\x8f?\xcf\xc4X\xa6R\xa9\x18\x36l\x14\xef\xdf\x7f`\xf9\xd2\x85\x98\x9b\xa7\xec;\xc4\xe8!WO\x9f>\x8bs}B\xf7{\xd1\xbdu\0|\xdau%O\xfe\xa2\x9c?\xef\x9f`y\x1e\x1e\x1e\xec\xd8\xbe\x8d\xda\xb5kca\x91\xfc\x44Rzi\xe3\xe4\x92\xf8\x8f_z96R#\xfe\xabU\xab\xca\xdd\xfb\x8f\xf8\xf8\xf1\x63\xdcu\b\xfg\xfd\x86\r\xf8\xf8\xb4\xa5q\x93\xafY\xb4x1\x9f>}\x2\xe0\xfcy\x7f|\xdau\x5\xa0\x64\x99\x8a\xe4\xc9_\x94\xa0\xa0\xa0\x4\xcb\x33\x31\x31\xd1$u\xa2\x45\xf%\x8b\x88\x88HR\xdd\xd3K;'\x97\x9c\x3\xe2\xa7\xaf\x63\x43\xa7=v\xe6\xcd\x99\xa1\xcb\xdd\xc5+$$\x84\xfc\xf9\xf2i~^\xbct\r\xcd\x9b\x36\xa0[\xb7.\xe4\xc8\x9e\x1d\x80\xdd{\xf6\x30o\xde\"F\x8c\x18\x8a\x83\xbd=;v\xec\xa2\x85\x97\xfG\x7f<\x80\xb3\xb3s\xaa\xd4\x13\xa0\x61\x83z\x9a\x86[8\x7f\x16\x80\xe6\xff\xff\xcd\xd0\xa5\x84\xa7\xa7\a\xd9\xb2\x65\xe5\xd5\xab\xd7\x9a\x65\x3\xfa\xf5\xc6wP\x7f\0\x66\xcfY\xc0\xb2\x15\xab\x35\xeb\\r\xe4\xa0\x62\x5O\xad\xfb}\xf6\xec\x19\xa7\x7f?\x13\xe7\xbajU+\x19M\x96\x39\xb5\xda\x41_R+\xb6\xc2#\x94\f\x19\x36\x86\x65\x8b\xe7R\xbf~]6\xfe\xb0\x19\xdf\xc1\x43\xd9\xb2\xf9\a*W\xaa\x44\x9f^\xddX\xb6r=\xbb\xb6o\xc2\xc6\xc6\x9a\\\xb9r\x11\x14\x14\x44\xdbv\x1d\xa9Z\xa5\x12\x13\xc6\x8f\xe1\xf1\xe3\xc7\x8c\x1d?\x99w\xef\xdf\xd1\xa3{\xf7\x64\xd5\xc3\xd8\xdbK\b!\xd7%m\x8c\xfd<\x17\x1e\x11\x41\xdf~\x83\xf9\xf9\xf8\t\xe6\xcc\x9aJ\xebVQs\"\xce\x98\x35\x97\xf3\xfe\x17\xd8\xb7\xc7\xf[\xdb\xc4\xcd\x83\x91\x90\xb0\xb0\x30\0\xb2\x66\x8d\xfb\xde-\xa1\xfb=GGG6o\\M\xc7.=\x18\xea\xdb\x97:uj\xe3\xe6\x96\xf0\x30~\x85\x42\x81\xa9\xa9i\x8a\xeb\r\xc6\xdf\xc6)!\xf1\x9f\x30\x63?6R+\xfe?\x7f\xfeLVg\xc7\x38\xe7\xb7Q\xab\xd5\x8c\x1d;\x8e\xe7\x1\x1|\xf7]/>\x87\x85\xb1`\xc1\"\xae\\\xb9\xca\xaa\x95+(R\xa4\x30\x43}\xfb\x32g\xfeR6o\\M\x96,N\xc9\x9a\x42\xe0\xfa\xf5\xeb\x14-R V\xc2'1\x8c\xbd\x9dSB\xce\x1\t\xd3\xc7\xb1\xa1\xd3\xc4\xce\xb2\x65\xab\x63-\x1b:t y\xf3\xe8\x66\xa2\xe3\xb0\xb0\x30.]\xba\xc4\xf6\x9d\xfb\xd8\xbay}\x8cu\xf3\xe6\xcd\xc1\xcc,\xea\xd7\t\xe\xe\x66\xca\xd4\x99,\x9c?\x9b\x1a\x35j\0P\xbe\x42\x5\x1a\x35\xfe\x9a\xbd\xfb\xf6%\xfb\x61\x33\xb9\xbel8@/\xc1\\\xa5rE\x8e\x1e\xdeKY\xcf\xaa\0\x64vp\xc0wP\x7f\xcd\x44\x61\x43\x87\fd\xd3V?\x82\x2\xa3\x32\xd5G\x7f\xdcK\xe6\xcc\x99\xb5\xee\xd7\xc5\xc5\x85K\x97\xaf\xb0\xcdog\x8c\xe5\xed\xdb\xf9\xe0\xe3\xed\xa5\xd3\xdf\x41\xdfR\xa3\x1d\xf4\x45\xdf\xb1\xf5\xa5\xcd\x1bWQ\xadZ5\0lll\xe9\xdc\xad\x17\xcf\x9f\a\x90+\x97\x1b\x15+V`\xd9\xca\xf5\xb8\x97p\xc7*S&\0\xb6l\xd9\x82\x83\x83=c\xc7\x8e\xd1\xdc\f[[\xdb\x30\xec\xfb\x31\xf8x{'jLs\\\x8c\xb9\xbd\x84\x10r]J\fc=\xcf\x45?\xd4]\xb9r\x95!\x83\a0|\xc4X\0\x2\x3?\xe1\xe7\xb7\x9b\xdd\xbb\xb6\x92-[\xd6\x14\x95\xa1V\xaby\xfb\xf6-\xeb\xd7o\xa0\x61\xfd\x9a\xe4\xc9\x93\x87\x37o\xde\xc6\xd8&1\xf7{%K\x96\x88Z^\xde\x93\x62\xc5\x8a\xa5\xa8N\xc9\x61\xacm\x9cR\x12\xff\xda\x19\xeb\xb1\x91\x1a\xf1\x1f\x19\x19\xc9\xe3\xc7\x8fY\xb7~#\xdf\xf5\xea\x1e\xe7\xc4\xc7W\xaf^\xc5o\xe7~\xfe:s\x92\xec\xff\xff\x62\xbf`\x81\x82\xd4m\xf0\x35\xfe\xfe\xfe\x94/_\x9e\xf2\xe5\xa3\x12\x5%K\x96H\xd6\xfd\xe8\x9b\x37o\x98;\x7f\x31sfMOv\xc2\xd7X\xdb\x39\xa5\xe4\x1c\xa0\x9d\xae\x8f\r\x9d&v\xea\x37\x88=\xfe\x31\xb3\x83}\x8a\xf7[\xb7\x41\x33\xcd\xff\xb3:;\xb2h\xc1,<=\xff}\x5t\xfd\xba\xd5\x35I\x1d\x80\xa7O\x9f\xf2\xfe\x43\x10\x45\x8a\x14\xd1,\xb3\xca\x94\x89\xbaujr\xfd\xda\x8d\x14\xd7'=H\xec\x30\x19\x13\x13\x13\xa6M\x99\x80\x42\xa1`\xeb\xb6\x1d\0th\xef\xc3\x94I\xe3\x13=\xbb\xbcH9}\xc5V\\\xbe|\xd3@\xee\xdc\xb9\0\b\xe\xfe\x14\xef\xf6W\xae\\\xa5J\xe5J1.xe\xcb\x96!b\fff\xe6l\xfe\x61\r\x85\n\xe6O\xf6\xfe\xb7\xf8\xed\x61\x8b\xdf\x1e\xcd\xcf\xad[6e\xca\xe4Iq>T\xc9\xfd^\xfa!\xf1o\x1c\xf4\x1d\xff\x83\x6\x8f\x64\xd0\xe0\x7f\xe7]\x19\x36\xb8\x1f\x9d:u\x8cs\xdb\xbbw\xefQ\xbchAMR\a\xa0P\xa1\x82\xb8\x64w\xe6\x9f\x7f\xfe\xd1\xbcM+\xb9\xc2\xc2\xc2\x18=f,^-\xbe\xa6v\xedZ)\xda\x97\xd0N\xce\x1)\xa7\xb3\xc4ND\x84\x92\x6\xf5\xea\xc6\xb9.\xba\x1b-\x80R\x99\xb4\xf1\x89\0\xa3G\f\xa6T\xa9\x92\x38::\xe2\xe6\xe6\xa6\xb5\x1b]xx8\x10\xfb\0\xb1\xb4\xb0$S&\xcb$\x97\x9fR_\x8e\x97\x3\xfdt\xc1\xfb\xe3\xcf\xb3\f\x1a<\\\xf3\xf3\x87\x8f\x1f\x99\x37\x7f\x31\x83}\xfb\xa3V\xab\x99=g\x81&#\n\xd0\xa8\x89\x17\xf3\xe7\xcd\xa4r%\xedo\x88\x30\x31\x31\x61\xea\xe4\xf1(\x14\x80\x42\xc1\x94\x89\xe3\x8c\x32pR\xa3\x1d\xf4\x41\x9f\xb1\xa5Mbf\xff\xf\r\xfdLddd\x8c\x65\xd1\x89\xd6\x94\xccO`\xac\xed%\x84\x88\"\xd7%\xed\x8c\xed<\x17\xe3\xa1n\xdb\xf\xe4\xc9\x13\x95\xfco\xe5\xd5\x2S33,\xcc\xcd\xf1\xf4(\x97\xe2rvl\xdb\x80\x99\x99\x39...\xb8\xb8\xe4\x88\xf7Z\x94\xd6\xee\xf7\xe2\x62lm\xac+\x12\xff\xda\x19\xdb\xb1\x91\x1a\xf1_\xad\x8a'\xfd\xfb\xf5\xc5\xda\xda\x1a\x37\x37\x37\x1c\x1d\x1d\xe3\xdd\x36,<\x8c\xf0\xf0\xd8\xf7\xbe\xb6\xb6\x36XXX\xa4\xa8\x1e\x91\x91\x91\xcc\x9c\x39\v\xa5R\xc9\xb0\xe1\xc3Rt|\x19[;\xeb\x8a\x9c\x3\xb4\xd3\xf5\xb1\xa1\x93\xc4\xce\xd3\xa7O\xa8\\\xadv\x8c\x31t\t\xf1(W6I\xfb\xafU\xab\xa6\xe6\xadX\x89\xe1\xe6\xe6\x86\xa9\xa9\t\x97.]\"g\xce\x9c@T\x80\xfe\xf1\xe7\xd9\x18]\xb4\xa2'\xc1\x32\x37\xff\xf7\xe1\x33<<\x1c\x85\x42\xa1uYb}\xd9`\xd1\x8d\xf4\xe5x:]\x5\xf5\xf9\xf3\xfe\xb1\xfe\xfe\x8b\x97\xae`\xd3\x16?P\xab\xf9\xf0\x9fI\xc7^\xbc|\xc9\xb9\xbf\xfc\x13\x15<\x10\x15@S&\x8d\xd7\xfc\xdf\xd8\xa4V;\xe8\x9a\xbe\x63K\x17\x8a\x17/\xc6\xde}\a\x18\x38p\x80\xe6\x42z\xeb\xd6-\x1c\xecm\x93=\xce\xd5X\xdbK\b\xf1/\xb9.%\xcc\x18\xcfs\x8b\x17/\xe3\xda\xb5\xeb\x31\x1e\xea\xa2\xb5h\xd6T'eth\xeb\x95\xa8\xd7\x92\x43\xe2\xef\xf7 \xe9\x13\x9f\xea\x82\x31\xb6\xb1\xaeH\xfc'\xcc\x18\x8f\x8d\xd4\x88\xffV\xad\x12\x1f\xff\xf9\xf3\xe5\xe3\xee\xfdG<|\xf8\x90\xbcy\xf3\x2\x10\x10\x10\xc0\xdd\xfb\x8f\xbexn\x8cJ\n'%\xfe###\x99={\xe\xf\x1e\x9b\xf1\x63G\xc6\x18\xd6\x95X\xc6\xdc^B\x88\x7f\xc9u)~\xc6z\x9e\xeb\xd9\xe3[\xda\xf8\xb4\xc6\xcd\xcd\x30\x93SF\xbf\x6\xf7\xf2\x95+8;;\x93;wn\xad\xf7{\xf6\xf6\xf6\x14\xc8\x97\x8b\x3\a\xe\x61gg\x87\xb3\xb3s\xaaL\xaei\xacm\xac+\x12\xff\xf1\x33\xd6\x63\xc3\xf0\xf1\x1fuO\xe9\xef\xff\x37\xc5\x8b\x17\xc3\xd3\xd3\x93\x9a\xd5+1p\xd0`\xfa\xf7\xeb\x83\xb9\xb9\x5\xab\xd7\xac\xa1\x9dO\vJ\x94\x88\x9a[+:\xe1\xbbw\xef^<<<(T\xa8\x10\xb6\xb6\xb6\xf1\x96\x11\x19\x19\xc9\xacY\xb3Y\xb1z#\xabW,\xe2\xf6\xed;\x9auNN\x8e\x14(P \xd1\xf5\x35\xd6v\xd6\x15\x39\a\xc4O_\xc7\x86N\x12;\x83\x6\xf4\x61\xd0\xc0~\xba\xd8\x95\xce\xf4\xec\xd1\x3'GGv\xee\xda\xc3\xbbw\xef\xa9S\xbb&~\xdb\x36k\x82\xd9\xc4\xc4\x84\xaf\x8a\x14\x46\xa1Ph\xe\x8a\xe8\x65\xe6\xe6\xe6\xb1\x96YXZ&\xf9\xe0\xf9\xea\xab\x42\xf1\x36J\xc3\x6\xf5X\xb6x>_}\x95\xf8\x9eH\t\xb1\xb6\xb6\x62\xcc\x98\xe1\\\xbcp\x99n];s\xde\xdf\x9f\xe1#\xc6\x10\x11\xa1\x4\xc0\xc2\xdc\x9c\x19\xd3'Q\xde\xd3\x93u\x1b\x36R\xb6li\xac\xac\xactRvr\x94,Q\x82+W\xaf\xf2\xfb\x1f\x7fj\xdd\xb6\x62\xc5\x94\x8d\x91M\xcdv\xd0\xb5\xb4\x18[\xff\x95\x35kV\xf6\xef\xdd\xc9\xf2\xe5+\x18>b4\xc5\x8a\x16\x61\xd2\xc4q\xd4\xad['Y\xfb\x33\xe6\xf6\x12\x42\xfcK\xaeK\xf1\x33\xd6\xf3\x9c\x9d\x9d-vv\xf1?\x14\xe9\xbf|;\xe6\xcc\x9c\xc4\xa2\xc5\xcb\xf9\xf0\xfe\x3\xbe\xbe\x83\xb4\xde\xef)\x14\n\x96,^\xc0\xf4\x19\x33\x19\x32\xf4{\x86\f\x1e\x98*\x89\x1d\x63mc]\x91\xf8\x8f\x9f\xb1\x1e\x1b\x86\x8e\xff\xd2\xa5K\xf1m\xb7\xe\x8c\x1b?\x91\xd1\xa3\x46P\xbf~=\x16/Z\xc0\x9a\x35k\x99=g\x1e\xe\xf6\xf6\x34i\xd2\x98v\xed\xdaj>\x93+\x97\x1b+\x96\xce\x63\xe1\xa2\xa5\x1c\xfd\xe9g\xa6M\x9d\x1c\x63N\xae\xffz\xf6\xec\x19+Vo\x4\xa0G\xef\x1\x31\xd6\xf5\xef\xdb\x9d\xa1\x43\x86$\xba\xbe\xc6\xda\xce\xba\"\xe7\x80\xf8\xe9\xeb\xd8P\xdc\xbe\xff\\\x9d+g\xf2g.?q\xe2\x64\xba\xcd\x34\x1a\xbbz\x8d\x9aq\xe7vTV\xb1\xc8W\x85\x39\xf6\xe3~\x3\xd7\bj\xd7mD\xd3&\x8d(U\xb2\xa4\xd6m\xb3\x38g\xa1t\xa9\x12\xa9P\xab\xa4{\xf2\xfc\x35\xda\xe2&1\xdb$DbK\b\x91\xde\xc8uI\x7fR\xe3\xba\x94\x1e\x4\x5\x5\xf1\xf0\xe1\xc3x\xd7[[['\xe9[y\x91x\x12\xff\xfa#\xf1\x9f\x38/^\xbc\xe0\xf5\xeb\xf8\xa7\x37\xc8\x96-[\x8c\xc9\x98\x85n\xc9\x39@\x7f\x9e<\x7f\x9d\xf2\x1e;\xf2\xe0\x99v\xd5\xadU\x13{[;\0*\x94\xf7\xd0\xb2u\xeap\xcb\x95\x8b\x43\x87\x8fp\xe8\xf0\x11\xad\xdbV\xacX>\xcd\x6Oj\x90\xd8\x12\x42\xa4\x37r]\x12\x86\xf6\xf0\xe1\x43\x9a\x36\xf7\x89w}\xab\x16M\x98\x37oN*\xd6(\xe3\x90\xf8\x17\x86\xf6\xcb\xaf\xbf\x32r\xf4\xa4x\xd7\xcf\x9a\x31\x91\x36>\xf1\x9f\x1f\x44\xca\xc8\x39@\xbfR\xdc\x63G\x88\x8cH\xbe\x19\x11\x42\b\x91\x96\xc8uI\x88\x8cK\xe2_\x88\x8c\xed\xc9\xf3\xd7\x18\xdf\x14\xd2\x42\b!\x84\x10\x42\b!\x84\x10\x2@\x12;B\b!\x84\x10\x42\b!\x84\x10\xc6J\x12;B\b!\x84\x10\x42\b!\x84\x10\x46J\x12;B\b!\x84\x10\x42\b!\x84\x10\x46*\xc6[\xb1\x9e<\x8f\xff\xf5o\"m\x92I\xd0\x8c\x83\xc4V\xda\"q#\x84\xc8\xe8\xe4\xba\x64\xfc\xe4Z&\x92K\xe2?}\x90s\x80\xf8\x92\xf4\xd8\x11\x42\b!\x84\x10\x42\b!\x84\x30R1z\xecH\xd6O\b\xfd\x90\xd8\x12\x42\b\x91\x96\xc8uI\x88\x8cK\xe2_\x88\xf4G\x86\x62\t\x91\x4\xc9\xbd\x10Jl\t!\x84\xd0\a\xb9.\t\x91qI\xfc\v\x91\xb1}y\xe\x88\xb3\xc7Np\xf0g\x2?\x5\xa3\x8cT\xa5n\xcd\x84Hc,,\xcc\xc9\x64i\x81\x83\x9du\x8a\xf6#\xb1%\x84\x10\x42\x17t}]\x12\x42\x64<\x12\xff\x42\xa4?f\xff]\x10\xf0\xea\x1dV\x16\xe6\xe4v\xcb\x8a\x99\xa9L\xc1#2\xb6\xb0\xb0\b>\x6\x86\xf2\xf4\xf9\x1b\xdcr:\xa7h_\x12[B\b!RJ\x97\xd7%!\x84\x10\x42\xa4\xf\x31\x12;\xc1\xc1\x9f\xc9\x64i\x8eKvGC\xd5G\x88\x34\xc5\xd2\xd2\x9clY\xcd\x31\x35\x83\xe0\x90\x30l\xac-\x93\xb5\x1f\x89-!\x84\x10\xba\xa0\xab\xeb\x92H\x9c\xf0\xf0p\x9e={\xc6\xa7\xa0 \"\x94\x11\x86\xaeN\xb2\x99\x9b\x99\x63kg\x87\xab\xab+\x16\x16\x16\x86\xae\x8e\x10\x46\x41\xe2_\x18\x93\x18\xdd\x6\x2?\x5\x93\xcd\xd9\xc1Pu\x11\"\xcdr\xb0\xb7\xe1\x63`p\xb2?/\xb1%\x84\x10\x42\x97Rz]\x12\xda\x85\x87\x87s\xf3\xe6M\xde\xbf\x7fg\xd4\xfu\0\x11\xca\b\xde\xbf\x7f\xc7\xcd\x9b\x37\t\xf\xf\x37tu\x84H\xf3$\xfe\x85\xb1\x89\x91\xd8QF\xaa\x64\x88\x88\x10q035%R\x15\x99\xec\xcfKl\t!\x84\xd0\xa5\x94^\x97\xd2\nCM\xe2\xfa\xe4\xf9k\xad\x65?{\xf6\x8c\xc8He*\xd5(\xb6\xac\x39r\xeb|\x9f\x91\x91J\x9e={\xa6\xf3\xfd\xeaRb\xda\x46\xa4\xf\x12\xff\xf1\x93\xf8\x17I%O\x9a\x42\b!\x84\x10\x42\xfc\xc7\xa7\xa0 CWA/>}J\x9f\xbf\x97\x10\xba$\xf1/\x8c\x8d$v\x84\x10\x42\b!\x84\xf8\xf\x63\x1f~\x11\x9f\x88\x88\xf4\xf9{\t\xa1K\x12\xff\xc2\xd8HbG\b!\x84\x10\x42\b!\x84\x10\xc2HIbG\b!\x84\x10\x42\b\x1dy\xf2\xf4\x39O\x9e>7t5\x84\x10\x6 \xf1/\f\xc5L\xfb&B\b!\x84\x10\"\xa3\xf1\xdb\xbe\x8b\xf3\xfe\x17\x12\xbd}\xa5\x8a\x9e\xb4n\xd5R\x8f\x35J\xfb\x9e<}\xce\xb8\tS\x1\x98\x34\x61\x34\xb9\xdcr\x1a\xb8\x46\xfa\xb3`\xe1\x92\x38\x1f`==\xca\xd2\xb6Mk\x3\xd4H\xbb\xf5\x1b\x36\x3\xd0\xadkG\x3\xd7$\xed\x93\xf8O:\x89\x7f\x89\x7f\x43\x92\xc4\x8e\x10\x42\b!\x84\x88\xe5\xbc\xff\x5\xe\x1d>B\xa1\x82\x5\xb4n\xfb\xe9\xd3'v\xed\xdeKh\xc8g:uj\x97\n\xb5K{\xa2\x1f\xea>|\xf8\b\xc0\xb8\tS\xd3\xf5\xc3\xdd\xc3GO\xb8w\xef\xbe\xe6\xe7\x9b\xb7n\xa1TF\xbd\xa9-\xad>\xd8\xfd\xf6\xfb\xef@\xfa}\xb0\xd3%\x89\xff\xa4\x91\xf8\x97\xf8\x37\x34I\xec\b!\x84\x10\x42\x88\x38\x15*X\x80\x3\xfbv\xf0\xf6\xed\xdbx\xb7\xb1\xb7\xb7\xc7\xd4\xd4\x8c!CG0f\xfc$\x80\f\xf7p\xf7\xe2\xc5+\xc6M\x98\x8a\x99\xa9\x19y\xf3\x44\xbd\xa6\xf8\xd3\xa7`\xc6M\x98\xca\xf4)\xe3\xc9\x91#\x9b\x81k\xa8{\v\xe6\xcd\xfc\xf7\xff\v\x97p\xf5\xdau\xcc\xccL\rX\xa3\xf4/44\x94\xb6\xed\xbb\xa0\x30\x31\xc1o\xcb\x6\x32\x65\xca\xa4\xd7\xf2$\xfe\x13G\xe2_\xe2?\xb5$t\xe\x90\xc4\x8e\x10\x42\b!\x84\x88\xd7\xdb\xb7o\xf1\xa8P=\xde\xf5\v\xe6\xcd\xa4\x65\x8b\x66\xcc\x9d\x33\x3\x80\x31\xe3'ae\x9d)C\r\xcbpr\xcaL\x99\xd2\xa5\xf0i\xdd\x82\xb5\xeb\x7f\0\xe0\xfb\x61\x83\xd8\xb1k\x1fNN\x99\r\\;\xfdZ\xb0p\t\xf3\x17.\xc5w`_\x8e\x9f\x38i\xe8\xea\xa4kVVVL\x99<\x1e\xafV\xed\x98\x34\x65:\xd3\xa6L\xd4{\x99\x12\xff\xdaI\xfcK\xfc\xa7\x96\x84\xce\x1\x32y\xb2\x10\x42\b!\x84H133S\xe6\xce\x99\x41\xbe\xbcy8s\xf6\xbc\xa1\xab\x93*\x2\x2^\x10\x1a\xfa\x19\v\v\v\x6\xf4\xeb\x19\xe3\x9b\xf9\x1c\x39\xb2\x31\xa0_O,,,\b\xf\xf\xe7\xc5\x8bW\x6\xac\xa9~|\xf9P7h`?CWG\xab:5kR\xa7\x66MCW#EJ\xb8\x17g\xcc\xe8\xef\xd9\xb2u\x87\xa1\xab\x12\x83\xc4\xbf\xc4\x7fZ\x97\x1e\xe2\x1f\xe2?\aH\x8f\x1d!\x84\x10\x42\b\x11/SSS\\]]\xe3]occ\xad\xf9\xbf\x99\x99)\xb6\xb6\xb6\xa9Q-\x83\x8b\x9eS\xa3\x84{1\x6\xf\xea\x9b\xe0\xb6+Vm\xe0\xe2\xa5\xcb\xe9j\xce\rc{\xa8\x3\xe8\xd8\xb1\xad\xa1\xab\xa0\x13\x9d;\xb5\xe7\xcc\xd9\xbfR\xa5,\x89\xff\xb8I\xfcK\xfc\x1bR\\\xe7\0I\xec\b!\x84\x10\x42\x88x999\xf1\xe7\xe9\xe3\xa9^\xee\xb3g\xcf\x38\xfd\xfb\x99\x38\xd7U\xabZ)\xc1\x87M}\x8b~\xa8\x33\x33\x35\xa3}[o\xad\xdb\xfb\xb4n\xc1\xd5\xab\xd7u6\xa1\xea\xeb\xd7o\xb8t\xe5Z\x9c\xebJ\x97t'kV\xe7\x14\xed_\x9b\xf8\x1e\xea\n\x16,\x90\x66\x1f\\\x95\xcaH\x8e\xfc\xf4\x13\x1f\xde}\xa4];\x1f\xa3\x9e\xf\x44\xa1P0{\xc6\x94T)K\xe2?6\x89\x7f\x89\x7f\x43\x8b\xeb\x1c \x89\x1d!\x84\x10\x42\b\x91\xe6\xb8\xb8\xb8p\xe9\xf2\x15\xb6\xf9\xed\x8c\xb1\xbc};\x1f|\xbc\xbd\fT\xab\x98o\xbf\xc9\x9b'\xb7\x66N\x8dhw\xee\x46\xbd)f\xea\xf4\x39\x31\x96\xdb\xda\xda\xf0\xf0\xd1\x63\x9d<\xdc\x65\xc9\xe2\xc4\xbd\xbb\xf7\x39v\xfc\xd7\x18\xcb\xeb\xd7\xabM\x9dZ\xf1\xcf\x87\xa2\v\t}S\xff\xe5\x84\xaa\x86\xb2~\xc3\x66\xcd\xdbo j\xf8\x45\xc7\x8em\xd9\xb6m\x87\x66r\xdf\xccN\xe|\xdd\xa4\x31\x9b\x37\xfbq\xe2\xe4I\xcd\xb6\xd5\xabV5\x9a\x37\xe6\xd8\xd9\xdb\x19\xba\nz%\xf1\x1f?\x89\xff\xf8\x65\x94\xf8\x87\xd8\xe7\0I\xec\b!\x84\x10\x42\x88\x44\v\v\vc\xdf\x81\xc3\0\xb4h\xd6\x4KKK\xbd\x94\x63\x62\x62\xc2\xb4)\x13P(\x14l\xdd\x16\x35\x97@\x87\xf6>L\x99\x34\x1e\x13\x93\x8c=M\xa4\x89\x89\t\xbdzv\x3\x85\x82\x63?\xff\x2@\x83\xfa\xb5\xe9\xd9\xbd\xab^\xff\x36\xc6\x38\x42K\xd5[\0\0 \0IDAT\xfc\"Zf'\a\xcd\xff?\xbc\xfbh\xc0\x9a\x18\x37\x89\x7f\xc3\x93\xf8O\xba\x8c\x10\xff\x92\xd8\x11\x42\b!\x84\x10\x89\xd6\xa9Kw\xce\xfd\xe5\xf\xc0\x9e=\xfb\xf0\xdb\xba\x11\x85\x42\xa1\x97\xb2LLL\x98:y<\n\x5\xa0P0e\xe2\x38\x83?\xd4\xe5r\xcb\xc9\xa4\t\xa3\x19\x37\x61*\x9f>\x5\xf3\xfd\xb0\x41\x31&M\x8d\xfe\xa6~\xf4\xc8\xa1\x9a\x65/^\xbc\x62\xec\xf8\xa9\x64\xce\xec\xa0\xb3y6LLL\xe8\xd5\xa3k\xd4\xdf\x6\x5=\xbbw\xd1\xd9\xdf\x66\xc1\xc2%<|\xf4$\xd6+\x8d\x8d\xe1\xa1\xae[\xd7\x8eq~\xeb\xde\xa8\x41\x3\xa6L\xfcHf'\a\x1a\x35h\0\x44\xcd\xb9\x61\xac\xf3n\x4\x6\x6\x62oo\x9f\xea\xe5J\xfcK\xfcK\xfc\xa7\r\xff=\ad\xect\xa7\x10\x42\b!\x84H\xb4\xe7\x1\x1\x9a\x87:\x80\xb3\xe7\xce\x13\xf0\xe2\x85^\xcb\x34\x31\x31\x61\xca\xa4\xf1i\xe2\xa1.Z\xf4\xc3\x9d\x32R\xc9\x8e]\xfb\xb4n\xbf\x63\xd7>\x94\x91J\x9dO\x9ejbbB\xcf\xee]u\xfaP\aQ\xc3M\xee\xdd\xbb\xaf\xf9\xd9X\x1e\xea\x12\x62\x66\x66J\xa7N\xed\xf8\xbaIc\xa3\x9f_C\xadV3|\xc4\xd8T/W\xe2?\x8a\xc4\xbf\xf1IO\xf1\xfq\x9f\x3\xa4\xc7\x8e\x10\x42\b!\x84H\x94,NNdvp\xe0\xc3\xc7\xa8\xae\xec\x99\x33g&\x8b\x93\x93\xde\xcbM+\xft_\xca\xe5\x96\x93\xe9S\xc6\xe3\xe4\x94Y\xeb\xb6\xbd{v\xe5\xdd\xbb\xf\x31\xbe\xd9\xd7\x15}\xffm\xd2\xc3\x43]\xb4\xcd\x9b\xfd\0\xe3\x7f;\xce\xf\x9b\xb6r\xe4\xe8\xb1T/W\xe2\xff_\x12\xff\xc6'\xbd\xc4?\xc4}\xeH{Q\"\x84\x10\x42\b!\xd2$KKK\xd6\xad]N\xe5J\x15\xa8R\xb9\"\xeb\xd7.\xd7\xdb\x1c\x1b\xc6 G\x8elXXX\xf0\xe6\xed[\xc6O\x9c\xce\xabW\xaf\x35\xeb^\xbe|\xc5\xf8\x89\xd3y\xf3\xf6-\x16\x16\x16zy\xa8\xd3\xb7\xf4\xf4P\ap\xe2\xe4\xc9\x18\x93\xa5\x1a\xa3+W\xaf\x31\x65\xeaL:\xb4\xf7I\xf5\xb2%\xfe\x63\x92\xf8\x37.\xe9!\xfe!\xfes\x80\xf4\xd8\x11\x42\b!\x84\x10\x89V\xael\x19\xb6m\xd9`\xe8j\xa4)\x9f?\x87\xf3\xf8\xc9SF\x8f\x9d\x82\xad\xad\r\0\x63\xc6ME\x19\xa9\xe4\xf3\xe7p\x3\xd7.yn\xde\xba\xc5\xd5k\xd7\xd3\xcd\x43]z\x10\x1a\x1a\xca\xd8q\x93(\xee^\x8cqcF\x1a\xa4\xe\x12\xff\xb1I\xfc\x8b\xd4\x92\xd0\x39@\x12;B\b!\x84\x10\"Nw\xef\xdd\xa7i\xb3\xd6I\xfeL\x91\"\x85\xf5T\xa3\xb4\xc9\xcd\xd5\x45\x33\xa1\xea\xc3G\x8f\x1\x34\x13\xa5\xba\xb9\xba\x18\xb8v\xc9\xa3TFbff\xca\xf1\x13'9~\xe2$\0\x5\v\x16H\x13\xaf\x34\xce\xa8\xac\xac\xac\xd8\xbfwG\xaa\x95'\xf1\x9f\x38\x12\xff\"\xb5$t\xe\x90\xc4\x8e\x10\x42\b!\x84\x88\xc5\xd3\xa3l\xb2>W\xa4H\xe1\x64\x7f\xd6\x98}\xf9\xb6\x1c@\xe7\x13\xa5\xa6\xa6\xf8\xda\xcfX\x7f\x9fh\xd5\xabV5t\x15\x8c\x86\xc4\x7f\xd2H\xfc\xa7}\xe9=\xfe%\xb1#\x84\x10\x42\b!bi\xdb\xa6\x35m\xdb$\xed\xdb\xfa\x8c.\xfa\xe1.\xfa\xff\xc6*\xbd\xb6}\\\xaf\x41\x16qK\xaf\xc7\x80>I\xfc\xa7m\xe9=\xfe%\xb1#\x84\x10\x42\b!\x84\x8e\x18\xf3\x3\x9d\x10\"e$\xfe\x85\xa1\xc8[\xb1\x84\x10\x42\b!\x84\x10\x42\b!\x8c\x94$v\x84\x10\x42\b!\x84\x10\x42\b!\x8c\x94$v\x84\x10\x42\b!\x84\xf8\xfs3sCWA/\xcc\xcd\xd3\xe7\xef%\x84.I\xfc\vc#\x89\x1d!\x84\x10\x42\b!\xfe\xc3\xd6\xce\xce\xd0U\xd0\v[\xdb\xf4\xf9{\t\xa1K\x12\xff\xc2\xd8\xc8\xe4\xc9\x42\b!\x84\x10\xc2 r\xe5\xcc\x9a\x66\xcbuuu%00\x90\xc8He*\xd4(\xb6\xd7/\x1e\xeb|\x9f\xa6\xa6\x66\xb8\xba\xba\xea|\xbf\xba\x64\xa8\x63\x42\xa4>\x89\xff\xf8I\xfc\x8b\xa4\x92\x1e;B\b!\x84\x10\x42\xfc\x87\x85\x85\x5\x45\x8b\x16\xc5\xd1\xd1\xc9\xe8\x87/\x98\x9b\x9b\xe3\xe8\xe8\x44\xd1\xa2\x45\xb1\xb0\xb0\x30tu\x84H\xf3$\xfe\x85\xb1\x91\x1e;B\b!\x84\x10\x42\xc4\xc1\xc2\xc2\x82|\xf9\xf2\x19\xba\x1a\x42\b\x3\x90\xf8\x17\xc6\x44z\xec\b!\x84\x10\x42\b!\x84\x10\x42\x18)\xe9\xb1#\x84\x10\x42\b!P\xab\x41\xa9R\x12\xa9\x8c$R\xa5\x44\xad\x6\xb5Zm\xe8j\t\x91\xe1(\x14\n\x14\n051\xc3\xd4\xcc\x14\x33\x13\x33\x14\n\xfd\x96)\xf1/D\xda\x91\x9cs\x80$v\x84\x10\x42\b!28e\xa4\x92\xb0\xb0\x30T\xf2 '\x84\xc1\xa9\xd5j\xd4jP\xa9\"\x88PF`\xa2P`ii\x89\x99\xa9~\x1e\xdd$\xfe\x85H[\x92s\xe\x90\xa1XB\b!\x84\x10\x19XXX\x18\xa1\x9f?\xcb\x43\x9d\x10i\x94J\xad&\xf4\xf3g\xc2\xc2\xc2t\xbeo\x89\x7f!\xd2\xbe\xc4\x9c\x3$\xb1#\x84\x10\x42\b\x91\x41\x85\x85\x85\x11\xae\x8c\x30t5\x84\x10\x89\x10\xae\x8c\xd0irG\xe2_\b\xe3\x92\xd0\x39@\x12;B\b!\x84\x10\x19\x90\x32R)\xfuB\x18\x99pe\x4\xcaHe\x8a\xf7#\xf1/\x84q\x8a\xef\x1c \x89\x1d!\x84\x10\x42\x88\xff\xb1w\xe7qQ\xd5\xfb\x1f\xc7\xdf\x3\x3\x88\b(\x8a\x81\xa0\x95\n\xa2h\xddr)\xcb\x35\xcb%\x97\xcc\f5\xcd%\xcd\xba\x95\x66\x66\x66\xe6\x9e\xb6\xd9\x66\x9aYf\x9aZ\x9a\x99\x9a\x99Kaz\xcb\xca\xd4J\xdb\x34\x97~\x99)\xb8\xa3 \xeb\xc0\xfc\xfe\xe0\x32W\x4\x64\x6\x6\xe6\f\xf3z>\x1e<\x84\x33\xe7\xcc\xf9\0\xf3>\xf2\xfd\x9cs\xbe\xe3\x61\xacV9\xed\xcc\xff\xef\xbf\xfd\xaa\xd8\x98(\x9d\a\x83\x39\xa0h\xa5\xc9\x30\xf9\a\xdc\xdf\xa5\x39\xa6\xb1\x3\0\0\xe0\x61\xac\x65\x38\xb2;t\xe8\xa0\xe2\xee\xbaSc\xc7\x8dW\x9f\xbe\xfd\x64\x36\x9b\x35\xea\xb1\xd1zp\xf8\x30\xf9\xf8\x98\xd5\xb0\x61\xac\x36\xc7\x7f\xa1\xd3\xa7O\xeb\xf6\xae\xdd\xcb\xac\xe\xa0\"+M\x86\xc9?\xe0\xfe.\xcd\x31\x8d\x1d\0\0\0\x38M@@\x15\xc5\xc6\x36VXX\xb8mY\xeb\x36m\xb5`\xe1{Z\xf9\xd1\n\xadY\xbdJm\xdb\xb6\xd7\xfc\x5\v\x15\x12\x12R\xe4\xf3p+\x6\xe0~\xc8?\xe0\x1a\x34v\0\0\0Pb\x8d\x62\x1b\xe7\x9b\xe8\x34,,L\x8b\xdf_V`\xbd\x1b[\xde\xa4\x1b[\xde\x64\xf7\xf3\x32\x98\x3\x8c\x8f\xfc\x3\xc6@c\a\0\0\0\x86\xc3\x19{\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe6\0\xcf\x45\xfe\x1\xc7x\xb9\xba\0\0\0\0\0\0\0\x94\fW\xec\0\0\0\xc0p\xb8\x15\x3\xf0\\\xe4\x1fp\f\x8d\x1d\0\0\0\x18\xe\x83\x39\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8s\xe7\xcc\xce\xf7\xf5\x43\x8f\x8cpQ%\0\xca\x1b\xf9\a\x1c\x43\x63\a\0\0\0\x86\xc3@\xe\xf0\\\xe4\x1fp\f\x93'\x3\0\0\0\0\0\xb8)\xae\xd8\x1\0\0\x80\xe1\x30y*\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p8c\xfx.\xf2\xf\x38\x86\xc6\xe\0\0\0\f\x87\xc1\x1c\xe0\xb9\xc8?\xe0\x18&O\x6\0\0\0\0\0pS\\\xb1\x3\0\0\0\x87\xa4\xa7\xa7\xeb\x83\xa5K\x14\x1f\xff\xb9\x2\x3\x83\xd4\xb5[wu\xefq\x87L&S\x91\xdb\xec\xd9\xb3[K\x17\xbf\xa7\x1f\x7f\xd8\xa5\xba\xf5\xea\xa9\xd7]w\xabS\xe7.\xf2\xf2*\xfc<#\xb7\x62\0\xc6\x44\xfe\x1\xe3\xa1\xb1\x3\0\0\0\xbbY,\x16=\xf5\xe4\x13JII\xd6\xb0\xfb\x1f\xd0\x99\x33g\xf4\xcc\xd4\xc9:{\xf6\xac\x6\r\x1eR\xe8\x36\xbf\xfd\xfa\xab\xee\xe9s\xb7\x46\x8d\x1e\xa3\x9ew\xf6\xd2\x1f\xfb\xf6i\xc2\xf8q\xca\xc8\xc8P\xcf;{\x15\xba\r\x83\x39\xc0x\xc8?`L4v\0\0\0`\xb7\x1d;\xbe\xd7\xb6\xaf\xbf\xd2\xa6\xf8-\n\t\t\x91$\x5\a\ak\xdc\xd8\x31\xea\xd6\xbd\x87\xaaW\xaf^`\x9b/\xbf\x8c\xd7M7\xdf\xac\xfb\x87? I\xba\xb9UkeY\xb2\xb4i\xe3\x86\"\av\x9c\xb1\a\x8c\x87\xfc\x3\xc6\x44\x63\a\0\0\0v\xdb\xb8\x61\xbd\xee\xe8y\xa7mP'I7\xdd\xdcJ\xd9\xd9\x39\xda\xb5sG\xa1\x83\xb0\x80\x80*JHHPZZ\x9a\xfc\xfd\xfd%I\xa7O\x9dR\xfd\xa8\xa8\"\xf7\xc3`\xe\x30\x1e\xf2\xf\x18\x13\x93'\x3\0\0\xc0n\a\xfe\xf8\x43u\xae\xbc*\xdf\xb2\x80\x80\0\x45\x37\x88VbBB\xa1\xdb\xf4\xb8\xe3\xe\xf9W\xf2\xd7\xe8Q#\xb5\x66\xf5*=\xff\xdc\f%&\x1e\xd7\xa0\xc1\xf7\x95\x43\xc5\0\x9c\x85\xfc\x3\xc6\xc4\x15;\0\0\0\xb0\x8b\xd5j\xd5\xde\xbd{\xd5;.\xae\xc0\x63\xa1\xa1\x35u\xe6\xec\x99\x42\xb7\xabQ#T#G\x8d\xd6\x83\xc3\x87\xea\xab\xffl\x95$-\xfd`\xb9j\xd4\xa8Q\xe4\xbe\x62\x63\n?\x9b\xff\xdb\xbe\x3\x8e\x17\xe\xa0\xd4\xc8?`\\4v\0\0\0`\x17\x93\xc9\xa4\xba\xf5\xea*99\xb9\xc0\x63g\xcf\x9eQhhh\xa1\xdb\xad\xfd\x64\x8d\xdezs\xae\xde~\xe7]5nr\x8d\xb6\x7f\xf7\xad\x46\x8d\x1c\xa1;{\xdd\xa5G\x1f\x1b]\xe8\xbb\xe9\x30\x80\x3\x8c\x85\xfc\x3\xc6\x45\x63\a\0\0\0v\x8b\x8en\xa0?\xff\xfc\x33\xdf\xb2\x8c\x8c\f\x1d:xHaa\xe1\x5\xd6\xb7X,zn\xc6t\x8d\x9f\x30Q7\xb7j-)w\xfe\x8c\xc0\xc0@\xdd?t\x88z\xdc\xd1Su\xeb\xd5+\xb0\x1d\x93\xa7\x2\xc6\x43\xfe\x1\x63\x62\x8e\x1d\0\0\0\xd8\xed\xb6\x8e\x9d\xb4v\xcd\x1a\x9dG\x8b\x16\xbd\xab\xd9\xaf\xbf\xa6\xf0\xf0Z\x1a?ab\xbe\x33\xeb\x35j\x84\xaa\xc9\x35\xd7*((H\x92\x34t\xd8p]u\xd5\xd5\xfa\x64\xcdj-\x98\xff\xb6\xae\xae[W\xb7\xde\xd6Q=\xef\xec%\xb3\xb9\xf0?G\xb9\x15\x3\x30\x1e\xf2\xf\x18\x93\xe9\x8f\x43\xc7\xac\xb5k\xe5Ntu\xe4\xd8IE\xd7-xo$\0i\xff\x9f\t\xba\x38+y\x9f\x17\xe5\xe2u\xc8\x16\0\xc0\xd9J\xf3\xffR\xf2\x85\x94\xcb\xae\xebj\xb1\x31QL\x9e\n\x14#0\xa0\x8a$\xf2\xfx\xaa\x8b\x8f\x1\xcc\xb1\x3\0\0\0\0\0\xe0\xa6\xb8\x15\v\0\0\0\x86\xc3\xad\x18\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p\x18\xcc\x1\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\xe1\x8c=\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63\x98<\x19\0\0\0\0\0\xc0Mq\xc5\xe\0\0\0\fg\xee\x9c\xd9\xf9\xbe~\xe8\x91\x11.\xaa\x4@y#\xff\x80\x63h\xec\0\0\0\xc0p\x18\xc8\x1\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\x61\xf2T\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe6\0\xcf\x45\xfe\x1\xc7\x30y2\0\0\0\0\0\x80\x9b\xe2\x8a\x1d\0\0\0\x18\xe\xb7\x62\0\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\x61\x30\ax.\xf2\xf\x38\x86\xc6\xe\0\0\0\f\x87\x33\xf6\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0!\xe9\xe9\xe9\xfa`\xe9\x12\xc5\xc7\x7f\xae\xc0\xc0 u\xed\xd6]\xdd{\xdc!\x93\xc9t\xd9\xed\xfe\xb3u\xab\xe2\xe3?\xd7\xf7\xdf}\xa7Z\x11\x11\xba\xf5\xb6\x8e\xba\xab\xf7\xdd\xf2\xf7\xf7/\xb0.\x83\x39\xc0\x98\xc8?`<4v\0\0\0`7\x8b\xc5\xa2\xa7\x9e|B))\xc9\x1av\xff\x3:s\xe6\x8c\x9e\x99:Yg\xcf\x9e\xd5\xa0\xc1\x43\x8a\xdc\xee\xcd\xb9oh\xe5\x8a\xe5\x1a\x36\xfc\x1\xddv[G%''\xeb\xc7\x1f~\x90\xaf\xafo9V\xf\xa0\x34\xc8?`L4v\0\0\0`\xb7\x1d;\xbe\xd7\xb6\xaf\xbf\xd2\xa6\xf8-\n\t\t\x91$\x5\a\ak\xdc\xd8\x31\xea\xd6\xbd\x87\xaaW\xaf^`\x9b\xbd\xbf\xff\xae\xb7\xde\x9c\xab\xcf\x36nRDD\xa4my\xd7n\xdd\x8b\xdc\xf\xb7\x62\0\xc6\x43\xfe\x1\x63\xa2\xb1\x3\0\0\0\xbbm\xdc\xb0^w\xf4\xbc\xd3\x36\xa8\x93\xa4\x9bnn\xa5\xec\xec\x1c\xed\xda\xb9\xa3\xd0\x41\xd8\xea\xd5\x1f\xab_\xff\xfe\xf9\x6u\xc5\x61\x30\a\x18\xf\xf9\a\x8c\x89\xc6\xe\0\0\0\xecv\xe0\x8f?\xd4\xa5k\xb7|\xcb\x2\x2\x2\x14\xdd Z\x89\t\t\x85n\xf3\xfbo\xbf\xa9[\xf7\x1eZ\xf7\xe9Z\xadX\xbeLg\xce\x9eQ\x97.]5p\xf0\x10\x5\x6\x6\x16\xbaMlLT\xa1\xcb\x7f\xdbw\xa0t\xdf\0\x80\x12#\xff\x80\x31\xd1\xd8\x1\0\0\x80]\xacV\xab\xf6\xee\xdd\xab\xdeqq\x5\x1e\v\r\xad\xa9\x33g\xcf\x14X\x9e\x93\x93\xa3\x9f\xf7\xec\xd6O?\xfe\xa0\xbb\xe3\xfaj\xe8\xfd\xc3\x95\x93\x93\xa3yo\xce\xd5\x91#\x7f\xeb\xb9\x17\x66\x16:\xe9*\x3\x38\xc0X\xc8?`\\4v\0\0\0`\x17\x93\xc9\xa4\xba\xf5\xea*99\xb9\xc0\x63g\xcf\x9eQhhh\x81\xe5^^^\n\xaeZUgN\x9f\xd6\xe4\xa9\xd3l\x83\xb8\xd0\x9a\x35\xd5\xa7w/=2\xe2QE\xd6\xae]\xe6\xb5\x3(\x1d\xf2\xf\x18\x17\x8d\x1d\0\0\0\xd8-:\xba\x81\xfe\xfc\xf3\xcf|\xcb\x32\x32\x32t\xe8\xe0!\x85\x85\x85\x17\xba\xcd\xbf\xfeu\x9d\x42\x42\xaa\xe7;3\x7f\xc5\x15\x61\x92\xa4\xa4\xa4\xa4\x42\avL\x9e\n\x18\xf\xf9\a\x8c\xc9\xcb\xd5\x5\0\0\0\xc0}\xdc\xd6\xb1\x93\xd6\xaeY\xa3\x93'N\xd8\x96\xfd\xb0k\xa7$\xa9y\x8b\x1b$\xe5\xde~\x91\x96\x96\x66{\xbc\x63\xa7\xce\xda\xba\xe5\xcb|g\xfa\xf7\xed\xdd+I\n\xafU\xab\xd0\xfdt\xea\xdc\xc5\xf6\x1\xc0\x18\xc8?`L\\\xb1\x3\0\0\0\xbb\xb5n\xdd\x46M\x9b\x35\xd3\x13\x63\x46k\xd0\xe0!JMM\xd5\x8b\xcf?\xa7\xc7\x9f\x18\xab\xe0\xe0`I\xd2\xf3\xcfN\xd7\xe6\xf8/\xb4z\xedg\n\n\n\xd2-\x1dn\xd5\xd2%\x8b\xf5\xc4\xe3\x8f\xe9\xee\xb8>\x92\xa4\xb7\xe6\xcd\xd5\xa8\xd1\x63\n}{d\x89\x33\xf6\x80\x11\x91\x7f\xc0\x98h\xec\0\0\0\xc0n\xbe~~\x9a\xf5\xfa\x1c-Z\xf4\xae\x66\xbf\xfe\x9a\xc2\xc3ki\xfc\x84\x89\xf9\x6_u\xae\xbcJ1\r\x1b\xc9\xd7\xd7WR\xee\xbb\xe6\xcc{\xfb\x1d\xbd\xbb`\xbe\x66\xbd\xfa\x8a\x42k\xd6\xd4\x80{\a\xa9k\xb7\xee\x45\xee\x87\xc1\x1c`<\xe4\x1f\x30&\xd3\x1f\x87\x8eYk\xd7\xca\x9d\xe8\xea\xc8\xb1\x93\x8a\xae[\xf8\xbd\x91\x80\xa7\xdb\xffg\x82.\xceJ\xde\xe7\x45\xb9x\x1d\xb2\x5\0p\xb6\xd2\xfc\xbf\x94|!\xa5\xcc\xeb+\x8d\xd8\x98(\xde\x15\a(F`@\x15I\xe4\x1f\xf0T\x17\x1f\x3\xb8\x62\a\0\0\0\x86\xc3\xad\x18\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p\x18\xcc\x1\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\xe1\x8c=\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63\xbc\\]\0\0\0\0\0\0\0J\x86+v\0\0\0`8s\xe7\xcc\xce\xf7\xf5\x43\x8f\x8cpQ%\0\xca\x1b\xf9\a\x1c\x43\x63\a\0\0\0\x86\xc3@\xe\xf0\\\xe4\x1fp\f\xb7\x62\x1\0\0\0\0\0\xb8)\xae\xd8\x1\0\0\x80\xe1\xf0\xae\x38\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p\x18\xcc\x1\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\xe1\x8c=\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63\x98<\x19\0\0\0\0\0\xc0Mq\xc5\xe\0\0\0\x1cr\xfcx\xa2\xde[\xb8P\xdb\xbe\xfeJ\x8d\x62\x63\x15\xd7\xa7\x9f\xaeo\xda\xd4\xaem-\x16\x8b\x1e\x1d\xf1\xb0rrr\xf4\xe6[\xf3\x8b\\\x8f[1\0\x63\"\xff\x80\xf1p\xc5\xe\0\0\0\xecv\xe2\xc4\t\r\x1d\x32H\xe9\xe9\xe9\x1a\x33\xf6IEDFj\xf0\xc0\xfe\xfa\x61\xd7N\xbb\xb6_\xf1\xe1rm\xdd\xf2\x65\xb1\xebu\xea\xdc\xc5\xf6\x1\xc0\x18\xc8?`L\\\xb1\x3\0\0\0\xbb\xad^\xb5Ru\xea\\\xa9\xa7'N\x92\xb7\xb7\xb7\xda\xb4m\xa7\x8c\xf4\f\xcd}c\x8e\xdeyw\x91L&S\x91\xdb\xee\xdb\xbbWo\xccy]=\xee\xe8\xa9\xa4\xa4\xa4\xcb\xee\x87\x33\xf6\x80\xf1\x90\x7f\xc0\x98\xb8\x62\a\0\0\0v\xb1X,z\xf7\x9dw\xd4\xad{\xfy{{\xdb\x96w\xec\xd4Y\xdb\xbf\xfbV\x7f\x1f>\\\xe4\xb6\x17.\\\xd0\xe4I\x13\x34i\xf2T\xc5\xc4\x34,v_\x9c\xb1\a\x8c\x85\xfc\x3\xc6\x45\x63\a\0\0\0vIJ:\xab\x94\x94\x64\x85\x85\x85\xe7[\x1e\x1e\x9e\xfb\xf5\xa9S'\x8b\xdc\xf6\xed\xb7\xdeTTt\xb4:v\xea\\\xa6\x35\x2(\x1b\xe4\x1f\x30.n\xc5\x2\0\0\x80]\xf2n\x9f\xf0\xf7\xf7\xcf\xb7\xbcr@eI\xd2\xf9\xf3\xe7\v\xdd\xee\xdbo\xbf\xd1\xba\xb5\x9fh\xc5\xc7\xab/{\xab\xc6\xc5\x62\x63\xa2\n]\xfe\xdb\xbe\x3\xf6\x96\v\xc0\x89\xc8?`\\4v\0\0\0`\x97\xc0\xc0@I\xd2\x85\xd4\v\xf9\x96\xa7\xa5\xa5K\x92\xaaV\xabV`\x9b\xd3\xa7Oi\xd2\x84\xf1\x9a\x36\xfdYU\xaf^C\x92\x64\x95\xb5\xd8}1\x80\x3\x8c\x85\xfc\x3\xc6\x45\x63\a\0\0\0v\xa9V-D\xde\xde\xde:v\xf4\xa8\xd4\xac\xb9m\xf9\xc9\x93'$I\xa1\x35\x42\vl\xb3`\xfe|%\x1c;\xa6\xe1\xc3\xee+\xf0XlL\x94\xee\x1d\x34X\xe3\x9ez\xba\xc0\x63L\x9e\n\x18\v\xf9\a\x8c\x8b\xc6\xe\0\0\0\xec\xe2\xeb\xeb\xab\x41\x43\xee\xd3\xa7k\xd7\xa8\xcb\xed]\xe5\xe3\xe3#I\xfa\xcf\xd6-j\xd7\xfe\x16\x45\x44\x46J\x92\x32\x32\x32\x64\x36\x9b\xe5\xed\xed\xad\xbb\xe3\xfa\xa8[\xf7\xee\xf9\x9eg\xca\xe4\x89\xfa\xed\xd7_\xf5\xd1\xc7\xabU\xe5\xbfW\x1\\\x8a\xc1\x1c`,\xe4\x1f\x30.\x1a;\0\0\0\xb0[\\\x9f\xbe\x1a\xd8\xff\x1eM\x9f\x36U\x1d;u\xd2\xfe\xfd\xfb\x35\xff\xadyZ\xb8x\xa9L&\x93\xd2\xd3\xd3\xd5\xe7\xee^\x8a\x8a\x8a\xd6K\xaf\xbc\xa6\xab\xeb\xd6-\xf0\x1c]n\xef\xa6\xea\xd5k\xa8Qlc\x17|\a\0J\x8a\xfc\x3\xc6\xc4\xbb\x62\x1\0\0\xc0n\xb5k\xd7\xd1\xa2%\xef\xcbl6k\xfa\x33Su\xf0\xe0\x1-^\xbaL\xd7\\s\xad$\xc9\xc7\xc7G\r\x1a\xc4\xa8^\xfd\xc2'?\xb5\xd7\xa6\x8d\x1bl\x1f\0\x8c\x81\xfc\x3\xc6\x64\xfa\xe3\xd0\x31k\xedZ\xb9\xf7\x43\x1e\x39vR\xd1u\xc3\x8b\xd9\x4\xf0L\xfb\xffL\xd0\xc5Y\xc9\xfb\xbc(\x17\xaf\x43\xb6\0\0\xceV\x9a\xff\x97\x92/\xa4\x94y}\xa5\x11\x1b\x13\xc5\xe4\xa9@1\x2\x3\xaaH\"\xff\x80\xa7\xba\xf8\x18\xc0\xadX\0\0\0\x30\x1c&O\x5<\x17\xf9\a\x1c\x43\x63\a\0\0\0\x86\xc3`\xe\xf0\\\xe4\x1fp\fs\xec\0\0\0\0\0\0\xb8)\xae\xd8\x1\0\0\x80\xe1p+\x6\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p\xe6\xce\x99\x9d\xef\xeb\x87\x1e\x19\xe1\xa2J\0\x94\x37\xf2\xf\x38\x86\xc6\xe\0\0\0\f\x87\x81\x1c\xe0\xb9\xc8?\xe0\x18&O\x6\0\0\0\0\0pS\\\xb1\x3\0\0\0\xc3\x61\xf2T\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe6\0\xcf\x45\xfe\x1\xc7\xd0\xd8\x1\0\0\x80\xe1p\xc6\x1e\xf0\\\xe4\x1fp\f\x8d\x1d\0\0\0\x18\xe\x83\x39\xc0s\x91\x7f\xc0\x31L\x9e\f\0\0\0\0\0\xe0\xa6\xb8\x62\a\0\0\0\xe;~:u\xee\x62\xfb\0`\x1c\xe4\x1f\x30\x1e\xae\xd8\x1\0\0\x80\x43V\xafZ\xa9:u\xae\xd4\xd3\x13'\xc9\xdb\xdb[m\xda\xb6SFz\x86\xe6\xbe\x31G\xef\xbc\xbbH&\x93)\xdf\xfa-o\xbaY\xad\xdb\xb4U\xe5\xca\x95%I\x1dn\xbdM>\xbe>\xfap\xf9\x32\xf5\xba\xab\xb7+\xbe\x5\0%D\xfe\x1\xe3\xe1\x8a\x1d\0\0\0\xd8\xcd\x62\xb1\xe8\xddw\xdeQ\xb7\xee=\xf2\xdd\x46\xd1\xb1Sgm\xff\xee[\xfd}\xf8p\x81m\x82\x82\x82l\x83\xba<5k^\xa1\v\x17R\x8a\xdc\xcf\xa6\x8d\x1bl\x1f\0\x8c\x81\xfc\x3\xc6\xc4\x15;\0\0\0\xb0[R\xd2Y\xa5\xa4$+,,<\xdf\xf2\xf0\xf0\xdc\xafO\x9d:\xa9+\xaf\xba\xaa\xd8\xe7\xf9\x61\xd7N\xdd\xd2\xe1\xd6\"\x1f\xe7\x16\f\xc0x\xc8?`L4v\0\0\0`\xb7\xa4\xa4$I\x92\xbf\xbf\x7f\xbe\xe5\x95\x3r\xcf\xc8\x9f?\x7f\xbe\xd8\xe7\xd8\xb9\x63\x87\xbe\xdc\xbcY\xeb\x37}^\xe4:\xb1\x31Q\x85.\xffm\xdf\x1{K\x5\xe0\x64\xe4\x1f\x30&\x1a;\0\0\0\xb0[``\xa0$\xe9\x42\xea\x85|\xcb\xd3\xd2\xd2%IU\xabU\xbb\xec\xf6\x89\t\t\x9a\xf8\xf4\x38=\xff\xe2L\xd5\xaa\x15Q\xe4z\f\xe0\0\xe3!\xff\x80\x31\x31\xc7\xe\0\0\0\xecV\xadZ\x88\xbc\xbd\xbdu\xec\xe8\xd1|\xcbO\x9e}\xbav\x8d\xb2\xb2\xb2l\xcb\xff\xb3u\x8b\xda\xb5\xbf\x45\x11\x91\x91\x92\xa4\x9c\x9c\x1c\xa5\xa5\xa5\xd9\x1e?w\xee\x9c\x9ex\xfc\x31\x35k\xd1\x42\xc3\x1f\xf8w\xb1\xfb\x61\xf2T\xc0x\xc8?`L\xdc\x8a\x5\0\0\0\x87\xc4\xf5\xe9\xab\x81\xfd\xef\xd1\xf4iS\xd5\xb1S'\xed\xdf\xbf_\xf3\xdf\x9a\xa7\x85\x8b\x97\xda\xde\xea\xf8\xf9g\xa7ks\xfc\x17Z\xbd\xf6\x33Y\xadV\r\x1fv\x9f~\xfd\xe5g\xdd?\xfc\x41\xed\xfe\xe9'\xdbs\xd5\xb9\xb2\x8ej\x14r\x96\x9f\xc9S\x1\x63\"\xff\x80\xf1\xd0\xd8\x1\0\0\x80\x43j\xd7\xae\xa3\x45K\xde\xd7\xe2\x45\v5\xfd\x99\xa9\xba\xbei3-^\xbaL\xb1\x8d\x1b\xdb\xd6\xa9s\xe5U\x8ai\xd8H\xbe\xbe\xbe\xda\xf6\xf5W\xfa\xf5\x97\x9f%I\x83\a\xf6\xcf\xf7\\s\xe7\xcdW\xdbv\xed\n\xec\xe3\xe2\x33\xf5\f\xf2\0\xe3 \xff\x80\xf1\x98\xfe\x38t\xccZ\xbbVn\x97\xf4\xc8\xb1\x93\x8a\xae\x1b^\xcc&\x80g\xda\xffg\x82.\xceJ\xde\xe7\x45\xb9x\x1d\xb2\x5\0p\xb6\xd2\xfc\xbf\x94|!\xa5\xcc\xeb+\x8d\xd8\x98(&O\x5\x8a\x11\x18PE\x12\xf9\a<\xd5\xc5\xc7\0\xe6\xd8\x1\0\0\0\0\0pS\xdc\x8a\x5\0\0\0\xc3\xe1V\f\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe6\0\xcf\x45\xfe\x1\xc7\xd0\xd8\x1\0\0\x80\xe1p\xc6\x1e\xf0\\\xe4\x1fp\f\x8d\x1d\0\0\0\x18\xe\x83\x39\xc0s\x91\x7f\xc0\x31L\x9e\f\0\0\0\0\0\xe0\xa6\xb8\x62\a\0\0\0\x86\x33w\xce\xec|_?\xf4\xc8\b\x17U\x2\xa0\xbc\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe4\0\xcf\x45\xfe\x1\xc7\xd0\xd8\x1\0\0\x80\xe1\x30y*\xe0\xb9\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6s\x80\xe7\"\xff\x80\x63\x98<\x19\0\0\0\0\0\xc0Mq\xc5\xe\0\0\0\f\x87[1\0\xcf\x45\xfe\x1\xc7\xd0\xd8\x1\0\0\x80\xe1\x30\x98\x3<\x17\xf9\a\x1c\x43\x63\a\0\0\0\x86\xc3\x19{\xc0s\x91\x7f\xc0\x31\xcc\xb1\x3\0\0\x80\x32\xb7g\xcfn==~\x9c\xbav\xee\xa8gg<\xa3\x84\x63\xc7.\xbb\xfe\xe8Q#\xd5\xa9s\x17\x43\xe\xea\x62\x63\xa2\\]B\x91\xa8\xad\x64\xa8\xadl\x91\xff\xf2\x41m%c\xe4\xda$\xfb\xea\xa3\xb1\x3\0\0\x80\x32\xf5\xc3\xae\x9d\x1a\x34\xa0\xbf\xea\xd5\xab\xaf\xb1\xe3\xc6\xeb\\R\x92\xee\x1b\x32P'N\x9c(\x97\xfd\xcf\x9d\x33\xbb\\\xf6S\x12\xd4V2\xd4\xe6>\xc8\x7f\xd1\x8c\\\x9b\x64\xec\xfa\xa8-?\x1a;\0\0\0(3999\x9a\xfd\xfa,\x8d|t\x94\xee\x1b:Lm\xdb\xb5\xd3\xd4gf\xa8jpU}\xbav\xcd\x65\xb7\xdd\xb4q\x83\xed\xe3\xd2\xe5\x8e|]\xd8\xf3:\xeb\xf9\xa8\x8d\xda\xca\xb3\x36wS\xd1\xf3_\xdc\xfa\x46\xae\xcd\xd1\xe7\x33rm\xc5\xado\xe4\xdaJ\xf2|\x85\xa1\xb1\x3\0\0\x80\x32s\xe8\xe0\x41\xed\xdc\xf1\xbd\xda\xdf\xd2\xc1\xb6\xacR\xa5J\xea\xd1\xb3\xa7\x16/Z$\xab\xd5Z\xe4\xb6\x46\xbd\x15\x3\x80}\xc8?P>h\xec\0\0\0\xa0\xcc\xe4\xddn\x11\x1a\x1a\x9aoy\xed\xdaut\xea\xd4I%''\xbb\xa2,\0\xe5\x80\xfc\x3\xe5\x83w\xc5\x2\0\0@\x99\x39s\xe6\xb4$\xc9\xafR\xa5|\xcb\x3\xaaT\x91$\xa5\xa4$+((\xa8\xd0m\v\x9b\x30\xf2\xb7}\a\x9c\\!\x80\xb2\x42\xfe\x81\xf2\x41\x63\a\0\0\0\x65&88X\x92\x94\x96\x96&\x1f\x1f\x1f\xdb\xf2\xb4\xd4\xd4|\x8f\xdb\xab\xa4\xef^\xf2\xc6\x9c\xd7K\xb4\x9d\xb3\xeb(\f\xb5\x95\x8c'\xd5\xe6\xae\xc8\x7f\xf1\x8c\\\x9b\xe4\xdc\xfa\xa8\xad\xec\xd0\xd8\x1\0\0@\x99\t\r\xad)I:\x9e\x98\x98\xef\xcc|BB\x82\"\"\"\x15\x10P\xa5\xc0\x36\x9c\x95\a*\x6\xf2\xf\x94\xf\xe6\xd8\x1\0\0@\x99\x89\x8a\x8eVL\xc3\x46\x8a\x8f\xff\xc2\xb6\xcc\x62\xb1h\xfdg\xeb\x34p\xf0`\xd7\x15\x6\xa0\xcc\x91\x7f\xa0|p\xc5\xe\0\0\0\xca\x8c\xd9l\xd6\xa8\xc7\x46\xeb\xc1\xe1\xc3\xe4\xe3\x63V\xc3\x86\xb1\xda\x1c\xff\x85N\x9f>\xad\xdb\xbbvwuy\0\xca\x10\xf9\a\xca\aW\xec\0\0\0\xa0L\xb5n\xd3V\v\x16\xbe\xa7}{\xf7\xea\xb9g\x9f\x91\xbf\xbf\xbf\xe6/X\xa8\x90\x90\x10W\x97\x6\xa0\x8c\x91\x7f\xa0\xecq\xc5\xe\0\0\0\xca\xdc\x8d-o\xd2\x8d-oru\x19\xc5\xb2Z\xad\xda\xb0\x61\xbd\xd6\xaf\xfbT\xc7\x13\x13uk\xc7\x8e\x1a\x38h\x88\xfc\xfd\xfd\v]\xff\xf4\xe9\xd3Z\xf9\xd1\x87\xda\xf1\xfd\xf7:\xfa\xcf\x11\xb5n\xd3V\x83\xef\x1b\xaa\x88\x88H\xa7\xd7v\xfcx\xa2\xde[\xb8P\xdb\xbe\xfeJ\x8d\x62\x63\x15\xd7\xa7\x9f\xaeo\xda\xd4\xaem-\x16\x8b\x1e\x1d\xf1\xb0rrr\xf4\xe6[\xf3\rQ[VV\x96V\xafZ\xa9\xef\xbe\xfdV?\xef\xd9\xad\x46\xb1\x8d\xd5\xad{\xfu\xea\xdc\xc5\xa5\xb5\xe5\xe4\xe4\xe8\xf3M\x1b\xb5\xea\xe3\x95:t\xf0\x80\xaeo\xdaL\x3\x6\xe\xd2\xb5\xd7\xfe\xcb\xa9uy\x92\x8a\x9a\xff\x8c\x8c\f\xad\xfb\xf4\x13m\xfb\xfak\xfd\xbcg\xb7\xae\xfd\xd7u\xbaw\xe0 ]w\xbd}\xb9tDzz\xba>X\xba\x44\xf1\xf1\x9f+00H]\xbbuW\xf7\x1ew\xc8\x64\x32\xd9\xb5\xfd\x82w\xde\xd6+/\xcd\xd4\xf7;\x7fT\x95\xc0@\xa7\xd6\xe6\xe8\xcf-\xcf\x99\x33g\xf4\xe1\xb2\xf\xb4s\xe7\xe\x1d;\xfa\x8fZ\xdc\xd8Rqq}\xd5\xb8I\x13\xa7\xd6\xb7g\xcfn\xad\xf8p\xb9v\xff\xf8\xa3nn\xddZC\x86\fUx\xadZE\xae\x7f\xe1\x42\x8a>\\\xbe\\\xf1_l\xd2\xb9\xa4sj\xd3\xae\x9d\xee\x1d\x38H\xb5jE8\xb5.g\xe2\x8a\x1d\0\0\0\xe0\xbf\x96,~O3\x9f\x7fN\x9d:w\xd1\xfd\xf<\xa8/7o\xd6\x93O\x8cQvvv\x81u-\x16\x8b\x6\xdc\xd3G\xa7N\x9eT\xdf~\xf7\xe8\xbe\x61\xc3\xf5\xfb\xef\xbf\xeb\xdf\xc3\xef\xd7\xf9\xf3\xe7\x9dZ\xd7\x89\x13'4t\xc8 \xa5\xa7\xa7k\xcc\xd8'\x15\x11\x19\xa9\xc1\x3\xfb\xeb\x87];\xed\xda~\xc5\x87\xcb\xb5u\xcb\x97N\xad\xa9\x34\xb5\xa5\xa6\xa6j\xf4\xa8\x91\xda\xb0~\xbd\xda\xb6k\xaf)\xd3\x66\xa8u\x9b\xb6JLHpym\x1f\xaf\xfcH3_xN\x9d\xbb\xdc\xaeIS\x9eQ\xb5\x90\x10\xdd{O_\xfd\xf1\xc7>\xa7\xd6\x6\xe3q$\xff\x92\xf4\xca\xcb\x33\x15\xff\xc5\x17j\x7fK\a\x8d\x19;N&\x93I\x3\xee\xe9\xab\xbd{\x7fwj]\x16\x8b\x45O=\xf9\x84\xbe\xfb\xee\x1b\r\xbb\xff\x1\xdd\xd6\xb1\x93\x9e\x99:Y\x8b\xdf[d\xd7\xf6{\xf6\xec\xd6+/\xcdtjM\x17s\xf4\xe7&I\x87\xf\x1f\xd6\xc0\x1\xfdt,\xe1\x98\xe2\xfa\xf4\xd5SOOT\x9d:ut\xf6\xecY\xa7\xd6\xf6\xc3\xae\x9d\x1a\x34\xa0\xbf\xea\xd5\xab\xaf\xb1\xe3\xc6\xeb\\R\x92\xee\x1b\x32P'N\x9c(t}\xab\xd5\xaag\xa6N\xd1\x37\xdb\xbe\xd6\x90\xfb\x86i\xc4\xa3\xa3t\xe4\xef\xbf\xf5\xef\a\xeeWJr\xb2Sks&\xae\xd8\x1\0\0\0$\x9d:uR\xaf\xbc\x34So/xW-Z\xdc Ij\xd8\xa8\x91:\xdf\xd6\x41?\xec\xda\xa9\x16\x37\xdc\x98o}\xb3\xd9\xacw\x17-Qxx\xb8mY\xab\xd6\xadu\xdb-\xed\xf4\xcb\xcf{ts\xab\xd6N\xabm\xf5\xaa\x95\xaaS\xe7J==q\x92\xbc\xbd\xbd\xd5\xa6m;e\xa4gh\xee\x1bs\xf4\xce\xbb\x8b.{\xd6~\xdf\xde\xbdzc\xce\xeb\xeaqGO%%%9\xad\xa6\xd2\xd4\xb6\x61\xfd:\xa5\xa4\xa4h\xee\xbc\xb7\x8b=\xab_\xde\xb5-\xfb`\xa9\x6\f\x1c\xac^w\xf5\x96$\xb5i\xdbV\xbf\xff\xf6\x9b\xbe\xdf\xbe]\r\x1a\xc4\x94Y\xadp-G\xf3/IC\x87\rW\xf5\xea\xd5\xe5\xed\xed-I\xea\xd8\xa9\xb3\xfe>|X[\xbf\xfcR\r\x1b\x36rZm;v|\xafm_\x7f\xa5M\xf1[l\xb7\xb0\x5\a\ak\xdc\xd8\x31\xea\xd6\xbd\x87\xaaW\xaf^\xe4\xb6\xe7\xcf\x9f\xd7\xa4\t\xe3\xd5\xfb\xee>Z\xf9\xd1\x87N\xab)OI~n\x92\x34o\xee\x1cu\xec\xd8Y#\x1e\x1d\x65\xcb\x61\xdbv\xed\x9dZ[NN\x8e\x66\xbf>K#\x1f\x1d\xa5\xfb\x86\xe\x93$\xddp\xe3\x8d\x1a\x32p\x80>]\xbb\x46\x43\x87\r/\xb0\xcd\x89\x13\xc7\xf5\xe9\xdaO\xf4\xde\x92\xf7\xd5\xacy\vIR\xf3\x16-\xd4\xe6\xe6\x96\xda\xbf\x7f\xbf\xddWI\x96\x37\xae\xd8\x1\0\0\0$}\xfb\xcd\x37\xaa^=D\xd7_t\x1b\x45\xed\xdautk\xc7N\xda\xbc\x39\xbe\xd0m.n\xeaHR\xd5\xaa\xd5$I\x99\x99YN\xab\xcb\x62\xb1\xe8\xddw\xdeQ\xb7\xee=l\x3H)w\x10\xb9\xfd\xbbo\xf5\xf7\xe1\xc3\x45n{\xe1\xc2\x5M\x9e\x34\x41\x93&OULLC\xa7\xd5T\x9a\xdarrr4\xe7\xf5\xd7\xd5\xef\x9e\xfe\x65\xda\xd4)\xe9\xcf\xadZ\xb5\x10\xfds\xe4\x88\xacV\xab\xa4\xdc[\xc6\x92\x92\xce*\"\xc2\xb8\xb7\x61\xa0\xf4J\x92\xff\x9a\x35k\xe6{my{{+,<\\\x19\x19\x19N\xadm\xe3\x86\xf5\xba\xa3\xe7\x9d\xf9\xe6%\xba\xe9\xe6V\xca\xce\xce\xd1\xae\x9d;\x8a\xdc\xcej\xb5j\xce\xecYjy\xd3\xcd\xea~\xc7\x1dN\xad)OI~n\x7f\xfd\xf5\x7fZ\xfb\xc9\x1a\xf5\xec\xd5\xcb\xee[\xc9J\xe2\xd0\xc1\x83\xda\xb9\xe3{\xb5\xbf\xa5\x83mY\xa5J\x95\xd4\xa3gO-^\xb4\xc8\x96\xf1\x8bU\xaaTI\x92\x94\x98\x98h[\x96|>\xf7J\x9d\x9aW\\Qf\xb5\x96\x16W\xec\0\0\0\0\x92\x8e\x1e=\xaa\xe8\x6\x31\x32\x9b\xf3\xff\x89\xdc \xba\x81\xf6\xec\xd9m\xd7s\x1c\x9fo\xbf\xfd\x46\xeb\xd6~\xa2\x15\x1f\xaf.\xb3\xb3\xe2%\xa9\xed\xcc\xe9\xd3\x92\xa4\xa1\x43\x6\xe9\x91\x91\xa3\xf4\xd4\xf8\tJHH\xd0\x8b\xcf?'\x8b\xc5\xa2\xbb\xe3\xfa\xb8\xac\x36Ij\xdc\xa4\x89\x86\xe\x1b\xae\x97g\xbe`[6n\xf3\xd6\x2\x3WT,\xa5\xcd\xbf$}\xb8|\xb9\xa2\xa2\xa3\xd5\xbaM[\xa7\xd5\x65\xb5Z\xb5w\xef^\xf5\x8e\x8b+\xf0XhhM\x9d\x39{\xa6\xd0\xed\x12\x13\x13\x35i\xe2\xd3\x9a\xf5\xfa\x1b\x5\x9a\x17\xceT\x92\x9f\xdb\xc9\x93\xb9\r\x97g\xa6MQ\xc7N\x9d\xd4\xb1s\x17\xfd\xb0k\x97\x6\xf\xec\xaf\x15+W+\xb6qc\xa7\xd4v\xe6L\xee\xb1\xc6\xef\xbfW\xe1\xe4\t\xa8RE\x92\x94\x92R\xb0\xb1#I]n\xef\xaao\xb6m\xd3\xf3\xcfN\x97$\xddyWo\xb5q\xe2\xef\xb4,pt\x2\0\0\0$U\xafQ]\xbf\xfc\xfcs\x81\xe5)))\xba\xea\xea\xab\x8b\xdd~\xd1\xc2\x5:\x7f\xfe\xbc\x86\xe\xbb\xdf\xa9u\x5\xfe\xf7\x1dl.\xa4^\xc8\xb7<--]\x92T\xb5Z\xb5\x2\xdb\x9c>}J\x93&\x8c\xd7\xb4\xe9\xcf\xaaz\xf5\x1a\x92$\xab\n\xdev\xe0\x8a\xda\xfc+\xe7\x36V\xbau\xef\xa1\x7f?\xf4\xb0myVV\xa6\xe6\xbf\x35O\xbd\xef\x8esJ#\xaa$\xb5Y\xadV=;\xfd\x19\x9d:uJ\x1f}\xbcZ5\xaf\xb8\x42k?\xf9\x44\xb7w\xee\xa8\x97_}M\x1dn\xbd\xad\xd4u\xc1\x98J\x9b\xff\x1d\xdfo\xd7[o\xce\xd5\a\xcbW\xc8\xc7\xc7\xc7iu\x99L&\xd5\xadWW\xc9\x85L\xdc{\xf6\xec\x99\x2W\xa3H\xb9M\xe6\x17\x9e\x9b\xa1{\xee\x19`\x9b\x13\xc6\xf9\xe9\xcfU\x92\x9f[@@\x80$\xe9\x89\xb1O\xaaQln\x13\xa7\x45\x8b\x1bt\xe8\xe0\x1mX\xff\x99\xd3\x1a;y\r\xa7\xb4\xb4\xb4|\xbf\x93\xb4\xd4\xd4|\x8f_,99YC\a\xf\xd4M\xadZ\xe9\xf3\xcd[\x94\x91\x9e\xa1\x45\x8b\xdeU\xcf;\xbai\xfe;\vuu\xdd\xbaN\xa9\xcd\xd9\x98\x63\a\0\0\0\x90T\xabV-\xed\xdd\xfb\xbb\x32\x33\x33\xf3-?\xfc\xd7_\xba\xfa\xea\xcb\xff\x31\xbf\xfe\xb3u\xfa`\xe9\x12\xbd\x38\xf3\x65\xa7\xbf\x95p\xb5j!\xf2\xf6\xf6\xd6\xb1\xa3G\xf3-\xcf;\xeb\x1dZ\xa3\xe0\xc0n\xc1\xfc\xf9J8vL\xc3\x87\xdd\xa7\xd8\x98(\xc5\xc6\x44i\xe6\v\xcf\xeb\xab\xfflUlL\x94\x9e\x7fn\x86\xcbj\xcb\x9b'\xa4]\xfb[\xf2-\x8f\x88\x88\xd4\xd1\xa3\xff\x38m~\x92\x92\xd4v\xf0\xc0\x1}\xb8\xfc\x3\x8d\x1c\x35J\x8d\x62\x1b\xab\x46\x8dP\xdd\x37t\x98\x1e|\xe8!M\x99<\xf1\xb2\xef\xf2\x3\xf7V\x9a\xfc\x1f\xd8\xbf_\x8f\x8exD\xaf\xbc\xf6\xba\xea\xd5\xaf\xef\xf4\xda\xa2\xa3\x1b\xe8\xcf?\xff\xcc\xb7,##C\x87\xe\x1e*p\xab\xa1$}\xb9\x39^\x9fo\xda\xa8\x39\xb3g\xd9\xf2?h\xc0=\x92\xa4\x1b\x9a_\xaf\xd6\x37\x17>\xa1qI\x94\xe4\xe7V\xa3\x46n\xb3\xb9ZH\xfeI\x9f\xeb\xd5\xab\xaf\x84\x84\x63N\xab-4\xb4\xa6\xa4\xdc\xab\x8a.\x96\x90\x90\xa0\x88\x88H\x5\x4T)\xb0\xcd\x96/7\xebX\xc2\x31\x8d\x18\x39J\x11\x11\x91\xaa[\xaf\x9e&O\x99\xa6\xda\xb5\xebh\xe9\x92\xc5N\xab\xcd\xd9h\xec\0\0\0\0\x92nly\x93\xce\x9c>\xad\x9d;\xfe\x37\x19\xe9\xc9\x93'\xf5\xd9\xbaO\xd5\xe1\xb6\xff]\xa9\x91\xfa\xdf\xb3\xbdy>\xdf\xb4Q\xaf\xbc\xfa\xe8\xe3\xd5\xf9>\xf2\xce\x82\x7f\xf4\xf1j\xdd\xd3\x7f@\xb9\xd6\x96\x93\x93\xa3\xb4\xb4\x34IR\xe5\xca\x95uw\\_m\xdd\xf2\xa5rrrl\xdb\xec\xfe\xe9'5m\xda\xcc\x36y\xa9+j\xcb\xca\xca\x1d\x9c^\xfa;\xaeU+B\x99\x19\x99\xf9\xea\x45\xc5R\xd2\xfc\x1f\xa7\x9b[\xb5*\x93\xdan\xeb\xd8Ik\xd7\xac\xd1\xc9\x8b\xde\xa2\xfb\x87];%I\xcd\xff\xfbNT\x17\xd7v\xed\xb5\xff*\x90\xff\x87\x1f\x19)I\x9a\xff\xee\"\xbd=\xff]\xa7\xd5V\x92\x9f[\x83\x98\x86\xaa]\xbb\xb6\xbe\xfa\xcfV\xdb\x32\xab\xd5\xaa\xad[\xb7\x38u\x92\xf7\xa8\xe8h\xc5\x34l\xa4\xf8\xf8/l\xcb,\x16\x8b\xd6\x7f\xb6N\x3\a\xf\xb6-\xbb\xf8\xd8\x99\x91\x91\xa1\xf4\xb4\xb4|\rfooo\xd5\xa9s\xa5\xd2\x33\xd2\x9dV\x9b\xb3y\x8fx\xf4\xf1)\xc1\x81\xb9\x97\x42\x9dONU\xf5j\xce=\xc3\0T\x14\xa7\xcf\xa6\xe8\xe2\xac\xe4}^\x94\x8b\xd7![\0\0g+\xcd\xffK\x99Y\x99\x97]\xd7S\x5\x5\x5\xc9j\xb5j\xd6k\xaf\xa8\x46\x8d\x1aJH8\xa6\x97g\xbe\xa8\x7f]w\x9d\xfa\xf\xb8W&\x93I\x87\xe\x1d\xd4\xed\x9dnU\xd5\xaa\xd5\xd4\xb8q\x13m\xda\xb8\x41\xa3G\x8d\xd4\xc8G\x1fS``\xa0\x12\x12\x12\x94\x90\x90\xa0\x93'O\xea\x8a\xb0\x30\xa7\xd5V\xbbvm-\x98?_\x87\xf\x1f\x96\xaf\xaf\x8f\x36\xc7\xc7k\xde\xdc\x39z\xe6\xd9\xe7\x14\x16\x16\xa6\xf4\xf4t\xf5\xb9\xbb\x97~\xfc\xe1\au\xec\xd4Y\xd5\xaaUSh\xcd\x9a\xf9>RR.\xc8l6k\xc8}\xc3\x14\x1c\\\xb5\xdcj\x93\xa4\xe7\x66<\xa3\x19\xcfL\xd5\x9d\xbdz\xcb\xcf\xcfO\x91\x91\xb5\xf5\xcaK3u\xea\xe4\t\x99L&\xed\xdc\xb9\x43\xf3\xde\x9c\xab\x89S\xa6*2\xb2\xb6\xcbj\xaby\xc5\x15\xda\xfb\xfb\xefZ\xfb\xc9j\x5\x4\x4\xe8\xdc\xb9$\xfd\xf4\xd3\x8f\x9a=\xeb\x35\xdd\x37l\x98\x9a\x36m\xe6\xb4\xdaJ\xcb\xcf\xd7W\x12\xf9w\x96\x92\xe4\xff\xe0\x81\x3\xba\xa3\xfb\xed\xba\xf5\xb6\x8e\xba\xbei3[\xfe\x13\x12\x12T-$\xc4i\xf3\x32\x45\x46\x44\xea\xc7\x1f\x7f\xd0\xa6M\x1b\x15\x1c\x1c\xac\xdf\x7f\xfbM\xd3\xa6L\xd6\xa3\x8f\x8d\xb6\xbd&7\xac\xffL}\xef\xbeK\xadZ\xb7Q\xddz\xf5\n\xe4\xdf\x64\x32i\xcd\xeaU\x9a\x30\x61\x92\xad\xa9\xe9\f%\xf9\xb9\x99\xcd\x66\x85\xd4\xa8\xa1\tO=\xa9*\x1U\x94\x9a\x96\xaauk?\xd1\x37\xdf|\xad\xc9S\x9f\xb1\xdd\xaaUZ^^^\xaaU\xab\x96&M\x18/???\xa5\xa5\xa5k\xf1\xa2\x85\xfa\xf3\xcf\x43\x1a\x33v\x9c\xfc\xfd\xfd\v\x1c;C\xaa\x85h\xdd\xa7\x9fj\xf7\xee\x9fT\xa9R%\x9d\x6\x30\xc7\xe\0\0\x80\x87\x31\x99L\x85\xbe\xcd+\xa4\x87\x1e\x1e\xa1\xb0\xb0p\xadY\xbdJ\xe9\xe9i\xba\xad\x63g\xf5\xef?@^^\xb9\x17\xba\a\x4TQllc\xdb\xed\xf\x33_|^\x92\xf4\xec\xf4i\xf9\x9e'*\xba\x81\xd6\xac]\xe7\xb4\xbaj\xd7\xae\xa3\x45K\xde\xd7\xe2\x45\v5\xfd\x99\xa9\xba\xbei3-^\xba\xccv\x15\x8e\x8f\x8f\x8f\x1a\x34\x88\xd1\xd5u\xeb\x39m\x9f\xce\xaaM\x92\xea\\y\x95\x62\x1a\x36\x92\xef\x7f\a\"\xf5\xa3\xa2\xb4\xe4\x83\x65Z\xb2\xf8=M\x9b\x32Y\x8d\x9b\x34\xd1\x9c\xb9o\xaai\xb3\xe6.\xad\xcd\xd7\xd7W/\xcc|Y\xabW}\xac\x8fW\xae\xd0_\xff\xf7\x97\x1a\x37i\xa2'\x9e\x1c\xa7[:\xdc\xea\xd4\xdaJ\xa3\x34s\x10\x91\xff\xa2\x39\x9a\xff\x35\xabW\xd9\xfe\xcd\xfb<\xcf\xe6\xad_\xdb\x9a\x87\xa5\xe5\xeb\xe7\xa7Y\xaf\xcf\xd1\xa2\x45\xefj\xf6\xeb\xaf)<\xbc\x96\xc6O\x98\xa8N\x9d\xbb\xd8\xd6\xa9Q#TM\xae\xb9\xb6L'J.\x8a\xa3?7I\xba\xfd\xf6\xae\xf2\xafTI\xabW\xad\xd2\xb2\xf\x96\xea\xe6Vm\xf4\xe1\x8a\x8fU\xb3\x66M\xa7\xd6\xd6\xbaM[-X\xf8\x9eV~\xb4\x42kV\xafR\xdb\xb6\xed\x35\x7f\xc1\x42\xdb-\xa1\x97\x1e;\xc3k\xd5\xd2\x92\xf7\x97i\xf9\xb2\xf\x34\xf7\x8d\x39\xba\x90\x92\xac\xa6\xcd[h\xd9\x8a\x95\xba\xe6\x9ak\x9dZ[i\\z\f0\xfdq\xe8\x98\xb5v\xad\xdc\xfbK\x8f\x1c;\xa9\xe8\xba\x5\xef\xd1\x3 \xed\xff\x33\x41\x17g%\xef\xf3\xa2\\\xbc\xe\xd9\x2\0\x38[i\xfe_\xba\x90vA99\f\xec\0w\xe5\xe5\x65R\x80\x7f\xeeU\r\xe4\x1f\xf0<\x97\x1e\x3\x98\x63\a\0\0\xc0\xc3x{q\xd1\x36\xe0\xceJ\x93\x61\xf2\xf\xb8\xbfKsLc\a\0\0\xc0\xc3x\x9b\xbd]]\x2\x80R(M\x86\xc9?\xe0\xfe.\xcd\x31\x8d\x1d\0\0\0\xf\x63\xf6\x32\xcb\xab\x14st\0p\x1d/\x93If\xef\x92_uC\xfe\x1\xf7V\xd8\x31\x80\xc6\xe\0\0\x80\x87\x31\x99$???W\x97\x1\xa0\x4\xfc\xfc\xfcT\x9a\xb6\f\xf9\a\xdc[a\xc7\0\x1a;\0\0\0\x1e\xc8\xecm\x96\xaf\xd9\xc7\xd5\x65\0p\x80\xaf\xd9\xa7TW\xeb\xe4!\xff\x80{*\xea\x18@c\a\0\0\xc0\x3\xf8\xf8\x98\x95\x95\x65\xc9\xb7\xcc\xcf\xcf\x8f\xc1\x1d\xe0&|\xcd>\x5\xae\xb4\xc9\xcc\xb2\xc8\xd7\xa7\xf8\x46\xf\xf9\a\xdc\xdf\xe5\x8e\x1\x34v\0\0\0<\x80\xaf\x8fY\x99\x97\f\xec\xa4\xdc\xc1\x9d\x7f\xa5J\xcc\xb9\x1\x18\x94\x97\xc9$\xffJ\x95\n\xbd}*+\xd3\"\x1f\xdf\xe2\x1b;\xe4\x1fp_\xf6\x1c\x3x\xaf;\0\0\0\xfP\xc9\xcfW\xe9\x99\x99\nP\xa5\x2\x8f\x99\xbd\xcd\xf2\xael\x96%\xdb\xa2lK\xb6\xb2s,\xb2Z%\xab\xd5\xea\x82J\x1\xcf\x66\x32\x99\x64\x32\xe5\xbe\x9d\xb1\xb7\xd9[fos\x91s\xea\x64\x64\x66\xca\xdf\xbf`\xa6/E\xfe\x1\xf7Q\x92\x63\0\x8d\x1d\0\0\0\xfP\xd9\xdfO)\xa9iJMKW\xe5\x42\x6\x82&I>\xde\x66\xf9x\x9b%1\xb1*`tg\x92\x92\x65\xc9\xc9\x91\x7f%\xdf\x62\xd7%\xff@\xc5s\xf1\x31\x80\xc6\xe\0\0\x80\x87\xa8^5P\xc7\x8e\x9f\xc9=\x1b(\xc9\xc7\xd7G\xde^\xdc\x99\xf\xb8\x8b\xcc,\x8b\xb2\x32-J\xcf\xccRNv\xb6j\xd6\xa8j\xf7\xb6\xe4\x1fp\x7f\x45\x1d\x3h\xec\0\0\0x\booo\xd5\xae\x15\xaa\xb3\xe7\x92\x65\xc9\xb6*+)E\xd9\x39\x39\xae.\v\x80\x9d|}\xcc\xf2\xf1\x35\xab\xb2\xbf\xaf\xfc+9ve\r\xf9\a\xdc_Q\xc7\0\x1a;\0\0\0\x1e\xa6Zp\xa0\xabK\0\xe0\"\xe4\x1f\xa8x\xb8\xf6\xe\0\0\0\0\0\xc0M\xd1\xd8\x1\0\0\0\0\0pS4v\0\0\0\0\0\0\xdc\x14\x8d\x1d\0\0\0\0\0\0\x37\x45\x63\a\0\0\0\0\0\xc0M\xd1\xd8\x1\0\0\0\0\0pS4v\0\0\0\0\0\0\xdc\x94\xd9\xd5\x5\0\0\0\xa0|\x9dIJVvN\x8e\xb2\x32-\xca\xce\xc9qu9\0\xec\xe4\xe3\x63\x96\xaf\x8fY\xfe~~\xf2\xf7\xf7-\xd1s\x90\x7f\xc0}\x15u\f\xa0\xb1\x3\0\0\xe0!22\xb3t\xe2T\x92\xaaW\v\x92\x97\xc9$\x9f\xaa\x66y{q\x1\x37\xe0.\xb2\xb2,\xca\xcc\xb2(-3C\xc9\xa9\xa9\xaa^5P\xde\xde\xdevmK\xfe\x1\xf7W\xd4\x31\x80\xc6\xe\0\0\x80\x87\x38q*I\xb5k\x85\xba\xba\f\0%\xe4\xe3\x63\x96\x8f\x8fY\x1\xaa\xa4\xd4\xb4t\x1d;~\xc6\xeeL\x93\x7f\xc0\xfd\x15u\f\xa0\xb1\x3\0\0\xe0\x1\xce$%\xabz\xb5 W\x97\x1\xc0I*\xfbW\x92\x64\xd2\xd9s\xc9\xaa\x16\x1cx\xd9u\xc9?P\xf1\\|\f\xe0\xda;\0\0\0\xf\x90\x9d\x93#/\x93\xc9\xd5\x65\0p\"/\x93\x64\xc9\xb6\x16\xbb\x1e\xf9\a*\xa6\xbc\x63\0\x8d\x1d\0\0\0\xf\x90\x95i\x91\x8f/\x17k\x3\x15\x89\x8f\xaf\x8f\xb2\x32\xb3\x8a]\x8f\xfc\x3\x15S\xde\x31\x80\xc6\xe\0\0\x80\a\xc8\xce\xc9\x61\xa2T\xa0\x82\xf1\xf6\xf2\xb2\xeb\x9d\xad\xc8?P1\xe5\x1d\x3H7\0\0\0\0\0\x80\x9b\xa2\xb1\x3\0\0\0\0\0\xe0\xa6\xb8\xd1\x12\0\0\0@\x99zq\xe6\xab\xfa~\xc7.I\xd2\r-\x9ai\xec\x13\x8f\xb9\xb8\xa2\xff\x19?a\xb2\xb6o\xdf\x61\xd7\xba\x91\xb5kk\xf1\xc2\xb7\xcb\xb8\"\xa0\xe2\xe1\x18P\xb6h\xec\0\0\0\xc0\xe9\x8e\x9f\x38!\xb3\xb7\xb7\xaaW\xafn\xd7\xfa\x63\xc7\x8e\xd3\x87+?\x91$U\xaa\xe4\xab\x9bnh\xa6\x1ewtW\xb7\xae]\xe5\xe3\xe3\xa3\xe3\xc7O\xa8\x45\xcb\xb6Z\xbd\xf2}]\x7f\xfd\xf5v=gNN\x8e\x92\x93\x93\xe5\xef\xef/___\xbb\xb6\x39t\xe8\x90>[\xbf^;v\xec\xd4\xfe\x3\x87\xd4\xf4\xbak5p\xe0\xbdj\xd9\xb2\xa5]\xdb\xa3p\xf1[\xb6\xea\x8f}\xfb%I\xc9\x17R\f5\xa8KH<\xae\x43\x7f\xfe\xe5\xea\x32*\x14G\xf2\x9f\x93\x93\xa3\xab\xeb\xc7\xda\xbe\xe\xadQM7\xdd\xd8\x42\x3\x6\xdc\xa3\x16-ZH\x92\xfe\xf3\x9f\xffh\xe0\x90\a\xf5\xfb/\xbb\x14\x10\x10`W\r\x99\x99\x99JKKS``\xa0\xbcJ8\xbf\xd0\x9b\xf3\xe6\xe9\xf9\x17g\xe9\xd7=;\x14\x18x\xf9\xb7\x94\xc7\xe5q\f([4v\0\0\0\xe0T\xc9\xe7\x93\x35p\xf0p\xdd\xd6\xa1\xbd\xc6<\xfe\xa8\xdd\xdbuhw\xb3\x1ex\xe0~eeei\xef\xbe}\x1a\x37~\xb2\xe\x1d:\xa4\x31\x8f?^\xb2:\x92\x93u\xcdu7j\xc5\xb2\x45\xba\xe1\x86\x1b\xec\xda\x66\xc9\x92\xa5\xf2\xf5\xf5U\xff\xfe\xf7\xc8\xdb\xcbK\xdb\xbf\xdf\xa1\xbe\xfd\xef\xd3\xf2\xf7\x17\xaa\x65\xcb\x1b\x1d\xda\xff\xd1\xa3G\xb5o\xdf\x1u\xe8\xd0\xae\xd0\xc7\x37o\xde\xaa\x98\x98(EDD8\xf4\xbc\xee\xe0\xd3\xcf\xd6\xeb\xc7\x1fw\xeb\xbe\xc1\x83\xf4\xe3\xee\xdd\xfa\xbf\x43\xffg{\xec\xff\xe\xfd\x9f>^\xb5\x46-\x9a\x37\xd7\x82\x85\x8b\xd4\xb4\xe9u\xea\xde\xf5\xf6r\xafq\xfc\x84\xc9\x8a\xbb\xbb\xb7$\xa9\x7f\xff>\xba\xbds\xa7\xcb\xae\xff\xe3O{\xb4\x66\xcd'\xa5\xda\xa7'\xbc&J\x9a\xffG\xfe=Tm\xda\xb4Vjj\xaa\xbe\xfez\x9b\xee\xee;H\v\xdfyC\xb7\xdcrK\x89\xea\xf8\xe9\xa7\x9f\x14\xd7o\xb0~\xfei\xbb\x82\x83\x83K\xb4\xfd\xf3/\xce*\xd1\xbe%\xcf\xf8]_\xe\xc7\x80\xc2\x95\xd5\xeb\x82\xc6\xe\0\0\0\x9c&3+K\xf<4R5Ck\xe8\xd1\x91\xf;\xb4mX\xd8\x15\xb6\x6L\xabV\xadT34T#\x1f\x1b\xa7~}\xfb\xcal\xf6)\x8br\v\x98\x34ib\xbe\xb3\xfb\x1d;vTZj\xaa\x36n\xdc\xe8pcg\xdf\xbe\x3zh\xc4\x63\x9a\xf5\xea\x8b\xea\xdc\xe9\xb6|\x8fm\xdc\xf4\x85\x1e}l\xac\xe6\xce~\xb5\xc2\r\xecRS\xd3\x34}\xfa\x8bJ<~\\\xef.\\R\xe0\xf1\xcc\xac,\x8d\x1e\xf3\x94\xed\xeb\r\x1b\xbeP\x87\xf6\xedU\xb9\xb2\x7fy\x96\xa9\xed\xdbw\xa8\x43\xfb\xf6\x92\xa4zu\xeb\xaa\xd5\xcd\x97\xbf*+#=\xa3\xd4\x83\xba\x8a\xfe\x9a(M\xfe\xebG\xd5\xb7\xe5\xbf}\xfb\xf6\xca\xcc\xcc\xd4\xb3\xcf\xbdX\xe2\xc6Ni\x9c?\x7f^c\x9f\x1c\xaf~q=\xb5l\xc5\x9a\x12=GE\xff]_\xe\xc7\x80\xa2\x95\xd5\xeb\x82\xc9\x93\x1\0\0\xe0\xb0\xcc\xac,}\xf3\xedw\xf9\x96\xe5\xe4\xe4\xe8\x89'\xc6\xeb\xec\xd9$\xbd\xf9\xc6,\xf9\xf8\x94\xee\x1c\x62\xde-W\xff\xfcs\xb4\xd0\xc7\xadV\xab\xe2\xe3\xe3u\xff\xf0\a\xd4\xb6]\aM\x98\x30Q\xff\xfc\xf3\x8f\xa4\xff]\xad#Iq\xfd\x6\xeb\xca\xba\r\xb5s\xe7\xae\x62\xf7y\xe9-\x1bV\xabUii\xe9%:\xe3\xdf\xa1\x43;\xcdz\xf5\x45=\xfa\xd8Xm\xdc\xf4\x85my\xde\x1f\xef\xb3^}\xb1\xc8\xb3\xb6\xee\xec\xed\xf9\v\x94x\xfcx\x81\xe5U\xabVU\xd5\x42~\x8e\x89\xc7\x8f\xeb\xad\xf9\xef\x96Gi.WQ^\x13\xe5\x91\xff\xd6\xad[\xe9\xc0\xa1\xc3:w\xee\\\xe1\x35\x64\x66j\xe1\xa2\x45\x8a\x8b\xeb\xab\xdb\xbbv\xd7\xeb\xb3g+%%E\x92\xb4s\xe7.\xc5\xf5\x1b,I\xba\xe6\xba\x1bue\xdd\x86JNN\xb6k\xbfV\xabU\xaf\xbc\xf2\xaaZ\xb7\xbaIw\xdeyg\x89\xeb\xaf(\xbf\xeb\x92\xe0\x18P\xb4\xb2z]p\xc5\xe\0\0\0\x1c\x92\x99\x95\xa5\x87\x1f\x19\xad/\xe2\x37\xeb\xa5\x17g\xa8\xf7]\xb9\x83\x9f\xe7_|Y;w\xfd\xa8\x35\xab\x96\xabJ\x15\xfb\xe6\xc1\xb8\x9c\x8c\x8c\fIRhh\x8d\x42\x1f\xffx\xd5*\xbd\xf2\xca\xeb\x1a\x37n\x8c\x82\x83\x82\xb4\x62\xc5J\xf5\xec\x15\xa7\x8d\xeb\xd7\xaaZ\xb5jZ\xfa\xde|\r\x18t\xbf\xc6<\xf6\xb0:t\xb8\x45\x91\x91\x91v\xed\x37;;[\xbbw\xef\xd1\x85\v)\xda\xfa\x9f\xafro\v\x1b\x37\xb6\x44\xdf\x43\xe7N\xb7\xd9\xfe\x88\x9f\xf5\xea\x8b\x92\x64\xfb\xfc\xd2\xb3\xb5\x15\x45\xf3\xe6\xcdT\xb3\x66\xa8N\x9c\x38i[6\xf2\x91\a\xf5\xd8\xa8\x11\x92\xa4\x99/\xbd\xa6\xb9\xf3\xe6\xdb\x1e\v\xf\v\xd3\x8d\x37\x34/\xf6y\x8f\x1e=\xaa\xaf\xb7}W\xe8\x63\xad[\xb5t\x9b+\x1f\xdc\xfd\x35Q^\xf9OOOWh\x8dj\x85\xcemc\xb5Z5q\xe2$\x1dKH\xd0\xbf\xff\xfd\x80\xd2\x33\x32\xf4\xdak\xaf\xeb\xe7\x9f\x7f\xd1\xdbo\xcdS\x83\x6\xd1\x1a\xf3\xd8\xc3z\xe9\xd5\x37\xb4\xf4\xbd\xf9\xaa^=D\x95+W\xb6k\xbf\xf1\xf1\xf1\xda\xf6\xcdw\xfax\xe5\x87\xfa\xe3\x8f?J\xf5=\xb8\xfb\xef\xba\xa4\x38\x6\\^Y\xbc.h\xec\0\0\0\xc0ny\x83\xba\x9f\x7f\xfe\x45\x8f\x8f\x1e\xa9\xb1\xe3&J\x92\xce\x9fO\xd1\xf2\xe5\x1f\xeb\xe3\x95\x1f\xa8\x66\xcd\xd0R\xed\xc3j\xb5\xea\xf4\xe9\xd3Z\xb8p\x91:wl\xa7+\xaf\xbcR\xa7N\x9d\xce\xb7\xce\x85\v\x17\x34}\xc6\v\x9a\xf5\xeaL\xb5m\xdbV\x92\xd4\xe2\x86\x1b\xd4\xe5\xf6\xeeZ\xbd\x66\x8d\xee\x1f\x36L\xd7\\\xd3$wy\x8b\xe6j\xd4\xa8\x91\xdd\xfbOOOW\xaf\xbb\xfb\xdb\xbe\xee}g\xb7\x12O\xbe*\xe5\xff#^R\x85\x1f\xd4\xdd|\xd3\x8d\xda\xf8\xd9j]\xdf\xbc\x95$\xa9jp\xb0\x1e\x1b\x35\xc2\xf6\x33\x1c\xf3\xf8\xa3Z\xf2\xc1r%\x9f\xcf\xbd\x82\x62\xe3\xfa\xd5\xaaZ\xb5j\xb1\xcf\x1b\x1e\x1e\xae\xdd{~\xd6\xb2\xe5\x1f\xe5[~O\xbf\x38\xc5\xdd\xdd\xcb\xc9\xdf\x45\xd9r\xd7\xd7\x44y\xe4?;;[\x7f\xff\xfd\xb7\xde]\xf8\x9e\xfe\xfd\xc0\xb0\x42\xb3\xf7\xcb/\xbfh\xf9G\x9fh\xc7w[u\xc5\x15WH\x92\xea\xd7\xab\xaf[;u\xd7\xae]\xbb\xd4\xa2\x45\v\xb5h\x91\xdb(\xb8\xe6\x9a&v_q\x97\x90\x90\xa0'\x9f\x9a\xa8\xb7\xde\x9cS\xa2\xab\xf4\n\xe3\xae\xbf\xeb\xd2\xe0\x18Pq\xbd\x15\x19\xe9\x9a\xc9)\xf3\xde\x6w\xcf\xcf?\xab\x46\x8d\x1a\xaaS\xa7\x8e\x9ex|\x84\xc6\x8d\x9f\xac\xd4\xd4TEDDh\xe3\xc6M\xbap\xe1\x82z\xf4\xe8!)\xb7\x11S\xef\xea\xdaZ\xbbv\x9d\x2\x3\x3U\xa3\x46\x8d\xcbN\xae\x99\x93\x93\xa3\x1ew\xdc\xa9\x36\xad[)66VAAAJLL\xd0[o/\xd0\xb0\xfb\x6\xd8=\xf9r\x9e\xcd\x9b\xb7\x16\xfa\x87\xfa\xc5s+\xcc\x9d\xfdj\x85{g\x9c\xe1\xf7\xf\xd5\xb2\xe5+\v\xbc+NRRR\xa1\xeb\x87\x87\x85\xe9\xc1\xe1\xf7\x39\xb4\xfg\xf\xe6\xb6o\xdfY\xec:\xfb\xf7\x1f(\xf5~\xdc\xf5\x35\xe1\xfa\xfc\xe7N\xc6\xbck\xd7\xf\x8a\x8dm\xa4\xe6\xcd\x9b\xab]\x9b\x96zt\xd4h\x8dx\xe4!\xf9\xf8\xf8j\xfe;\xef\xa8_\\O5i\x92;\xb7V^\xc3w\xf5\xea\xd5j\xd6\xac\x99\xa2\xa2\xa2T\xa5J\x95\"\xf7\x11\x16\x16\xa6\xb0\xb0\xb0|\xcbRSS\xf5\xea\xeb\xf3\xf4\xaf\x7f][\xe8$\xce\x97\xe3\xae\xbfkg\xe0\x18P\xb4\xb2z]x\x8fx\xf4\xf1)\xc1\x81\xb9\x41\x39\x9f\x9c\xaa\xea\xd5\x1c{\xc1\x2\x9e\xe2\xf4\xd9\x14]\x9c\x95\xbc\xcf\x8br\xf1:d\v\0\xe0l\xa5\xf9\x7f\xa9$\xfc\xfc|Kt\xc5\x8a\xbd\xbe\xf8\"^^^^\xea\xd0\xa1\x43\x11\xfb\xf7Sd\xad+4\xef\xad\x5\x92\x35G-[\xde\xa8\xeb\xaf\xbbN5\xaaW\xd5\xc7\xab\xd6h\xfd\x86M\x8a\x8e\x8e\xd2s\xcfN\xb7M\xc2i2\x99\xd4\xbcySm\xd8\xb8Q\x1f\xafZ\xa3zu\xaf\xbe\xe8l~AV\xabUQQ\xf5\xb5\xff\xc0\x1m\xdc\xb8I\x1f\xad\\\xad\xd4\xd4\v\x1aW\x9d;\xdd\xaa\x86\x31\x31\x85\xaeS\xb3\x66\xa8.\\8\xafw\xdf]\xa4\xa8\xfa\xf5\x15\x15U_\x1dni\xaf\x33\xa7Ok\xf1\x92\xf7\xf5\xf3\xcf?\xabs\xe7N\x1a\x39r\x84mN\xae\xe0\xe0 \xc5\x44\xd7\xd3\xc2\x45K\xb4}\xfb\xf7j\xd1\xbc\x99j\xd4(\xfc\x1d\xf6\x8ar\xf4\xe8Q}\xf4\xf1\x1a=\xf4\xef\xfb\x1d\x9e_\xcbS\xf3/q\f\xb8\x9c\xb2:\x6\x98\xfe\x38t\xccZ\xbbV\xee\xcc\xe5G\x8e\x9dTt\xdd\xf0\x12\x17\tTd\xfb\xffL\xd0\xc5Y\xc9\xfb\xbc(\x17\xaf\x43\xb6\0\0\xceV\x9a\xff\x97\xdb\xa0u\x9fm(v\xdd\x1bola\xd8\x41\x1d\xec\xf3\xe5\x96-z\xea\xe9iE>\xfe\xe2\xf3S\xd5'\xae\xe8\xe3\x3J\x87\x63@\xd9\xe2\x8a\x1d\xc0N\\\xb1\x3\0\x30\x12\xae\xd8\x1 \x91\x7f\xc0\xd3\x1d\x39vR\xee\x37\x85\x34\0\0\0\0\0\0$\x89\xc6\xe\0\0\0\0\0\x80\xbb\xa2\xb1\x3\0\0\0\0\0\xe0\xa6h\xec\0\0\0\0\0\0\xb8)\xde\x15\v\0\0\xc0\xc3\x1c\x39V\xf4[\xfe\xc2\xbd\x30!.\x1c\x45\xfe+\xe\xf2\x8f<\\\xb1\x3\0\0\0\0\0\xe0\xa6\xb8\x62\a\0\0\xc0\xc3p\x96\x17\xf0\\\xe4\x1f\xa8xh\xec\0\0\0x\x18n\xc5\0\xdc_I\x1b\x34\xe4\x1f\xa8\x18.>\x6\xd0\xd8\x1\0\0\xf0\x30\x9c\xb1\a<\x17\xf9\a*\x1e\xe6\xd8\x1\0\0\0\0\0pS\\\xb1\x3\0\0\0\x14\"33SG\x8f\x1eUJr\xb2\xb2,Y\xae.\xa7\xc4|\xcc>\xaa\x12\x18\xa8\x88\x88\b\xf9\xfa\xfa\xba\xba\x1c\xc0-\x90\x7f\xb8\x13\x1a;\0\0\0\xc0%233\xb5w\xef^eg[\\]J\xa9\x65Y\xb2t\xf6\xec\x19\x9d?\x7f^\r\x1b\x36\x64p\a\x14\x83\xfc\xc3\xddp+\x16\0\0\0\\\xc2U\x93\xb8\x1e\x39v\xb2\xd8}\x1f=z\xd4\xa5\x83\xba\xd0\xb0:N\x7f\xce\xecl\x8b\x8e\x1e=\xea\xf4\xe7u&{~7\xa8\x18\xc8\x7f\xd1\xc8?\x1c\x45\x63\a\0\0\0\xb8\x44Jr\xb2\xabK(\x13))\x15\xf3\xfb\x2\x9c\x89\xfc\xc3\xdd\xd0\xd8\x1\0\0\0.\xe1\xcesj\\NVV\xc5\xfc\xbe\0g\"\xffp74v\0\0\0\0\0\0\xdc\x14\x8d\x1d\0\0\0\xc0I\x8e\xfcsLG\xfe\x39\xe6\xea\x32\0\xb8\0\xf9\x87\xab\xf0\xaeX\0\0\0(`\xf9\x87+\xb5s\xd7\x8fv\xaf\xdf\xf2\xc6\xe6\xea}\xd7\x9d\x65X\x91\xf1\x1d\xf9\xe7\x98&M\x99!I\x9a\x36\xe5i\xd5\x8e\xac\xe5\xe2\x8a\xca\xcek\xb3\xe6\x14:\x80m\xde\xecz\xf5\xed\xd3\xdb\x5\x15\x15o\xe1\xa2\xa5\x92\xa4!\x83\a\xb8\xb8\x12\xe3#\xff\x8e#\xff\xe4\xdf\x95h\xec\0\0\0\xa0\x80\x9d\xbb~\xd4\xba\xcf\x36(\xaa~\xbd\x62\xd7MII\xd1\xca\x8fW+-5]\xf7\xde\xdb\xaf\x1c\xaa\x33\x9e\xbc\x41]R\xd2\x39I\xd2\xa4)3*\xf4\xe0\xee\xaf\xc3Gt\xf0\xe0!\xdb\xd7{\xf7\xed\x93\xc5\x92-I\x86\x1d\xd8}\xb5m\x9b\xa4\x8a;\xb0s&\xf2\xef\x18\xf2O\xfe]\x8d\xc6\xe\0\0\0\n\x15U\xbf\x9e\xd6\xaeY\xa1\xd3\xa7O\x17\xb9NPP\x90\xbc\xbd\xcdz|\xcc\x38M\x98Yaa5]\\\xa1\xf3\xbd\xf6\xca\v\xff\xfb|\xd6\x1c\xfd\xf2\xebo2\x9b\xbd]XQ\xc5\x97\x96\x96\xa6\xbe\xf7\f\x92\xc9\xcbK\xcb\xdf_\xa4J\x95*\x95\xe9\xfe\xc8\xbf}\xc8?\xf9//\x97;\x6\xd0\xd8\x1\0\0@\x91N\x9f>\xad\x66\x37\xb4)\xf2\xf1\xd7^yAw\xf6\xec\xa1\x97_z^\x92\x34\x61\xf2\x34\xf9W\xae\xe4Q\xb7\x65\x84\x84T\xd5u\xff\xbaVq\xbd{j\xc1\xc2\xc5\x92\xa4'\x9f\x18\xa5\x15+\xd7($\xa4\xaa\x8b\xab+[\xaf\xcd\x9a\xa3Wg\xbd\xa1\xc7\x1e}X\xf1\x9b\xb7\xba\xba\x9c\n\xcd\xdf\xdf_\xd3\x9f\x99\xac^w\xf5\xd3\xb4\xe9\xcf\xe9\xd9\xe9S\xcb|\x9f\xe4\xbfx\xe4\x9f\xfc\x97\x97\xcb\x1d\x3\x98<\x19\0\0\0\xa5\x66\x36{\xeb\xe5\x97\x9e\xd7\xd5W]\xa9\xef\xb6\xeftu9\xe5\"!!Qii\xe9\xf2\xf5\xf5\xd5\xc8G\x86\xe7;3\x1f\x16VS#\x1f\x19.___eff*1\xf1\x84\v+-\x1b\x17\xf\xea\x46=\xfa\x88\xab\xcb)V\x87v\xed\xd4\xa1];W\x97Q*M\x1a\xc7j\xc2\xd3O\xea\xfd\xfV\xb8\xba\x94|\xc8?\xf9\x37\xba\x8a\x90\x7f\xa9\xe8\x63\0W\xec\0\0\0\xa0H\xde\xde\xde\x8a\x88\x88(\xf2\xf1\x80\x80\xca\xb6\xcf\xcd\x66oU\xa9R\xa5<\xcar\xb9\xbc\x39\x35\x9a\x34n\xa4\xd1\xa3\x1e\xbe\xec\xba\xf3\xde^\xa4\x9fv\xef\xa9Psn\xb8\xdb\xa0N\x92\x6\f\xe8\xeb\xea\x12\x9c\x62\xe0\xbd\xf7\xe8\xbb\xed;\xca\x65_\xe4\xbfp\xe4\x9f\xfc\xbbRa\xc7\0\x1a;\0\0\0(RHH\x88\xbe\xfd:\xbe\xdc\xf7{\xf4\xe8Q}\xbd\xed\xbb\x42\x1fk\xdd\xaa\xe5\x65\a\x9b\x65-oPg\xf6\x36\xeb\x9e\xbew\x17\xbb~\\\xef\x9e\xfa\xe5\x97\xdf\x9c\x36\xa1\xea\xc9\x93\xa7\xb4\xfb\xe7_\v}\xec_\xd7\x34Vhh\x8dR=\x7fq\x8a\x1a\xd4\xd5\xaf_\xcf\xb0\x3W\x8b%[\x1b\x36mR\xd2\x99s\xea\xd7/\xce\xad\xe7\x3\x31\x99L\x9a\xf9\xfc\xf4r\xd9\x17\xf9/\x88\xfc\x93\x7fW+\xec\x18@c\a\0\0\0\x86\x13\x1e\x1e\xae\xdd{~\xd6\xb2\xe5\x1f\xe5[~O\xbf\x38\xc5\xdd\xdd\xcb\x45U\xe5\x7f\xf7\x9b\xab\xae\xac\x63\x9bS#\xcf\xfe\x3\xb9\xef\x14\x33\xe3\xb9\x97\xf2-\xafR%@\x7f\x1d\xfe\xdb)\x83\xbb\xea\xd5\x43t\xf0\xc0!}\x1e\xbf%\xdf\xf2\x8e\xb7\xdd\xa2\xe\xed\x8b\x9e\xf\xc5\x19.w\xa6\xfe\xe2\tU]e\xe1\xa2\xa5\xb6w\xbf\x91ro\xbf\x18\x30\xa0\xaf\x96-[a\x9b\xdc\xb7jH\xb0\xbaw\xbd]K\x97.\xd7\xe6\xad[m\xeb\xb6i\xd5\xcam\xde\x31'0(\xd0\xd5%\x94)\xf2_4\xf2_4O\xc9\xbfT\xf0\x18@c\a\0\0\0v\xcb\xc8\xc8\xd0\x9a\xb5\x9fI\x92z\xf6\xe8*??\xbf\x32\xd9\x8f\x97\x97\x97\x9e\x9d>E&\x93I\x1f,\xcb\x9dK\xa0\xff=q\x9a>m\xb2\xbc\xbc<{\x9aH///=0|\x88\x64\x32\xe9\xf3/\xbe\x94$u\xeax\x8b\x86\xf\x1b\\\xa6?\x1bw\xbc\xfd\"O\xd5\x90`\xdb\xe7Ig\xce\xb9\xb0\x12\xf7\x46\xfe]\x8f\xfc;\xce\x13\xf2Oc\a\0\0\0v\xbbw\xd0\x30}\xbf\x63\x97$i\xd5\xaa\x35Z\xfe\xc1{2\x99Le\xb2////\xcdxf\xb2L&I&\x93\xa6O\x9d\xe4\xf2\x41]\xed\xc8Z\x9a\x36\xe5iM\x9a\x32\x43))\x17\xf4\xe4\x13\xa3\xf2M\x9a\x9aw\xa6\xfe\xe9\xa7\xc6\xd8\x96%&\x9e\xd0\xc4\xc9\x33T\xb5j\xb0\xd3\xe6\xd9\xf0\xf2\xf2\xd2\x3\xf7\xf\xce\xfd\xd9\xc8\xa4\xe1\xc3\x6\x39\xedg\xf3\xda\xac\x39\xfa\xeb\xf0\x91\x2oi\xec\xe\x83\xba!\x83\a\x14z\xd6\xbdK\xa7N\x9a>\xf5\x9c\xaa\x86\x4\xabK\xa7N\x92r\xe7\xdcp\xd7y7\xce\x9f?\xaf\xa0\xa0\xa0r\xdf/\xf9'\xff\xe4\xdf\x18.=\x6xv\xbb\x13\0\0\0v;\x96\x90`\x1b\xd4I\xd2\xf6\xefw*!1\xb1L\xf7\xe9\xe5\xe5\xa5\xe9\xd3&\x1b\x62P\x97'opg\xc9\xb6h\xc5\xca\x35\xc5\xae\xbf\x62\xe5\x1aY\xb2-N\x9f<\xd5\xcb\xcbK\xc3\x87\rv\xea\xa0N\xca\xbd\xdd\xe4\xe0\xc1\x43\xb6\xaf\xdd\x65Pw9f\xb3\xb7\xee\xbd\xb7\x9f\xbaw\xbd\xdd\xed\xe7\xd7\xb0Z\xad\x1a;nb\xb9\xef\x97\xfc\xe7\"\xff\xee\xa7\"\xb9\xf9\x90\x39\0\0 \0IDAT\xe5_*\xfc\x18\xc0\x15;\0\0\0\xb0K\xf5\x90\x10U\r\xeV\xd2\xb9\xdcK\xd9\xabV\xad\xaa\xea!!e\xbe_\xa3\f\xe8.V;\xb2\x96\x9e\x9b>Y!!U\x8b]\xf7\xc1\xe1\x83u\xe6LR\xbe\x33\xfb\xceR\xd6?\x9b\x8a\x30\xa8\xcb\xb3t\xe9rI\xee\xff\xee\x38\x8b\x97|\xa0\r\x1b?/\xf7\xfd\x92\xff\xff!\xff\xee\xa7\xa2\xe4_*\xfc\x18`\xbc\x94\0\0\0\xc0\x90\xfc\xfc\xfc\xf4\xee\x82\x37uS\xcb\x1bt\xf3M7j\xe1\x82\x37\xcbl\x8e\rw\x10\x16VS\xbe\xbe\xbe:u\xfa\xb4&O}N'N\x9c\xb4=v\xfc\xf8\tM\x9e\xfa\x9cN\x9d>-__\xdf\x32\x19\xd4\x95\xb5\x8a\x34\xa8\x93\xa4\xcd[\xb7\xe6\x9b,\xd5\x1d\xfd\xfc\xcb\xaf\x9a>\xe3\x5\xf5\xbf'\xae\xdc\xf7M\xfe\xf3#\xff\xee\xa5\"\xe4_*\xfa\x18\xc0\x15;\0\0\0\xb0[\xd3\xeb\xaf\xd3\xb2\xf7\x17\xb9\xba\fCIO\xcf\xd4\xdfG\xfe\xd1\xd3\x13\xa7\xabJ\x95\0I\xd2\x84I3d\xc9\xb6(==\xd3\xc5\xd5\x95\xcc\xde}\xfb\xf4\xcb\xaf\xbfU\x98\x41]E\x90\x96\x96\xa6\x89\x93\xa6)\xb6q#M\x9a\xf0\x94Kj \xff\x5\x91\x7f\x94\x97\xcb\x1d\x3h\xec\0\0\0\xa0P\a\xe\x1eR\xb7\x1e\xbd\x1d\xde\xa6\x41\x83\xe8\x32\xaa\xc8\x98\"#\xc2m\x13\xaa\xfeu\xf8oI\xb2M\x94\x1a\x19\x11\xee\xe2\xeaJ\xc6\x62\xc9\x96\xd9\xec\xad\xf8\xcd[\x15\xbfy\xab$\xa9~\xfdz\x86xKcO\xe5\xef\xef\xafOV\xaf(\xb7\xfd\x91\x7f\xfb\x90\x7f\x94\x97\xcb\x1d\x3h\xec\0\0\0\xa0\x80\xe6\xcd\xae/\xd1v\r\x1a\x44\x97x[wv\xf1\xbb\xe5Hr\xfa\x44\xa9\xe5\xa9\xa8\xdf\x9f\xbb~?y\xda\xb4j\xe5\xea\x12\xdc\x6\xf9w\f\xf9\x37\xbe\x8a\x9e\x7f\x1a;\0\0\0(\xa0o\x9f\xde\xea\xdb\xc7\xb1\xb3\xf5\x9e.op\x97\xf7\xb9\xbb\xaa\xa8\xbf\xfb\xc2\xde\x6\x19\x85\xab\xa8\xaf\x81\xb2\x44\xfe\x8d\xad\xa2\xe7\x9f\xc6\xe\0\0\0\xe0$\xee<\xa0\x3P:\xe4\x1f\xae\xc2\xbb\x62\x1\0\0\0\0\0\xb8)\x1a;\0\0\0\0\0\0n\x8a\xc6\xe\0\0\0p\t\x1f\xb3\x8f\xabK(\x13>>\x15\xf3\xfb\x2\x9c\x89\xfc\xc3\xdd\xd0\xd8\x1\0\0\0.Q%0\xd0\xd5%\x94\x89*U*\xe6\xf7\x5\x38\x13\xf9\x87\xbb\x61\xf2\x64\0\0\0\xb8\x44\xedZ\xa1\x86\xddoDD\x84\xce\x9f?\xaf\xeclK9TT\xd0\xc9\xc4\xbf\x9d\xfe\x9c\xde\xde\x66\x45\x44\x44\x38\xfdy\x9d\xc9U\xaf\t\x94?\xf2_4\xf2\xfGq\xc5\xe\0\0\0p\t___5l\xd8P\xd5\xaa\x85\xb8\xfd\xed\v>>>\xaaV-D\r\x1b\x36\x94\xaf\xaf\xaf\xab\xcb\x1\f\x8f\xfc\xc3\xddp\xc5\xe\0\0\0P\b___]}\xf5\xd5\xae.\x3\x80\v\x90\x7f\xb8\x13\xae\xd8\x1\0\0\0\0\0pS\\\xb1\x3\0\0\0Y\xad\x92%\xc7\xa2lK\xb6\xb2s,\xb2Z%\xab\xd5\xea\xea\xb2\0\x8f\x63\x32\x99\x64\x32I\xde^fy\x9b\xbd\x65\xf6\x32\xcb\x64*\xdb}\x92\x7f\xc0\x38Jr\f\xa0\xb1\x3\0\0\xe0\xe1,\xd9\x16\x65\x64\x64(\x87\x81\x1c\xe0rV\xabUV\xab\x94\x93\x93\xa5,K\x96\xbcL&\xf9\xf9\xf9\xc9\xec]6C7\xf2\xf\x18KI\x8e\x1\xdc\x8a\x5\0\0\xe0\xc1\x32\x32\x32\x94\x96\x9e\xce\xa0\xe\x30\xa8\x1c\xabUi\xe9\xe9\xca\xc8\xc8p\xfas\x93\x7f\xc0\xf8\xec\x39\x6\xd0\xd8\x1\0\0\xf0P\x19\x19\x19\xca\xb4\x64\xb9\xba\f\0v\xc8\xb4\x64\x39\xb5\xb9\x43\xfe\x1\xf7r\xb9\x63\0\x8d\x1d\0\0\0\xf\x64\xc9\xb6\x30\xa8\x3\xdcL\xa6%K\x96lK\xa9\x9f\x87\xfc\x3\xee\xa9\xa8\x63\0\x8d\x1d\0\0\0\xf\x63\xb5\xcaig\xfe\x7f\xff\xedW\xc5\xc6\x44\xe9\xe4\x89\x13\x85>\xfe\xcd\xb6m\x8a\x8d\x89RJrr\xbe\xe5\xdb\xbe\xfeJ\xb1\x31Q:\x7f\xfe|\x81mbc\xa2\x9cR\x1bP\x11\x65\x64\x64\xa8\x34\x37N\x91\x7f\xc0\xbd\x15v\f`\xf2\x64\0\0\0\xf\x63\xc9\xb1\x94\xdb\x9c\x1ag\xce\x9c\x96$\xf9U\xaa\x94oy@\x95*\x92\xa4\x94\x94\x64\x5\x5\x5\x15\xd8\xae\xa8\xc1\xddo\xfb\xe\x38\xb9\x42\xc0\xbd\xe4X\xad\xb2\x64[\xe4S\xc2\xc9\x94\xc9?\xe0\xde\n;\x6\xd0\xd8\x1\0\0\xf0\x30\xd9\x96\xecr\xdbWpp\xb0$)--M>>>\xb6\xe5i\xa9\xa9\xf9\x1e\xbf\x14\x3\x38\xa0h\xd9\x96\xec\x12\x37v\xc8?\xe0\xfe.=\x6\xd0\xd8\x1\0\0\xf0\x30\xd9\x39\xa5\x9f\xa3\xc3^\xa1\xa1\x35%I\xc7\x13\x13\xf3\x9d\x99OHHPDD\xa4\x2\x2\xaa\x14\xba\xdd\xa6\x8d\x1bl\x9fw\xea\xdc\xa5l\x8b\x4\xdcLn\x86\xfdJ\xb1m\xf9 \xff@\xd9\xb8\xf4\x18@c\a\0\0\xc0\xc3\x94\xf5]\x18\xa9\xa9\xa9\xaa\\\xb9\xb2$)*:Z1\r\x1b)>\xfe\vEEGK\x92,\x16\x8b\xd6\x7f\xb6N\x3\a\xf.\xf2\x39\x18\xcc\x1\x45+M\x86\xc9?\xe0\xfe.\xcd\x31\x8d\x1d\0\0\0\xf\x63-\xc3\x91\xdd\xa1\x43\a\x15w\xd7\x9d\x1a;n\xbc\xfa\xf4\xed'\xb3\xd9\xacQ\x8f\x8d\xd6\x83\xc3\x87\xc9\xc7\xc7\xac\x86\rc\xb5\x39\xfe\v\x9d>}Z\xb7w\xed^fu\0\x15Yi2L\xfe\x1\xf7wi\x8ei\xec\0\0\0\xc0i\x2\x2\xaa(6\xb6\xb1\xc2\xc2\xc2m\xcbZ\xb7i\xab\x5\v\xdf\xd3\xca\x8fVh\xcd\xeaUj\xdb\xb6\xbd\xe6/X\xa8\x90\x90\x90\"\x9f\x87[1\0\xf7\x43\xfe\x1\xd7\xa0\xb1\x3\0\0\x80\x12k\x14\xdb\x38\xdf\x44\xa7\x61\x61\x61Z\xfc\xfe\xb2\x2\xeb\xdd\xd8\xf2&\xdd\xd8\xf2&\xbb\x9f\x97\xc1\x1c`|\xe4\x1f\x30\x6\x1a;\0\0\0\x30\x1c\xce\xd8\x3\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\x61\x30\ax.\xf2\xf\x38\xc6\xcb\xd5\x5\0\0\0\0\0\0\xa0\x64\xb8\x62\a\0\0\0\x86\xc3\xad\x18\x80\xe7\"\xff\x80\x63h\xec\0\0\0\xc0p\x18\xcc\x1\x9e\x8b\xfc\x3\x8e\xa1\xb1\x3\0\0\0\xc3\x99;gv\xbe\xaf\x1fzd\x84\x8b*\x1P\xde\xc8?\xe0\x18\x1a;\0\0\0\x30\x1c\x6r\x80\xe7\"\xff\x80\x63\x98<\x19\0\0\0\0\0\xc0Mq\xc5\xe\0\0\0\f\x87\xc9S\x1\xcf\x45\xfe\x1\xc7\xd0\xd8\x1\0\0\x80\xe1\x30\x98\x3<\x17\xf9\a\x1c\x43\x63\a\0\0\0\x86\xc3\x19{\xc0s\x91\x7f\xc0\x31\x34v\0\0\0`8\f\xe6\0\xcf\x45\xfe\x1\xc7\x30y2\0\0\0\0\0\x80\x9b\xe2\x8a\x1d\0\0\0\x38$==]\x1f,]\xa2\xf8\xf8\xcf\x15\x18\x18\xa4\xae\xdd\xba\xab{\x8f;d2\x99\x8a\xdc\x66\xcf\x9e\xddZ\xba\xf8=\xfd\xf8\xc3.\xd5\xadWO\xbd\xee\xba[\x9d:w\x91\x97W\xe1\xe7\x19\xb9\x15\x3\x30&\xf2\xf\x18\xf\x8d\x1d\0\0\0\xfc?{w\x1e\x17U\xd9\xfeq\xfc;\xac\"\x2\x8a\xa0 \xb8\xa4\x82\xb8\xf6\x94K\x8bkY\xee[fhj.i\xf6\xb4hfjf\xeeiV\xb6\xb9\x64\x8b\x95\x66\x96\x66\xa6\x66\xe5RnO\xab\xa9\x95Y\xa6\xe5\xf2\xd3Lq\x5\x15\x64\x1d\x98\xdf\x1f<\xcc#\x2\x32\x3\x3s\x86\xf9\xbc_/^\xc2=\xe7>\xe7\x2\xe6:r_\xe7>\xf7\xb1\x99\xd9l\xd6SO\x8eSRR\xa2\x86?\xf0\xa0\xe2\xe3\xe3\xf5\xcc\xf4\xa9JHH\xd0\xe0!C\xf3\xed\xb3\xef\xf7\xdf\xd5\xbf\xef=\x1a=f\xacz\xdd\xd5[\x7f\x1e\x38\xa0I\x13'(--M\xbd\xee\xea\x9do\x1f\x6s\x80\xf1\x90\xff\x80\x31Q\xd8\x1\0\0\x80\xcdv\xee\xfcQ\xdf~\xf3\xb5\x36m\xde\xa6\xe0\xe0`IRPP\x90&\x8c\x1f\xabn\xdd{\xa8r\xe5\xcay\xfal\xdd\xbaY\xb7\xb6l\xa9\aF<(Ij\xd9\xaa\xb5\x32\xcc\x19\xda\xb4qC\x81\x3;\xae\xd8\x3\xc6\x43\xfe\x3\xc6\x44\x61\a\0\0\0\x36\xdb\xb8\x61\xbdz\xf6\xba\xcb:\xa8\x93\xa4[[\xb6Rff\x96v\xef\xda\x99\xef \xcc\xdf\xbf\x82\xe2\xe2\xe2\x94\x92\x92\"???I\xd2\xf9s\xe7T7*\xaa\xc0\xe3\x30\x98\x3\x8c\x87\xfc\a\x8c\x89\xc5\x93\x1\0\0`\xb3\x83\x7f\xfe\xa9\x1a\x35k\xe5j\xf3\xf7\xf7Wt\xbdh\x9d\x8a\x8b\xcb\xb7O\x8f\x9e=\xe5W\xceOcF\x8f\xd2\xda\x35\xab\xf5\xdc\xecY:u\xea\xb4\x6\xf\xb9\xbf\x14\"\x6\xe0(\xe4?`L\xcc\xd8\x1\0\0\x80M,\x16\x8b\xf6\xef\xdf\xaf>\xb1\xb1y^\v\r\xad\xa2\xf8\x84\xf8|\xfb\x85\x84\x84j\xd4\xe8\x31\xfa\xf7\x88\x61\xfa\xfa?\xdb%I\xcb>\\\xa1\x90\x90\x90\x2\x8f\xd5\x30&\xff\xab\xf9\xfb\xe\x1c\xb4?p\0\xc5\x46\xfe\x3\xc6\x45\x61\a\0\0\0\x36\x31\x99L\xaa]\xa7\xb6\x12\x13\x13\xf3\xbc\x96\x90\x10\xaf\xd0\xd0\xd0|\xfb\xad\xfbt\xad\xde|}\xa1\xdez\xfb]5j\xdc\x44;~\xf8^\xa3G\x8d\xd4]\xbd\xef\xd6\x63\x8f\x8f\xc9\xf7i:\f\xe0\0\x63!\xff\x1\xe3\xa2\xb0\x3\0\0\0\x9b\x45G\xd7\xd3\x91#Gr\xb5\xa5\xa5\xa5\xe9\xf0\xa1\xc3\n\v\v\xcf\xb3\xbd\xd9l\xd6\xecY35q\xd2\x64\xb5l\xd5ZR\xf6\xfa\x19\x1\x1\x1z`\xd8P\xf5\xe8\xd9K\xb5\xeb\xd4\xc9\xd3\x8f\xc5S\x1\xe3!\xff\x1\x63\x62\x8d\x1d\0\0\0\xd8\xec\xce\xe\x1d\xb5n\xedZ\x9d=s\xc6\xda\xf6\xd3\xee]\x92\xa4\xe6-n\xb2\xb6%''K\xca\x1e\xd8\xa5\xa4\xa4()))\xd7~\xaaT\xa9*I\xca\x30g\xe4{\x9c\x8e\x9d:[?\0\x18\x3\xf9\xf\x18\x13\x33v\0\0\0`\xb3\xd6\xad\xdb\xa8i\xb3\x66\x1a\x37v\x8c\x6\xf\x19\xaa\xe4\xe4\x64\xbd\xf0\xdcl=1n\xbc\x82\x82\x82$I\x1b\xd6\x7f\xa1'\xc7=\xa1\xf\x96\xafT\xe3&M\xf4\xd0#\x8f\xe8\xc5\x17\x9eSFF\x86\xa2\xa3\xa3\x95\x92\x92\xaa\xe5\x1f.S\xfb;:\xa8N\x9d\xbaN\xfe\x8e\0\xd8\x8a\xfc\a\x8c\x89\xc2\xe\0\0\0l\xe6\xe3\xeb\xab\xb9\xf3\x16h\xc9\x92w5\x7f\xde\xab\n\xf\xaf\xa6\x89\x93&\xe7\xba\xb2\x1e\x12\x12\xaa\xc6M\xaeW``\xa0$i\xd8\xf0\x11\xaaU\xeb:}\xbav\x8d\xdeY\xf4\x96\xae\xab][w\xdc\xd9\x41\xbd\xee\xea-/\xaf\xfc\xff\x1c\xe5V\f\xc0x\xc8\x7f\xc0\x98L\x7f\x1e>i\xa9^-{\xa1\xab\xe3'\xcf*\xbav\xde{#\x1H\x7f\x1d\x89\xd3\x95\xb9\x92\xf3yA\xae\xdc\x86\xdc\x2\0\x38Zq\xfe_J\xbc\x9ct\xcdm\x9d\xad\x61L\x14\x8b\xa7\x2\x85\b\xf0\xaf \x89\xfc\a\xdc\xd5\x95\xe7\0\x66\xec\0\0\0\xc0p\xb8\x62\xf\xb8/\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xe\x83\x39\xc0}\x91\xff\x80}x*\x16\0\0\0\0\0\x80\x8b\x62\xc6\xe\0\0\0\f\x87[1\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\x61\x30\a\xb8/\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xce\xc2\x5\xf3s}\xfd\xf0\xa3#\x9d\x14\t\x80\xd2\x46\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\x61 \a\xb8/\xf2\x1f\xb0\xf\x8b'\x3\0\0\0\0\0\xb8(f\xec\0\0\0\xc0pX<\x15p_\xe4?`\x1f\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xfbP\xd8\x1\0\0\x80\xe1p\xc5\x1ep_\xe4?`\x1f\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xfb\xb0x2\0\0\0\0\0\x80\x8b\x62\xc6\xe\0\0\0\xec\x92\x9a\x9a\xaa\xf\x97\xbd\xaf\xcd\x9b\xbfT@@\xa0\xbav\xeb\xae\xee=z\xca\x64\x32]\xb3\xdf\x7f\xb6o\xd7\xe6\xcd_\xea\xc7\x1f~P\xb5\x88\b\xddqg\a\xdd\xdd\xe7\x1e\xf9\xf9\xf9\xe5\xd9\x96[1\0\x63\"\xff\x1\xe3\xa1\xb0\x3\0\0\0\x9b\x99\xcd\x66=\xf5\xe4\x38%%%j\xf8\x3\xf*>>^\xcfL\x9f\xaa\x84\x84\x4\r\x1e\x32\xb4\xc0~\xaf/|M\xabV\xae\xd0\xf0\x11\xf\xea\xce;;(11Q?\xff\xf4\x93|||\xf2\xdd\x9e\xc1\x1c`<\xe4?`L\x14v\0\0\0`\xb3\x9d;\x7f\xd4\xb7\xdf|\xadM\x9b\xb7)88X\x92\x14\x14\x14\xa4\t\xe3\xc7\xaa[\xf7\x1e\xaa\\\xb9r\x9e>\xfb\xff\xf8\x43o\xbe\xbeP_l\xdc\xa4\x88\x88Hk{\xd7n\xdd\v<\xeW\xec\x1\xe3!\xff\x1\x63\xa2\xb0\x3\0\0\0\x9bm\xdc\xb0^={\xdd\x65\x1d\xd4I\xd2\xad-[)33K\xbbw\xed\xccw\x10\xb6\x66\xcd'\xbaw\xc0\x80\\\x83\xba\xc2\x30\x98\x3\x8c\x87\xfc\a\x8c\x89\xc5\x93\x1\0\0`\xb3\x83\x7f\xfe\xa9\x1a\x35k\xe5j\xf3\xf7\xf7Wt\xbdh\x9d\x8a\x8b\xcb\xb7\xcf\x1f\xfb\xf6\xa9\x66\xcdZ\xfa\xfc\xb3u\x1a\x34\xe0^u\xeb\xd2Q\xaf\xcd\x9f\xa7\xc4\xc4\xc4R\x88\x18\x80\xa3\x90\xff\x80\x31\x31\x63\a\0\0\0\x36\xb1X,\xda\xbf\x7f\xbf\xfa\xc4\xc6\xe6y-4\xb4\x8a\xe2\x13\xe2\xf3\xb4geei\xef\xaf{\xf4\xcb\xcf?\xe9\x9e\xd8~\x1a\xf6\xc0\beee\xe9\x8d\xd7\x17\xea\xf8\xf1\xbf\x35\xfb\xf9\x39\xf9.\xba\xda\x30&*\xdf\x18\xf6\x1d\x38X\xfco\x4\x80\xdd\xc8\x7f\xc0\xb8(\xec\0\0\0\xc0&&\x93I\xb5\xeb\xd4\xce\xf7J{BB\xbc\x42\x43\x43\xf3\xb4{xx(\xa8\x62\x45\xc5\x9f?\xaf\xa9\xd3gX\aq\xa1U\xaa\xa8o\x9f\xdezt\xe4\x63\x8a\xac^=O?\x6p\x80\xb1\x90\xff\x80qQ\xd8\x1\0\0\x80\xcd\xa2\xa3\xeb\xe9\xc8\x91#\xb9\xda\xd2\xd2\xd2t\xf8\xd0\x61\x85\x85\x85\xe7\xdb\xe7_\xff\xba\x41\xc1\xc1\x95s]\x99\xafZ5L\x92t\xe1\xc2\x85|\av,\x9e\n\x18\xf\xf9\xf\x18\x13k\xec\0\0\0\xc0\x66wv\xe8\xa8uk\xd7\xea\xec\x99\x33\xd6\xb6\x9fv\xef\x92$5oq\x93\xa4\xec\xdb/RRR\xac\xafw\xe8\xd8I\xdb\xb7m\xcdu\xa5\xff\xc0\xfe\xfd\x92\xa4\xf0j\xd5\xf2=N\xc7N\x9d\xad\x1f\0\x8c\x81\xfc\a\x8c\x89\xc2\xe\0\0\0l\xd6\xbau\x1b\x35m\xd6L\xe3\xc6\x8e\xd1\xb6\xad[\xf4\xc5\xe7\x9f\xe9\xa9'\xc7\xeb\x89q\xe3\x15\x14\x14$Iz\xee\xd9\x99\xea\xd6\xb9\x83.]\xba$I\xba\xbd\xfd\x1d\n\v\xf\xd7\xb8'\x1e\xd7\x96\xcd_i\xcb\xe6\xaf\x34\x7f\xde+\x1a=fl\xbe\x8fG\x6`L\xe4?`L\xdc\x8a\x5\0\0\0\x9b\xf9\xf8\xfaj\xee\xbc\x5Z\xb2\xe4]\xcd\x9f\xf7\xaa\xc2\xc3\xabi\xe2\xa4\xc9\xb9\xae\xac\xd7\xa8YK1\xf5\x1b\xc8\xc7\xc7GR\xf6Ss\xdex\xebm\xbd\xfb\xce\"\xcd}\xe5\x65\x85V\xa9\xa2\x81\xf7\rV\xd7n\xdd\v<\xe\xb7\x62\0\xc6\x43\xfe\x3\xc6\x64\xfa\xf3\xf0IK\xf5j\xd9\v]\x1d?yV\xd1\xb5\xf3\xbf\x37\x12pw\x7f\x1d\x89\xd3\x95\xb9\x92\xf3yA\xae\xdc\x86\xdc\x2\0\x38Zq\xfe_J\xbc\x9cT\xe2\xf1\x15G\xc3\x98(\x16O\x5\n\x11\xe0_A\x12\xf9\xf\xb8\xab+\xcf\x1\xcc\xd8\x1\0\0\x80\xe1p\xc5\x1ep_\xe4?`\x1f\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xfb\xb0x2\0\0\0\0\0\x80\x8b\x62\xc6\xe\0\0\0\f\x87[1\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\x61\x30\a\xb8/\xf2\x1f\xb0\xf\xb7\x62\x1\0\0\0\0\0\xb8(f\xec\0\0\0\xc0p\x16.\x98\x9f\xeb\xeb\x87\x1f\x1d\xe9\xa4H\0\x94\x36\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xe\x3\x39\xc0}\x91\xff\x80}(\xec\0\0\0\xc0pX<\x15p_\xe4?`\x1f\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xfb\xb0x2\0\0\0\0\0\x80\x8b\x62\xc6\xe\0\0\0\f\x87[1\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\x61\x30\a\xb8/\xf2\x1f\xb0\xf\x85\x1d\0\0\0\xd8\xe5\xf4\xe9Szo\xf1\x62}\xfb\xcd\xd7j\xd0\xb0\xa1\x62\xfb\xde\xab\x1b\x9b\x36\xb5\xa9\xaf\xd9l\xd6\x63#\x1fQVV\x96^\x7fsQ\x81\xdbq\xc5\x1e\x30&\xf2\x1f\x30\x1e\xd6\xd8\x1\0\0\x80\xcd\xce\x9c\x39\xa3\x61\x43\a+55Uc\xc7?\xa9\x88\xc8H\r\x19\x34@?\xed\xde\x65S\xff\x95\x1f\xad\xd0\xf6m[\v\xdd\xae\x63\xa7\xce\xd6\xf\0\xc6@\xfe\x3\xc6\xc4\x8c\x1d\0\0\0\xd8l\xcd\xeaU\xaaQ\xa3\xa6\x9e\x9ev\xac\xc0\xbe\x97/_\xd6\xd4)\x93\x34\x65\xeat\xc5\xc4\xd4/\xf4X\x9b\x36n\xb0~\0p>\xf2\x1f\x30.f\xec\0\0\0\xc0&\x17.$())Qaa\xe1\xb9\xda\xc3\xc3\xb3\xbf>w\xee\xacj\xd6\xaa\x95o\xdf\xb7\xde|]Q\xd1\xd1\xea\xd0\xb1\x93\x96.Y\\\xe8\xb1\xb8\x5\x3\x30\x16\xf2\x1f\x30.\n;\0\0\0\xb0I\xce\xed\x13~~~\xb9\xda\xcb\xfb\x97\x97$]\xbat)\xdf~\xdf\x7f\xff\x9d>_\xf7\xa9V~\xb2\xe6\x9a\xb7j\\\xa9\x61LT\xbe\xed\xfb\xe\x1c\xb4\x35\\\0\xe\x44\xfe\x3\xc6\x45\x61\a\0\0\0\x36\t\b\b\x90$]N\xbe\x9c\xab=%%U\x92T\xb1R\xa5<}\xce\x9f?\xa7)\x93&j\xc6\xccgU\xb9r\x88$\xc9\"K\xa1\xc7\x62\0\a\x18\v\xf9\xf\x18\x17\x85\x1d\0\0\0\xd8\xa4R\xa5`yzz\xea\xe4\x89\x13R\xb3\xe6\xd6\xf6\xb3g\xcfH\x92\x42\x43\x42\xf3\xf4yg\xd1\"\xc5\x9d<\xa9\x11\xc3\xef\xcf\xf3Z\xc3\x98(\xdd\x37x\x88&<\xf5t\xc9\x5\r\xc0!\xc8\x7f\xc0\xb8(\xec\0\0\0\xc0&>>>\x1a<\xf4~}\xb6n\xad:w\xe9*oooI\xd2\x7f\xb6oS\xbb\xdbnWDd\xa4$)--M^^^\xf2\xf4\xf4\xd4=\xb1}\xd5\xad{\xf7\\\xfb\x99\x36u\xb2\xf6\xfd\xfe\xbb>\xfe\x64\x8d*\xfcw\x16\xc0\xd5\xae\\4\x95\xf5\x36\0\xe7#\xff\x1\xe3\xa2\xb0\x3\0\0\0\x9b\xc5\xf6\xed\xa7\x41\x3\xfak\xe6\x8c\xe9\xea\xd0\xb1\xa3\xfe\xfa\xeb/-z\xf3\r-^\xbaL&\x93I\xa9\xa9\xa9\xea{OoEEE\xeb\xc5\x97_\xd5u\xb5k\xe7\xd9G\xe7.\xddT\xb9r\x88\x1a\x34lT\xe0q\x18\xcc\x1\xc6\x43\xfe\x3\xc6\xc4\xe3\xce\x1\0\0`\xb3\xea\xd5kh\xc9\xfb\x1f\xc8\xcb\xcbK3\x9f\x99\xae\x43\x87\xej\xe9\xb2\xe5j\xd2\xe4zI\x92\xb7\xb7\xb7\xea\xd5\x8bQ\x9d\xba\xf9/~j+\x1ew\f\x18\xf\xf9\xf\x18\x93\xe9\xcf\xc3'-\xd5\xab\x65\xdf\xfy\xfc\xe4YE\xd7\xe/\xa4\v\xe0\x9e\xfe:\x12\xa7+s%\xe7\xf3\x82\\\xb9\r\xb9\x5\0p\xb4\xe2\xfc\xbf\x94x9\xa9\xc4\xe3+\x8e\x86\x31Q,\x9e\n\x14\"\xc0\xbf\x82$\xf2\x1fpWW\x9e\x3\x98\xb1\x3\0\0\0\0\0\xe0\xa2Xc\a\0\0\0\x86\xc3\xe2\xa9\x80\xfb\"\xff\x1\xfbP\xd8\x1\0\0\x80\xe1\x30\x98\x3\xdc\x17\xf9\xf\xd8\x87\xc2\xe\0\0\0\f\x87+\xf6\x80\xfb\"\xff\x1\xfbP\xd8\x1\0\0\x80\xe1\x30\x98\x3\xdc\x17\xf9\xf\xd8\x87\xc5\x93\x1\0\0\0\0\0\\\x14\x33v\0\0\0`8\v\x17\xcc\xcf\xf5\xf5\xc3\x8f\x8etR$\0J\x1b\xf9\xf\xd8\x87\xc2\xe\0\0\0\f\x87\x81\x1c\xe0\xbe\xc8\x7f\xc0>\x14v\0\0\0`8,\x9e\n\xb8/\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xe\x83\x39\xc0}\x91\xff\x80}X<\x19\0\0\0\0\0\xc0\x45\x31\x63\a\0\0\0\x86\xc3\xad\x18\x80\xfb\"\xff\x1\xfbP\xd8\x1\0\0\x80\xe1\x30\x98\x3\xdc\x17\xf9\xf\xd8\x87\xc2\xe\0\0\0\f\x87+\xf6\x80\xfb\"\xff\x1\xfbP\xd8\x1\0\0\x80\xddN\x9f>\xa5\xf7\x16/\xd6\xb7\xdf|\xad\x6\r\x1b*\xb6\xef\xbd\xba\xb1i\xd3|\xb7\xb5X,\xfa\xf6\x9bo\xf4\xe5\x97\x1b\xb5\xeb\xc7\x1d\x8a\x88\xac\xae\xbbz\xdf\xad\xce]\xba\xca\xd3\xd3\x33\xdf>\f\xe6\0\xe3\"\xff\x1\x63\x61\xf1\x64\0\0\0\xd8\xe5\xcc\x99\x33\x1a\x36t\xb0RSS5v\xfc\x93\x8a\x88\x8c\xd4\x90\x41\x3\xf4\xd3\xee]\xf9n\xff\xe5\xa6\x8dz\xfe\xb9Yj\xd4\xa8\xb1\xc6=\xf9\x94\xea\xd5\x8b\xd1\x93\xe3\x9e\xd0\xa7k\xd7\x94r\xe4\0\x8a\x8b\xfc\a\x8c\x87\x19;\0\0\0\xb0\xcb\x9a\xd5\xabT\xa3\x46M==y\x8a<==\xd5\xa6m;\xa5\xa5\xa6i\xe1k\v\xf4\xf6\xbbKd2\x99rm\x7f\xcb\xad-\xd5\xbaM[\x95/_^\x92\xd4\xfe\x8e;\xe5\xed\xe3\xad\x8fV,W\xef\xbb\xfb\xe4{\fn\xc5\0\x8c\x89\xfc\a\x8c\x87\xc2\xe\0\0\0lf6\x9b\xf5\xee\xdbok\xea\xf4\x19\xb9n\xa3\xe8\xd0\xb1\x93\x16\xbf\xfb\xb6\xfe>vL5k\xd5\xca\xd5'000\xcf~\xaaT\xa9\xaa\xcb\x97\x93\n<\xe\x83\x39\xc0x\xc8\x7f\xc0\x98(\xec\0\0\0\xc0\x66\x17.$())Qaa\xe1\xb9\xda\xc3\xc3\xb3\xbf>w\xeel\x9e\x81]~~\xda\xbdK\xb7\xb7\xbf\xa3\xc0\xd7\x1b\xc6\x44\xe5\xdb\xbe\xef\xc0\x41\xdb\x83\x5\xe0P\xe4?`L\x14v\0\0\0`\xb3\v\x17.H\x92\xfc\xfc\xfcr\xb5\x97\xf7\xcf\xbe\xcd\xe2\xd2\xa5K\x85\xee\x63\xd7\xce\x9d\xda\xba\x65\x8b\xd6o\xfa\xb2\xc0m\x18\xc0\x1\xc6\x43\xfe\x3\xc6\xc4\xe2\xc9\0\0\0\xb0Y@@\x80$\xe9r\xf2\xe5\\\xed))\xa9\x92\xa4\x8a\x95*]\xb3\xff\xa9\xb8\x38M~z\x82\x9e{a\x8e\xaaU\x8b(\x99 \x1\x94\b\xf2\x1f\x30&f\xec\0\0\0\xc0\x66\x95*\x5\xcb\xd3\xd3S'O\x9c\x90\x9a\x35\xb7\xb6\x9f={F\x92\x14\x1a\x12Z`\xdf\x8b\x17/j\xfc\xd8\x31\xea\xd5\xfbnu\xea\xdc\xe5\x9a\xc7\x61\xf1T\xc0x\xc8\x7f\xc0\x98(\xec\0\0\0\xc0\x66>>>\x1a<\xf4~}\xb6n\xad:w\xe9*oooI\xd2\x7f\xb6oS\xbb\xdbnWDd\xa4$)++Kiii\xd6[6.^\xbc\xa8qO<\xae\x66-Zh\xc4\x83\xf\x15z\x1c\x6s\x80\xf1\x90\xff\x80\x31Q\xd8\x1\0\0\x80]b\xfb\xf6\xd3\xa0\x1\xfd\x35s\xc6tu\xe8\xd8Q\x7f\xfd\xf5\x97\x16\xbd\xf9\x86\x16/]f}\xd4\xf1s\xcf\xce\xd4\x96\xcd_i\xcd\xba/d\xb1X4b\xf8\xfd\xfa\xfd\xb7\xbdz`\xc4\xbf\xb5\xe7\x97_\xac\xfb\xaaQ\xb3\x86\x42\xf2\xb9\xca\xcf\x15{\xc0\x98\xc8\x7f\xc0x(\xec\0\0\0\xc0.\xd5\xab\xd7\xd0\x92\xf7?\xd0\xd2%\x8b\x35\xf3\x99\xe9\xba\xb1i3-]\xb6\\\r\x1b\x35\xb2nS\xa3\x66-\xc5\xd4o \x1f\x1f\x1f}\xfb\xcd\xd7\xfa\xfd\xb7\xbd\x92\xa4!\x83\x6\xe4\xda\xd7\xc2\x37\x16\xa9m\xbbvy\x8e\xc1`\xe\x30&\xf2\x1f\x30\x1e\xd3\x9f\x87OZ\xaaW\xcb\xae\x92\x1e?yV\xd1\xb5\xc3\v\xe9\x2\xb8\xa7\xbf\x8e\xc4\xe9\xca\\\xc9\xf9\xbc WnCn\x1\0\x1c\xad\x38\xff/%^N*\xf1\xf8\x8a\xa3\x61L\x14O\xc5\x1\n\x11\xe0_A\x12\xf9\xf\xb8\xab+\xcf\x1\xcc\xd8\x1\0\0\x80\xe1p+\x6\xe0\xbe\xc8\x7f\xc0>\x14v\0\0\0`8\f\xe6\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\xe1\x8a=\xe0\xbe\xc8\x7f\xc0>\x14v\0\0\0`8\f\xe6\0\xf7\x45\xfe\x3\xf6\xf1pv\0\0\0\0\0\0\0(\x1a\x66\xec\0\0\0\xc0p\x16.\x98\x9f\xeb\xeb\x87\x1f\x1d\xe9\xa4H\0\x94\x36\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xe\x3\x39\xc0}\x91\xff\x80}(\xec\0\0\0\xc0pX<\x15p_\xe4?`\x1f\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xfb\xb0x2\0\0\0\0\0\x80\x8b\x62\xc6\xe\0\0\0\f\x87[1\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3\x61\x30\a\xb8/\xf2\x1f\xb0\xf\x85\x1d\0\0\0\x18\xeW\xec\x1\xf7\x45\xfe\x3\xf6\x61\x8d\x1d\0\0\0\x94\xb8_\x7f\xdd\xa3\xa7'NP\xd7N\x1d\xf4\xec\xacg\x14w\xf2\xe4\x35\xb7\x1f\x33z\x94:v\xeal\xc8\x41]\xc3\x98(g\x87P b+\x1a\x62+Y\xe4\x7f\xe9 \xb6\xa2\x31rl\x92m\xf1Q\xd8\x1\0\0@\x89\xfai\xf7.\r\x1e\x38@u\xea\xd4\xd5\xf8\t\x13u\xf1\xc2\x5\xdd?t\x90\xce\x9c\x39S*\xc7_\xb8`~\xa9\x1c\xa7(\x88\xadh\x88\xcdu\x90\xff\x5\x33rl\x92\xb1\xe3#\xb6\xdc(\xec\0\0\0\xa0\xc4\x64\x65\x65i\xfe\xbc\xb9\x1a\xf5\xd8h\xdd?l\xb8\xda\xb6k\xa7\xe9\xcf\xccR\xc5\xa0\x8a\xfal\xdd\xdak\xf6\xdd\xb4q\x83\xf5\xe3\xeav{\xbe\xceo\xbf\x8e\xda\x1f\xb1\x11[i\xc6\xe6j\xcaz\xfe\x17\xb6\xbd\x91\x63\xb3w\x7f\x46\x8e\xad\xb0\xed\x8d\x1c[Q\xf6\x97\x1f\n;\0\0\0(1\x87\xf\x1d\xd2\xae\x9d?\xea\xb6\xdb\xdb[\xdb\xca\x95+\xa7\x1e\xbdzi\xe9\x92%\xb2X,\x5\xf6\x35\xea\xad\x18\0lC\xfe\x3\xa5\x83\xc2\xe\0\0\0JL\xce\xed\x16\xa1\xa1\xa1\xb9\xda\xabW\xaf\xa1s\xe7\xce*11\xd1\x19\x61\x1(\x5\xe4?P:x*\x16\0\0\0JL|\xfcyI\x92o\xb9r\xb9\xda\xfd+T\x90$%%%*000\xdf\xbe\xf9-\x18\xb9\xef\xc0\x41\aG\b\xa0\xa4\x90\xff@\xe9\xa0\xb0\x3\0\0\x80\x12\x13\x14\x14$IJII\x91\xb7\xb7\xb7\xb5=%99\xd7\xeb\xb6*\xea\xd3K^[0\xafH\xfd\x1c\x1dG~\x88\xadh\xdc)6WE\xfe\x17\xce\xc8\xb1I\x8e\x8d\x8f\xd8J\xe\x85\x1d\0\0\0\x94\x98\xd0\xd0*\x92\xa4\xd3\xa7N\xe5\xba\x32\x1f\x17\x17\xa7\x88\x88H\xf9\xfbW\xc8\xd3\x87\xab\xf2@\xd9@\xfe\x3\xa5\x83\x35v\0\0\0Pb\xa2\xa2\xa3\x15S\xbf\x81\x36o\xfe\xca\xda\x66\x36\x9b\xb5\xfe\x8b\xcf\x35h\xc8\x10\xe7\x5\x6\xa0\xc4\x91\xff@\xe9`\xc6\xe\0\0\0J\x8c\x97\x97\x97\x46?>F\xff\x1e\x31\\\xde\xde^\xaa_\xbf\xa1\xb6l\xfeJ\xe7\xcf\x9fW\x97\xae\xdd\x9d\x1d\x1e\x80\x12\x44\xfe\x3\xa5\x83\x19;\0\0\0(Q\xad\xdb\xb4\xd5;\x8b\xdf\xd3\x81\xfd\xfb\x35\xfb\xd9g\xe4\xe7\xe7\xa7\x45\xef,Vpp\xb0\xb3\x43\x3P\xc2\xc8\x7f\xa0\xe4\x31\x63\a\0\0\0%\xee\xe6[n\xd5\xcd\xb7\xdc\xea\xec\x30\ne\xb1X\xb4\x61\xc3z\xad\xff\xfc\x33\x9d>uJwt\xe8\xa0\x41\x83\x87\xca\xcf\xcf/\xdf\xed\xcf\x9f?\xafU\x1f\x7f\xa4\x9d?\xfe\xa8\x13\xff\x1cW\xeb\x36m5\xe4\xfe\x61\x8a\x88\x88txl\xa7O\x9f\xd2{\x8b\x17\xeb\xdbo\xbeV\x83\x86\r\x15\xdb\xf7^\xdd\xd8\xb4\xa9M}\xcd\x66\xb3\x1e\x1b\xf9\x88\xb2\xb2\xb2\xf4\xfa\x9b\x8b\f\x11[FF\x86\xd6\xac^\xa5\x1f\xbe\xff^{\x7f\xdd\xa3\x6\r\x1b\xa9[\xf7\x1e\xea\xd8\xa9\xb3Sc\xcb\xca\xca\xd2\x97\x9b\x36j\xf5'\xabt\xf8\xd0\x41\xdd\xd8\xb4\x99\x6\xe\x1a\xac\xeb\xaf\xff\x97\x43\xe3r'e5\xff\xd3\xd2\xd2\xf4\xf9g\x9f\xea\xdbo\xbe\xd1\xde_\xf7\xe8\xfa\x7f\xdd\xa0\xfb\x6\r\xd6\r7\xda\x96\x97\xf6HMM\xd5\x87\xcb\xde\xd7\xe6\xcd_* P]\xbbuW\xf7\x1e=e2\x99l\xea\xff\xce\xdbo\xe9\xe5\x17\xe7\xe8\xc7]?\xab\x42@\x80\x43\x63\xb3\xf7\xe7\x96#>>^\x1f-\xffP\xbbv\xed\xd4\xc9\x13\xff\xa8\xc5\xcd\xb7(6\xb6\x9f\x1a\x35n\xec\xd0\xf8~\xfdu\x8fV~\xb4\x42{~\xfeY-[\xb7\xd6\xd0\xa1\xc3\x14^\xadZ\x81\xdb_\xbe\x9c\xa4\x8fV\xac\xd0\xe6\xaf\x36\xe9\xe2\x85\x8bj\xd3\xae\x9d\xee\x1b\x34X\xd5\xaa\x45\x38\x34.Gb\xc6\xe\0\0\0\xf0_\xef/}Os\x9e\x9b\xad\x8e\x9d:\xeb\x81\a\xff\xad\xad[\xb6\xe8\xc9qc\x95\x99\x99\x99g[\xb3\xd9\xac\x81\xfd\xfb\xea\xdc\xd9\xb3\xeawo\x7f\xdd?|\x84\xfe\xf8\xe3\xf=4\xe2\x1]\xbat\xc9\xa1q\x9d\x39sF\xc3\x86\xeVjj\xaa\xc6\x8e\x7fR\x11\x91\x91\x1a\x32h\x80~\xda\xbd\xcb\xa6\xfe+?Z\xa1\xed\xdb\xb6:4\xa6\xe2\xc4\x96\x9c\x9c\xac\x31\xa3Gi\xc3\xfa\xf5j\xdb\xee\x36M\x9b\x31K\xad\xdb\xb4\xd5\xa9\xb8\x38\xa7\xc7\xf6\xc9\xaa\x8f\x35\xe7\xf9\xd9\xea\xd4\xb9\x8b\xa6L{F\x95\x82\x83u_\xff~\xfa\xf3\xcf\x3\xe\x8d\r\xc6\x63O\xfeK\xd2\xcb/\xcd\xd1\xe6\xaf\xbe\xd2m\xb7\xb7\xd7\xd8\xf1\x13\x64\x32\x99\x34\xb0\x7f?\xed\xdf\xff\x87\x43\xe3\x32\x9b\xcdz\xea\xc9q\xfa\xe1\x87\xef\x34\xfc\x81\aug\x87\x8ezf\xfaT-}o\x89M\xfd\x7f\xfdu\x8f^~q\x8e\x43\x63\xba\x92\xbd?7I:v\xec\x98\x6\r\xbcW'\xe3N*\xb6o?=\xf5\xf4\x64\xd5\xa8QC\t\t\t\xe\x8d\xed\xa7\xdd\xbb\x34x\xe0\0\xd5\xa9SW\xe3'L\xd4\xc5\v\x17t\xff\xd0\x41:s\xe6L\xbe\xdb[,\x16=3}\x9a\xbe\xfb\xf6\x1b\r\xbd\x7f\xb8\x46>6Z\xc7\xff\xfe[\xf=\xf8\x80\x92\x12\x13\x1d\x1a\x9b#1c\a\0\0\0\x90t\xee\xdcY\xbd\xfc\xe2\x1c\xbd\xf5\xce\xbbj\xd1\xe2&IR\xfd\x6\r\xd4\xe9\xce\xf6\xfai\xf7.\xb5\xb8\xe9\xe6\\\xdb{yy\xe9\xdd%\xef+<<\xdc\xda\xd6\xaauk\xddy{;\xfd\xb6\xf7W\xb5l\xd5\xda\x61\xb1\xadY\xbdJ5j\xd4\xd4\xd3\x93\xa7\xc8\xd3\xd3Sm\xda\xb6SZj\x9a\x16\xbe\xb6@o\xbf\xbb\xe4\x9aW\xed\xf\xec\xdf\xaf\xd7\x16\xccS\x8f\x9e\xbdt\xe1\xc2\x5\x87\xc5T\x9c\xd8\x36\xac\xff\\IIIZ\xf8\xc6[\x85^\xd5/\xed\xd8\x96\x7f\xb8L\x3\a\rQ\xef\xbb\xfbH\x92\xda\xb4m\xab?\xf6\xed\xd3\x8f;v\xa8^\xbd\x98\x12\x8b\x15\xce\x65o\xfeK\xd2\xb0\xe1#T\xb9reyzzJ\x92:t\xec\xa4\xbf\x8f\x1d\xd3\xf6\xad[U\xbf~\x3\x87\xc5\xb6s\xe7\x8f\xfa\xf6\x9b\xaf\xb5i\xf3\x36\xeb-lAAA\x9a\x30~\xac\xbau\xef\xa1\xca\x95+\x17\xd8\xf7\xd2\xa5K\x9a\x32i\xa2\xfa\xdc\xd3W\xab>\xfe\xc8\x61\x31\xe5(\xca\xcfM\x92\xdeX\xb8@\x1d:t\xd2\xc8\xc7\x46[\xf3\xb0m\xbb\xdb\x1c\x1a[VV\x96\xe6\xcf\x9b\xabQ\x8f\x8d\xd6\xfd\xc3\x86K\x92n\xba\xf9\x66\r\x1d\x34P\x9f\xad[\xab\x61\xc3G\xe4\xe9s\xe6\xcci}\xb6\xeeS\xbd\xf7\xfe\aj\xd6\xbc\x85$\xa9y\x8b\x16j\xd3\xf2\x16\xfd\xf5\xd7_6\xcf\x92,m\xcc\xd8\x1\0\0\0$}\xff\xddw\xaa\\9X7^q\x1b\x45\xf5\xea\x35tG\x87\x8e\xda\xb2\x65s\xbe}\xae,\xeaHR\xc5\x8a\x95$I\xe9\xe9\x19\xe\x8b\xcbl6\xeb\xdd\xb7\xdfV\xb7\xee=\xac\x3H){\x10\xb9\xe3\x87\xef\xf5\xf7\xb1\x63\x5\xf6\xbd|\xf9\xb2\xa6N\x99\xa4)S\xa7+&\xa6\xbe\xc3\x62*NlYYYZ0o\x9e\xee\xed?\xa0\x44\x8b:E\xfd\xb9U\xaa\x14\xac\x7f\x8e\x1f\x97\xc5\x62\x91\x94}\xcb\xd8\x85\v\t\x8a\x88\x30\xeem\x18(\xbe\xa2\xe4\x7f\x95*Ur\xbd\xb7<==\x15\x16\x1e\xae\xb4\xb4\x34\x87\xc6\xb6q\xc3z\xf5\xecuW\xaeu\x89nm\xd9J\x99\x99Y\xda\xbdkg\x81\xfd,\x16\x8b\x16\xcc\x9f\xab[nm\xa9\xee={:4\xa6\x1c\x45\xf9\xb9\x1d=\xfa\x7fZ\xf7\xe9Z\xf5\xea\xdd\xdb\xe6[\xc9\x8a\xe2\xf0\xa1\x43\xda\xb5\xf3G\xddv{{k[\xb9r\xe5\xd4\xa3W/-]\xb2\xc4\x9a\xe3W*W\xae\x9c$\xe9\xd4\xa9S\xd6\xb6\xc4K\xd9\x33u\xaaT\xadZb\xb1\x16\x17\x33v\0\0\0\0I'N\x9cPt\xbd\x18yy\xe5\xfe\x13\xb9^t=\xfd\xfa\xeb\x1e\x9b\xf6q\xe8\xd0\x41IRt\xbdh\x87\xc5u\xe1\x42\x82\x92\x92\x12\x15\x16\x96\xbb\x88\x94ST:w\xee\xacj\xd6\xaa\x95o\xdf\xb7\xde|]Q\xd1\xd1\xea\xd0\xb1\x93\x96.Y\xec\xb0\x98\x8a\x13[BB\x82\xce\x9c\x39\xad\xc0\xc0@-z\xf3\r}\xfe\xf9:\x5\x4\x4\xea\xee>\xf7\xa8G\xcf^\xb9\x6\xca\xa5\x1d\x9b$\x8d\x1c\x35Z\x8f<\xfc\xa0\xbc\xbc\xbdT\xaf^\x8c\xb6o\xdb\xa6;\xee\xe8\xe0\xf0\xd9\x4\x30\x16G\xe4\xff\xe5\xcbI\xda\xf1\xc3\xf7\xba\xab\xf7\xdd\xe\x8d\xed\xe0\x9f\x7f\xaas\xd7n\xb9\xda\xfc\xfd\xfd\x15]/\xfa\x9a\xb7/n\xdf\xb6U;\xbe\xff^\xcb\x96\x7f\xa4\x83\x7f\xfd\xe9\xd0\x98r\x14\xe5\xe7\x16w2;fs\x86Y3gL\xd3\xf6\xed\xdbT\xaf^\x8c\x86\f\x1d\xa6\xe6-Z8,\xb6\x9c\xdb\xad\x42\x43\x43s\xb5W\xaf^C\xe7\xce\x9dUbb\xa2\x2\x3\x3s\xbd\x16\x14TQS\xa6\xcd\xd0\xa4\x89O)!!A&\x93\xb4\xfe\x8b\xcf\x35\xe7\xa5W\x14\x19\xe9\xf8\xb5\xd3\x1c\x85\xc2\xe\0\0\0 \xe9\xf4\xa9S\n\n\n\xca\xd3\x1e\x18\x14\xa8\x7f\x8e\x1f/\xb4\x7f\x66\x66\xa6\xdeX\xf8\x9a\x46<\xf8\x90\x43\x17O\xce\xb9}\xea\xea\xd9-\xe5\xfd\xcbKR\x81\xeb\xf9|\xff\xfdw\xfa|\xdd\xa7Z\xf9\xc9\x9a\x12\xbb*^\x94\xd8\xe2\xcf\x9f\x97$\r\x1b:X\x8f\x8e\x1a\xad\xa7&NR\\\\\x9c^xn\xb6\xcc\x66\xb3\xee\x89\xed\xeb\xb4\xd8$\xa9Q\xe3\xc6\x1a\x36|\x84^\x9a\xf3\xbc\xb5m\xc2\x96\xedy\x6\xae([\x8a\x9b\xff\x92\xf4\xd1\x8a\x15\x8a\x8a\x8eV\xeb\x36m\x1d\x16\x97\xc5\x62\xd1\xfe\xfd\xfb\xd5'66\xcfk\xa1\xa1U\x14\x9f\x10\x9f\xf7%g\xf9\0\0 \0IDATo\xbfS\xa7Ni\xca\xe4\xa7\x35w\xdeky\x8a\x17\x8eT\x94\x9f\xdb\xd9\xb3\xd9\x5\x97gfLS\x87\x8e\x1d\xd5\xa1Sg\xfd\xb4{\xb7\x86\f\x1a\xa0\x95\xab\xd6\xa8\x61\xa3\x46\xe\x89->>\xfb\\\xe3\xfb\xdfY89\xfc+T\x90$%%\xe5-\xecHR\xe7.]\xf5\xdd\xb7\xdf\xea\xb9ggJ\x92\xee\xba\xbb\x8f\xda\x38\xf0wZ\x12\x38;\x1\0\0\0\x92*\x87T\xd6o{\xf7\xe6iOJJR\xad\xeb\xae+\xb4\xff\x92\xc5\xef\xe8\xd2\xa5K\x1a\x36\xfc\x1\x87\xc6\x15\xf0\xdf'\xd8\\N\xbe\x9c\xab=%%U\x92T\xb1R\xa5<}\xce\x9f?\xa7)\x93&j\xc6\xccgU\xb9r\x88$\xc9\xa2\xbc\xb7\x1d\x38#6\xbf\xf2\xd9\x85\x95n\xdd{\xe8\xa1\x87\x1f\xb1\xb6gd\xa4k\xd1\x9bo\xa8\xcf=\xb1\xe)D\x15%6\x8b\xc5\xa2gg>\xa3s\xe7\xce\xe9\xe3O\xd6\xa8J\xd5\xaaZ\xf7\xe9\xa7\xea\xd2\xa9\x83^z\xe5U\xb5\xbf\xe3\xce\x62\xc7\x5\x63*n\xfe\xef\xfcq\x87\xde|}\xa1>\\\xb1R\xde\xde\xde\xe\x8b\xcb\x64\x32\xa9v\x9d\xdaJ\xccg\xe1\xde\x84\x84\xf8<\xb3Q\xa4\xec\"\xf3\xf3\xb3g\xa9\x7f\xff\x81\xd6\x35\x61\x1c\x9f\xfd\xd9\x8a\xf2s\xf3\xf7\xf7\x97$\x8d\x1b\xff\xa4\x1a\x34\xcc.\xe2\xb4hq\x93\xe\x1f:\xa8\r\xeb\xbfpXa'\xa7\xe0\x94\x92\x92\x92\xebw\x92\x92\x9c\x9c\xeb\xf5+%&&j\xd8\x90\x41\xba\xb5U+}\xb9\x65\x9b\xd2R\xd3\xb4\x64\xc9\xbb\xea\xd5\xb3\x9b\x16\xbd\xbdX\xd7\xd5\xae\xed\x90\xd8\x1c\x8d\x35v\0\0\0\0I\xd5\xaaU\xd3\xfe\xfd\x7f(===W\xfb\xb1\xa3Gu\xddu\xd7\xfe\x63~\xfd\x17\x9f\xeb\xc3\x65\xef\xeb\x85\x39/9\xfcQ\xc2\x95*\x5\xcb\xd3\xd3S'O\x9c\xc8\xd5\x9es\xd5;4$\xef\xc0\xee\x9d\x45\x8b\x14w\xf2\xa4\x46\f\xbf_\rc\xa2\xd4\x30&Js\x9e\x7fN_\xffg\xbb\x1a\xc6\x44\xe9\xb9\xd9\xb3\x9c\x16[\xce:!\xedn\xbb=W{DD\xa4N\x9c\xf8\xc7\x61\xeb\x93\x14%\xb6\x43\a\xf\xea\xa3\x15\x1fj\xd4\xe8\xd1j\xd0\xb0\x91\x42\x42\x42u\xff\xb0\xe1\xfa\xf7\xc3\xfk\xda\xd4\xc9\xd7|\xca\xf\\[q\xf2\xff\xe0_\x7f\xe9\xb1\x91\x8f\xea\xe5W\xe7\xa9N\xdd\xba\xe\x8f-:\xba\x9e\x8e\x1c\x39\x92\xab---M\x87\xf\x1d\xces\xab\xa1$m\xdd\xb2Y_n\xda\xa8\x5\xf3\xe7Z\xf3\x7f\xf0\xc0\xfe\x92\xa4\x9b\x9a\xdf\xa8\xd6-\xf3_\xd0\xb8(\x8a\xf2s\v\t\xc9.6W\n\xce\xbd\xe8s\x9d:u\x15\x17w\xd2\x61\xb1\x85\x86V\x91\x94=\xab\xe8Jqqq\x8a\x88\x88\x94\xbf\x7f\x85<}\xb6m\xdd\xa2\x93q'5r\xd4hEDD\xaav\x9d:\x9a:m\x86\xaaW\xaf\xa1\x65\xef/uXl\x8e\x46\x61\a\0\0\0\x90t\xf3-\xb7*\xfe\xfcy\xed\xda\xf9\xbf\xc5H\xcf\x9e=\xab/>\xffL\xed\xef\xfc\xdfL\x8d\xe4\xff^\xed\xcd\xf1\xe5\xa6\x8dz\xf9\xa5\x39Z\xf4\xce\x12U+\x81\x5v}||4x\xe8\xfd\xfal\xddZed\xfcoQ\xe6\xffl\xdf\xa6v\xb7\xdd\xae\x88\xff\xae\xfb\x90\x96\x96\x66-<\xdc\x13\xdbW\x1f\x7f\xb2&\xd7G\xceU\xf0\x8f?Y\xa3\xfe\x3\x6\x96jlYYYJII\x91$\x95/_^\xf7\xc4\xf6\xd3\xf6m[\x95\x95\x95\x65\xed\xb3\xe7\x97_\xd4\xb4i3\xeb\xe2\xa5\xce\x88-##{pz\xf5\xef\xb8Z\xb5\b\xa5\xa7\xa5\xe7\x8a\x17\x65KQ\xf3\xff\xd0\xa1\x83\xfa\xf7\x88\x61\x9a\xf9\xecl\xb5l\xd5\xaa\x44\x62\xbb\xb3\x43G\xad[\xbbVg\xafxD\xf7O\xbbwI\x92\x9a\xff\xf7ITW\xc6v\xfd\xf5\xff\xca\x93\xff\x8f<:J\x92\xb4\xe8\xdd%zk\xd1\xbb\xe\x8b\xad(?\xb7z1\xf5U\xbdzu}\xfd\x9f\xed\xd6\x36\x8b\xc5\xa2\xed\xdb\xb7\x39t\x91\xf7\xa8\xe8h\xc5\xd4o\xa0\xcd\x9b\xbf\xb2\xb6\x99\xcd\x66\xad\xff\xe2s\r\x1a\x32\xc4\xdav\xe5\xb9\x33--M\xa9))\xb9\n\xcc\x9e\x9e\x9e\xaaQ\xa3\xa6R\xd3R\x1d\x16\x9b\xa3y\x8e|\xec\x89iA\x1\xd9S\xa1.%&\xabr%\xc7^a\0\xca\x8a\xf3\tI\xba\x32Wr>/\xc8\x95\xdb\x90[\0\0G+\xce\xffK\xe9\x19\xe9\xd7\xdc\xd6]\x5\x6\x6\xca\x62\xb1h\xee\xab/+$$Dqq'\xf5\xd2\x9c\x17\xf4\xaf\x1bn\xd0\x80\x81\xf7\xc9\x64\x32\xe9\xf0\xe1\x43\xea\xd2\xf1\xeU\xacXI\x8d\x1a\x35\xd6\xa6\x8d\x1b\x34\x66\xf4(\x8dz\xecq\x5\x4\x4(..Nqqq:{\xf6\xac\xaa\x86\x85\x39,\xb6\xea\xd5\xab\xeb\x9d\x45\x8bt\xec\xd8\x31\xf9\xf8xk\xcb\xe6\xcdzc\xe1\x2=\xf3\xecl\x85\x85\x85)55U}\xef\xe9\xad\x9f\x7f\xfaI\x1d:vR\xa5J\x95\x14Z\xa5J\xae\x8f\xa4\xa4\xcb\xf2\xf2\xf2\xd2\xd0\xfb\x87+(\xa8\x62\xa9\xc5&I\xb3g=\xa3Y\xcfL\xd7]\xbd\xfb\xc8\xd7\xd7W\x91\x91\xd5\xf5\xf2\x8bst\xee\xec\x19\x99L&\xed\xda\xb5So\xbc\xbeP\x93\xa7MWddu\xa7\xc5V\xa5jU\xed\xff\xe3\xf\xad\xfbt\x8d\xfc\xfd\xfdu\xf1\xe2\x5\xfd\xf2\xcb\xcf\x9a?\xf7U\xdd?|\xb8\x9a\x36m\xe6\xb0\xd8\x8a\xcb\xd7\xc7G\x12\xf9\xef(E\xc9\xff\x43\a\xf\xaag\xf7.\xba\xe3\xce\xe\xba\xb1i3k\xfe\xc7\xc5\xc5\xa9Rp\xb0\xc3\xd6\x65\x8a\x8c\x88\xd4\xcf?\xff\xa4M\x9b\x36*((H\x7f\xec\xdb\xa7\x19\xd3\xa6\xea\xb1\xc7\xc7X\xdf\x93\x1b\xd6\x7f\xa1~\xf7\xdc\xadV\xad\xdb\xa8v\x9d:y\xf2\xdf\x64\x32i\xed\x9a\xd5\x9a\x34i\x8a\xb5\xa8\xe9\bE\xf9\xb9yyy)8$D\x93\x9ezR\x15\xfc+(9%Y\x9f\xaf\xfbT\xdf}\xf7\x8d\xa6N\x7f\xc6z\xabVqyxx\xa8Z\xb5j\x9a\x32i\xa2|}}\x95\x92\x92\xaa\xa5K\x16\xeb\xc8\x91\xc3\x1a;~\x82\xfc\xfc\xfc\xf2\x9c;\x83+\x5\xeb\xf3\xcf>\xd3\x9e=\xbf\xa8\\\xb9r:{\xe6\xac\xb6n\xd9\xac\x45o\xbe\xae\xa7&N*\x91\xe2}Q]y\xe`\x8d\x1d\0\0\0\x37\x63\x32\x99\xf2}\xcc+\xa4\x87\x1f\x19\xa9\xb0\xb0p\xad]\xb3Z\xa9\xa9)\xba\xb3\x43'\r\x18\x30P\x1e\x1e\xd9\x13\xdd\xfd\xfd+\xa8\x61\xc3\x46\xd6\xdb\x1f\xe6\xbc\xf0\x9c$\xe9\xd9\x99\x33r\xed'*\xba\x9e\xd6\xae\xfb\xdc\x61qU\xaf^CK\xde\xff@K\x97,\xd6\xccg\xa6\xeb\xc6\xa6\xcd\xb4t\xd9r\xeb,\x1cooo\xd5\xab\x17\xa3\xebj\xd7q\xd8\x31\x1d\x15\x9b$\xd5\xa8YK1\xf5\x1b\xc8\xe7\xbf\x3\x91\xbaQQz\xff\xc3\xe5z\x7f\xe9{\x9a\x31m\xaa\x1a\x35n\xac\x5\v_W\xd3\x66\xcd\x9d\x1a\x9b\x8f\x8f\x8f\x9e\x9f\xf3\x92\xd6\xac\xfe\x44\x9f\xacZ\xa9\xa3\xffwT\x8d\x1a\x37\xd6\xb8''\xe8\xf6\xf6w84\xb6\xe2(\xce\x1a\x44\xe4\x7f\xc1\xec\xcd\xff\xb5kV[\xff\xcd\xf9<\xc7\x96\xed\xdfX\x8b\x87\xc5\xe5\xe3\xeb\xab\xb9\xf3\x16h\xc9\x92w5\x7f\xde\xab\n\xf\xaf\xa6\x89\x93&\xab\x63\xa7\xce\xd6mBBB\xd5\xb8\xc9\xf5%\xbaPrA\xec\xfd\xb9IR\x97.]\xe5W\xae\x9c\xd6\xac^\xad\xe5\x1f.S\xcbVm\xf4\xd1\xcaOT\xa5J\x15\x87\xc6\xd6\xbaM[\xbd\xb3\xf8=\xad\xfax\xa5\xd6\xaeY\xad\xb6mo\xd3\xa2w\x16[o\t\xbd\xfa\xdc\x19^\xad\x9a\xde\xff`\xb9V,\xffP\v_[\xa0\xcbI\x89j\xda\xbc\x85\x96\xaf\\\xa5&M\xaewhl\xc5q\xf5\x39\xc0\xf4\xe7\xe1\x93\x96\xea\xd5\xb2\xef/=~\xf2\xac\xa2k\xe7\xbdG\xf\x80\xf4\xd7\x91\x38]\x99+9\x9f\x17\xe4\xcam\xc8-\0\x80\xa3\x15\xe7\xff\xa5\xcb)\x97\x95\x95\xc5\xc0\xepU\x1e\x1e&\xf9\xfb\x65\xcfj \xff\x1\xf7s\xf5\x39\x80\x35v\0\0\0\xdc\x8c\xa7\a\x93\xb6\x1WV\x9c\x1c&\xff\x1\xd7wu\x1eS\xd8\x1\0\0p3\x9e^\x9e\xce\xe\x1@1\x14'\x87\xc9\x7f\xc0\xf5]\x9d\xc7\x14v\0\0\0\xdc\x8c\x97\x87\x97<\x8a\xb1\x46\a\0\xe7\xf1\x30\x99\xe4\xe5Y\xf4Y7\xe4?\xe0\xda\xf2;\aP\xd8\x1\0\0p3&\x93\xe4\xeb\xeb\xeb\xec\x30\0\x14\x81\xaf\xaf\xaf\x8aS\x96!\xff\x1\xd7\x96\xdf\x39\x80\xc2\xe\0\0\x80\x1b\xf2\xf2\xf4\x92\x8f\x97\xb7\xb3\xc3\0`\a\x1f/\xef\x62\xcd\xd6\xc9\x41\xfe\x3\xae\xa9\xa0s\0\x85\x1d\0\0\0\x37\xe0\xed\xed\xa5\x8c\fs\xae\x36___\x6w\x80\x8b\xf0\xf1\xf2\xce\x33\xd3&=\xc3,\x1f\xef\xc2\v=\xe4?\xe0\xfa\xaeu\xe\xa0\xb0\x3\0\0\xe0\x6|\xbc\xbd\x94~\xd5\xc0N\xca\x1e\xdc\xf9\x95+\xc7\x9a\x1b\x80\x41y\x98L\xf2+W.\xdf\xdb\xa7\x32\xd2\xcd\xf2\xf6)\xbc\xb0\x43\xfe\x3\xae\xcb\x96s\0\xcf\xba\x3\0\0p\x3\xe5|}\x94\x9a\x9e.\x7f\x95\xcb\xf3\x9a\x97\xa7\x97<\xcb{\xc9\x9ciV\xa6\x39S\x99YfY,\x92\xc5\x62qB\xa4\x80{3\x99L2\x99\xb2\x1fg\xec\xe9\xe5)/O\xaf\x2\xd7\xd4IKO\x97\x9f_\xde\x9c\xbe\x1a\xf9\xf\xb8\x8e\xa2\x9c\x3(\xec\0\0\0\xb8\x81\xf2~\xbeJJNQrJ\xaa\xca\xe7\x33\x10\x34I\xf2\xf6\xf4\x92\xb7\xa7\x97$\x16V\x5\x8c.\xfe\x42\xa2\xccYY\xf2+\xe7S\xe8\xb6\xe4?P\xf6\\y\xe\xa0\xb0\x3\0\0\xe0&*W\f\xd0\xc9\xd3\xf1\xd9W\x3%y\xfbx\xcb\xd3\x83;\xf3\x1W\x91\x9e\x61VF\xbaY\xa9\xe9\x19\xca\xca\xccT\x95\x90\x8a\x36\xf7%\xff\x1\xd7W\xd0\x39\x80\xc2\xe\0\0\x80\x9b\xf0\xf4\xf4T\xf5j\xa1J\xb8\x98(s\xa6\x45\x19\x17\x92\x94\x99\x95\xe5\xec\xb0\0\xd8\xc8\xc7\xdbK\xde>^*\xef\xe7#\xbfr\xf6\xcd\xac!\xff\x1\xd7W\xd0\x39\x80\xc2\xe\0\0\x80\x9b\xa9\x14\x14\xe0\xec\x10\0\x38\t\xf9\xf\x94=\xcc\xbd\x3\0\0\0\0\0pQ\x14v\0\0\0\0\0\0\\\x14\x85\x1d\0\0\0\0\0\0\x17\x45\x61\a\0\0\0\0\0\xc0\x45Q\xd8\x1\0\0\0\0\0pQ\x14v\0\0\0\0\0\0\\\x14\x85\x1d\0\0\0\0\0\0\x17\xe5\xe5\xec\0\0\0\0P\xba\xe2/$*3+K\x19\xe9\x66\x65\x66\x65\x39;\x1c\0\x36\xf2\xf6\xf6\x92\x8f\xb7\x97\xfc|}\xe5\xe7\xe7S\xa4}\x90\xff\x80\xeb*\xe8\x1c@a\a\0\0\xc0M\xa4\xa5g\xe8\xcc\xb9\v\xaa\\)P\x1e&\x93\xbc+z\xc9\xd3\x83\t\xdc\x80\xab\xc8\xc8\x30+=\xc3\xac\x94\xf4\x34%&'\xabr\xc5\0yzz\xda\xd4\x97\xfc\a\\_A\xe7\0\n;\0\0\0n\xe2\xcc\xb9\v\xaa^-\xd4\xd9\x61\0(\"oo/y{{\xc9_\xe5\x94\x9c\x92\xaa\x93\xa7\xe3m\xcei\xf2\x1fp}\x5\x9d\x3(\xec\0\0\0\xb8\x81\xf8\v\x89\xaa\\)\xd0\xd9\x61\0p\x90\xf2~\xe5$\x99\x94p1Q\x95\x82\x2\xae\xb9-\xf9\xf\x94=W\x9e\x3\x98{\a\0\0\xe0\x6\x32\xb3\xb2\xe4\x61\x32\x39;\f\0\xe\xe4\x61\x92\xcc\x99\x96\x42\xb7#\xff\x81\xb2)\xe7\x1c@a\a\0\0\xc0\rd\xa4\x9b\xe5\xed\xc3\x64m\xa0,\xf1\xf6\xf1VFzF\xa1\xdb\x91\xff@\xd9\x94s\xe\xa0\xb0\x3\0\0\xe0\x6\x32\xb3\xb2X(\x15(c<=w\xdf\xa5!C\x6\xdb\xb4\xcf\xac\xac,%&&\xca\xcf\xcfO>>>6\xf5\x39z\xf4\xa8\xda\xde\xde\x39O\xfb\xc8G\x86k\xec\x13O\xd8\xb4\xf\xe4\xb5y\xdbv\xfdy\xe0/IR\xe2\xe5$C\r\xea\xe2N\x9d\xd6\xe1#G\x9d\x1d\x46\x99\x62O\xfegee\xe9\xba\xba\r\xad_\x87\x86T\xd2\xad\x37\xb7\xd0\xc0\x81\xfd\xd5\xa2\x45\x8b\xec\xfd\x9d>\xa3\x16\xb7\xb4\xd5\x9aU\x1f\xe8\xc6\x1bo\xb4)\x86\xf4\xf4t\xa5\xa4\xa4( @\x1ev\xae/\xb4u\xebVm\xdc\xf8\xa5\xbe\xfb\x61\x87\"#\xaa\xa9S\xa7\xe\xea\x1b\x1b\xab\xf2\xe5\xcb\xdb\xb5\x1f\xfc\xf\xe7\x80\x92\x45\x61\a\0\0\0\xe\x95x)Q\x83\x86\x8c\xd0\x9d\xedo\xd3\xd8'\x1e\xb3\xb9_\xfbv-\xf5\xe0\x83\xf(##C\xfb\xf\x1c\xd0\x84\x89Su\xf8\xf0\xe1\"\x17T\x12\x13\x13\xd5\xe4\x86\x9b\xb5r\xf9\x12\xddt\xd3M6\xf5IKK\x93$-\x9c\xff\x92\xaaT\xa9\x62m\xf\xe\xae\x64\xf7\xf1O\x9c\x38\xa1\x3\a\xe\xaa}\xfbv\xf9\xbe\xbe\x65\xcbv\xc5\xc4\x44)\"\"\xc2\xee}\x1b\xddg_\xac\xd7\xcf?\xef\xd1\xfd\x43\x6\xeb\xe7={\xf4\x7f\x87\xff\xcf\xfa\xda\xff\x1d\xfe?}\xb2z\xadZ4o\xaew\x16/Q\xd3\xa6\x37\xa8{\xd7.\xa5\x1e\xe3\xc4IS\x15{O\x1fI\xd2\x80\x1}\xd5\xa5S\xc7kn\xff\xf3/\xbfj\xed\xdaO\x8buLwxO\x14\x35\xff\x1f}h\x98\xda\xb4i\xad\xe4\xe4\x64}\xf3\xcd\xb7\xba\xa7\xdf`-~\xfb\x35\xdd~\xfb\xed\x45\x8a\xe3\x97_~Q\xec\xbd\x43\xb4\xf7\x97\x1d\n\n\n\xb2\xb9\xdf\xdcy\xf3\xb4|\xf9\xc7z\xf8\xa1\a\xd4\xa9sG%^\xba\xa4]\xbbv\xcb\xd7\xd7\xd7\xae\xe3\xbb\xc3\xef\xfaZ8\a\xe4\xaf\xa4\xde\x17\x14v\0\0\0\xe0\x30\xe9\x19\x19z\xf0\xe1Q\xaa\x12\x1a\xa2\xc7\x46=bW\xdf\xb0\xb0\xaa\xd6\x2L\xabV\xadT%4T\xa3\x1e\x9f\xa0{\xfb\xf5+\xb5\xc1Ojj\xaa$\xa9\x45\x8b\xe6\n\r\r-\xd6\xbe\xe\x1c\x38\xa8\x87G>\xae\xb9\xaf\xbc\xa0N\x1d\xef\xcc\xf5\xda\xc6M_\xe9\xb1\xc7\xc7k\xe1\xfcW\xca\xdc\xc0.99E3g\xbe\xa0S\xa7O\xeb\xdd\xc5\xef\xe7y==#Cc\xc6>e\xfdz\xc3\x86\xaf\xd4\xfe\xb6\xdbT\xbe\xbc_i\x86\xa9\x1d;v\xaa\xfdm\xb7I\x92\xea\xd4\xae\xadV-o\xb9\xe6\xf6i\xa9i\xc5\x1e\xd4\x95\xf5\xf7\x44q\xf2\xbfnT]k\xfe\xdfv\xdbmJOO\xd7\xb3\xb3_(ra\xa7(~\xff}\x9f\x16\xbc\xb6H[7oP\xf5\xea\x91\xd6\xf6\x9e={\xda\xbd\xaf\xb2\xfe\xbb\xbe\x16\xce\x1\x5+\xa9\xf7\x5\x8b'\x3\0\0\xc0n\xe9\x19\x19\xfa\xee\xfb\x1fr\xb5\x65\x65\x65i\xdc\xb8\x89JH\xb8\xa0\xd7_\x9b+o\xef\xe2]C\xcc\xb9\xe5\xe2\x9f\x7fN\xe4\xfb\xba\xc5\x62\xd1\xe6\xcd\x9b\xf5\xc0\x88\a\xd5\xb6]{M\x9a\x34Y\xff\xfc\xf3\x8f\xa4\xff\xcd\xd6\x91\xa4\xd8{\x87\xa8\x66\xed\xfa\xda\xb5kw\xa1\xc7LM\xcd\x9e\xb1\x63\xef\xd5\xf9\xfc\xb4o\xdfNs_yA\x8f=>^\x1b\x37}em\xcf\xf9\xe3}\xee+/\x14x\xd5\xd6\x95\xbd\xb5\xe8\x1d\x9d:}:O{\xc5\x8a\x15U1\x9f\x99\x13\xa7N\x9f\xd6\x9b\x8b\xde-\x8d\xd0\x9c\xae\xac\xbc'J#\xff[\xb7n\xa5\x83\x87\x8f\xe9\xe2\xc5\x8b\xf9\xc7\x90\x9e\xae\xc5K\x96(6\xb6\x9f\xbat\xed\xaey\xf3\xe7+))I\x92\xb4k\xd7n\xc5\xde;D\x92\xd4\xe4\x86\x9bU\xb3v}%&&\x16z\xccU\xabV\xe9\xbe\x81}s\x15u\x8a\xaa\xac\xfc\xae\x8b\x82s@\xc1J\xea}\xc1\x8c\x1d\0\0\0\xd8%=#C\x8f<:F_m\xde\xa2\x17_\x98\xa5>w\xdf%Iz\xee\x85\x97\xb4k\xf7\xcfZ\xbbz\x85*T\xf0/\xf6qrn\x8b\n\r\r\xc9\xf7\xf5OV\xaf\xd6\xcb/\xcf\xd3\x84\tc\x15\x14\x18\xa8\x95+W\xa9W\xefXm\\\xbfN\x95*U\xd2\xb2\xf7\x16i\xe0\xe0\a4\xf6\xf1G\xd4\xbe\xfd\xed\x8a\x8c,|\xb0\x96\x33\x63\xa7k\xb7^\xca\xcc\xcc\xd4\x8d\x37\\\xaf~\xfd\x62\xd5\xb2\x65K\x99L&\xbb\xbf\x87N\x1d\xef\xb4\xfe\x11?\xf7\x95\x17$\xc9\xfa\xf9\xd5Wk\xcb\x8a\xe6\xcd\x9b\xa9J\x95P\x9d\x39s\xd6\xda\x36\xea\xd1\x7f\xeb\xf1\xd1#%Is^|U\v\xdfXd}-<,L7\xdf\xd4\xbc\xd0\xfd\x9e\x38qB\xdf|\xfb\x43\xbe\xaf\xb5nu\x8b\xcb\xcc|p\xf5\xf7\x44i\xe5\x7fjj\xaa\x42\x43*) \xc0Zp\xcd\x61\xb1X4y\xf2\x14\x9d\x8c\x8b\xd3\x43\xf=\xa8\xd4\xb4\x34\xbd\xfa\xea<\xed\xdd\xfb\x9b\xdez\xf3\r\xd5\xab\x17\xad\xb1\x8f?\xa2\x17_yM\xcb\xde[\xa4\xca\x95\x83mZ\x1f\xe7\xb7\xdf\xf7\xa9W\xaf\x1eZ\xb3v\xad>\xf8`\xb9\xe2\xe3\x13\xd4\xbd[\x17\r\x1bv\xbf\x2\x3\x3\xed\xfe\x1e\\\xfdw]T\x9c\x3\xae\xad$\xde\x17\x14v\0\0\0`\xb3\x9c\x41\xdd\xde\xbd\xbf\xe9\x89\x31\xa3\x34~\xc2\x64I\xd2\xa5KIZ\xb1\xe2\x13}\xb2\xea\x43U\xa9R\xbc[\x98,\x16\x8b\xce\x9f?\xaf\xc5\x8b\x97\xa8S\x87v\xaaY\xb3\x66\x9em._\xbe\xac\x99\xb3\x9e\xd7\xdcW\xe6\xa8m\xdb\xb6\x92\xa4\x16\x37\xdd\xa4\xce]\xbak\xcd\xda\xb5z`\xf8p5i\xd2\x38\xbb\xbd\x45s5h\xd0\xc0\xa6\x63W\xadZE/\xcd\x99\xa9\xb0\xaaU\x95\x99\x99\xa5\x9d;wj\xc0\xa0\a4\xef\x95\xe7\x8at;\x86\x94\xfb\x8fxIe~P\xd7\xf2\xd6\x9b\xb5\xf1\x8b\x35\xba\xb1y+IR\xc5\xa0 =>z\xa4u\x1\xdb\xb1O<\xa6\xf7?\\\xa1\xc4K\xd9\x33(6\xae_\xa3\x8a\x15+\x16\xba\xdf\xf0\xf0p\xed\xf9u\xaf\x96\xaf\xf8\x38W{\xff{c\x15{Oo\a\x7f\x17%\xcbU\xdf\x13\xa5\x91\xff\x99\x99\x99\xfa\xfb\xef\xbf\xf5\xee\xe2\xf7\xf4\xd0\x83\xc3\xf3]\xf8\xf8\xb7\xdf~\xd3\x8a\x8f?\xd5\xce\x1f\xb6\xabj\xd5\xaa\x92\xa4\xbau\xea\xea\x8e\x8e\xdd\xb5{\xf7n\xb5h\xd1\x42-Zd\x17\n\x9a\x34il\xd3\x1a;YYY\xfa\xe5\xd7}\xda\xfd\xf3o\xea\xdf\xf7.=\xf4\xef\x11\xca\xca\xca\xd2\xfc\x5\vu\xec\xd8\xdfz\xe5\x95\x97\x8a]\xdc\x95\\\xe7w]\x1c\x9c\x3\n\xe7\xe8\xf7\x5\x85\x1d\0\0\0\xd8$gP\xf7\xdbo\xbf\xeb\xa3\x15KU\xab\x66\rU\v\xf\xd3\xf8\t\x93\xe4\xe5\xe5\xad\x65K\xdfVT\xdd\xda\x45\xde\xff\a+V\xeb\x83\x15\xab\xad_\xf7\xb9\xab\x9b\x66>3C\x9e\x9e\x9ey\xb6\xfd\xe7\x9f\x7f\x94p!Q\xf5\xea\xd5\xb3\xb6\xf9\x95+\xa7;\xda\xb7\xd3\xbe\xdf\xff(r\f111\x8a\x89\x89\xb1~\xdd\xb6m\x1b\x99M\xcb\x1e\x1e\x1e\x1e\xaa\x18\x14\xa0\xf3\xf1\x17\xf5\xec\xb3\xb3\xac\x45\x9c\xaaU\xab\xaa{\xaf\xbe\x1a\x33\x66\xb4j\xd4\xa8\x61\xf7~Q8\xce\x1\xc5Ga\a\0\0\0\x85\xca\x35\xa8[\xbeT5kV\x97$\xdd\xdd\xbb\x97<\xbd\xbc\xe4\xe3\xed\xad\xe6\xcd\x9a\x16\xfb\x38+\x97/\x91\x97\x97\xb7\xc2\xc3\xc3\x15\x1e\x1eV\xe0\x15\xf2\xf4\xf4tIy\a\x4\xbe>\xbe*W\xae\xf8\xeb\xe3\\\xe9\xfa&M4\xff\xb5\xb7\x95\x98\x98h\xd7\xd3ur\\\xb9v\x82T\xf6o\xc5\xf8\xee\xfb\x1d\x1a=f\xbc\xf5\xeb\v\x17/\xea\xe5W\xe6k\xcc\xe3#e\xb1X4\xe7\xc5W\xadW\xea%\xa9s\xd7\xdez\xe5\xe5\xe7u\xeb-\x85?\xb9\xcc\xc3\xc3\x43\xb3\x9e\x99*\x93I\x92\xc9\xa4\x99\xd3\xa7\xb8\xe4\x80\xce\xd5\xde\x13\xa5\x91\xff\xad[6\xd7\xc8G\x1fQ\xf9\xf2\xe5\x15\x19\x19\xa9J\x95\n~\x12]Zz\x9a\xd2\xd3\x33\xf2\xb4W\xa8\xe0/\x1f\x1f\x9f\"\xc7\xd0\xf4\xc6\xebU988\xd7y',,L\x92\x94\x90\x90P\xa4\xc2\x8e\xab\xfd\xae\x1d\x81s@\xe1\x1c\xfd\xbe\xa0\xb0\x3\0\0\x80\x42\xcd\x9f\xbfP\xbf\xff\xbe/\xd7\xa0.G\xaf\x1e\xdd\x1cr\x8c\x1\xfdz\xdb\xfcX\xf2\xc8\xc8Hyzzh\xcf\x9e=\xaaV\xad\x9a\xa4\xec[8\xbe\xfb~G\x9e)\xf9\x19\x19y\a\x80\x5\xc9\xcc\xcc\xcc\x33\x43\xe8\xc0\x81?\x15U\xa7\x66\xb1\x8b:9\x7f\xb0_\xb9\xb6\x42Y\x1c\xdc\xed\xda\xb5;\xd7\xda\x1a\x92\x34\xff\xb5\x37\xf4\xfe\a+$\x8b\x45\x17\xaeZ\f\xf7\xd4\xe9\xd3\xfaq\xe7n\x9b\x6uR\xf6\xc0n\xe6\x8c\xa9\xd6\xcf]\x8d+\xbe'J#\xff\xef\xbe\xdb\xf6\xfc\xaf}\xddu:x\xf8\x98\x8e\x1e=\xaaZ\xb5jI\x92\xe2\xe2\xe2t\xf0\xf0\x31\x45\x45\x45\xfdw\xab\xec\xe2\x8c=\xf9\xdf\xa5s'\xcd\x9a=G\x97.]\xb2\xae\xa9\xf3\xc7\x1f\xd9\x33\0\x8b\xb2~\x8b+\xfe\xae\x1d\x81s\xc0\xb5\x95\xc4\xfb\x82\xc2\xe\0\0\0\n5\xe2\x81\x61\xea\x1b\xdbG\x91\x91\xce[\x9c\x32\x34\xa4\xb2\xfe\xf8\x63\xbf\xe\x1c\x38\xa0\x98\x98\x18\x8d{b\xa4&L\x9c\xaa\xe4\xe4\x64\x45\x44\x44h\xe3\xc6M\xba|\xf9\xb2z\xf4\xe8!I\n\f\fT\x9d\xeb\xaak\xdd\xba\xcf\x15\x10\x10\xa0\x90\x90\x90\x42\ag3f<#___\xddx\xe3\r\xf2\xf3+\xaf\xbd{\xf7\xea\xc5W^\xd3\xfbK\xde\xb4;\xde-[\xb6\xe7\xfb\x87\xfa\x95k+,\x9c\xffJ\x99{2\xce\x88\a\x86i\xf9\x8aUy\x9e\x8as\xe1\xc2\x85|\xb7\xf\xf\v\xd3\xbfG\xdco\xd7\x31\x1c=\x98\xdb\xb1\x63W\xa1\xdb\xfc\xf5\xd7\xc1\x62\x1f\xc7U\xdf\x13\xce\xce\xff\x9c\xc7`\xff\xbaw\xaf\x42\x42\x42\xd4\xbcys\xb5ks\x8b\x1e\x1b=F#\x1f}X\xde\xde>Z\xf4\xf6\xdb\xba\x37\xb6\x97\x1a\x37\xce^[+\xa7\xe0\xbb\x66\xcd\x1a\x35k\xd6LQQQ\xaaP\xa1\xc2\x35\x8f\xd3\xa1\x43\a-^\xb2T#G\x8d\xd6\x80\xfe\xfd\x64\xb1X4\x7f\xc1\xebzr\xecH\x85\x84\xe4\xbf\x88{A\\\xf5w\xed\b\x9c\x3\nVR\xef\v\xcf\x91\x8f=1-( {\xd5\xf2K\x89\xc9\xaa\\)\xa0\xd8\xc1\x2\x65\xd1\xf9\x84$]\x99+9\x9f\x17\xe4\xcam\xc8-\0\x80\xa3\x15\xe7\xff\xa5\xa2\xf0\xf5\xf5)\xd2Sal\xf5\xd5W\x9b\xe5\xe1\xe1\xa1\xf6\xed\xdb\x17\xb8M\xadZ5\xf5\xc1\x87+\xf4\xcb\x9e=\xea\xd2\xb9\x93\x9a\x37o\xae\x90\xca\x15\xf5\xc9\xea\xb5Z\xbf\x61\x93\xa2\xa3\xa3\x34\xfb\xd9\x99\xd6\x45\x38M&\x93\x9a\x37o\xaa\r\x1b\x37\xea\x93\xd5kU\xa7\xf6uW\\\xcd\xcf_@@\x5\xed\xdf\x7f@k\xd6\xae\xd3\x86\x8d_\xaa\x42\x5\x7f=\xf7\xec\f\x9bg\x12\\\xc9\xd7\xd7[7\xb5h\xae\xew\xe6\xfd\x9e\xea\xd6\xad\xa3\x6\x31\x31\x8a\x89\x89*\xd1\x9f\xab\x33x{{+,\xbc\x8a\xc2\xc3\xc2\x34\x7f\xeeKj\xdc\xb8\x81\xb6n\xdbn\xbdm\xce\xc7\xdb[s^\x98\xa5\xc9O?%\x99,\xea\xdb\xf7n5\xb4qqkGzo\xe9\aj\xd3\xba\x95\x64\x32)\xeeT\x9c\xfe>~\xfc\x9a\x1f\x97\x93\x93U?\xa6\x9e\xda\xdf~[\x91\x8fY\x12\xef\tW\xcf\x7f\x8b\xc5\xa2\xb9\xf3\x16\xaaS\xc7;T\xff\x8a\xf5\xadr\x1f\xdfW\x91\xd5\xaa\xea\x8d\x37\xdf\x91,Y\xba\xf5\xd6[\xd5\xfe\xf6\xdb\x14\x7f\xfe\xbc\x96\xbe\xff\x81\xf6\xee\xdd\xabN\x9d:j\xd4\xa8\x91\xd6\x19wAA\x81\x8a\x89\xae\xa3\xc5K\xde\xd7\x8e\x1d?\xaa\x45\xf3\x66\x85\x16g|||\xd4\xb1\x63\a\x1d;zT\xcb>X\xae\xc3\x87\xe\xab\xff\xbd\xfd\x34p\xe0\0\xbb\v\t\xee\x9a\xff\x12\xe7\x80k)\xa9s\x80\xe9\xcf\xc3'-\xd5\xab\x65\xaf\\~\xfc\xe4YE\xd7\xe/r\x90@Y\xf6\xd7\x91\x38]\x99+9\x9f\x17\xe4\xcam\xc8-\0\x80\xa3\x15\xe7\xff%w\x95\x98\x98\xa8\xa3G\x8f\x16\xf8z\xf9\xf2\xe5U\xa7N\x9d\xd2\v\xc8\x8d\xdc\xd9\xb9\x87\xfe\xfa\x33\xfbjw\xbd\x98h}\xb9\xfeS'G$\xdd~Ggu\xeb\xdaY\xd7\x37iR\xe8\xb6\x95\x43*\xeb_\xd7\x37.\x85\xa8\xecG\xfe\xdb\xe6\xd4\xa9S:{\xf6l\x81\xafW\xa9R%\xd7\x62\xccp,\xce\x1%\xe7\xf8\xc9\xb3\xdc\x8a\x5\0\0\0\xf7p\xf4\xe8Qu\xeb\x19[\xe0\xebw\xf7\xea\xaa\x97_~\xb1\x14#r\x1fw\xdc\xd6N\x81\x15\xb2g/\xdf\xd4\xa2\x99\x93\xa3\xc9\x16Y\xbd\xba>\xff\x62\x83>\xff\x62\x43\xa1\xdb\xde|s\v\xc3\xe\xea`\x9b\xad\xdb\xb6\xe9\xa9\xa7g\x14\xf8\xfa\v\xcfMW\xdf\xd8\x82\xcf\xf(\x1e\xce\x1%\x8b\x19;\x80\x8d\x98\xb1\x3\0\x30\x12\x66\xec\0\x90\xc8\x7f\xc0\xdd\x1d?yV\xae\xb7\x84\x34\0\0\0\0\0\0$\x89\xc2\xe\0\0\0\0\0\x80\xab\xa2\xb0\x3\0\0\0\0\0\xe0\xa2(\xec\0\0\0\0\0\0\xb8(\x9e\x8a\x5\0\0\xe0\x66\x8e\x9f,\xf8\x91\xbfp-,\x88\v{\x91\xff\x65\a\xf9\x8f\x1c\xcc\xd8\x1\0\0\0\0\0pQ\xcc\xd8\x1\0\0p3\\\xe5\x5\xdc\x17\xf9\xf\x94=\x14v\0\0\0\xdc\f\xb7\x62\0\xae\xaf\xa8\x5\x1a\xf2\x1f(\x1b\xae<\aP\xd8\x1\0\0p3\\\xb1\a\xdc\x17\xf9\xf\x94=\xac\xb1\x3\0\0\0\0\0\xe0\xa2\x98\xb1\x3\0\0\0\xe4#==]'N\x9cPRb\xa2\x32\xcc\x19\xce\xe\xa7\xc8\xbc\xbd\xbcU! @\x11\x11\x11\xf2\xf1\xf1qv8\x80K \xff\xe1J(\xec\0\0\0\0WIOO\xd7\xfe\xfd\xfb\x95\x99ivv(\xc5\x96\x61\xcePBB\xbc.]\xba\xa4\xfa\xf5\xeb\x33\xb8\x3\nA\xfe\xc3\xd5p+\x16\0\0\0\x9c\xc2Y\x8b\xb8\x1e?y\xb6\xd0\x63\x9f\x38q\xc2\xa9\x83\xba\xd0\xb0\x1a\xe\xdfgf\xa6Y'N\x9cp\xf8~\x1d\xc9\x96\xdf\r\xca\x6\xf2\xbf`\xe4?\xec\x45\x61\a\0\0\0\xb8JRb\xa2\xb3\x43(\x11IIe\xf3\xfb\x2\x1c\x89\xfc\x87\xab\xa1\xb0\x3\0\0\0\\\xc5\x95\xd7\xd4\xb8\x96\x8c\x8c\xb2\xf9}\x1\x8e\x44\xfe\xc3\xd5P\xd8\x1\0\0\0\0\0pQ\x14v\0\0\0\0\a9\xfe\xcfI\x1d\xff\xe7\xa4\xb3\xc3\0\xe0\x4\xe4?\x9c\x85\xa7\x62\x1\0\0 \x8f\x15\x1f\xad\xd2\xae\xdd?\xdb\xbc\xfd-77W\x9f\xbb\xef*\xc1\x88\x8c\xef\xf8?'5e\xda,I\xd2\x8ciO\xabzd5'GTr^\x9d\xbb \xdf\x1l\xf3\x66\x37\xaa_\xdf>N\x88\xa8p\x8b\x97,\x93$\r\x1d\x32\xd0\xc9\x91\x18\x1f\xf9o?\xf2\x9f\xfcw&\n;\0\0\0\xc8\x63\xd7\xee\x9f\xf5\xf9\x17\x1b\x14U\xb7N\xa1\xdb&%%i\xd5'k\x94\x92\x9c\xaa\xfb\xee\xbb\xb7\x14\xa2\x33\x9e\x9c\x41\xdd\x85\v\x17%IS\xa6\xcd*\xd3\x83\xbb\xa3\xc7\x8e\xeb\xd0\xa1\xc3\xd6\xaf\xf7\x1f\x38 \xb3\x39S\x92\f;\xb0\xfb\xfa\xdbo%\x95\xdd\x81\x9d#\x91\xff\xf6!\xff\xc9\x7fg\xa3\xb0\x3\0\0\x80|E\xd5\xad\xa3ukW\xea\xfc\xf9\xf3\x5n\x13\x18\x18(OO/=1v\x82&M\x9d!In7\xb8;u\xea\x8c\xa6L\x9b%/O/\xd5\xaa\x99\xfd\x98\xe2\xa4\xa4\xcb\x9a\x32m\x96\x66\xcf\x9c\xaa\xb0\xb0*N\x8e\xd0\xf1^}\xf9\xf9\xff}>w\x81~\xfb}\x9f\xbc\xbc<\x9d\x18Q\xd9\x97\x92\x92\xa2~\xfd\a\xcb\xe4\xe1\xa1\x15\x1f,Q\xb9r\xe5J\xf4x\xe4\xbfm\xc8\x7f\xf2\xbf\xb4\\\xeb\x1c@a\a\0\0\0\x5:\x7f\xfe\xbc\x9a\xdd\xd4\xa6\xc0\xd7_}\xf9y\xdd\xd5\xab\x87^z\xf1\x39I\xd2\xa4\xa9\x33\xe4W\xbe\x9c[\xdd\x96\x11\x1c\\Q7\xfc\xebz\xc5\xf6\xe9\xa5w\x16/\x95$=9n\xb4V\xaeZ\xab\xe0\xe0\x8aN\x8e\xae\x64\xbd:w\x81^\x99\xfb\x9a\x1e\x7f\xec\x11m\xde\xb2\xdd\xd9\xe1\x94i~~~\x9a\xf9\xccT\xf5\xbe\xfb^\xcd\x98\x39[\xcf\xce\x9c^\xe2\xc7$\xff\vG\xfe\x93\xff\xa5\xe5Z\xe7\0\x16O\x6\0\0@\xb1yyy\xea\xa5\x17\x9f\xd3u\xb5j\xea\x87\x1d\xbb\x9c\x1dN\xa9\x88\x8b;\xa5\x94\x94T\xf9\xf8\xf8h\xd4\xa3#r]\x99\xf\v\xab\xa2Q\x8f\x8e\x90\x8f\x8f\x8f\xd2\xd3\xd3u\xea\xd4\x19'FZ2\xae\x1c\xd4\x8d~\xecQg\x87S\xa8\xf6\xed\xda\xa9}\xbbv\xce\xe\xa3X\x1a\x37j\xa8IO?\xa9\xf>\\\xe9\xecPr!\xff\xc9\x7f\xa3+\v\xf9/\x15|\xe`\xc6\xe\0\0\0\n\xe4\xe9\xe9\xa9\x88\x88\x88\x2_\xf7\xf7/o\xfd\xdc\xcb\xcbS\x15*T(\x8d\xb0\x9c.gM\x8d\xc6\x8d\x1ah\xcc\xe8G\xae\xb9\xed\x1bo-\xd1/{~-Skn\xb8\xda\xa0N\x92\x6\xe\xec\xe7\xec\x10\x1c\x62\xd0}\xfd\xf5\xc3\x8e\x9d\xa5r,\xf2?\x7f\xe4?\xf9\xefL\xf9\x9d\x3(\xec\0\0\0\xa0@\xc1\xc1\xc1\xfa\xfe\x9b\xcd\xa5~\xdc\x13'N\xe8\x9bo\x7f\xc8\xf7\xb5\xd6\xadn\xb9\xe6`\xb3\xa4\xe5\f\xea\xbc<\xbd\xd4\xbf\xdf=\x85n\x1f\xdb\xa7\x97~\xfbm\x9f\xc3\x16T={\xf6\x9c\xf6\xec\xfd=\xdf\xd7\xfe\xd5\xa4\x91\x42\x43\x43\x8a\xb5\xff\xc2\x14\x34\xa8\xab[\xb7\x8e\x61\a\xae\x66s\xa6\x36l\xda\xa4\v\xf1\x17u\xef\xbd\xb1.\xbd\x1e\x88\xc9\x64\xd2\x9c\xe7\x66\x96\xca\xb1\xc8\xff\xbc\xc8\x7f\xf2\xdf\xd9\xf2;\aP\xd8\x1\0\0\x80\xe1\x84\x87\x87k\xcf\xaf{\xb5|\xc5\xc7\xb9\xda\xfb\xdf\x1b\xab\xd8{z;)\xaa\xdcO\xbf\xa9U\xb3\x86uM\x8d\x1c\x7f\x1d\xcc~R\xcc\xac\xd9/\xe6j\xafP\xc1_G\x8f\xfd\xed\x90\xc1]\xe5\xca\xc1:t\xf0\xb0\xbe\xdc\xbc-W{\x87;oW\xfb\xdb\n^\xf\xc5\x11\xaeu\xa5\xfe\xca\x5U\x9d\x65\xf1\x92\x65\xd6\xa7\xdfH\xd9\xb7_\f\x1c\xd8O\xcb\x97\xaf\xb4.\xee[18H\xdd\xbbv\xd1\xb2\x65+\xb4\x65\xfbv\xeb\xb6mZ\xb5r\x99'\xe6\x4\x4\x6\x38;\x84\x12\x45\xfe\x17\x8c\xfc/\x98\xbb\xe4\xbf\x94\xf7\x1c@a\a\0\0\0\x36KKK\xd3\xdau_H\x92z\xf5\xe8*__\xdf\x12\x39\x8e\x87\x87\x87\x9e\x9d\x39M&\x93I\x1f.\xcf^K`@\xffX\xcd\x9c\x31U\x1e\x1e\xee\xbdL\xa4\x87\x87\x87\x1e\x1c\x31T2\x99\xf4\xe5W[%I\x1d;\xdc\xae\x11\xc3\x87\x94\xe8\xcf\xc6\x15o\xbf\xc8Q18\xc8\xfa\xf9\x85\xf8\x8bN\x8c\xc4\xb5\x91\xff\xceG\xfe\xdb\xcf\x1d\xf2\x9f\xc2\xe\0\0\0lv\xdf\xe0\xe1\xfaq\xe7nI\xd2\xea\xd5k\xb5\xe2\xc3\xf7\x64\x32\x99J\xe4X\x1e\x1e\x1e\x9a\xf5\xccT\x99L\x92L&\xcd\x9c>\xc5\xe9\x83\xba\xea\x91\xd5\x34\x63\xda\xd3\x9a\x32m\x96\x92\x92.\xeb\xc9q\xa3s-\x9a\x9as\xa5\xfe\xe9\xa7\xc6Z\xdbN\x9d:\xa3\xc9Sg\xa9\x62\xc5 \x87\xad\xb3\xe1\xe1\xe1\xa1\a\x1f\x18\x92\xfd\xb3\x91I#\x86\xfv\xd8\xcf\xe6\xd5\xb9\vt\xf4\xd8\xf1<\x8f\x34v\x85\x41\xdd\xd0!\x3\xf3\xbd\xea\xde\xb9\x63G\xcd\x9c~Q\x15\x83\x83\xd4\xb9\x63GI\xd9kn\xb8\xea\xba\x1b\x97.]R```\xa9\x1f\x97\xfc'\xff\xc9\x7f\x63\xb8\xfa\x1c\xe0\xde\xe5N\0\0\0\xd8\xec\x64\\\x9cuP'I;~\xdc\xa5\xb8S\xa7J\xf4\x98\x1e\x1e\x1e\x9a\x39\x63\xaa!\x6u9r\x6w\xe6L\xb3V\xaeZ[\xe8\xf6+W\xad\x95\x39\xd3\xec\xf0\xc5S=<<4b\xf8\x10\x87\xe\xea\xa4\xec\xdbM\xe\x1d:l\xfd\xdaU\x6u\xd7\xe2\xe5\xe5\xa9\xfb\xee\xbbW\xdd\xbbvq\xf9\xf5\x35,\x16\x8b\xc6O\x98\\\xea\xc7%\xff\xb3\x91\xff\xae\xa7,\xe5\xbf\x94\xff\x39\x80\x19;\0\0\0\xb0I\xe5\xe0`U\f\n\xd2\x85\x8b\xd9S\xd9+V\xac\xa8\xca\xc1\xc1%~\\\xa3\f\xe8\xaeT=\xb2\x9a\x66\xcf\x9c\xaa\xe0\xe0\x8a\x85n\xfb\xef\x11\x43\x14\x1f\x7f!\xd7\x95}G)\xe9\x9fMY\x18\xd4\xe5X\xb6l\x85$\xd7\x7f:\xce\xd2\xf7?\xd4\x86\x8d_\x96\xfaq\xc9\xff\xff!\xff]OY\xc9\x7f)\xffs\x80\xf1\xb2\x4\0\0\0\x86\xe4\xeb\xeb\xabw\xdfy]\xb7\xder\x93Z\xdez\xb3\x16\xbf\xf3z\x89\xad\xb1\xe1\n\xc2\xc2\xaa\xc8\xc7\xc7G\xe7\xce\x9f\xd7\xd4\xe9\xb3u\xe6\xccY\xebk\xa7O\x9f\xd1\xd4\xe9\xb3u\xee\xfcy\xf9\xf8\xf8\x94\xc8\xa0\xae\xa4\x95\xa5\x41\x9d$m\xd9\xbe=\xd7\x62\xa9\xaeh\xefo\xbfk\xe6\xac\xe7\x35\xa0\x7fl\xa9\x1f\x9b\xfc\xcf\x8d\xfcw-e!\xff\xa5\x82\xcf\x1\xcc\xd8\x1\0\0\x80\xcd\x9a\xdex\x83\x96\x7f\xb0\xc4\xd9\x61\x18Jjj\xba\xfe>\xfe\x8f\x9e\x9e\xea\xd7\xd7\xbe\xab\xf5\xee.gp\x97\xf3\xb9\xab*\xab\xbf\xfb\xfc\x1e\x83\x8c\xfc\x95\xd5\xf7@I\"\xff\x8d\xad\xac\xe7?\x85\x1d\0\0\0\xc0\x41\\y@\a\xa0x\xc8\x7f\x38\vO\xc5\x2\0\0\0\0\0pQ\x14v\0\0\0\0\0\0\\\x14\x85\x1d\0\0\0\xe0*\xde^\xde\xce\xe\xa1\x44x{\x97\xcd\xef\vp$\xf2\x1f\xae\x86\xc2\xe\0\0\0p\x95\n\x1\x1\xce\xe\xa1\x44T\xa8P6\xbf/\xc0\x91\xc8\x7f\xb8\x1a\x16O\x6\0\0\x80ST\xaf\x16j\xd8\xe3\x46\x44\x44\xe8\xd2\xa5K\xca\xcc\x34\x97\x42\x44y\x9d=\xf5\xb7\xc3\xf7\xe9\xe9\xe9\xa5\x88\x88\b\x87\xef\xd7\x91\x9c\xf5\x9e@\xe9#\xff\vF\xfe\xc3^\xcc\xd8\x1\0\0\0\xae\xe2\xe3\xe3\xa3\xfa\xf5\xeb\xabR\xa5`\x97\xbf}\xc1\xdb\xdb[\x95*\x5\xab~\xfd\xfa\xf2\xf1\xf1qv8\x80\xe1\x91\xffp5\xcc\xd8\x1\0\0\0\xf2\xe1\xe3\xe3\xa3\xeb\xae\xbb\xce\xd9\x61\0p\x2\xf2\x1f\xae\x84\x19;\0\0\0\0\0\0.\x8a\x19;\0\0\0\x90\xc5\"\x99\xb3\xcc\xca\x34g*3\xcb,\x8b\x45\xb2X,\xce\xe\vp;&\x93I&\x93\xe4\xe9\xe1%O/Oyyx\xc9\x64*\xd9\x63\x92\xff\x80q\x14\xe5\x1c@a\a\0\0\xc0\xcd\x99\x33\xcdJKKS\x16\x3\x39\xc0\xe9,\x16\x8b,\x16)++C\x19\xe6\fy\x98L\xf2\xf5\xf5\x95\x97g\xc9\f\xdd\xc8\x7f\xc0X\x8ar\xe\xe0V,\0\0\0\x37\x96\x96\x96\xa6\x94\xd4T\x6u\x80\x41\x65Y,JIMUZZ\x9a\xc3\xf7M\xfe\x3\xc6g\xcb\x39\x80\xc2\xe\0\0\x80\x9bJKKS\xba\x39\xc3\xd9\x61\0\xb0\x41\xba\x39\xc3\xa1\xc5\x1d\xf2\x1fp-\xd7:\aP\xd8\x1\0\0pC\xe6L3\x83:\xc0\xc5\xa4\x9b\x33\x64\xce\x34\x17{?\xe4?\xe0\x9a\n:\aP\xd8\x1\0\0p3\x16\x8b\x1cv\xe5\xff\x8f}\xbf\xab\x61L\x94\xce\x9e\x39\x93\xef\xeb\xdf}\xfb\xad\x1a\xc6\x44))11W\xfb\xb7\xdf|\xad\x86\x31Q\xbat\xe9R\x9e>\rc\xa2\x1c\x12\x1bP\x16\xa5\xa5\xa5\xa9\x38\x37N\x91\xff\x80k\xcb\xef\x1c\xc0\xe2\xc9\0\0\0n\xc6\x9c\x65.\xb5\x35\x35\xe2\xe3\xcfK\x92|\xcb\x95\xcb\xd5\xee_\xa1\x82$)))Q\x81\x81\x81y\xfa\x15\x34\xb8\xdbw\xe0\xa0\x83#\x4\\K\x96\xc5\"s\xa6Y\xde\x45\\L\x99\xfc\a\\[~\xe7\0\n;\0\0\0n&\xd3\x9cYj\xc7\n\n\n\x92$\xa5\xa4\xa4\xc8\xdb\xdb\xdb\xda\x9e\x92\x9c\x9c\xeb\xf5\xab\x31\x80\x3\n\x96i\xce,ra\x87\xfc\a\\\xdf\xd5\xe7\0\n;\0\0\0n&3\xab\xf8kt\xd8*4\xb4\x8a$\xe9\xf4\xa9S\xb9\xae\xcc\xc7\xc5\xc5)\"\"R\xfe\xfe\x15\xf2\xed\xb7i\xe3\x6\xeb\xe7\x1d;u.\xd9 \x1\x17\x93\x9d\xc3\xbe\xc5\xe8[:\xc8\x7f\xa0\x64\\}\xe\xa0\xb0\x3\0\0\xe0\x66J\xfa.\x8c\xe4\xe4\x64\x95/_^\x92\x14\x15\x1d\xad\x98\xfa\r\xb4y\xf3W\x8a\x8a\x8e\x96$\x99\xcd\x66\xad\xff\xe2s\r\x1a\x32\xa4\xc0}0\x98\x3\nV\x9c\x1c&\xff\x1\xd7wu\x1eS\xd8\x1\0\0p3\x96\x12\x1c\xd9\x1d>|H\xb1w\xdf\xa5\xf1\x13&\xaao\xbf{\xe5\xe5\xe5\xa5\xd1\x8f\x8f\xd1\xbfG\f\x97\xb7\xb7\x97\xea\xd7o\xa8-\x9b\xbf\xd2\xf9\xf3\xe7\xd5\xa5k\xf7\x12\x8b\x3(\xcb\x8a\x93\xc3\xe4?\xe0\xfa\xae\xce\x63\n;\0\0\0p\x18\x7f\xff\nj\xd8\xb0\x91\xc2\xc2\xc2\xadm\xad\xdb\xb4\xd5;\x8b\xdf\xd3\xaa\x8fWj\xed\x9a\xd5j\xdb\xf6\x36-zg\xb1\x82\x83\x83\v\xdc\xf\xb7\x62\0\xae\x87\xfc\a\x9c\x83\xc2\xe\0\0\0\x8a\xac\x41\xc3\x46\xb9\x16:\r\v\v\xd3\xd2\xf\x96\xe7\xd9\xee\xe6[n\xd5\xcd\xb7\xdcj\xf3~\x19\xcc\x1\xc6G\xfe\x3\xc6@a\a\0\0\0\x86\xc3\x15{\xc0}\x91\xff\x80}(\xec\0\0\0\xc0p\x18\xcc\x1\xee\x8b\xfc\a\xec\xe3\xe1\xec\0\0\0\0\0\0\0P4\xcc\xd8\x1\0\0\x80\xe1p+\x6\xe0\xbe\xc8\x7f\xc0>\x14v\0\0\0`8\f\xe6\0\xf7\x45\xfe\x3\xf6\xa1\xb0\x3\0\0\0\xc3Y\xb8`~\xae\xaf\x1f~t\xa4\x93\"\x1P\xda\xc8\x7f\xc0>\x14v\0\0\0`8\f\xe4\0\xf7\x45\xfe\x3\xf6\x61\xf1\x64\0\0\0\0\0\0\x17\xc5\x8c\x1d\0\0\0\x18\xe\x8b\xa7\x2\xee\x8b\xfc\a\xec\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?`\x1f\n;\0\0\0\x30\x1c\xae\xd8\x3\xee\x8b\xfc\a\xec\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?`\x1f\x16O\x6\0\0\0\0\0pQ\xcc\xd8\x1\0\0\x80]RSS\xf5\xe1\xb2\xf7\xb5y\xf3\x97\n\b\bT\xd7n\xdd\xd5\xbdGO\x99L\xa6\x2\xfb\xfc\xfa\xeb\x1e-[\xfa\x9e~\xfei\xb7j\xd7\xa9\xa3\xdew\xdf\xa3\x8e\x9d:\xcb\xc3#\xff\xeb\x8c\xdc\x8a\x1\x18\x13\xf9\xf\x18\xf\x85\x1d\0\0\0\xd8\xccl6\xeb\xa9'\xc7)))Q\xc3\x1fxP\xf1\xf1\xf1zf\xfaT%$$h\xf0\x90\xa1\xf9\xf6\xd9\xf7\xfb\xef\xea\xdf\xf7\x1e\x8d\x1e\x33V\xbd\xee\xea\xad?\xf\x1c\xd0\xa4\x89\x13\x94\x96\x96\xa6^w\xf5\xce\xb7\xf\x83\x39\xc0x\xc8\x7f\xc0\x98(\xec\0\0\0\xc0\x66;w\xfe\xa8o\xbf\xf9Z\x9b\x36oSpp\xb0$)((H\x13\xc6\x8fU\xb7\xee=T\xb9r\xe5<}\xb6n\xdd\xac[[\xb6\xd4\x3#\x1e\x94$\xb5l\xd5Z\x19\xe6\fm\xda\xb8\xa1\xc0\x81\x1dW\xec\x1\xe3!\xff\x1\x63\xa2\xb0\x3\0\0\0\x9bm\xdc\xb0^={\xdd\x65\x1d\xd4I\xd2\xad-[)33K\xbbw\xed\xccw\x10\xe6\xef_AqqqJII\x91\x9f\x9f\x9f$\xe9\xfc\xb9s\xaa\x1b\x15U\xe0q\x18\xcc\x1\xc6\x43\xfe\x3\xc6\xc4\xe2\xc9\0\0\0\xb0\xd9\xc1?\xffT\x8d\x9a\xb5r\xb5\xf9\xfb\xfb+\xba^\xb4N\xc5\xc5\xe5\xdb\xa7G\xcf\x9e\xf2+\xe7\xa7\x31\xa3Gi\xed\x9a\xd5zn\xf6,\x9d:uZ\x83\x87\xdc_\n\x11\x3p\x14\xf2\x1f\x30&f\xec\0\0\0\xc0&\x16\x8b\x45\xfb\xf7\xefW\x9f\xd8\xd8<\xaf\x85\x86VQ|B|\xbe\xfd\x42\x42\x42\x35j\xf4\x18\xfd{\xc4\x30}\xfd\x9f\xed\x92\xa4\x65\x1f\xaePHHH\x81\xc7j\x18\x93\xff\xd5\xfc}\a\xe\xda\x1f\x38\x80\x62#\xff\x1\xe3\xa2\xb0\x3\0\0\0\x9b\x98L&\xd5\xaeS[\x89\x89\x89y^KH\x88Whhh\xbe\xfd\xd6}\xbaVo\xbe\xbePo\xbd\xfd\xae\x1a\x35n\xa2\x1d?|\xaf\xd1\xa3\x46\xea\xae\xdew\xeb\xb1\xc7\xc7\xe4\xfb\x34\x1d\x6p\x80\xb1\x90\xff\x80qQ\xd8\x1\0\0\x80\xcd\xa2\xa3\xeb\xe9\xc8\x91#\xb9\xda\xd2\xd2\xd2t\xf8\xd0\x61\x85\x85\x85\xe7\xd9\xdel6k\xf6\xac\x99\x9a\x38i\xb2Z\xb6j-){\xfd\x8c\x80\x80\0=0l\xa8z\xf4\xec\xa5\xdau\xea\xe4\xe9\xc7\xe2\xa9\x80\xf1\x90\xff\x80\x31\xb1\xc6\xe\0\0\0lvg\x87\x8eZ\xb7v\xad\xce\x9e\x39\x63m\xfbi\xf7.IR\xf3\x16\x37Y\xdb\x92\x93\x93%e\xf\xecRRR\x94\x94\x94\x94k?U\xaaT\x95$e\x98\x33\xf2=N\xc7N\x9d\xad\x1f\0\x8c\x81\xfc\a\x8c\x89\x19;\0\0\0\xb0Y\xeb\xd6m\xd4\xb4Y3\x8d\x1b;F\x83\x87\fUrr\xb2^xn\xb6\x9e\x18\x37^AAA\x92\xa4\r\xeb\xbf\xd0\x93\xe3\x9e\xd0\a\xcbW\xaaq\x93&z\xe8\x91G\xf4\xe2\v\xcf)##C\xd1\xd1\xd1JII\xd5\xf2\xf\x97\xa9\xfd\x1d\x1dT\xa7N]'\x7fG\0lE\xfe\x3\xc6\x44\x61\a\0\0\0\x36\xf3\xf1\xf5\xd5\xdcy\v\xb4\x64\xc9\xbb\x9a?\xefU\x85\x87W\xd3\xc4I\x93s]Y\xf\t\tU\xe3&\xd7+00P\x92\x34l\xf8\b\xd5\xaau\x9d>]\xbb\x46\xef,zK\xd7\xd5\xae\xad;\xee\xec\xa0^w\xf5\x96\x97W\xfe\x7f\x8er+\x6`<\xe4?`L\xa6?\xf\x9f\xb4T\xaf\x96\xbd\xd0\xd5\xf1\xffo\xef\xce\xe3\xa2\xae\xf6?\x8e\xbf\xc7\x81\x1\x44@TL\xd9*MD\xb1[7\xcdV\x97J\xc1][\xd4Js\xcf\xee\xad,o\xb7\xac\xcc\xdc[\xbduK\xcd%7Ro.\xa9\x98\x9aK\xb9\x95\x65\xe5R\xda\xbdi\xb9\xfd\x32\xaf\xa0\xa0\x80\xb2\xcb\xc8\xfc\xfe \xe6\x32\x1\xc2\xc0\0_\x9a\xd7\xf3\xf1\xe0\xd1p\xe6{f>\x8e~\xbe\xdf\xce\xe7{\xce\x99\xf8$E4)\xba\x36\x12\x80t\xe4\x44\x82\n\xe7J\xc1\xe3\x92\x14>\x86\xdc\x2\0\xb8ZE\xaeKi\x19\xe9W<\xb6\xba\x45\x45\x36\x63\xf3T\xa0\x14~\xbeu$\x91\xff\x80\xbb*|\xe`\x8f\x1d\0\0\0\0\0\x80\x1a\x8a\xa5X\0\0\0\x30\x1c\x96\x62\0\xee\x8b\xfc\a\x9c\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\xee\xd8\x3\xee\x8b\xfc\a\x9c\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?\xe0\x1c\x36O\x6\0\0\0\0\0\xa8\xa1\x98\xb1\x3\0\0\0\xc3\x99\x35s\x86\xc3\xef\x8f?9\xaa\x9a\"\x1P\xd5\xc8\x7f\xc0\x39\x14v\0\0\0`8\f\xe4\0\xf7\x45\xfe\x3\xce\xa1\xb0\x3\0\0\0\xc3\x61\xf3T\xc0}\x91\xff\x80s(\xec\0\0\0\xc0p\x18\xcc\x1\xee\x8b\xfc\a\x9c\xc3\xe6\xc9\0\0\0\0\0\0\x35\x14\x33v\0\0\0`8,\xc5\0\xdc\x17\xf9\xf\x38\x87\xc2\xe\0\0\0\f\x87\xc1\x1c\xe0\xbe\xc8\x7f\xc0\x39\x14v\0\0\0`8\xdc\xb1\a\xdc\x17\xf9\xf\x38\x87\xc2\xe\0\0\0\x9c\x92\x9d\x9d\xad\xf\x97.\xd1\xd6\xad\x9f\xca\xcf\xcf_\xdd{\xf4T\xcf^\xbd\x65\x32\x99\xae\xd8\xef\xf3\x9d;\xb5u\xeb\xa7\xfa\xf6\xeb\xaf\x15\x1c\x12\xa2N\x9d\xa3u\xff\x3}\xe5\xe3\xe3S\xe4X\x6s\x80\x31\x91\xff\x80\xf1P\xd8\x1\0\0@\x99Y\xadV\xbd\xf8\xfcsJOO\xd3\x88G\x1fSrr\xb2\xa6L\x9a\xa0\x94\x94\x14\r\x1e\x32\xb4\xc4~\xb3g\xbd\xa7U+\x97k\xc4\xc8\xc7\xd4\xb9s\xb4\xd2\xd2\xd2\xf4\xdd\xfe\xfd\xb2X,U\x18=\x80\x8a \xff\x1\x63\xa2\xb0\x3\0\0\x80\x32\xdb\xb3\xe7[}\xb9\xeb\vm\xd9\xba\x43\xf5\xea\xd5\x93$\x5\x4\x4\xe8\x85\x31\xcf\xaaG\xcf^\xaa_\xbf~\x91>\x87\xf\x1d\xd2\xdc\xd9\xb3\xf4\xc9\xe6-\n\t\t\xb5\xb7w\xef\xd1\xb3\xc4\xf7\x61)\x6`<\xe4?`L\x14v\0\0\0Pf\x9b\x37mT\xef>\xf7\xda\au\x92t\xfb\x1dw\xea\xf2\xe5<\xed\xdb\xbb\xa7\xd8\x41X\\\xdcj=4`\x80\xc3\xa0\xae\x34\f\xe6\0\xe3!\xff\x1\x63\xa2\xb0\x3\0\0\x80\x32;\xfa\xf3\xcf\xea\xda\xbd\x87\x43\x9b\xaf\xaf\xaf\"\x9aG\xe8LBB\xb1}\xe\xfd\xf8\xa3z\xf4\xec\xa5\r\xeb\xd7i\xe5\xf2\x65JNIV\xd7\xae\xdd\x35h\xc8P\xf9\xf9\xf9\x15\xdb'*\xb2Y\xb1\xed?\xfet\xb4\x62\x7f\0\0\xe5\x46\xfe\x3\xc6\x44\x61\a\0\0\0\x65\x62\xb3\xd9t\xf8\xf0\x61=\xd0\xaf_\x91\xe7\x82\x82\x1a*9%\xb9H{^^\x9e~8x@\xdf\x7f\xb7_}\xfb=\xa8\xe1\x8f\x8eT^^\x9e\xe6\xcc\x9e\xa5S\xa7~\xd5koL+v\xd3U\x6p\x80\xb1\x90\xff\x80qQ\xd8\x1\0\0@\x99\x98L&5i\xda\x44iiiE\x9eKIIVPPP\x91\xf6Z\xb5j)\xa0n]%\x9f?\xaf\t\x93&\xdb\aqA\r\x1b\xaa\xff\x3\xf7\xe9\xc9QO+4,\xac\xd2\x63\aP1\xe4?`\\\x14v\0\0\0Pf\x11\x11\xcdu\xe2\xc4\t\x87\xb6\x9c\x9c\x1c\x1d?v\\\x8d\x1a\x35.\xb6\xcf\x8d\x37\xfeY\xf5\xea\xd5w\xb8\x33\x7f\xd5U\x8d$I\xa9\xa9\xa9\xc5\xe\xec\xd8<\x15\x30\x1e\xf2\x1f\x30\xa6Z\xd5\x1d\0\0\0\0j\x8e\xce\xd1\x31Z\xb7v\xad\x92\x12\x13\xedm\xfb\xf7\xed\x95$\xdd\xdc\xf6\x16I\xf9\xcb/\xb2\xb2\xb2\xec\xcfG\xc7t\xd1\xce\x1d\xdb\x1d\xee\xf4\xfft\xf8\xb0$\xa9qpp\xb1\xef\x13\xd3\xa5\xab\xfd\a\x80\x31\x90\xff\x80\x31\x31\x63\a\0\0\0\x65\xd6\xae]{\xb5n\xd3\x46\xcf=\xfb\x8c\x6\xf\x19\xaa\xcc\xccL\xbd\xf9\xfak\xfa\xfbsc\x14\x10\x10 Iz\xfd\xd5\xa9\xda\xb6\xf5\x33\xc5\xad\xfb\x44\xfe\xfe\xfe\xba\xfb\x9eNZ\xba\x64\xb1\x9e\xfb\xfb\xdf\xd4\xb7_\x7fI\xd2\xdc\x39\xb3\x34\xfa\x99g\x8b\xfdzd\x89;\xf6\x80\x11\x91\xff\x80\x31Q\xd8\x1\0\0@\x99Y\xbc\xbc\xf4\xee\xf4\x99\x8a\x8d]\xa8\x19\xd3\xdfQ\xe3\xc6\xc1\x1a;\xee\x65\x87\xc1W\xf8\xd5\xd7(\xb2\x45KY,\x16I\xf9\xdf\x9a\x33\xe7\xfd\xf9Z\xb8`\x9e\xde\xfd\xe7\xdb\nj\xd8P\x3\x1f\x19\xac\xee=z\x96\xf8>\f\xe6\0\xe3!\xff\x1\x63\x32\xfd|<\xde\x16\x16\x9c\xbf\xd1\xd5\xa9\xf8$E4)~m$\xe0\xee\x8e\x9cHP\xe1\\)x\\\x92\xc2\xc7\x90[\0\0W\xab\xc8u)-#\xbd\xd2\xe3\xab\x88\xa8\xc8\x66|+\xeP\n?\xdf:\x92\xc8\x7f\xc0]\x15>\a0c\a\0\0\0\x86\xc3R\f\xc0}\x91\xff\x80s(\xec\0\0\0\xc0p\x18\xcc\x1\xee\x8b\xfc\a\x9c\x43\x61\a\0\0\0\x86\xc3\x1d{\xc0}\x91\xff\x80s(\xec\0\0\0\xc0p\x18\xcc\x1\xee\x8b\xfc\a\x9cS\xab\xba\x3\0\0\0\0\0\0@\xf9\x30\x63\a\0\0\0\x86\x33k\xe6\f\x87\xdf\x1f\x7frT5E\x2\xa0\xaa\x91\xff\x80s(\xec\0\0\0\xc0p\x18\xc8\x1\xee\x8b\xfc\a\x9c\xc3R,\0\0\0\0\0\x80\x1a\x8a\x19;\0\0\0\x30\x1c\xbe\x15\ap_\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xe7P\xd8\x1\0\0\x80\xe1p\xc7\x1ep_\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xe7\xb0y2\0\0\0\0\0@\r\xc5\x8c\x1d\0\0\0\x38\xe5\xec\xd9\x33\xfa`\xd1\"}\xb9\xeb\v\xb5\x8c\x8aR\xbf\xfe\xf\xe9\xa6\xd6\xad\xcb\xd4\xd7j\xb5\xea\xe9QO(//O\xb3\xe7\xce+\xf1\x38\x96\x62\0\xc6\x44\xfe\x3\xc6\xc3\x8c\x1d\0\0\0\x94Ybb\xa2\x86\xf\x1d\xac\xec\xecl=;\xe6y\x85\x84\x86j\xc8\xa0\x1\xda\xbfoo\x99\xfa\xaf\\\xb1\\;wl/\xf5\xb8\x98.]\xed?\0\x8c\x81\xfc\a\x8c\x89\x19;\0\0\0(\xb3\xb8\x35\xab\x14\x1e~\xb5^zy\xbc\xcc\x66\xb3\xdaw\xe8\xa8\x9c\xec\x1c\xcdzo\xa6\xe6/\x8c\x95\xc9\x64*\xb1\xefO\x87\xf\xeb\xbd\x99\xd3\xd5\xabw\x1f\xa5\xa6\xa6^\xf1}\xb8\x63\xf\x18\xf\xf9\xf\x18\x13\x33v\0\0\0P&V\xabU\v\xe7\xcfW\x8f\x9e\xbd\x64\x36\x9b\xed\xed\xd1\x31]\xf4\xcd\xd7\xbb\xf5\xeb\xc9\x93%\xf6\xcd\xc8\xc8\xd0\x84\xf1\xe3\x34~\xc2$EF\xb6(\xf5\xbd\xb8\x63\xf\x18\v\xf9\xf\x18\x17\x85\x1d\0\0\0\x94Ijj\x8a\xd2\xd3\xd3\xd4\xa8Qc\x87\xf6\xc6\x8d\xf3\x7f?w.\xa9\xc4\xbe\xef\xcf\x9d\xad\x66\x11\x11\x8a\x8e\xe9R\xa9\x31\x2\xa8\x1c\xe4?`\\,\xc5\x2\0\0@\x99\x14,\x9f\xf0\xf1\xf1qh\xaf\xed[[\x92t\xf1\xe2\xc5\x62\xfb\xed\xde\xfd\x95\x36\xac\xfbX+W\xc7]q\xa9\x46\x61Q\x91\xcd\x8am\xff\xf1\xa7\xa3\x65\r\x17\x80\v\x91\xff\x80qQ\xd8\x1\0\0@\x99\xf8\xf9\xf9I\x92\x32\x32\x33\x1c\xda\xb3\xb2\xb2%Iu\x3\x3\x8b\xf4\x39\x7f\xfe\x9c\xc6\x8f\x1b\xab\xc9S_U\xfd\xfa\r$I6\xd9J}/\x6p\x80\xb1\x90\xff\x80qQ\xd8\x1\0\0@\x99\x4\x6\xd6\x93\xd9lV\xfc\xe9\xd3R\x9b\x9b\xed\xedII\x89\x92\xa4\xa0\x6\x41\x45\xfa,\x98\x37O\t\xf1\xf1\x1a\x39\x62X\x91\xe7\xa2\"\x9b\xe9\x91\xc1\x43\xf4\xc2\x8b/\x15y\x8e\xcdS\x1\x63!\xff\x1\xe3\xa2\xb0\x3\0\0\x80\x32\xb1X,\x1a\x86\xdc\x2\0\xb8ZE\xaeKi\x19\xe9\x95\x1e_EDE6c\xf3T\xa0\x14~\xbeu$\x91\xff\x80\xbb*|\xe`)\x16\0\0\0\f\x87\xcdS\x1\xf7\x45\xfe\x3\xce\xa1\xb0\x3\0\0\0\xc3\x61\x30\a\xb8/\xf2\x1fp\xe{\xec\0\0\0\0\0\0\xd4P\xcc\xd8\x1\0\0\x80\xe1\xb0\x14\x3p_\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\x6s\x80\xfb\"\xff\x1\xe7P\xd8\x1\0\0\x80\xe1\xcc\x9a\x39\xc3\xe1\xf7\xc7\x9f\x1cUM\x91\0\xa8j\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\x6r\x80\xfb\"\xff\x1\xe7\xb0y2\0\0\0\0\0@\r\xc5\x8c\x1d\0\0\0\x18\xe\x9b\xa7\x2\xee\x8b\xfc\a\x9c\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?\xe0\x1c\n;\0\0\0\x30\x1c\xee\xd8\x3\xee\x8b\xfc\a\x9c\x43\x61\a\0\0\0\x86\xc3`\xep_\xe4?\xe0\x1c\x36O\x6\0\0\0\0\0\xa8\xa1\x98\xb1\x3\0\0\0\xa7\x9d={F\x1f,Z\xa4/w}\xa1\x96QQ\xea\xd7\xff!\xdd\xd4\xbau\xb1\xc7\xdal6}\xb9k\x97>\xfdt\xb3\xf6~\xfb\x8d\x42\x42\xc3t\xef}\xf7\xabk\xb7\xee\x32\x9b\xcd\xc5\xf6\x61)\x6`\\\xe4?`,\xcc\xd8\x1\0\0\x80S\x12\x13\x13\x35|\xe8`egg\xeb\xd9\x31\xcf+$4TC\x6\r\xd0\xfe}{\x8b=\xfe\xd3-\x9b\xf5\xc6\xeb\xaf\xa8U\xab\xeb\xf5\xdc\xf3/\xaay\xf3H=\xff\xdc\xdf\xf5\xf1\xda\xb8\x12\xdf#\xa6KW\xfb\xf\0\xe3 \xff\x1\xe3\x61\xc6\xe\0\0\0\x9c\x12\xb7\x66\x95\xc2\xc3\xaf\xd6K/\x8f\x97\xd9lV\xfb\xe\x1d\x95\x93\x9d\xa3Y\xef\xcd\xd4\xfc\x85\xb1\x32\x99L\xe\xc7\xdfv\xfb\x1dj\xd7\xbe\x83j\xd7\xae-I\xba\xa7SgyZ<\xb5\x62\xf9\x32\xddw\xff\x3\xd5\xf1G\0PN\xe4?`<\xcc\xd8\x1\0\0@\x99Y\xadV-\x9c?_=z\xf6rXF\x11\x1d\xd3\x45\xdf|\xbd[\xbf\x9e}\xb4\x38\x36V6\x9b\xad\xc4\xbe\x46]\x8a\x1\xa0l\xc8\x7f\xa0jP\xd8\x1\0\0@\xa5)Xn\x11\x14\x14\xe4\xd0\x1e\x16\x16\xaes\xe7\x92\x94\x96\x96V\x1d\x61\x1\xa8\x2\xe4?P5\xf8V,\0\0\0T\x9a\xe4\xe4\xf3\x92$/oo\x87v\xdf:u$I\xe9\xe9i\xf2\xf7\xf7/\xb6oq\x1b\x46\xfe\xf8\xd3Q\x17G\b\xa0\xb2\x90\xff@\xd5\xa0\xb0\x3\0\0\x80J\x13\x10\x10 I\xca\xca\xca\x92\xa7\xa7\xa7\xbd=+3\xd3\xe1\xf9\xb2*\xef\xb7\x97\xbc\x37sz\xb9\xfa\xb9:\x8e\xe2\x10[\xf9\xb8Sl5\x15\xf9_:#\xc7&\xb9\x36>b\xab<\x14v\0\0\0Pi\x82\x82\x1aJ\x92\xce\x9e\x39\xe3pg>!!A!!\xa1\xf2\xf5\xadS\xa4\xfw\xe5\x81?\x6\xf2\x1f\xa8\x1a\xec\xb1\x3\0\0\x80J\xd3,\"B\x91-Zj\xeb\xd6\xcf\xecmV\xabU\x1b?\xd9\xa0\x41\x43\x86T_`\0*\x1d\xf9\xfT\rf\xec\0\0\0\xa0\xd2xxxh\xf4\xdf\x9e\xd1_F\x8e\x90\xa7\xa7\x87Z\xb4\x88\xd2\xb6\xad\x9f\xe9\xfc\xf9\xf3\xea\xd6\xbdgu\x87\a\xa0\x12\x91\xff@\xd5`\xc6\xe\0\0\0*U\xbb\xf6\x1d\xb4`\xd1\a\xfa\xe9\xf0\x61\xbd\xf6\xea\x14\xf9\xf8\xf8h\xde\x82\x45\xaaW\xaf^u\x87\x6\xa0\x92\x91\xff@\xe5\x63\xc6\xe\0\0\0*\xdd\xad\xb7\xdd\xae[o\xbb\xbd\xba\xc3(\x95\xcd\x66\xd3\xa6M\x1b\xb5q\xc3z\x9d=sF\x9d\xa2\xa3\x35h\xf0P\xf9\xf8\xf8\x14{\xfc\xf9\xf3\xe7\xb5\xea\xa3\x15\xda\xf3\xed\xb7:\xfd\xdfSj\xd7\xbe\x83\x86\f\x1b\xae\x90\x90P\x97\xc7v\xf6\xec\x19}\xb0h\x91\xbe\xdc\xf5\x85ZFE\xa9_\xff\x87tS\xeb\xd6\x65\xeak\xb5Z\xf5\xf4\xa8'\x94\x97\x97\xa7\xd9s\xe7\x19\"\xb6\xdc\xdc\\\xc5\xadY\xa5\xafw\xef\xd6\xf\a\xf\xa8\x65T+\xf5\xe8\xd9K1]\xbaVklyyy\xfat\xcb\x66\xadY\xbdJ\xc7\x8f\x1d\xd5M\xad\xdbh\xe0\xa0\xc1\xba\xe1\x86\x1b]\x1a\x97;\xf9\xa3\xe6\x7fNN\x8e\x36\xac\xffX_\xee\xda\xa5\x1f\xe\x1e\xd0\r7\xfeY\x8f\f\x1a\xac?\xdfT\xb6\xbctFvv\xb6>\\\xba\x44[\xb7~*??\x7fu\xef\xd1S={\xf5\x96\xc9\x64*S\xff\x5\xf3\xdf\xd7\xdb\xff\x98\xa6o\xf7~\xa7:~~.\x8d\xcd\xd9\xcf\xad@rr\xb2V,\xfbP{\xf7\xeeQ\xfc\xe9\xff\xaa\xed\xad\xb7\xa9_\xbf\a\xd5\xea\xfa\xeb]\x1a\xdf\xc1\x83\a\xb4r\xc5r\x1d\xf8\xee;\xdd\xd1\xae\x9d\x86\xe\x1d\xae\xc6\xc1\xc1%\x1e\x9f\x91\x91\xae\x15\xcb\x97k\xebg[t!\xf5\x82\xdaw\xec\xa8G\x6\rVpp\x88K\xe3r%f\xec\0\0\0\0\xbfY\xb2\xf8\x3M{\xfd\x35\xc5t\xe9\xaaG\x1f\xfb\x8b\xb6o\xdb\xa6\xe7\x9f{V\x97/_.r\xac\xd5j\xd5\xc0\x87\xfb\xeb\\R\x92\x1e|\xe8\x61\r\x1b\x31R\x87\xe\x1d\xd2_G>\xaa\x8b\x17/\xba\x34\xae\xc4\xc4\x44\r\x1f:X\xd9\xd9\xd9zv\xcc\xf3\n\t\r\xd5\x90\x41\x3\xb4\x7f\xdf\xde\x32\xf5_\xb9\x62\xb9v\xee\xd8\xee\xd2\x98*\x12[ff\xa6\x9e\x19\xfd\x94\x36m\xdc\xa8\xe\x1d\xef\xd2\xc4\xc9\xaf\xa8]\xfb\xe:\x93\x90P\xed\xb1\xad^\xf5\x91\xa6\xbd\xf1\x9a\xbat\xed\xa6\xf1\x13\xa7(\xb0^==\xf2\xf0\x83\xfa\xf9\xe7\x9f\\\x1a\x1b\x8c\xc7\x99\xfc\x97\xa4\xb7\xdf\x9a\xa6\xad\x9f}\xa6\xbb\xee\xbeG\xcf\x8eyA&\x93I\x3\x1f~P\x87\xf\x1fri\\V\xabU/>\xff\x9c\xbe\xfe\xfa+\x8dx\xf4\x31u\x8e\x8e\xd1\x94I\x13\xb4\xf8\x83\xd8\x32\xf5?x\xf0\x80\xde\xfe\xc7\x34\x97\xc6T\x98\xb3\x9f\x9b$\x9d\xa4\xf8\x84x\xf5\xeb\xff\xa0^|\xe9\x65\x85\x87\x87+%%\xc5\xa5\xb1\xed\xdf\xb7W\x83\a\xeP\xd3\xa6\xd7i\xcc\vcu!5U\xc3\x86\xeRbbb\xb1\xc7\xdbl6M\x99\x34Q_}\xb9KC\x87\x8d\xd0\xa8\xa7G\xeb\xd4\xaf\xbf\xea\xaf\x8f=\xaa\xf4\xb4\x34\x97\xc6\xe6J\xcc\xd8\x1\0\0\0$\x9d;\x97\xa4\xb7\xff\x31M\xef/X\xa8\xb6mo\x91$\xb5h\xd9R]:\xdf\xa3\xfd\xfb\xf6\xaa\xed-\xb7:\x1c\xef\xe1\xe1\xa1\x85\xb1K\xd4\xb8qc{\xdb\x9d\xed\xda\xa9\xf3\xdd\x1d\xf5\xef\x1f\xe\xea\x8e;\xdb\xb9,\xb6\xb8\x35\xab\x14\x1e~\xb5^zy\xbc\xcc\x66\xb3\xdaw\xe8\xa8\x9c\xec\x1c\xcdzo\xa6\xe6/\x8c\xbd\xe2]\xfb\x9f\xe\x1f\xd6{3\xa7\xabW\xef>JMMuYL\x15\x89m\xd3\xc6\rJOO\xd7\xac\x39\xef\x97zW\xbf\xaa\x63[\xf6\xe1R\r\x1c\x34\x44\xf7\xdd\xff\x80$\xa9}\x87\xe:\xf4\xe3\x8f\xfa\xf6\x9bo\xd4\xbcyd\xa5\xc5\x8a\xea\xe5l\xfeK\xd2\xf0\x11#U\xbf~}\x99\xcd\x66IRtL\x17\xfdz\xf2\xa4vn\xdf\xae\x16-Z\xba,\xb6={\xbe\xd5\x97\xbb\xbe\xd0\x96\xad;\xecK\xd8\x2\x2\x2\xf4\xc2\x98g\xd5\xa3g/\xd5\xaf_\xbf\xc4\xbe\x17/^\xd4\xf8qc\xf5@\xdf\xfeZ\xf5\xd1\n\x97\xc5T\xa0<\x9f\x9b$\xcd\x99\x35S\xd1\xd1]4\xea\xe9\xd1\xf6<\xec\xd0\xf1.\x97\xc6\x96\x97\x97\xa7\x19\xd3\xdf\xd5SO\x8f\xd6\xb0\xe1#$I\xb7\xdcz\xab\x86\xe\x1a\xa8\xf5\xeb\xd6j\xf8\x88\x91\x45\xfa$&\x9e\xd5\xfau\x1f\xeb\x83%\xffR\x9b\x9b\xdbJ\x92nn\xdbV\xed\xef\xb8MG\x8e\x1c)\xf3,\xc9\xaa\xc6\x8c\x1d\0\0\0@\xd2\xee\xaf\xbeR\xfd\xfa\xf5tS\xa1\x65\x14\x61\x61\xe1\xea\x14\x1d\xa3m\xdb\xb6\x16\xdb\xa7pQG\x92\xea\xd6\r\x94$]\xba\x94\xeb\xb2\xb8\xacV\xab\x16\xce\x9f\xaf\x1e={\xd9\a\x90R\xfe \xf2\x9b\xafw\xeb\xd7\x93'K\xec\x9b\x91\x91\xa1\t\xe3\xc7i\xfc\x84I\x8a\x8cl\xe1\xb2\x98*\x12[^^\x9e\x66N\x9f\xae\x87\x1e\x1eP\xa9\x45\x9d\xf2~n\x81\x81\xf5\xf4\xdfS\xa7\x64\xb3\xd9$\xe5/\x19KMMQH\x88q\x97\x61\xa0\xe2\xca\x93\xff\r\x1b\x36t\xf8\xb7\x65\x36\x9b\xd5\xa8qc\xe5\xe4\xe4\xb8\x34\xb6\xcd\x9b\x36\xaaw\x9f{\x1d\xf6%\xba\xfd\x8e;u\xf9r\x9e\xf6\xed\xddSb?\x9b\xcd\xa6\x99\x33\xde\xd5m\xb7\xdf\xa1\x9e\xbd{\xbb\x34\xa6\x2\xe5\xf9\xdc~\xf9\xe5\xff\xb4\xee\xe3\xb5\xeas\xdf}e^JV\x1e\xc7\x8f\x1d\xd3\xde=\xdf\xea\xae\xbb\xef\xb1\xb7y{{\xabW\x9f>Z\x1c\x1bk\xcf\xf1\xc2\xbc\xbd\xbd%Ig\xce\x9c\xb1\xb7\xa5]\xcc\x9f\xa9\xd3\xf0\xaa\xab*-\xd6\x8a\x62\xc6\xe\0\0\0 \xe9\xf4\xe9\xd3\x8ah\x1e)\xf\xf\xc7\xff\x45n\x1e\xd1\\\a\xf\x1e(\xd3k\x1c;vT\x92\x14\xd1<\xc2\x65q\xa5\xa6\xa6(==M\x8d\x1a\x39\x16\x91\n\x8aJ\xe7\xce%\xe9\xeak\xae)\xb6\xef\xfbsg\xabYD\x84\xa2\x63\xbahq\xec\"\x97\xc5T\x91\xd8RRR\x94\x98xV\xfe\xfe\xfe\x9a\x37w\x8e\x36lX'??\x7f\xdd\xff@_\xf5\xea\xdd\xc7\x61\xa0\\\xd5\xb1I\xd2\xa8\xa7\x46\xeb\x89\xc7\x1f\x93\x87\xa7\x87\x9a\x37\x8f\xd4\xce\x1d;\xd4\xa9S\xb4\xcbg\x13\xc0X\\\x91\xff\x19\x19\xe9\xfa\xe6\xeb\xdd\xba\xf7\xbe\xfb]\x1a\xdb\xd1\x9f\x7fV\xd7\xee=\x1c\xda|}}\x15\xd1<\xe2\x8a\xcb\x17w\xee\xd8\xaeov\xef\xd6\xd2\x65+t\xf4\xc8\xcf.\x8d\xa9@y>\xb7\x84\xf8\xfc\x98\xad\xb9VM\x9d\xf5\xfb\xd9-\xb5}kKR\x89\xfb\xf9\xec\xde\xfd\x95\x36\xac\xfbX+W\xc7U\xda]\xf1\xf2\xc4\x96|\xfe\xbc$i\xf8\xd0\xc1z\xf2\xa9\xd1zq\xec\x38%$$\xe8\xcd\xd7_\x93\xd5jU\xdf~\xfd\xab-6Iju\xfd\xf5\x1a>b\xa4\xde\x9a\xf6\x86\xbd\xed\x85m;\x8b\f\\\xf1\xc7R\xd1\xfc\x97\xa4\x15\xcb\x97\xabYD\x84\xda\xb5\xef\xe0\xb2\xb8l6\x9b\xe\x1f>\xac\a\xfa\xf5+\xf2\\PPC%\xa7$\x17\xdb\xef\xcc\x99\x33\x1a\xff\xf2Kzw\xfa{E\x8a\x17\xaeT\x9e\xcf-))\xbf\xe0\x32\x65\xf2\x44\x45\xc7\xc4(\xbaKW\xed\xdf\xb7OC\x6\r\xd0\xcaUq\x8aj\xd5\xca%\xb1%'\xe7\x9fk\xbc~\x9b\x85S\xc0\xb7N\x1dIRzz\xd1\xc2\x8e$u\xed\xd6]_}\xf9\xa5^\x7fu\xaa$\xe9\xde\xfb\x1fP{\x17\xfe\x9dV\x6\xceN\0\0\0\x80\xa4\xfa\r\xea\xeb\xdf?\xfcP\xa4===]\xd7\\{m\xa9\xfd\x63\x17-\xd0\xc5\x8b\x17\x35|\xc4\xa3.\x8d\xcb\xef\xb7o\xb0\xc9\xc8\xccph\xcf\xca\xca\x96$\xd5\r\f,\xd2\xe7\xfc\xf9s\x1a?n\xac&O}U\xf5\xeb\x37\x90$\xd9Tt\xd9\x41u\xc4\xe6S;\xbf\xb0\xd2\xa3g/\xfd\xf5\xf1'\xec\xed\xb9\xb9\x97\x34o\xee\x1c=\xd0\xb7\x9fK\nQ\xe5\x89\xcd\x66\xb3\xe9\xd5\xa9St\xee\xdc\x39}\xb4:N\r\xaf\xbaJ\xeb>\xfeX\xdd\xba\x44\xeb\xad\x7f\xbe\xa3{:u\xaep\\0\xa6\x8a\xe6\xff\x9eo\xbf\xd1\xdc\xd9\xb3\xf4\xe1\xf2\x95\xf2\xf4\xf4tY\\&\x93IM\x9a\x36QZ1\x1b\xf7\xa6\xa4$\x17\x99\x8d\"\xe5\x17\x99\xdfx\xed\x15=\xfc\xf0@\xfb\x9e\x30\xae\xcf\xfe|\xe5\xf9\xdc|}}%I\xcf\x8dy^-\xa3\xf2\x8b\x38m\xdb\xde\xa2\xe3\xc7\x8ej\xd3\xc6O\\V\xd8)(8eee9\xfc\x9d\x64\x65\x66:<_XZZ\x9a\x86\xf\x19\xa4\xdb\xef\xbcS\x9fn\xdb\xa1\x9c\xec\x1c\x91\xf3\xcb\x9b\0\0\b\x8bIDAT\xc5\xc6.T\x9f\xde=4o\xfe\"]\xdb\xa4\x89Kbs5\xf6\xd8\x1\0\0\0$\x5\a\a\xeb\xf0\xe1\x43\xbat\xe9\x92\x43\xfb\xc9_~\xd1\xb5\xd7^\xf9\x7f\xe6\x37~\xb2\x41\x1f.]\xa2\x37\xa7\xbd\xe5\xf2\xaf\x12\xe\f\xac'\xb3\xd9\xac\xf8\xd3\xa7\x1d\xda\v\xeez\a5(:\xb0[0o\x9e\x12\xe2\xe3\x35r\xc4\x30\x45\x45\x36STd3M{\xe3u}\xf1\xf9NEE6\xd3\xeb\xaf\xbdRm\xb1\x15\xec\x13\xd2\xf1\xae\xbb\x1d\xda\x43\x42\x42u\xfa\xf4\x7f]\xb6?Iyb;v\xf4\xa8V,\xffPO\x8d\x1e\xad\x96Q\xad\xd4\xa0\x41\x90\x86\r\x1f\xa1\xbf<\xfe\xb8&Nx\xf9\x8a\xdf\xf2\x83\x9a\xad\"\xf9\x7f\xf4\xc8\x11==\xeaI\xbd\xfd\xcet5\xbd\xee:\x97\xc7\x16\x11\xd1\\'N\x9cph\xcb\xc9\xc9\xd1\xf1\x63\xc7\x8b,5\x94\xa4\xed\xdb\xb6\xea\xd3-\x9b\x35s\xc6\xbb\xf6\xfc\x1f<\xf0\x61I\xd2-7\xdf\xa4vw\x14\xbf\xa1qy\x94\xe7sk\xd0 \xbf\xd8\x1cX\xcfq\xd3\xe7\xa6M\xafSBB\xbc\xcb\x62\v\nj()\x7fVQa\t\t\t\n\t\t\x95\xafo\x9d\"}vl\xdf\xa6\xf8\x84x\x8dzj\xb4\x42\x42\x42\xd5\xa4iSM\x98\x38Yaa\xe1Z\xba\x64\xb1\xcb\x62s5\n;\0\0\0\x80\xa4[o\xbb]\xc9\xe7\xcfk\xef\x9e\xffmF\x9a\x94\x94\xa4O6\xac\xd7=\x9d\xff\x37S#\xf3\xb7\xbb\xbd\x5>\xdd\xb2Yo\xbf\x35M\xf3\x16\xc4*\xb8\x12\x36\xd8\xb5X,\x1a1\xc0/\x7f*\xd4\xc5\xb4L\xd5\xft\xed\x1d\x6\xe0\x8f\xe2|J\xba\n\xe7J\xc1\xe3\x92\x14>\x86\xdc\x2\0\xb8ZE\xaeK\x97r/]\xf1Xw\xe5\xef\xef/\x9b\xcd\xa6w\xdfy[\r\x1a\x34PBB\xbc\xde\x9a\xf6\xa6n\xfc\xf3\x9f\x35`\xe0#2\x99L:~\xfc\x98\xba\xc5tR\xdd\xba\x81j\xd5\xeazm\xd9\xbcI\xcf\x8c~JO=\xfd\x37\xf9\xf9\xf9)!!A\t\t\tJJJ\xd2U\x8d\x1a\xb9,\xb6\xb0\xb0\x30-\x98\x37O'O\x9e\x94\xc5\xe2\xa9m[\xb7j\xce\xac\x99\x9a\xf2\xeakj\xd4\xa8\x91\xb2\xb3\xb3\xd5\xbf\xef}\xfan\xff~E\xc7tQ``\xa0\x82\x1a\x36t\xf8IO\xcf\x90\x87\x87\x87\x86\xe\x1b\xa1\x80\x80\xbaU\x16\x9b$\xbd\xf6\xca\x14\xbd\x32\x65\x92\xee\xbd\xef\x1yyy)44Lo\xff\x63\x9a\xce%%\xca\x64\x32i\xef\xde=\x9a\x33{\x96^\x9e\x38I\xa1\xa1\x61\xd5\x16[\xc3\xab\xae\xd2\xe1\x43\x87\xb4\xee\xe3\x38\xf9\xfa\xfa\xea\xc2\x85T}\xff\xfdw\x9a\xf1\xee;\x1a\x36\x62\x84Z\xb7n\xe3\xb2\xd8*\xca\xcb\x62\x91\x44\xfe\xbbJy\xf2\xff\xd8\xd1\xa3\xea\xdd\xb3\x9b:u\x8e\xd6M\xad\xdb\xd8\xf3?!!A\x81\xf5\xea\xb9l_\xa6\xd0\x90P}\xf7\xdd~m\xd9\xb2Y\x1\x1\x1:\xf4\xe3\x8f\x9a>>E\xce\x9d\xf5\x2\xebi\xc3\xfa\xf5:p\xe0{y{{+)1I\xdb\xb7m\xd5\xbc\xb9\xb3\xf5\xe2\xd8q\x95R\xbc/\xaf\xc2\xe7\0\xf6\xd8\x1\0\0p3&\x93\xa9\xd8\xafy\x85\xf4\xf8\x13\xa3\xd4\xa8Qc\xad\x8d[\xa3\xec\xec,u\x8e\xee\xa2\x1\x3\x6\xaaV\xad\xfc\x89\xee\xbe\xbeu\x14\x15\xd5\xca\xbe\xfc\x61\xda\x9b\xafK\x92^\x9d:\xd9\xe1u\x9a\x45\x34\xd7\xdau\x1b\\\x16WXX\xb8\x62\x97\xfcK\x8b\x63\x17i\xea\x94I\xba\xa9u\x1b-^\xba\xcc>\v\xc7\xd3\xd3S\xcd\x9bG\xea\xda&M]\xf6\x9e\xae\x8aM\x92\xc2\xaf\xbe\x46\x91-Z\xca\xf2\xdb@\xe4\xba\x66\xcd\xb4\xe4\xc3\x65Z\xb2\xf8\x3M\x9e\x38\x41\xad\xae\xbf^3g\xcdV\xeb\x36\x37Wkl\x16\x8b\x45oL{KqkVk\xf5\xaa\x95\xfa\xe5\xff~Q\xab\xeb\xaf\xd7s\xcf\xbf\xa0\xbb\xef\xe9\xe4\xd2\xd8*\xa2\"{\x10\x91\xff%s6\xff\xd7\xc6\xad\xb1\xff\xb7\xe0q\x81m;w\xd9\x8b\x87\x15\x65\xf1\xf2\xd2\xbb\xd3g*6v\xa1\x66L\x7fG\x8d\x1b\ak\xec\xb8\x97\x15\xd3\xa5\xab\xfd\x98\x6\r\x82t\xfd\x9fn\xa8\xd4\x8d\x92K\xe2\xec\xe7&I\xdd\xbau\x97\x8f\xb7\xb7\xe2\xd6\xac\xd1\xb2\xf\x97\xea\x8e;\xdbk\xc5\xca\xd5j\xd8\xb0\xa1Kck\xd7\xbe\x83\x16,\xfa@\xab>Z\xa9\xb5qk\xd4\xa1\xc3]\x9a\xb7`\x91}I\xe8\xef\xcf\x9d\x8d\x83\x83\xb5\xe4_\xcb\xb4|\xd9\x87\x9a\xf5\xdeLe\xa4\xa7\xa9\xf5\xcdm\xb5l\xe5*\xfd\xe9O7\xb8\x34\xb6\x8a\xf8\xfd\x39\xc0\xf4\xf3\xf1x[Xp\xfe\xfa\xd2S\xf1I\x8ahRt\x8d\x1e\0\xe9\xc8\x89\x4\x15\xce\x95\x82\xc7%)|\f\xb9\x5\0p\xb5\x8a\\\x97\x32\xb2\x32\x94\x97\xc7\xc0\xe\xa8\xa9j\xd5\x32\xc9\xd7'\x7fV\x3\xf9\xf\xb8\x9f\xdf\x9f\x3\xd8\x63\a\0\0\xc0\xcd\x98k1i\x1b\xa8\xc9*\x92\xc3\xe4?P\xf3\xfd>\x8f)\xec\0\0\0\xb8\x19\xb3\x87\xb9\xba\x43\0P\x1\x15\xc9\x61\xf2\x1f\xa8\xf9~\x9f\xc7\x14v\0\0\0\xdc\x8cG-\xf\xd5\xaa\xc0\x1e\x1d\0\xaaO-\x93I\x1e\xe6\xf2\xcf\xba!\xff\x81\x9a\xad\xb8s\0\x85\x1d\0\0\0\x37\x63\x32I^^^\xd5\x1d\x6\x80r\xf0\xf2\xf2RE\xca\x32\xe4?P\xb3\x15w\xep(\xecX,\x9e\xca\xc9\xc9\xad\xc2\x90\x80\x9a!;'W^\x16\xcfr\xf7'\xb7\0\0\xaeT\xd1\xeb\x92$y\x98=d\xf1\xa8\xd8k\0\xa8Z\x16\xf\xcf\n\xcd\xd6)@\xfe\x3\x35SI\xe7\x80Z\x9e\x9e\x1e\xca\xcd\xb5J\x92|\xbc,\xba\x90\x96Y\xd5\xb1\x1\x86\x97\x93\x93+/\xef\xfc\x8b\xdf\xa5\\\xab,\x9e\xa5_P\xc9-\0@e\xa9\xe8u\xa9\x80\x97\x97\x17\x83;\xa0\x86\xb0xx\x16\x99iC\xfe\x3\xee\xe3J\xe7\x80Z\x16O\xf]\xfa-\xc9\xfd\xfdj\xeb\xc2\xc5,\x9dO\xb9(\xeb\xe5\xcb\xd5\x11+`(\xd9\x39\xb9\xba\x90\x96\xa9\xb3\xe7.(\xa0N\xfe\xd7\xc9\xe5^\xb2\xca\xd3R\xfa\x5\x94\xdc\x2\0\xb8\x9a\xab\xaeK\x85yyy\xc9\xc7\xdb\x9b=7\0\x83\xaa\x65\x32\xc9\xc7\xdb\xbb\xd8\xe5S\xe4?\xf0\xc7W\x96s\x80\xe9\\r\xba-\xfb\xd2%\xd5\v\xf0\xb3?\x99\x91\x95\xad\v\x17\x32u9\x8f\x1(\xdc\x9b\x97\xc5S\xde^\x16\xf9\xfb\xd5\xb6\xb7%\xa7\\\x94\x8f\x8f\xb7|\xbc-W\xec\x9b\x99\x95#r\v\0\xe0J\xae\xbe.\x15\x66\x93\x64\xbdl\xd5\x65\xeb\x65]\xce\xb3\xca\x66\x93l6\x9b+\xc3\aP\x6&\x93I&S\xfe\xd7\x19\x9b=\xcc\xf2\x30{\x94\xb8\xa7\xe\xf9\xf\xfc\xf1\x94\xe7\x1c\xe0Q\xdb\xc7K\xe9\x99Y\xca\xcc\xcaVm\x1foI\x92\xaf\x8f\xb7|\x7f{\f\xe0\x7f\x92S\xd3\x64\xcd\xcb+\xf5\xe2)I\xe4\x16\0\xa0\xb2U\xf4\xbaT\x98I\x92\xa7\xd9\x43\x9e\x66\xfIl\xac\n\x18\x1d\xf9\xf\xb8\xb7\xc2\xe7\0\xfI\xaa_\xd7O\xf1g\x93\xf3+C\x92<-\x9e\x32\xd7\xe2\v\xb3\0)\x7f\xdd\x62\xee%\xab\xb2/\xe5*\xef\xf2\x65\x35lP\xb7\xcc}\xc9-\0\x80\xabq]\x2\xdc\x17\xf9\xf\xb8\xb7\x92\xce\x1\xa6\xcc\xac\\\xfb\x1c\xbb\x94\vi\xb2^\xb6)\xf7R\xae.\xe7\xe5U[\xb0\x80\x91X<=\xe4i\xf1\x90\x8f\x97\x45>\xde\xe5\xbb\x83\x41n\x1\0\\\x85\xeb\x12\xe0\xbe\xc8\x7f\xc0\xbd\x95t\xe\xf8\x7f:nq\xb7\x13\x82QF\0\0\0\0IEND\xae\x42`\x82)" +screenshot_at=2026-10-10T20:20:15Z diff --git a/pxii_bec/device_configs/pxii-devices.yaml b/pxii_bec/device_configs/pxii-devices.yaml index 33d9cd0..45ee246 100644 --- a/pxii_bec/device_configs/pxii-devices.yaml +++ b/pxii_bec/device_configs/pxii-devices.yaml @@ -560,6 +560,8 @@ dcm_pitch: enabled: true onFailure: buffer readoutPriority: baseline + userParameter: + offset: 0 dcm_x: description: DCM Lateral deviceClass: ophyd_devices.EpicsMotorEC @@ -1310,6 +1312,17 @@ samcam_sigma: onFailure: buffer readOnly: true readoutPriority: monitored +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_x: description: Sample Camera X deviceClass: ophyd.EpicsSignalRO @@ -1333,17 +1346,6 @@ samcam_xcurs: 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 diff --git a/pxii_bec/macros/analyse_luts.py b/pxii_bec/macros/analyse_luts.py index dfaf726..1ffa8f9 100644 --- a/pxii_bec/macros/analyse_luts.py +++ b/pxii_bec/macros/analyse_luts.py @@ -12,7 +12,7 @@ from pxii_bec.scripts import beamline_context as bl def get_data(filename): """Open the raw DCM calibration data file.""" - filename = bl.env.lut_dir / f"{filename}.csv" + filename = bl.env.lut_dir / f"{filename}" return pd.read_csv(filename) @@ -168,8 +168,69 @@ def lut_plot( return fit +def pitch_analysis(filename, save=False): + """Compare DCM pitch calibration against energy and Bragg angle.""" -def pitch_analysis(filename, fit_order=3, save=False): + df = get_data(filename) + + fits = {} + + for xname, fit_order in [("energy", 4), ("bragg", 2)]: + + x = df[xname].to_numpy() + pitch = df["pitch"].to_numpy() + + fit = lut_plot( + df[[xname, "pitch"]].to_numpy(), + xname="Energy" if xname == "energy" else "Bragg angle", + yname="pitch", + title=f"DCM_Pitch_Calibration_{xname}", + fit_order=fit_order, + save=save, + ) + + # Calculate residuals + fitted_pitch = np.polyval(fit, x) + residuals = pitch - fitted_pitch + + rms = np.sqrt(np.mean(residuals**2)) + max_residual = np.max(np.abs(residuals)) + + print(f"\n{xname.capitalize()} fit (order {fit_order})") + print(f"RMS residual: {rms:.6g}") + print(f"Max residual: {max_residual:.6g}") + + for xi, residual in zip(x, residuals): + print(f" {xi:10.3f}: {residual:+.6g}") + + fits[xname] = { + "fit": fit, + "residuals": residuals, + "rms": rms, + "max_residual": max_residual, + } + + return fits + +def pitch_analysis_bragg(filename, fit_order=2, save=False): + """Fit and plot the DCM pitch calibration.""" + + df = get_data(filename) + + pitch = df[["bragg", "pitch"]] + + fit = lut_plot( + pitch.to_numpy(), + xname="Bragg angle", + yname="pitch", + title="DCM_Pitch_Calibration", + fit_order=fit_order, + save=save, + ) + + return fit + +def pitch_analysis_energy(filename, fit_order=3, save=False): """Fit and plot the DCM pitch calibration.""" df = get_data(filename) @@ -328,8 +389,8 @@ def ge_edge_analysis(filename, save=False): # ---------------------------------------------------------- if save: - signal_file = bl.env.lut_dir / f"{filename}_signal.png" - derivative_file = bl.env.lut_dir / f"{filename}_derivative.png" + signal_file = bl.env.daily_setup_dir / "Ge_edge_signal.png" + derivative_file = bl.env.daily_setup_dir / "Ge_edge_derivative.png" fig1.savefig(signal_file, bbox_inches="tight", dpi=150) fig2.savefig(derivative_file, bbox_inches="tight", dpi=150) @@ -347,7 +408,8 @@ def ge_edge_analysis(filename, save=False): "expected_edge": expected_edge, "measured_edge": measured_edge, "energy_offset": energy_offset, - "derivative": derivative, + "signal_png": signal_file, + "derivative_png": derivative_file, } def perp_analysis(filename): diff --git a/pxii_bec/macros/bcu_bpm_positions.py b/pxii_bec/macros/bcu_bpm_positions.py index 165085e..448fda7 100644 --- a/pxii_bec/macros/bcu_bpm_positions.py +++ b/pxii_bec/macros/bcu_bpm_positions.py @@ -6,7 +6,7 @@ import time import numpy as np -def compute_norm(n_readings=5): +def compute_norm_bcu(n_readings=5): """Average normalized BPM X/Y from repeated sets of four channels.""" xn_values = [] @@ -31,20 +31,36 @@ def compute_norm(n_readings=5): time.sleep(0.1) - return float(np.mean(xn_values)), float(np.mean(yn_values)) + return { + "bcu_Xnorm": float(np.mean(xn_values)), + "bcu_Ynorm": float(np.mean(yn_values)), + } def get_bcu_beampos(): - """Determine beam position""" - bpm_xn, bpm_yn = compute_norm() + """Determine beam position from BCU BPM""" + # Check that there is a reading first + if get_value(dev.bcu_bpmsum) < 1: + return { + "ok": False, + "message": "Reading too low on BCU BPM" + } + + result = compute_norm_bcu() bpm_x = dev.bcu_bpm_x.position bpm_y = dev.bcu_bpm_y.position - beam_x = beam_offset_um(bpm_xn, bpm_x, BPM_X_Calibration) + beam_x = beam_offset_um(result["bcu_Xnorm"], bpm_x, BCU_BPM_X_Calibration) - beam_y = beam_offset_um(bpm_yn, bpm_y, BPM_Y_Calibration) + beam_y = beam_offset_um(result["bcu_Ynorm"], bpm_y, BCU_BPM_Y_Calibration) - return beam_x, beam_y + return { + "ok": True, + "bcu_Xnorm": result["bcu_Xnorm"], + "bcu_Ynorm": result["bcu_Ynorm"], + "bcu_x_um": beam_x, + "bcu_y_um": beam_y, + } def beam_offset_um(bpm_signal, bpm_motor_position, calibration): diff --git a/pxii_bec/macros/calibrate_bpm.py b/pxii_bec/macros/calibrate_bpm.py index e0a2860..47e4573 100644 --- a/pxii_bec/macros/calibrate_bpm.py +++ b/pxii_bec/macros/calibrate_bpm.py @@ -11,32 +11,7 @@ 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.""" -# xn_values = [] -# yn_values = [] - -# for _ in range(n_readings): -# bpm1 = dev.bcu_bpm1.read()["bcu_bpm1"]["value"] -# bpm2 = dev.bcu_bpm2.read()["bcu_bpm2"]["value"] -# bpm3 = dev.bcu_bpm3.read()["bcu_bpm3"]["value"] -# bpm4 = dev.bcu_bpm4.read()["bcu_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_ss(n_readings=5): """Average normalized BPM X/Y from repeated sets of four channels.""" @@ -62,7 +37,10 @@ def compute_norm_ss(n_readings=5): time.sleep(0.1) - return float(np.mean(xn_values)), float(np.mean(yn_values)) + return { + "ss_Xnorm": float(np.mean(xn_values)), + "ss_Ynorm": float(np.mean(yn_values)), + } def compute_norm_lu(n_readings=5): @@ -90,7 +68,11 @@ def compute_norm_lu(n_readings=5): time.sleep(0.1) - return float(np.mean(xn_values)), float(np.mean(yn_values)) + return { + "lu_Xnorm": float(np.mean(xn_values)), + "lu_Ynorm": float(np.mean(yn_values)), + } + def run_calibration_ss(): diff --git a/pxii_bec/macros/create_daily_setup_gui.py b/pxii_bec/macros/create_daily_setup_gui.py new file mode 100644 index 0000000..555cd24 --- /dev/null +++ b/pxii_bec/macros/create_daily_setup_gui.py @@ -0,0 +1,29 @@ +def create_daily_setup(): + dock = bec.gui.new("DailySetup") + # plot1 = dock.new(widget="Waveform", object_name= "Plot_1") + # plot2 = dock.new(widget="Waveform", object_name= "Plot_2", relative_to="Plot_1", where="bottom") + # plot3 = dock.new(widget="Waveform", object_name= "Plot_3", relative_to = "Plot_2", where="bottom") + # plot4 = dock.new(widget="Waveform", object_name= "Plot_4", relative_to="Plot_1", where="right") + # plot5 = dock.new(widget="Waveform", object_name= "Plot_5", relative_to="Plot_2", where="right") + # plot6 = dock.new(widget="Waveform", object_name= "Plot_6", relative_to="Plot_3", where="right") + # log = dock.new(widget="TextBox", object_name= "Info", relative_to="Plot_1",where="left") + # progress = dock.new(widget="TextBox", object_name= "Progress", relative_to="Info", where="left") + progress = dock.new(widget="TextBox", object_name= "Progress") + log = dock.new(widget="TextBox", object_name= "Info", relative_to="Progress", where ="right") + plot1 = dock.new(widget="Waveform", object_name= "Plot_1", relative_to="Info", where ="right") + plot2 = dock.new(widget="Waveform", object_name= "Plot_2", relative_to="Plot_1", where="right") + plot3 = dock.new(widget="Waveform", object_name= "Plot_3", relative_to = "Plot_1", where="bottom") + plot4 = dock.new(widget="Waveform", object_name= "Plot_4", relative_to="Plot_2", where="bottom") + plot5 = dock.new(widget="Waveform", object_name= "Plot_5", relative_to = "Plot_3", where="bottom") + plot6 = dock.new(widget="Waveform", object_name= "Plot_6", relative_to="Plot_4", where="bottom") + dock.set_layout_ratios( + horizontal=[1,2,1.2,1.2], + vertical=[1,1,1], + ) + + progress.set_plain_text("") + log.set_plain_text("") + + dock.save_profile() + + diff --git a/pxii_bec/macros/daily.py b/pxii_bec/macros/daily.py index 869f46c..75e1cd4 100644 --- a/pxii_bec/macros/daily.py +++ b/pxii_bec/macros/daily.py @@ -49,33 +49,21 @@ def daily_setup( align_collimator=False, measure_flux=True, save_scilog=True, + weekly=None, ): """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, + # Weekly checks run automatically on Wednesday unless explicitly overridden. + # Keep this as one task: weekly.py owns the weekly sequence. + if weekly is None: + weekly = datetime.now().weekday() == 2 - 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, + "Weekly checks": weekly, "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, } @@ -91,6 +79,10 @@ def daily_setup( update_progress(ctx.progress, ctx.progress_state) update_log(ctx.log, ctx.messages, "Starting daily beamline setup") + update_log(ctx.log, ctx.messages,"\nDaily setup normally takes about 1 min.") + update_log(ctx.log, ctx.messages,"\nWith Wednesday checks, allow 8-10 minutes.") + update_log(ctx.log, ctx.messages,"Good time to get a coffee.\n") + try: # --------------------------------------------------------- @@ -119,6 +111,15 @@ def daily_setup( ) return results + # --------------------------------------------------------- + # Weekly checks (Wednesday by default) + # --------------------------------------------------------- + # Machine pre-checks deliberately happen first: the weekly checks move + # beamline hardware and should not bypass the normal safety gate. + if weekly: + with time_step(results, "Weekly checks"): + results["Weekly checks"] = run_weekly_checks(ctx) + # --------------------------------------------------------- # Set energy # --------------------------------------------------------- @@ -130,27 +131,6 @@ def daily_setup( 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 # --------------------------------------------------------- @@ -244,7 +224,7 @@ def daily_setup( 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() + plot1, plot2, plot3, plot4, plot5, plot6, log, progress = select_daily_setup_gui() # Clear text from previous run log.set_plain_text("") @@ -288,7 +268,6 @@ def run_pre_checks(ctx): task = "Pre checks" - clear_daily_plots() set_progress( ctx.progress, ctx.progress_state, @@ -575,7 +554,7 @@ def check_beamstop_position(): if i2 < cutoff: return { "ok": True, - "message": f"Beamstop blocking beam: I2 = {i2:.2e}", + "message": f"Beamstop blocking beam: I2 = {i2:.2e}\n", "value": i2, "realigned": False, } @@ -672,13 +651,60 @@ def check_detector_height(): # Energy # ============================================================================= +def run_weekly_checks(ctx, save_scilog=True): + """Run weekly beamline checks and report results to Daily Setup GUI.""" + + task = "Weekly checks" + + set_progress(ctx.progress, ctx.progress_state, task, "running") + clear_daily_plots() + try: + result = weekly_checks(save_scilog=save_scilog) + + except Exception as exc: + result = { + "ok": False, + "message": f"Weekly checks failed: {exc}", + } + + if not result.get("ok", True): + status = "failed" + + elif result.get("warning"): + status = "warning" + + else: + status = "done" + + # Basic summary + lines = [] + + edge = result.get("edge") + if edge: + lines.append( + f"Ge edge: {edge['measured_edge']:.2f} eV " + f"(expected {edge['expected_edge']:.2f} eV, " + f"offset {edge['energy_offset']:+.2f} eV)" + ) + + if result.get("edge_warning"): + lines.append(f"WARNING: {result['edge_warning']}") + + if result.get("edge_error"): + lines.append(f"ERROR: Ge edge scan failed: {result['edge_error']}") + + update_log(ctx.log, ctx.messages, "\n".join(lines)) + + set_progress(ctx.progress, ctx.progress_state, task, status) + + return result + 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: @@ -714,55 +740,6 @@ def run_daily_energy(energy, mono_scan, ctx): 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, - } @@ -797,11 +774,11 @@ def run_daily_movebeam(ctx): "message": f"Align beam step failed: {exc}", } - if result.get("ok"): - status = "done" - - elif result.get("warning"): + if result.get("warning"): status = "warning" + + elif result.get("ok"): + status = "done" else: status = "failed" @@ -848,9 +825,11 @@ def daily_movebeam(meas="scint", iterations=5): 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"] + # Starting BCU BPM offsets + bpm_x_start, bpm_y_start, bpm_warning_start = read_bcu_beampos_safe() + # 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" @@ -1010,9 +989,16 @@ def daily_movebeam(meas="scint", iterations=5): 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"] + # bpm_result = get_bcu_beampos() + # bpm_x_end = bpm_result["bcu_x_um"] + # bpm_y_end = bpm_result["bcu_y_um"] + # Final BCU BPM offsets + bpm_x_end, bpm_y_end, bpm_warning_end = read_bcu_beampos_safe() + + bpm_warnings = [ + w for w in (bpm_warning_start, bpm_warning_end) + if w is not None + ] success = ( abs(diff_x) <= tolerance @@ -1041,6 +1027,9 @@ def daily_movebeam(meas="scint", iterations=5): f"{iterations} corrections." ) warning = True + if bpm_warnings: + message += "\nWARNING: " + " | ".join(bpm_warnings) + warning = True print(message) @@ -1078,7 +1067,13 @@ def daily_movebeam(meas="scint", iterations=5): def format_movebeam_for_gui(result): "Format move beam output for Daily GUI" - icon = "✓" if result["ok"] else "✗" + if result.get("warning"): + icon = "⚠" + elif result.get("ok"): + icon = "✓" + else: + icon = "✗" + return f"\n{icon} {result['message']}" @@ -1119,7 +1114,7 @@ def run_daily_beamstop(ctx): def daily_beamstop(): """Centre the beamstop against the detector cover diode, i2""" - plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + plot1, plot2, plot3, plot4, plot5, plot6, log, progress = select_daily_setup_gui() result = {} @@ -1146,7 +1141,7 @@ def daily_beamstop(): relative=True, confirm=False, window_name="DailySetup", - plot_widget=plot2, + plot_widget=plot6, ) # Wait for bs_y to setlle time.sleep(2) @@ -1162,7 +1157,7 @@ def daily_beamstop(): relative=True, confirm=False, window_name="DailySetup", - plot_widget=plot1, + plot_widget=plot5, ) result["bs_x_pos"] = x_result["centre"] @@ -1391,6 +1386,8 @@ def run_daily_flux(ctx): set_progress(ctx.progress, ctx.progress_state, task, "running") try: + dev.transm.put(EnergyDefaults.default_trans) + time.sleep(3) flux, full_flux = bpm2flux() message = ( @@ -1427,7 +1424,16 @@ def format_flux_for_gui(result): # ============================================================================= # Daily-setup GUI helpers # ============================================================================= +def read_bcu_beampos_safe(): + """Read calibrated BCU beam position without failing beam alignment.""" + try: + result = get_bcu_beampos() + return result["bcu_x_um"], result["bcu_y_um"], None + except Exception as exc: + warning = f"BCU BPM position unavailable: {exc}" + print(f"WARNING: {warning}") + return None, None, warning def select_daily_setup_gui(): """Return widgets belonging to the Daily Setup GUI.""" @@ -1444,7 +1450,7 @@ def select_daily_setup_gui(): # create_empty_daily_report(filename) # Restore the standard Daily Setup layout - gui.load_profile("daily_setup") + gui.load_profile("DailySetup") # gui.Report.load_pdf(str(filename)) # gui.Report.fit_to_page() @@ -1456,17 +1462,21 @@ def select_daily_setup_gui(): gui.Plot_2, gui.Plot_3, gui.Plot_4, + gui.Plot_5, + gui.Plot_6, gui.Info, gui.Progress, ) def clear_daily_plots(): - plot1, plot2, plot3, plot4, log, progress = select_daily_setup_gui() + plot1, plot2, plot3, plot4, plot5, plot6, log, progress = select_daily_setup_gui() plot1.clear_all() plot2.clear_all() plot3.clear_all() plot4.clear_all() + plot5.clear_all() + plot6.clear_all() def update_log(log_box, messages, message): @@ -2255,6 +2265,7 @@ def create_daily_html_report(results, total_duration=None, messages=None): task_labels = [ ("Pre checks", "Pre checks"), + ("Weekly checks", "Weekly checks"), ("Set energy", "Set energy"), ("Move beam", "Move beam to crosshair"), ("Align beamstop", "Align beamstop"), diff --git a/pxii_bec/macros/katscripts.py b/pxii_bec/macros/katscripts.py index 7cb22e1..5670025 100755 --- a/pxii_bec/macros/katscripts.py +++ b/pxii_bec/macros/katscripts.py @@ -657,14 +657,6 @@ def write_lut(): f.write(f"{energy:.0f},{gap:.5g},{pitch:.5g},{roll:.4g},{perp:.5g},{x:.4g},{y:.4g}\n") -def measure_beamsize(cam="samcam"): - a = dev.samcam_xsig.read()["samcam_xsig"]["value"] - b = dev.samcam_ysig.read()["samcam_ysig"]["value"] - zoom = dev.scam_zoom.read()["scam_zoom"]["value"] - sx = calc_scam_microns(a, zoom=zoom) * 2.35 - sy = calc_scam_microns(b, zoom=zoom) * 2.35 - print(f"FWHM in um : {sx, sy}") - return sx, sy def write_beamsize(): @@ -685,106 +677,6 @@ 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. - - Parameters - ---------- - edge : float - Approximate edge energy in eV around which to centre the scan. - - 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, - ) - - energy = convert_from_bragg(bragg)["energy_ev"] - ss=get_value(dev.ss_bpmsum) - inten = average_inten() - 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(): @@ -1180,72 +1072,6 @@ from epics import caget, caput import time -def auto_exposure( - cam="samcam", target=200, tol=5, gain=0.9, min_exp=0.00001, max_exp=0.2, max_iter=10 -): - """ - Adjust camera exposure time to reach a target max pixel value. - - Parameters - ---------- - target : float - Desired MaxValue (counts) - tol : float - Acceptable deviation from target - min_exp, max_exp : float - Exposure time limits (s) - max_iter : int - Maximum number of adjustment iterations - """ - if cam == "samcam": - exposure = dev.samcam_exp.read()["samcam_exp"]["value"] - else: - exposure = dev.ss_xicam_exp.read()["ss_xicam_exp"]["value"] - - print(f"Starting exposure = {exposure:.4f} s") - - for i in range(max_iter): - time.sleep(0.1) # allow a frame or two to update - if cam == "samcam": - maxval = dev.samcam_max.read()["samcam_max"]["value"] - if maxval is None: - print("No reading — check camera.") - break - if cam == "ss_xicam": - maxval = dev.ss_xicam_max.read()["ss_xicam_max"]["value"] - if maxval is None: - print("No reading — check camera.") - break - - error = target - maxval - # print(f"Iter {i+1}: MaxValue = {maxval:.1f}, error = {error:.1f}") - - # stop if within tolerance - if abs(error) <= tol: - # print(f"Target reached (MaxValue={maxval:.1f}) after {i+1} iterations.") - break - - # proportional correction (simple gain control) - gain_setting = gain # moderate response - new_exp = exposure * (1 + gain_setting * error / target) - - # limit exposure bounds - new_exp = max(min_exp, min(max_exp, new_exp)) - - # update camera perp = dev.dcm_perp.read()["dcm_perp"]["value"] - y = dev.ss_xicam_x.read()["ss_xicam_x"]["value"] - x = dev.ss_xicam_y.read()["ss_xicam_y"]["value"] - if cam == "samcam": - dev.samcam_exp.put(new_exp) - # print(f" → Adjusting exposure to {new_exp:.4f} s") - else: - dev.ss_xicam_exp.put(new_exp) - # print(f" → Adjusting exposure to {new_exp:.4f} s") - - exposure = new_exp - - print(f"New exposure time is {exposure:.3g}") - return exposure # def get_new_beamsizes(mirror): @@ -1544,14 +1370,6 @@ def vfm_yaw_check(): plt.tight_layout() plt.show() -@dataclass -class SlitAxis: - name: str - centre_motor: object # e.g. dev.slit1_x - size_motor: object # e.g. dev.slit1_xsize - scan_range: float = 2.0 - - # @dataclass # class SlitAxis: # name: str @@ -1563,50 +1381,6 @@ class SlitAxis: # min_size: float = -0.4 # max_size: float = 5.0 - -@dataclass -class Slit: - name: str - x: SlitAxis - y: SlitAxis - -def centre_axis(axis, detector, slit_size=0.5): - """Measure the beam centre on one slit axis.""" - - original_size = axis.size_motor.position - start_centre = axis.centre_motor.position - - try: - umv(axis.size_motor, slit_size) - - go_to_peak( - axis.centre_motor, - detector, - start_centre - axis.scan_range, - start_centre + axis.scan_range, - 50, - confirm=False, - ) - - measured_centre = get_value(axis.centre_motor) - - print( - f"{axis.name}: " - f"start={start_centre:.4f}, " - f"measured={measured_centre:.4f}, " - f"shift={measured_centre - start_centre:+.4f}" - ) - - return { - "start": start_centre, - "centre": measured_centre, - "shift": measured_centre - start_centre, - } - - finally: - umv(axis.size_motor, original_size) - - # def read_intensity(detector, n=5, delay=0.1): # values = [] # for _ in range(n): @@ -1719,114 +1493,11 @@ def centre_axis(axis, detector, slit_size=0.5): # 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}") - - # Set a known gap for the scan - umv(axis.size_motor, slit_size) - - start = axis.centre_motor.position - - go_to_peak( - axis.centre_motor, - detector, - start - axis.scan_range, - start + axis.scan_range, - 50, - confirm=False, - ) - - centre = get_value(axis.centre_motor) - print(f"{axis.name}: centre = {centre:.4f}") - - # Open slit again - umv(axis.size_motor, final_size) - - return centre - -def centre_slit(slit, detector, slit_size=0.5, final_size=3.0): - print(f"\n=== Aligning {slit.name} ===") - - 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: - try: - centre_slit(slit, detector) - except Exception as exc: - 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: - if slit.name == name: - centre_slit(slit, detector) -def define_slits(): - return [ - Slit( - name="bsf", - x=SlitAxis( - name="bsf_sl_x", - centre_motor=dev.bsf_sl_xcen, - size_motor=dev.bsf_sl_xsize, - ), - y=SlitAxis( - name="bsf_sl_y", - centre_motor=dev.bsf_sl_ycen, - size_motor=dev.bsf_sl_ysize, - ), - ), - Slit( - name="ss", - x=SlitAxis( - name="ss_sl_x", - centre_motor=dev.ss_sl_xcen, - size_motor=dev.ss_sl_xsize, - ), - y=SlitAxis( - name="ss_sl_y", - centre_motor=dev.ss_sl_ycen, - size_motor=dev.ss_sl_ysize, - ), - ), - Slit( - name="bcu", - x=SlitAxis( - name="bcu_sl_x", - centre_motor=dev.bcu_sl_xcen, - size_motor=dev.bcu_sl_xsize, - ), - y=SlitAxis( - name="bcu_sl_y", - centre_motor=dev.bcu_sl_ycen, - size_motor=dev.bcu_sl_ysize, - ), - ), - ] + + # def define_slits(): @@ -2492,7 +2163,7 @@ def dcm_pitch_calibration(): 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") + f.write("energy,bragg,pitch\n") # for energy in range(start_energy, end_energy, step): try: for energy in energies: @@ -2505,55 +2176,158 @@ def dcm_pitch_calibration(): # pitch_new = get_value(dev.dcm_pitch) bl_energy(energy) pitch_new = get_value(dev.dcm_pitch) + bragg = get_value(dev.dcm_bragg) with open(filename, "a", encoding="utf-8") as f: - f.write(f"{energy:.0f},{pitch_new:.5g}\n") + f.write(f"{energy:.0f},{bragg:.3f},{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.""" +# def plot_rolls(): +# energies = [6000, 9000, 12000, 15000, 18000, 21000] +# for energy in energies: +# filename = bl.env.lut_dir / f"roll_{energy}.csv" +# data = np.loadtxt(filename, delimiter=",", skiprows=1) +# x = data[:, 0] +# y = data[:, 1] - scanpoints = np.linspace(start, stop, steps) - print(f"Scanning roll from {start} to {stop}") +# m, c = np.polyfit(x,y,1) +# from pathlib import Path - 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 +# import numpy as np +# import pandas as pd +# import matplotlib.pyplot as plt +def analyse_roll_calibration( + energies=(6000, 9000, 12000, 15000, 18000, 21000), +): + """ + Analyse roll calibration scans at several energies. + + Reads: + bl.env.lut_dir / roll_{energy}.csv + + Each CSV must contain: + Roll,Y pos + + Fits Y position vs roll independently for each energy and finds + the roll where the fitted Y positions have minimum spread. + """ + + lut_dir = Path(bl.env.lut_dir) + fits = {} + + fig, ax = plt.subplots(figsize=(10, 7)) + + # ------------------------------------------------------------ + # Read and fit each energy + # ------------------------------------------------------------ + + for energy in energies: + + filename = lut_dir / f"roll_{energy}.csv" + df = pd.read_csv(filename) + + roll = df["Roll"].to_numpy() + y = df["Y pos"].to_numpy() + + # Linear fit: y = m * roll + b + m, b = np.polyfit(roll, y, 1) + fits[energy] = (m, b) + + # Plot measured points + ax.scatter( + roll, + y, + label=f"{energy / 1000:g} keV", + ) + + # Plot fitted line + xx = np.linspace(roll.min(), roll.max(), 100) + ax.plot(xx, m * xx + b, "--") + + print( + f"{energy / 1000:4.1f} keV: " + f"Y = {m:8.3f} × roll + {b:8.3f}" + ) + + # ------------------------------------------------------------ + # Find roll where fitted lines have minimum spread + # ------------------------------------------------------------ + + slopes = np.array([m for m, b in fits.values()]) + intercepts = np.array([b for m, b in fits.values()]) + + dm = slopes - slopes.mean() + db = intercepts - intercepts.mean() + + best_roll = -np.sum(dm * db) / np.sum(dm**2) + + # Predicted Y position at best roll + predicted_y = np.array( + [ + m * best_roll + b + for m, b in fits.values() + ] + ) + + mean_y = predicted_y.mean() + std_y = predicted_y.std() + + # ------------------------------------------------------------ + # Results + # ------------------------------------------------------------ + + print("\nBest roll:") + print(f" {best_roll:.6f}") + + print("\nPredicted Y at best roll:") + + for energy, y in zip(energies, predicted_y): + print( + f" {energy / 1000:4.1f} keV : " + f"{y:8.2f} px" + ) + + print( + f"\nMean Y = {mean_y:.2f} px" + f"\nSpread = {std_y:.2f} px" + ) + + # ------------------------------------------------------------ + # Plot optimum + # ------------------------------------------------------------ + + ax.axvline( + best_roll, + color="black", + linestyle=":", + label=f"Best roll = {best_roll:.4f}", + ) + + ax.axhline( + mean_y, + color="grey", + linestyle=":", + alpha=0.5, + ) + + ax.set_xlabel("Roll") + ax.set_ylabel("Scintillator Y position (px)") + ax.set_title("DCM roll calibration") + ax.legend() + + plt.tight_layout() + plt.show() + + return { + "best_roll": best_roll, + "mean_y": mean_y, + "std_y": std_y, + "fits": fits, + "predicted_y": dict(zip(energies, predicted_y)), + } 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.""" @@ -2612,10 +2386,64 @@ def analyse_roll(low=6000, high=25000, low_file=None, high_file=None): return x_intersect +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(zoom=800) + + 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) + + auto_exposure("samcam") + + 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 measure_rolls(): + energies = [6000, 9000, 12000, 15000, 18000, 21000] + try: + for energy in energies: + scan_roll(energy, start=2, stop=5, overwrite=True) + finally: + print("done") + # bl_energy(12400) + + def find_best_roll( low=6000, high=25000, - low_range=(3.0, 5.0), + low_range=(2.0, 4.0), high_range=(1.0, 8.0), steps=9, ): @@ -3257,3 +3085,174 @@ def gap_beam(): finish_ss_xi_measurement() finish_scint_measurement() # bl_energy(12400) + +def gap_test(): + for energy in range(6000, 30001, 500): + try: + harmonic, gap = Gap.calculate_gap(energy) + print( + f"{energy:5d} eV " + f"H{harmonic:2d} " + f"{gap:.4f} mm" + ) + except ValueError as exc: + print(energy, exc) + +def energy_beam_position(): + """ + Measure the beam trajectory as a function of energy. + + At each energy: + 1. Perform a normal energy change. + 2. Insert the SS X-ray eye and measure beam position. + 3. Remove the SS X-ray eye. + 4. Prepare the sample scintillator measurement. + 5. Record LU, BCU and sample-camera beam positions and flux. + + No DCM perp correction or beam steering is applied. + """ + + energies = [ + 6000, + 8000, + 10000, + 12400, + 15000, + 18000, + 20000, + 22000, + 25000, + ] + + filename = bl.env.lut_dir / f"energy_beam_position_{filenow()}.csv" + + with open(filename, "w", encoding="utf-8") as f: + f.write( + "energy,harmonic,gap," + "dcm_pitch,dcm_perp,dcm_roll," + "lu_bpmsum,lu_Xnorm,lu_Ynorm," + "xi_cam_x,xi_cam_y," + "bcu_bpmsum,bcu_Xnorm,bcu_Ynorm,bcu_x,bcu_y," + "samcam_x,samcam_y," + "flux\n" + ) + + try: + for energy in energies: + print(f"\n{'=' * 60}") + print(f"Measuring beam position at {energy} eV") + print(f"{'=' * 60}") + + # ---------------------------------------------------------- + # Normal energy change + # ---------------------------------------------------------- + bl_energy(energy) + + # Record DCM / undulator state after energy change + dcm_pitch = get_value(dev.dcm_pitch) + dcm_perp = get_value(dev.dcm_perp) + dcm_roll = get_value(dev.dcm_froll) + + harmonic, gap = Gap.calculate_gap(energy) + + # ---------------------------------------------------------- + # SS X-ray eye + # This blocks the downstream beam, so measure it first + # and remove it before continuing. + # ---------------------------------------------------------- + try: + prepare_ss_xi_measurement() + time.sleep(0.5) + + xi_read = read_ss_xicamXY() + xicam_x = xi_read["X_pixels"] + xicam_y = xi_read["Y_pixels"] + + time.sleep(0.5) + + finally: + finish_ss_xi_measurement() + + # ---------------------------------------------------------- + # Downstream measurements + # ---------------------------------------------------------- + prepare_scint_measurement() + + # LU BPM + lu_bpmsum = get_value(dev.lu_bpmsum) + + lu = compute_norm_lu() + lu_Xnorm = lu["lu_Xnorm"] + lu_Ynorm = lu["lu_Ynorm"] + + # BCU BPM + bcu_bpmsum = get_value(dev.bcu_bpmsum) + + bcu_norm = compute_norm_bcu() + bcu_Xnorm = bcu_norm["bcu_Xnorm"] + bcu_Ynorm = bcu_norm["bcu_Ynorm"] + + try: + bcu = get_bcu_beampos() + bcu_x = bcu["bcu_x_um"] + bcu_y = bcu["bcu_y_um"] + + except ValueEror as exc: + print(f"WARNING: BCU beam position unavailable {exc}") + bcu_x = float("nan") + bcu_y = float("nan") + + # Sample scintillator + scint = read_scintXY(image=False) + samcam_x = scint["X_diff_microns"] + samcam_y = scint["Y_diff_microns"] + + # Flux + _,flux = bpm2flux() + + # ---------------------------------------------------------- + # Save immediately + # ---------------------------------------------------------- + with open(filename, "a", encoding="utf-8") as f: + f.write( + f"{energy:.0f}," + f"H{harmonic},{gap:.4f}," + f"{dcm_pitch:.5f},{dcm_perp:.5f},{dcm_roll:.5f}," + f"{lu_bpmsum:.1f},{lu_Xnorm:.3f},{lu_Ynorm:.3f}," + f"{xicam_x:.1f},{xicam_y:.1f}," + f"{bcu_bpmsum:.1f},{bcu_Xnorm:.3f},{bcu_Ynorm:.3f}," + f"{bcu_x:.2f},{bcu_y:.2f}," + f"{samcam_x:.2f},{samcam_y:.2f}," + f"{flux:.3e}\n" + ) + + print( + f"{energy:5.0f} eV | " + f"H{harmonic} gap={gap:.4f} | " + f"XI=({xicam_x:.1f}, {xicam_y:.1f}) px | " + f"LU=({lu_Xnorm:.3f}, {lu_Ynorm:.3f}) | " + f"BCU=({bcu_x:.1f}, {bcu_y:.1f}) um | " + f"Sample=({samcam_x:.1f}, {samcam_y:.1f}) um | " + f"Flux={flux:.2e}" + ) + except Exception as exc: + print(f"\nEnergy beam-position measurement FAILED at {energy} eV") + print(f"{type(exc).__name__}: {exc}") + raise + + finally: + # Make sure neither screen is accidentally left in the beam + try: + finish_ss_xi_measurement() + except Exception: + pass + + try: + finish_scint_measurement() + except Exception: + pass + + print("\nReturning to 12400 eV") + bl_energy(12400) + + print(f"\nResults saved to:\n{filename}") \ No newline at end of file diff --git a/pxii_bec/macros/luts/roll_6000.csv b/pxii_bec/macros/luts/roll_6000.csv index cc5e249..520d502 100644 --- a/pxii_bec/macros/luts/roll_6000.csv +++ b/pxii_bec/macros/luts/roll_6000.csv @@ -1,10 +1,10 @@ -Roll, Y pos -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 +Roll,Y pos +2.000000,333.32351717113295 +2.375000,458.83075423152076 +2.750000,576.4915204490983 +3.125000,688.162043927042 +3.500000,806.7231283084715 +3.875000,925.9491968173602 +4.250000,1031.01771701488 +4.625000,1125.1857495953072 +5.000000,1208.846194057293 diff --git a/pxii_bec/macros/mxcomm_utils.py b/pxii_bec/macros/mxcomm_utils.py index f38d6f6..70e4b16 100644 --- a/pxii_bec/macros/mxcomm_utils.py +++ b/pxii_bec/macros/mxcomm_utils.py @@ -4,135 +4,16 @@ 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): @@ -322,27 +203,5 @@ def create_empty_daily_report(filename): 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/pxii_energy.py b/pxii_bec/macros/pxii_energy.py index c9c542e..303a8f1 100755 --- a/pxii_bec/macros/pxii_energy.py +++ b/pxii_bec/macros/pxii_energy.py @@ -88,82 +88,126 @@ 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"]-0.04, relative=False) + pitch_offset = dev.dcm_pitch.user_parameter["offset"] + scans.umv(EnergyDefaults.mono_pitch, pos["dcm_pitch"]+pitch_offset, relative=False) # Go to peak using default parameters from EnergyDefaults - if plot: - print("Scanning monochromator pitch and moving to peak, with plotting.") + result = go_to_peak( + EnergyDefaults.mono_pitch, + EnergyDefaults.signals["sig1"], + -EnergyDefaults.pitch_scan["halfwidth"], + EnergyDefaults.pitch_scan["halfwidth"], + steps=EnergyDefaults.pitch_scan["steps"], + relative=True, + settle=0.01, + plot=plot, + confirm=False, + min_fwhm_steps=1.1, + window_name=window_name if plot else None, + plot_widget=plot_widget if plot else None, + ) + + extended_scan = False + + if not result["success"]: + extended_scan = True + + 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"], - -EnergyDefaults.pitch_scan["halfwidth"], - EnergyDefaults.pitch_scan["halfwidth"], - steps=EnergyDefaults.pitch_scan["steps"], + -extended_range, + extended_range, + steps=extended_steps, relative=True, settle=0.01, - plot=True, + plot=plot, confirm=False, min_fwhm_steps=1.1, - window_name=window_name, - plot_widget=plot_widget, + window_name=window_name if plot else None, + plot_widget=plot_widget if plot else None, ) - 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( - EnergyDefaults.mono_pitch, - EnergyDefaults.signals["sig1"], - -EnergyDefaults.pitch_scan["halfwidth"], - EnergyDefaults.pitch_scan["halfwidth"], - steps=EnergyDefaults.pitch_scan["steps"], - 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 + result["extended_scan"] = extended_scan return result + # if plot: + # print("Scanning monochromator pitch and moving to peak, with plotting.") + # result = go_to_peak( + # EnergyDefaults.mono_pitch, + # EnergyDefaults.signals["sig1"], + # -EnergyDefaults.pitch_scan["halfwidth"], + # EnergyDefaults.pitch_scan["halfwidth"], + # steps=EnergyDefaults.pitch_scan["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"] = 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( + # EnergyDefaults.mono_pitch, + # EnergyDefaults.signals["sig1"], + # -EnergyDefaults.pitch_scan["halfwidth"], + # EnergyDefaults.pitch_scan["halfwidth"], + # steps=EnergyDefaults.pitch_scan["steps"], + # 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 + # Specific functions - need to be edited for each beamline @@ -212,12 +256,13 @@ def get_dcm_motors_positions(energy_ev): """ # dcm_motor_values = get_value_from_lut(energy_ev) dcm_motor_values = {} - pitch = float(np.poly1d(Calibration.pitch_calib)(energy_ev)) + # pitch = float(np.poly1d(Calibration.pitch_calib)(energy_ev)) # pitch = -5.5 fpitch = 5.0 roll = EnergyDefaults.mono_roll_value perp = calc_perp_position(energy_ev, print_result=False) bragg_angle = convert_from_energy(energy_ev, print_result=False)["bragg_angle_mrad"] + pitch = float(np.poly1d(Calibration.pitch_calib)(bragg_angle)) dcm_motor_values.update( { "bragg_angle": bragg_angle, @@ -349,24 +394,36 @@ def bl_energy( # return gap +# def calc_gap(energy_ev, print_result=True): +# """Calculate the undulator gap for the given energy.""" + +# harmonic_values = Gap.get_harmonic_values(energy_ev) + +# if harmonic_values is None: +# return None + +# gap = np.poly1d(harmonic_values)(energy_ev) +# gap *= Gap.correction_factor + +# if print_result: +# harmonic = Gap.get_harmonic_by_energy(energy_ev) +# print(f"Calculated gap is {gap:.3f} mm " f"using {harmonic}") + +# return gap + def calc_gap(energy_ev, print_result=True): """Calculate the undulator gap for the given energy.""" - harmonic_values = Gap.get_harmonic_values(energy_ev) - - if harmonic_values is None: - return None - - gap = np.poly1d(harmonic_values)(energy_ev) - gap *= Gap.correction_factor + harmonic, gap = Gap.calculate_gap(energy_ev) 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:.4f} mm " + f"using H{harmonic}" + ) return gap - def set_gap(energy_ev, print_result=False): """Set the undulator gap.""" diff --git a/pxii_bec/macros/pxii_parameters.py b/pxii_bec/macros/pxii_parameters.py index 64ef12a..d3f9e44 100755 --- a/pxii_bec/macros/pxii_parameters.py +++ b/pxii_bec/macros/pxii_parameters.py @@ -20,7 +20,7 @@ class EnergyDefaults: mono_fpitch = dev.dcm_fpitch mono_perp = dev.dcm_perp mono_roll = dev.dcm_froll - mono_roll_value = 4.36 + mono_roll_value = 3.88 stripe_thresholds = {"silicon": 9000, "rhodium": 20000, "platinum": 40000} pitch_scan = {"halfwidth": 0.075, "steps": 20} @@ -29,127 +29,105 @@ class EnergyDefaults: class Calibration: """Calibration parameters for PXII optics""" - pitch_calib = [-7.60082220e-19, 6.09893342e-14, -1.81702221e-09, 2.45457520e-05, -5.61275367e00] + # pitch_calib = [-7.60082220e-19, 6.09893342e-14, -1.81702221e-09, 2.45457520e-05, -5.61275367e00] + + pitch_calib = [-1.84031410e-07,-6.76777668e-05, -5.59684124] # roll = np.array([2.28291039e-03, -2.41928101e01]) @dataclass(frozen=True) class Gap: - """Fit parameters to calculate undulator gap from energy.""" + """PXII U19 undulator gap calibration. + + Harmonic fits are from Anuschka's September 2026 calibration. + For a requested energy, use the highest odd harmonic whose + calculated gap remains >= minimum_gap. + """ minimum_gap = 4.5 + selection_min_gap = 4.505 maximum_gap = 20.0 - correction_factor = 0.9994 + # Anuschka's September 2026 harmonic fits. + # Energy input is in keV, gap output is in mm. harmonics = { - "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]), + 3: np.array([0.095, -0.276, 4.154]), + 5: np.array([0.00269444, -0.0616905, 0.967925, -0.295405]), + 7: np.array([0.00110017, -0.0369968, 0.77334, -0.688198]), + 9: np.array([0.00021633, -0.00619517, 0.315392, 0.971667]), + 11: np.array([0.000589549, -0.0328631, 0.837705, -2.90326]), + 13: np.array([0.197702, 0.757869]), + 15: np.array([0.170381, 0.783155]), + 17: np.array([0.1466, 0.8906]), + 19: np.array([0.128, 0.985]), } @classmethod - def minimum_energy(cls, harmonic): - """ - Return the lowest energy that can be reached with this harmonic - without the corrected gap going below the minimum gap. - """ - slope, intercept = cls.harmonics[harmonic] - - # calc_gap() applies the correction factor afterwards, - # so account for it here as well. - raw_minimum_gap = cls.minimum_gap / cls.correction_factor - - return (raw_minimum_gap - intercept) / slope + def calculate_gap_for_harmonic(cls, energy_ev, harmonic): + """Calculate gap for a specified harmonic.""" + energy_kev = energy_ev / 1000.0 + return np.polyval(cls.harmonics[harmonic], energy_kev) @classmethod def get_harmonic_by_energy(cls, energy_ev): """ - Return the appropriate harmonic for the requested energy. + Select the highest odd harmonic that gives a valid gap. - Switch to the next harmonic as soon as it can produce the - requested energy without requiring a gap below minimum_gap. + This selects the harmonic whose gap is closest to the + minimum allowed gap of 4.5 mm, while maintaining a small + safety margin of 0.005 mm. """ - selected = None + + valid = [] for harmonic in cls.harmonics: - if energy_ev >= cls.minimum_energy(harmonic): - selected = harmonic - else: - break + gap = cls.calculate_gap_for_harmonic( + energy_ev, + harmonic, + ) - return selected + if cls.selection_min_gap <= gap <= cls.maximum_gap: + valid.append((harmonic, gap)) - @classmethod - def get_harmonic_values(cls, energy_ev): - """ - Return the fit coefficients for the appropriate harmonic. - """ - harmonic = cls.get_harmonic_by_energy(energy_ev) - - if harmonic is None: + if not valid: return None - return cls.harmonics[harmonic] + # Closest valid gap to the minimum gap + harmonic, gap = min( + valid, + key=lambda x: x[1] - cls.selection_min_gap, + ) + return harmonic + @classmethod def calculate_gap(cls, energy_ev): - """ - Calculate the corrected undulator gap for the requested energy. + """Return selected harmonic and calculated gap.""" - Returns - ------- - tuple - (harmonic, 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"No valid harmonic found for {energy_ev:.0f} eV." + ) - slope, intercept = cls.harmonics[harmonic] - - gap = slope * energy_ev + intercept - gap *= cls.correction_factor + gap = cls.calculate_gap_for_harmonic( + energy_ev, + harmonic, + ) + # Final check against actual motor limits if not cls.minimum_gap <= gap <= cls.maximum_gap: raise ValueError( f"Calculated gap {gap:.3f} mm for {energy_ev:.0f} eV " - f"using {harmonic} is outside the allowed range " + f"using H{harmonic} is outside the allowed range " f"{cls.minimum_gap:.1f}-{cls.maximum_gap:.1f} mm." ) return harmonic, gap -@dataclass(frozen=True) -class Harmonics: - """Anuschka's harmonics data for PXII U19 Undulator""" - - min_gap_value = 4.5 - map = { - 3: np.array([1.07141725, -0.52834258]), - 4: np.array([0.007111, -0.13678409, 1.52295567, -1.06882785]), - 5: np.array([0.00315051, -0.0766359, 1.13107469, -0.86442062]), - 6: np.array([0.00162862, -0.04452336, 0.82948259, -0.37329674]), - 7: np.array([0.00080764, -0.02418882, 0.59212947, 0.15702886]), - 8: np.array([1.16699242e-03, -4.68207543e-02, 9.43972396e-01, -1.94201019e00]), - 9: np.array([0.00048419, -0.02010648, 0.55514114, -0.38180067]), - 10: np.array([2.11583333e-04, -8.03503571e-03, 3.43141595e-01, 6.02318000e-01]), - 11: np.array([0.00334392, -0.18405336, 3.58338994, -19.3640241]), - 12: np.array([3.20520798e-05, 2.39253145e-03, 8.09198503e-02, 2.22897377e00]), - 13: np.array([0.00278744, 0.07979874, 2.05143916]), - } - energy_ranges = {3: (0, 7), 5: (7, 10), 7: (10, 13), 9: (13, 16), 11: (16, 19), 13: (19, 22)} - high_energy = [(15, (23, 25)), (17, (25, 29)), (19, (29, float("inf")))] - - @dataclass(frozen=True) class CamConversion: """Convert pixels to microns for sample camera""" @@ -164,8 +142,8 @@ class BCU_BPM_X_Calibration: signal_limit = 0.90 # centred_position = 0.441 centred_position = -0.009 # where the bpm was during calibration - crosshair_signal = -0.00339158 - crosshair_bpm_position = 0.0532 + crosshair_signal = -0.0209 + crosshair_bpm_position = 0.0738 # @dataclass(frozen=True) @@ -173,8 +151,8 @@ 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 + crosshair_signal = 0.00156 + crosshair_bpm_position = -1.5459 # @dataclass(frozen=True) diff --git a/pxii_bec/macros/pxii_setup.py b/pxii_bec/macros/pxii_setup.py new file mode 100644 index 0000000..a361a80 --- /dev/null +++ b/pxii_bec/macros/pxii_setup.py @@ -0,0 +1,353 @@ +"""PXII-specific operational helpers used by routine daily/weekly setup. + +BEC loads macro files into a shared namespace, so these functions deliberately use +the standard BEC globals (dev, umv, get_value, etc.) rather than importing other +macro files. Nothing in this module should be promoted to mxcomm_* unless it is +truly beamline-independent. +""" + +import time +from dataclasses import dataclass +from datetime import datetime + +import numpy as np +from pxii_bec.scripts import beamline_context as bl +from pxii_bec.macros.beamline_planner import BeamlineState + + +# ============================================================================= +# Energy +# ============================================================================= + +def daily_energy(energy, scan_mono=True, plot_widget=None): + """Set PXII energy/gap and optionally optimise DCM pitch.""" + + current_energy = get_current_energy() + mono_result = {"extended_scan": False} + + if abs(energy - current_energy) > 1: + bl_energy(energy, window_name="DailySetup", plot_widget=plot_widget) + # bl_energy performs the normal energy move. Keep mono_result benign + # here so the result handling below is defined for both code paths. + 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.get("extended_scan", False): + 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, + } + + return { + "ok": True, + "warning": True, + "message": ( + "Warning - DCM pitch is not within expected tolerance. " + "The DCM may need to be re-calibrated. " + f"Pitch offset = {pitch_offset:.3f} mrad" + ), + "energy": new_energy, + "pitch_offset": pitch_offset, + } + + +# ============================================================================= +# Sample-camera scintillator measurements +# ============================================================================= + +def predict_scint_transmission(target_flux=None): + """Predict PXII 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( + "Cannot predict scintillator transmission: " + f"invalid full flux ({full_flux:.3g} ph/s)" + ) + return float(np.clip(target_flux / full_flux, 0.001, 1.0)) + + +def auto_exposure(cam="samcam", target=200, tol=5, gain=0.9, + min_exp=0.00001, max_exp=0.2, max_iter=10): + """Adjust PXII camera exposure to reach the requested maximum signal.""" + if cam == "samcam": + exposure = dev.samcam_exp.read()["samcam_exp"]["value"] + else: + exposure = dev.ss_xicam_exp.read()["ss_xicam_exp"]["value"] + + print(f"Starting exposure = {exposure:.4f} s") + for _ in range(max_iter): + time.sleep(0.1) + if cam == "samcam": + maxval = dev.samcam_max.read()["samcam_max"]["value"] + else: + maxval = dev.ss_xicam_max.read()["ss_xicam_max"]["value"] + if maxval is None: + print("No reading — check camera.") + break + error = target - maxval + if abs(error) <= tol: + break + new_exp = exposure * (1 + gain * error / target) + new_exp = max(min_exp, min(max_exp, new_exp)) + if cam == "samcam": + dev.samcam_exp.put(new_exp) + else: + dev.ss_xicam_exp.put(new_exp) + exposure = new_exp + print(f"New exposure time is {exposure:.3g}") + return exposure + + +def prepare_scint_measurement(autoexp=True, zoom=1000): + """Put PXII into the standard state for a scintillator measurement.""" + transmission = predict_scint_transmission() + print(f"Predicted scintillator transmission: {transmission:.2%}") + dev.transm.put(transmission) + time.sleep(3) + if get_value(dev.scam_zoom) != zoom: + umv(dev.scam_zoom, zoom) + if not bl.planner.is_state(BeamlineState.BEAM_VISUALISATION): + bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) + dev.bcu_shutter.put(1) + time.sleep(0.5) + if autoexp: + auto_exposure(cam="samcam", max_iter=25) + return zoom + + +def measure_beamsize(cam="samcam"): + """Return PXII sample-camera beam FWHM in microns.""" + a = dev.samcam_xsig.read()["samcam_xsig"]["value"] + b = dev.samcam_ysig.read()["samcam_ysig"]["value"] + zoom = dev.scam_zoom.read()["scam_zoom"]["value"] + sx = calc_scam_microns(a, zoom=zoom) * 2.35 + sy = calc_scam_microns(b, zoom=zoom) * 2.35 + print(f"FWHM in um : {(sx, sy)}") + return sx, sy + + +def read_scintXY(image=False, measure_size=True): + """Read PXII beam position from the sample-camera scintillator image.""" + x_target = BeamMove.scint_target_x + y_target = BeamMove.scint_target_y + zoom = get_value(dev.scam_zoom) + max_signal = get_value(dev.samcam_max) + thresh = get_value(dev.samcam_thresh) + if max_signal < thresh: + return { + "ok": False, + "message": ( + "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"] + diff_x = x_target - x_px + diff_y = y_target - y_px + results = { + "ok": True, + "message": f"Beam detected at X={x_px:.1f}, Y={y_px:.1f} px", + "max_signal": max_signal, + "X_pixels": x_px, + "Y_pixels": y_px, + "X_diff_px": diff_x, + "Y_diff_px": diff_y, + "X_diff_microns": calc_scam_microns(diff_x, zoom), + "Y_diff_microns": calc_scam_microns(diff_y, zoom), + } + if measure_size: + results["X_FWHM"], results["Y_FWHM"] = measure_beamsize() + print( + f"Beam position: X = {x_px:.1f} px, Y = {y_px:.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: + results["image"] = get_cam_image(Cameras.sample) + return results + + +def finish_scint_measurement(): + dev.bcu_shutter.put(0) + + +# ============================================================================= +# Slit centring used by weekly checks +# ============================================================================= + +@dataclass +class SlitAxis: + name: str + centre_motor: object + size_motor: object + scan_range: float = 2.0 + +@dataclass +class Slit: + name: str + x: SlitAxis + y: SlitAxis + +def centre_axis(axis, detector, slit_size=0.5, final_size=3.0, window_name=None, plot_widget=None): + print(f"\nAligning {axis.name}") + umv(axis.size_motor, slit_size) + start = axis.centre_motor.position + try: + go_to_peak(axis.centre_motor, detector, start-axis.scan_range, + start+axis.scan_range, 50, confirm=False, window_name=window_name, + plot_widget=plot_widget) + centre = get_value(axis.centre_motor) + print(f"{axis.name}: centre = {centre:.4f}") + return centre + finally: + umv(axis.size_motor, final_size) + +def centre_slit(slit, detector, slit_size=0.5, final_size=3.0, window_name=None, x_plot_widget=None, y_plot_widget=None): + print(f"\n=== Aligning {slit.name} ===") + x = centre_axis(slit.x, detector, slit_size, final_size,window_name=window_name,plot_widget=x_plot_widget) + y = centre_axis(slit.y, detector, slit_size, final_size, window_name=window_name,plot_widget=y_plot_widget) + print(f"=== Finished {slit.name} ===") + return {"x": x, "y": y} + +def define_slits(): + def axis(prefix, direction): + return SlitAxis( + name=f"{prefix}_sl_{direction}", + centre_motor=getattr(dev, f"{prefix}_sl_{direction}cen"), + size_motor=getattr(dev, f"{prefix}_sl_{direction}size"), + ) + return [Slit(name, axis(name, "x"), axis(name, "y")) + for name in ("bsf", "ss", "bcu")] + +def centre_one(name, detector, window_name=None, x_plot_widget=None, y_plot_widget=None): + for slit in define_slits(): + if slit.name == name: + return centre_slit(slit, detector,window_name=window_name,x_plot_widget=x_plot_widget,y_plot_widget=y_plot_widget) + raise ValueError(f"Unknown PXII slit set: {name}") + + +# ============================================================================= +# Ge energy calibration diagnostic +# ============================================================================= + +def scan_ge(edge=11103.1, mode="coarse"): + """Scan the PXII Ge K edge and return the generated CSV filename.""" + if mode == "coarse": + scan_range, scanpoints = 100, 41 + if mode == "medium": + scan_range, scanpoints = 40, 41 + elif mode == "fine": + scan_range, scanpoints = 15, 61 + else: + raise ValueError(f"Unknown scan mode '{mode}'. Use 'coarse', 'medium' or 'fine'.") + + start_eV, end_eV = edge-scan_range, edge+scan_range + 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) + filename = bl.env.lut_dir / f"{datetime.now():%Y%m%d}_ge_edge_{mode}.csv" + + with open(filename, "w") as f: + f.write("Energy,Bragg,ss_bpmsum,bcu_bpmsum,transmission\n") + + print(f"\nGe edge {mode} scan: {start_eV:.1f}-{end_eV:.1f} eV") + bl_energy(start_eV) + dev.transm.put(1) + time.sleep(2) + umv(dev.ss_f4_x, 6.0) + time.sleep(3) + + try: + for bragg in bragg_points: + scans.umv(dev.dcm_bragg, bragg, relative=False) + energy = convert_from_bragg(bragg)["energy_ev"] + ss = get_value(dev.ss_bpmsum) + vals = [] + for _ in range(5): + vals.append(dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"]) + time.sleep(0.1) + inten = float(np.mean(vals)) + transmission = inten / ss + with open(filename, "a") as f: + f.write(f"{energy:.2f},{bragg:.2f},{ss:.2f},{inten:.2f},{transmission:.6f}\n") + finally: + bl_energy(12400) + dev.transm.put(0.1) + time.sleep(2) + + print(f"Scan complete. Results written to {filename}") + return filename + +def prepare_ss_xi_measurement(autoexp=True): + """Prepare for taking a reading of the beam from the SS Xray eye""" + + # move to x-ray eye into the beam + umv(dev.ss_xi_x, 7.0) + + # set autoexposure and check there is a reading in the image + if autoexp: + auto_exposure(cam="ss_xicam", max_iter=25) + +def finish_ss_xi_measurement(): + '''Move the SS xray eye out of the beam''' + umv(dev.ss_xi_x, -7) + +def read_ss_xicamXY(): + """Read the beam position at the SS xray eye camera""" + results = {} + max_signal = get_value(dev.ss_xicam_max) + results["max_signal"] = max_signal + # thresh = get_value(dev.samcam_thresh) + thresh = 150 + 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.ss_xicam_x.read()["ss_xicam_x"]["value"] + y_px = dev.ss_xicam_y.read()["ss_xicam_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" + ) + print(f"Beam position: X = {results['X_pixels']:.1f} px, " + f"Y = {results['Y_pixels']:.1f} px\n") + + return results diff --git a/pxii_bec/macros/weekly.py b/pxii_bec/macros/weekly.py index c93a696..a38a9f8 100644 --- a/pxii_bec/macros/weekly.py +++ b/pxii_bec/macros/weekly.py @@ -1,347 +1,201 @@ -# """Weekly beamline checks""" - -"""Weekly beamline checks.""" +"""PXII 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.""" - + """Put PXII into the standard conditions used 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" - ) - - + energy_result = daily_energy(BLSetup.energy, scan_mono=True, plot_widget="Plot_1") return { - "ok": True, + "ok": energy_result.get("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""" + """Scan PXII FE slits in X and Y and restore the standard gaps.""" 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) + go_to_peak(dev.fe_sl_xcen, dev.lu_bpmsum, -1, 1, 25, + confirm=False, relative=True, window_name=window_name, plot_widget="Plot_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) + go_to_peak(dev.fe_sl_ycen, dev.lu_bpmsum, -1, 1, 25, + confirm=False, relative=True, window_name=window_name, plot_widget="Plot_2") ycen = get_value(dev.fe_sl_ycen) - - return { - "fe_xcen": xcen, - "fe_ycen": 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) + umv(dev.fe_sl_xsize, BLSetup.fe_xsize) + umv(dev.fe_sl_ysize, BLSetup.fe_ysize) + + +def weekly_fe_scan(window_name="DailySetup"): + try: + result = scan_fe(window_name) + return {"ok": True, "x": result["fe_xcen"], "y": result["fe_ycen"]} + except Exception as exc: + print(f"FE slit scan failed: {exc}") + return {"ok": False, "error": str(exc)} 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, - } - + result = centre_one("bsf", dev.lu_bpmsum, window_name="DailySetup", x_plot_widget="Plot_3", y_plot_widget="Plot_4") + return {"ok": True, "x": result["x"], "y": result["y"]} except Exception as exc: - reason = str(exc) - print(f"BSF slit scan failed: {reason}") - - return { - "ok": False, - "error": reason, - } + print(f"BSF slit scan failed: {exc}") + return {"ok": False, "error": str(exc)} 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, - } - + result = centre_one("ss", dev.bcu_bpmsum, window_name="DailySetup", x_plot_widget="Plot_5", y_plot_widget="Plot_6") + return {"ok": True, "x": result["x"], "y": result["y"]} except Exception as exc: - reason = str(exc) - print(f"SS slit scan failed: {reason}") + print(f"SS slit scan failed: {exc}") + return {"ok": False, "error": str(exc)} + - return { - "ok": False, - "error": reason, - } -# ============================================================================= -# BPM readings -# ============================================================================= def measure_bpm_positions(): - lubpm = compute_norm_lu() - ssbpm = compute_norm_ss() - bcubpm = get_bcu_beampos() + """Read all three PXII BPM systems into one structured result.""" + return { + "lu": compute_norm_lu(), + "ss": compute_norm_ss(), + "bcu": 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.""" - + """Append weekly PXII 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", {}) - - + fe, bsf, ss = results.get("FE", {}), results.get("BSF", {}), results.get("SS", {}) + lubpm, ssbpm, bcubpm = results.get("lubpm", {}), results.get("ssbpm", {}), results.get("bcubpm", {}) + edge_result = results.get("edge", {}) 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" - ), - + "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"), + "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"), + "energy_offset_eV": format_value(edge_result.get("energy_offset"), ".2f") } - fieldnames = list(row.keys()) - - file_exists = filename.exists() - + exists = filename.exists() with open(filename, "a", newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - - if not file_exists: + writer = csv.DictWriter(csvfile, fieldnames=list(row)) + if not exists: writer.writeheader() - writer.writerow(row) - - print(f"\nWeekly results saved to:") - print(filename) - + print(f"\nWeekly results saved to:\n{filename}") return filename -# ============================================================================= -# SciLog results -# ============================================================================= -def send_week_to_scilog(): - filename = bl.env.daily_setup_dir / "weekly_beamline_health.csv" + +def send_week_to_scilog(filename=None): + if filename is None: + 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.add_tags(["weekly-check", "PXII"]) msg.send() -# ============================================================================= -# Main weekly check -# ============================================================================= -def weekly_checks(): - """Run standard weekly beamline checks.""" +def weekly_checks(save_scilog=True): + """Run the complete PXII weekly sequence. + This remains independently callable, while daily_setup() invokes it + automatically on Wednesdays after the normal machine pre-checks. + """ print("\n========================================") print("PXII WEEKLY CHECKS") print("========================================") - - results = {} - - # ------------------------------------------------------------------ - # Standard conditions - # ----------------------------------SS-------------------------------- + results = {"ok": True} try: results["setup"] = setup_weekly() + if not results["setup"].get("ok", False): + results["ok"] = False + results["error"] = "Weekly setup failed" + results["filename"] = save_weekly_results(results) + return results + 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) - + results.update(ok=False, error=f"Weekly setup failed: {exc}") + results["setup"] = {"ok": False, "error": str(exc)} + results["filename"] = save_weekly_results(results) return results - # ------------------------------------------------------------------ - # BPM positions - # ------------------------------------------------------------------ - results["lubpm"] = compute_norm_lu() - results["ssbpm"] = compute_norm_ss() - results["bcubpm"] = get_bcu_beampos() + # Record BPM positions before moving slit centres. + try: + bpm = measure_bpm_positions() + results["lubpm"], results["ssbpm"], results["bcubpm"] = bpm["lu"], bpm["ss"], bpm["bcu"] + except Exception as exc: + results["ok"] = False + results["bpm_error"] = str(exc) + print(f"BPM readings failed: {exc}") - - # ------------------------------------------------------------------ - # Slit scans - # ------------------------------------------------------------------ results["FE"] = weekly_fe_scan() results["BSF"] = weekly_bsf_slits_scan() results["SS"] = weekly_ss_slits_scan() + if not all(results[k].get("ok", False) for k in ("FE", "BSF", "SS")): + results["ok"] = False - # ------------------------------------------------------------------ - # Save results - # ------------------------------------------------------------------ + # Run the Ge edge scan. + + try: + filename = scan_ge(11103, mode="medium") + edge = ge_edge_analysis(filename, save=True) + + results["edge"] = edge + + # Weekly-check acceptance criterion + if abs(edge["energy_offset"]) > 1.2: + results["warning"] = True + results["edge_warning"] = ( + f"Energy calibration is {edge['energy_offset']:+.2f} eV off " + f"(measured {edge['measured_edge']:.2f} eV, " + f"expected {edge['expected_edge']:.2f} eV)" + ) + print(f"WARNING: {results['edge_warning']}") + + except Exception as exc: + results["ok"] = False + results["edge_error"] = str(exc) + print(f"Ge edge scan failed: {exc}") results["filename"] = save_weekly_results(results) - # ------------------------------------------------------------------ - # Edge scan - # ------------------------------------------------------------------ - results["edge"] = scan_ge(11103, mode="coarse") - # ------------------------------------------------------------------ - # Save results to SciLog - # ------------------------------------------------------------------ + if save_scilog: + try: + send_week_to_scilog(results["filename"]) + except Exception as exc: + print(f"WARNING: could not send weekly results to SciLog: {exc}") + print("\nWeekly checks complete.") - return results - -- 2.54.0 From 7607dc941b693caaff2c1812023f884365d0b384 Mon Sep 17 00:00:00 2001 From: x10sa Date: Sun, 11 Oct 2026 15:03:37 +0200 Subject: [PATCH 4/6] fixed error propagation in daily/weekly checks --- pxii_bec/macros/daily.py | 22 +++++++++++++++++++++- pxii_bec/macros/weekly.py | 7 ++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pxii_bec/macros/daily.py b/pxii_bec/macros/daily.py index 75e1cd4..3198779 100644 --- a/pxii_bec/macros/daily.py +++ b/pxii_bec/macros/daily.py @@ -677,14 +677,34 @@ def run_weekly_checks(ctx, save_scilog=True): status = "done" # Basic summary + # Summary for Daily Setup GUI lines = [] + # Overall/outer failure + if result.get("message"): + lines.append(f"ERROR: {result['message']}") + + if result.get("error"): + lines.append(f"ERROR: {result['error']}") + + # BPM readings + if result.get("bpm_error"): + lines.append(f"WARNING: BPM readings failed: {result['bpm_error']}") + + # Slit scans + for name in ("FE", "BSF", "SS"): + check = result.get(name, {}) + if check and not check.get("ok", True): + error = check.get("error", "unknown error") + lines.append(f"WARNING: {name} slit scan failed: {error}") + + # Ge edge edge = result.get("edge") if edge: lines.append( f"Ge edge: {edge['measured_edge']:.2f} eV " f"(expected {edge['expected_edge']:.2f} eV, " - f"offset {edge['energy_offset']:+.2f} eV)" + f"energy offset {edge['energy_offset']:+.2f} eV)" ) if result.get("edge_warning"): diff --git a/pxii_bec/macros/weekly.py b/pxii_bec/macros/weekly.py index a38a9f8..27f938b 100644 --- a/pxii_bec/macros/weekly.py +++ b/pxii_bec/macros/weekly.py @@ -156,15 +156,16 @@ def weekly_checks(save_scilog=True): bpm = measure_bpm_positions() results["lubpm"], results["ssbpm"], results["bcubpm"] = bpm["lu"], bpm["ss"], bpm["bcu"] except Exception as exc: - results["ok"] = False + results["warning"] = True results["bpm_error"] = str(exc) print(f"BPM readings failed: {exc}") results["FE"] = weekly_fe_scan() results["BSF"] = weekly_bsf_slits_scan() results["SS"] = weekly_ss_slits_scan() + if not all(results[k].get("ok", False) for k in ("FE", "BSF", "SS")): - results["ok"] = False + results["warning"] = True # Run the Ge edge scan. @@ -185,7 +186,7 @@ def weekly_checks(save_scilog=True): print(f"WARNING: {results['edge_warning']}") except Exception as exc: - results["ok"] = False + results["warning"] = True results["edge_error"] = str(exc) print(f"Ge edge scan failed: {exc}") -- 2.54.0 From 13b2eb32d9e31e9b2f2cb4b5e3c65278d0ef0adc Mon Sep 17 00:00:00 2001 From: x10sa Date: Wed, 28 Oct 2026 20:18:25 +0100 Subject: [PATCH 5/6] Integrate latest PXII production calibrations and fixes --- pxii_bec/device_configs/pxii-devices.yaml | 10 +- pxii_bec/macros/daily.py | 37 ++- pxii_bec/macros/katscripts.py | 361 ++++++++++------------ pxii_bec/macros/pxii_parameters.py | 7 +- pxii_bec/macros/pxii_setup.py | 2 +- pxii_bec/macros/weekly.py | 44 ++- 6 files changed, 236 insertions(+), 225 deletions(-) diff --git a/pxii_bec/device_configs/pxii-devices.yaml b/pxii_bec/device_configs/pxii-devices.yaml index 99e95ee..4342d08 100644 --- a/pxii_bec/device_configs/pxii-devices.yaml +++ b/pxii_bec/device_configs/pxii-devices.yaml @@ -65,15 +65,15 @@ aerotech: mount: 0 work: 0 x: - in: 0.0599968 + in: 0.06 out: -10 safe: -50 y: mount: -0.0 - work: -0.03699256 + work: -0.035 z: mount: 0 - work: -0.00900414 + work: -0.009 bcu_bpm1: description: BCU BPM Channel 1 deviceClass: ophyd.EpicsSignalRO @@ -302,7 +302,7 @@ bs_x: onFailure: buffer readoutPriority: baseline userParameter: - in: 0.44000227 + in: 0.34999599 bs_y: description: Beamstop Y deviceClass: ophyd_devices.EpicsMotor @@ -314,7 +314,7 @@ bs_y: onFailure: buffer readoutPriority: baseline userParameter: - in: -3.3000017 + in: -3.5 bs_z: description: Beamstop Z deviceClass: ophyd_devices.EpicsMotor diff --git a/pxii_bec/macros/daily.py b/pxii_bec/macros/daily.py index 3198779..40820c7 100644 --- a/pxii_bec/macros/daily.py +++ b/pxii_bec/macros/daily.py @@ -1182,7 +1182,7 @@ def daily_beamstop(): result["bs_x_pos"] = x_result["centre"] result["bs_y_pos"] = y_result["centre"] - result["intensity after alignment"] = read_i2() + result['intensity after alignment'] = read_i2() # message = ( # "Intensity after Y scan: " @@ -1324,8 +1324,8 @@ def daily_collimator(): get_value(dev.i1) - result["intensity_coll_out"] = get_value(dev.i1) - print(f"Starting intensity is {result["intensity_coll_out"]:.3e}") + result['intensity_coll_out'] = get_value(dev.i1) + print(f"Starting intensity is {result['intensity_coll_out']:.3e}") bl.d["coll_y"].move("in") @@ -1448,7 +1448,7 @@ def read_bcu_beampos_safe(): """Read calibrated BCU beam position without failing beam alignment.""" try: result = get_bcu_beampos() - return result["bcu_x_um"], result["bcu_y_um"], None + return result['bcu_x_um'], result['bcu_y_um'], None except Exception as exc: warning = f"BCU BPM position unavailable: {exc}" @@ -1930,11 +1930,34 @@ def log_daily_setup_to_csv(results): get_value(dev.hfm_pitch), ".5f" ), } - + fieldnames = list(row.keys()) file_exists = filename.exists() + # Check whether the existing CSV has the current schema + if file_exists: + with open(filename, "r", newline="", encoding="utf-8") as f: + reader = csv.reader(f) + existing_header = next(reader, None) + + if existing_header != fieldnames: + # Archive the old CSV rather than writing incompatible rows to it + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_filename = filename.with_name( + f"{filename.stem}_archive_{timestamp}{filename.suffix}" + ) + + filename.rename(archive_filename) + + print( + "⚠ Daily beamline CSV schema has changed.\n" + f" Previous history archived as: {archive_filename.name}\n" + f" Starting new history file: {filename.name}" + ) + + file_exists = False + with open(filename, "a", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=row.keys()) + writer = csv.DictWriter(f, fieldnames=fieldnames) if not file_exists: writer.writeheader() @@ -2478,4 +2501,4 @@ def create_daily_html_report(results, total_duration=None, messages=None): """ - return html \ No newline at end of file + return html diff --git a/pxii_bec/macros/katscripts.py b/pxii_bec/macros/katscripts.py index 5670025..57751ee 100755 --- a/pxii_bec/macros/katscripts.py +++ b/pxii_bec/macros/katscripts.py @@ -190,7 +190,6 @@ def energy_checks(start_energy=8000, end_energy=20000): f.close() - # def scan_gap(gomax = False, close = True, fine = False): # """Scan the gap and monitor on lu_bpmsum or bcu_bpmsum""" # close_fe_slits(close) @@ -352,14 +351,13 @@ def beam_centre_from_ss(): def generate_dcm_lut( - start_energy=6000, - end_energy=30000, - step=1000, - scan_id_gap=False, - scint=True, - prefix = "dcm_calibration", + start_energy=6000, + end_energy=30000, + step=1000, + scan_id_gap=False, + scint=True, + prefix="dcm_calibration", ): - """Generate a DCM calibration lookup table. Parameters @@ -393,13 +391,19 @@ def generate_dcm_lut( 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") + 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") + 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") + 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") @@ -500,15 +504,14 @@ def generate_dcm_lut( # ------------------------------------------------------------ res = get_bcu_beampos() - bcu_x = res['bcu_x_um'] - bcu_y = res['bcu_y_um'] + 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'] + 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'] - + lu_Xnorm = lu_res["lu_Xnorm"] + lu_Ynorm = lu_res["lu_Ynorm"] pitch = get_value(dev.dcm_pitch) @@ -547,7 +550,7 @@ def generate_dcm_lut( 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: @@ -555,12 +558,13 @@ def generate_dcm_lut( 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"{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...") @@ -600,45 +604,24 @@ def generate_dcm_lut( print(f"\nCalibration data written to:\n" f"{filename}") - return { - "filename": filename, - "failure_filename": failure_filename, - "failures": failures, - } + return {"filename": filename, "failure_filename": failure_filename, "failures": failures} -def calibrate_dcm_pitch( - start_energy=6000, - end_energy=30000, - step=2000, - fit_order=3, -): + +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" - ) + 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, + 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, - ) + fit = pitch_analysis(filedate=filenow(date_only=True), fit_order=fit_order) - return { - "fit": fit, - "failures": failures, - } + return {"fit": fit, "failures": failures} def write_lut(): @@ -657,8 +640,6 @@ def write_lut(): f.write(f"{energy:.0f},{gap:.5g},{pitch:.5g},{roll:.4g},{perp:.5g},{x:.4g},{y:.4g}\n") - - def write_beamsize(): """Write beam size and flux to file""" filename = "luts/beamsizes.csv" @@ -677,8 +658,6 @@ def write_beamsize(): f.write(f"{lens_config},{sx:.4g},{sy:.4g},{i1_reading:.4g}\n") - - def cal_samcam_fixed_x(): # lat = dev.hfm_x.read()["hfm_x"]["value"] # pitch = dev.hfm_pitch.read()["hfm_pitch"]["value"] @@ -1072,8 +1051,6 @@ from epics import caget, caput import time - - # def get_new_beamsizes(mirror): # filename = f"luts/{mirror}_size.csv" # if mirror == "hfm": @@ -1300,7 +1277,6 @@ def mirror_pitch(mirror="vfm"): results = np.array(results) - def vfm_yaw_check(): slit_size = 0.02 slit_point1 = -1.2 @@ -1370,6 +1346,7 @@ def vfm_yaw_check(): plt.tight_layout() plt.show() + # @dataclass # class SlitAxis: # name: str @@ -1495,11 +1472,6 @@ def vfm_yaw_check(): # print(f"=== Finished {slit.name} ===") - - - - - # def define_slits(): # slits = [ # Slit( @@ -1728,9 +1700,9 @@ import matplotlib.pyplot as plt from epics import caget -def scan_scin_focus(planner, type="fine"): +def scan_scin_focus(type="fine"): dev.bcu_shutter.put(0) - planner.move_to(BeamlineState.BEAM_VISUALISATION) + # bl.planner.move_to(BeamlineState.BEAM_VISUALISATION) dev.bcu_shutter.put(1) z0 = dev.diag_z.position @@ -1746,11 +1718,12 @@ def scan_scin_focus(planner, type="fine"): umv(dev.scam_zoom, 1000) dev.transm.put(0.1) + time.sleep(3) auto_exposure("samcam", target=200, max_iter=20) time.sleep(0.2) - go_to_peak(dev.diag_z, dev.scam, min, max, steps, relative=True, gomax=True, confirm=False) + go_to_peak(dev.diag_z, dev.scam, min, max, steps, relative=False, gomax=True, confirm=False) dev.bcu_shutter.put(0) @@ -1829,7 +1802,6 @@ def centre_bcu_bpm_y(): return centre - def centre_ss_bpm_x(): positions = [] errors = [] @@ -1866,7 +1838,7 @@ def centre_ss_bpm_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) + ch4 = get_value(dev.ss_bpm4) _, y_error = bpm_errors(ch1, ch2, ch3, ch4) print(f"BPM_Y = {float(y)}, Yerror = {y_error}") @@ -1879,6 +1851,7 @@ def centre_ss_bpm_y(): umv(dev.ss_bpm_y, centre) return centre + def centre_lu_bpm_x(): positions = [] errors = [] @@ -1915,7 +1888,7 @@ def centre_lu_bpm_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) + ch4 = get_value(dev.lu_bpm4) _, y_error = bpm_errors(ch1, ch2, ch3, ch4) print(f"BPM_Y = {float(y)}, Yerror = {y_error}") @@ -1928,6 +1901,8 @@ def centre_lu_bpm_y(): umv(dev.lu_bpm_y, centre) return centre + + def read_i1(): return dev.i1.read()["i1"]["value"] @@ -2159,10 +2134,11 @@ def measure_omega_runout( # 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] + # energies = [5500, 8000, 10000, 12000, 14000, 16000, 18000, + # 20000, 22000, 24000, 27000, 30000] + energies = [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: + with open(filename, "a", encoding="utf-8") as f: f.write("energy,bragg,pitch\n") # for energy in range(start_energy, end_energy, step): try: @@ -2199,9 +2175,7 @@ def dcm_pitch_calibration(): # import matplotlib.pyplot as plt -def analyse_roll_calibration( - energies=(6000, 9000, 12000, 15000, 18000, 21000), -): +def analyse_roll_calibration(energies=(6000, 9000, 12000, 15000, 18000, 21000)): """ Analyse roll calibration scans at several energies. @@ -2237,20 +2211,13 @@ def analyse_roll_calibration( fits[energy] = (m, b) # Plot measured points - ax.scatter( - roll, - y, - label=f"{energy / 1000:g} keV", - ) + ax.scatter(roll, y, label=f"{energy / 1000:g} keV") # Plot fitted line xx = np.linspace(roll.min(), roll.max(), 100) ax.plot(xx, m * xx + b, "--") - print( - f"{energy / 1000:4.1f} keV: " - f"Y = {m:8.3f} × roll + {b:8.3f}" - ) + print(f"{energy / 1000:4.1f} keV: " f"Y = {m:8.3f} × roll + {b:8.3f}") # ------------------------------------------------------------ # Find roll where fitted lines have minimum spread @@ -2265,12 +2232,7 @@ def analyse_roll_calibration( best_roll = -np.sum(dm * db) / np.sum(dm**2) # Predicted Y position at best roll - predicted_y = np.array( - [ - m * best_roll + b - for m, b in fits.values() - ] - ) + predicted_y = np.array([m * best_roll + b for m, b in fits.values()]) mean_y = predicted_y.mean() std_y = predicted_y.std() @@ -2285,33 +2247,17 @@ def analyse_roll_calibration( print("\nPredicted Y at best roll:") for energy, y in zip(energies, predicted_y): - print( - f" {energy / 1000:4.1f} keV : " - f"{y:8.2f} px" - ) + print(f" {energy / 1000:4.1f} keV : " f"{y:8.2f} px") - print( - f"\nMean Y = {mean_y:.2f} px" - f"\nSpread = {std_y:.2f} px" - ) + print(f"\nMean Y = {mean_y:.2f} px" f"\nSpread = {std_y:.2f} px") # ------------------------------------------------------------ # Plot optimum # ------------------------------------------------------------ - ax.axvline( - best_roll, - color="black", - linestyle=":", - label=f"Best roll = {best_roll:.4f}", - ) + ax.axvline(best_roll, color="black", linestyle=":", label=f"Best roll = {best_roll:.4f}") - ax.axhline( - mean_y, - color="grey", - linestyle=":", - alpha=0.5, - ) + ax.axhline(mean_y, color="grey", linestyle=":", alpha=0.5) ax.set_xlabel("Roll") ax.set_ylabel("Scintillator Y position (px)") @@ -2329,6 +2275,7 @@ def analyse_roll_calibration( "predicted_y": dict(zip(energies, predicted_y)), } + 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.""" @@ -2417,36 +2364,79 @@ def scan_roll(energy, start, stop, steps=9, settle=0.2, filename=None, overwrite 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']}" + 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}" ) - 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") + f.write(f"{roll:.6f},{results['Y_pixels']}\n") finally: finish_scint_measurement() return filename -def measure_rolls(): + +def scan_roll_bcu(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_{filenow(date_only=True)}_{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) + + 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) + + result = get_bcu_beampos() + + print( + f"{energy / 1000:.1f} keV | " + f"roll = {roll:.3f} | " + f"Y = {result['bcu_y_um']:.2f}" + ) + + f.write(f"{roll:.6f},{result['bcu_y_um']}\n") + + return filename + + +def measure_rolls(monitor="scint"): energies = [6000, 9000, 12000, 15000, 18000, 21000] - try: - for energy in energies: - scan_roll(energy, start=2, stop=5, overwrite=True) - finally: - print("done") - # bl_energy(12400) + if monitor == "scint": + try: + for energy in energies: + scan_roll(energy, start=2.0, stop=4, overwrite=True) + finally: + print("done") + # bl_energy(12400) + else: + try: + for energy in energies: + scan_roll_bcu(energy, start=2.8, stop=4, overwrite=True) + finally: + print("done") + # bl_energy(12400) -def find_best_roll( - low=6000, - high=25000, - low_range=(2.0, 4.0), - high_range=(1.0, 8.0), - steps=9, -): +def find_best_roll(low=6000, high=25000, low_range=(2.0, 4.0), high_range=(1.0, 8.0), steps=9): """Collect missing roll scans and calculate optimum roll.""" filename_prefix = filenow(date_only=True) @@ -2454,17 +2444,12 @@ def find_best_roll( 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), - ] + 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]) - ) + 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") @@ -2472,12 +2457,7 @@ def find_best_roll( # 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, - ) + scan_roll(energy, *scan_range, steps=steps, filename=filename) else: print(f"Using existing {filename.name}") @@ -2577,6 +2557,7 @@ def test_vfm3(dmove, offset=0.002): f"Position error = {(final-target)*1000:+.3f} µm" ) + def beamstop_check_energies(): bs_in = 0.904 @@ -2588,9 +2569,9 @@ def beamstop_check_energies(): 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): + for energy in range(start_energy, end_energy, -2000): print(f"Moving to energy {energy}") bl_energy(energy) dev.transm.put(0.1) @@ -2614,6 +2595,7 @@ def beamstop_check_energies(): umv(dev.bs_y, bs_in) bl_energy(12400) + def check_perp(): Xref = -0.0059957 Yref = -0.000993 @@ -2622,9 +2604,9 @@ def check_perp(): start_perp = get_value(dev.dcm_perp) - perps = [start_perp, start_perp-0.1, start_perp, start_perp+0.1, start_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: + with open(filename, "w", encoding="utf-8") as f: f.write("Delta perp, Pitch, Xnorm, Delta X") for perp in perps: @@ -2633,9 +2615,9 @@ def check_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: + 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}, " @@ -2644,6 +2626,7 @@ def check_perp(): f"delta X: {Xdelta:.5g}" ) + def calibrate_dcm_perp(): """ Determine the DCM perp position required to maintain a fixed beam @@ -2679,16 +2662,7 @@ def calibrate_dcm_perp(): # beam displacement (mm) / perp displacement (mm) beam_per_perp = 1.46 - energies = [ - 6000, - 8000, - 10000, - 12000, - 16000, - 20000, - 24000, - 30000, - ] + energies = [6000, 8000, 10000, 12000, 16000, 20000, 24000, 30000] filename = bl.env.lut_dir / "dcm_perp_calibration.csv" @@ -2800,6 +2774,8 @@ def calibrate_dcm_perp(): print(filename) finally: bl_energy(12400) + + from datetime import datetime @@ -2833,10 +2809,7 @@ def scan_dcm_exit_vs_energy(): 30000, ] - filename = ( - bl.env.lut_dir - / f"dcm_exit_scan_{datetime.now():%Y%m%d_%H%M%S}.csv" - ) + filename = bl.env.lut_dir / f"dcm_exit_scan_{datetime.now():%Y%m%d_%H%M%S}.csv" # -------------------------------------------------------------- # Create CSV @@ -2869,7 +2842,6 @@ def scan_dcm_exit_vs_energy(): # 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) @@ -2923,13 +2895,16 @@ def scan_dcm_exit_vs_energy(): 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") + 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) @@ -2941,13 +2916,12 @@ def perp_again(): for offset in perp_offsets: perp = perp_value + offset - umv(dev.dcm_perp,perp) + 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"] @@ -2961,7 +2935,7 @@ def perp_again(): 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}," @@ -2971,6 +2945,7 @@ def perp_again(): finish_scint_measurement() bl_energy(12400) + def perp_ss_xi(): # energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] energies = [25000] @@ -2978,11 +2953,11 @@ def perp_ss_xi(): 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") + 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) @@ -2990,23 +2965,21 @@ def perp_ss_xi(): for offset in perp_offsets: perp = perp_value + offset - umv(dev.dcm_perp,perp) + 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}," @@ -3016,6 +2989,7 @@ def perp_ss_xi(): finish_ss_xi_measurement() # bl_energy(12400) + def gap_beam(): # energies = [8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] energies = [22000] @@ -3023,13 +2997,13 @@ def gap_beam(): 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") + 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") + 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) @@ -3047,7 +3021,7 @@ def gap_beam(): # 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}," @@ -3059,7 +3033,7 @@ def gap_beam(): finish_ss_xi_measurement() prepare_scint_measurement() - scanpoints=np.linspace(4.535,4.555,20) + scanpoints = np.linspace(4.535, 4.555, 20) for i in scanpoints: umv(dev.id_gap, i) time.sleep(0.2) @@ -3071,33 +3045,30 @@ def gap_beam(): 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) + def gap_test(): for energy in range(6000, 30001, 500): try: harmonic, gap = Gap.calculate_gap(energy) - print( - f"{energy:5d} eV " - f"H{harmonic:2d} " - f"{gap:.4f} mm" - ) + print(f"{energy:5d} eV " f"H{harmonic:2d} " f"{gap:.4f} mm") except ValueError as exc: print(energy, exc) + def energy_beam_position(): """ Measure the beam trajectory as a function of energy. @@ -3112,17 +3083,7 @@ def energy_beam_position(): No DCM perp correction or beam steering is applied. """ - energies = [ - 6000, - 8000, - 10000, - 12400, - 15000, - 18000, - 20000, - 22000, - 25000, - ] + energies = [6000, 8000, 10000, 12400, 15000, 18000, 20000, 22000, 25000] filename = bl.env.lut_dir / f"energy_beam_position_{filenow()}.csv" @@ -3208,7 +3169,7 @@ def energy_beam_position(): samcam_y = scint["Y_diff_microns"] # Flux - _,flux = bpm2flux() + _, flux = bpm2flux() # ---------------------------------------------------------- # Save immediately @@ -3255,4 +3216,4 @@ def energy_beam_position(): print("\nReturning to 12400 eV") bl_energy(12400) - print(f"\nResults saved to:\n{filename}") \ No newline at end of file + print(f"\nResults saved to:\n{filename}") diff --git a/pxii_bec/macros/pxii_parameters.py b/pxii_bec/macros/pxii_parameters.py index d3f9e44..9b08a3e 100755 --- a/pxii_bec/macros/pxii_parameters.py +++ b/pxii_bec/macros/pxii_parameters.py @@ -20,7 +20,7 @@ class EnergyDefaults: mono_fpitch = dev.dcm_fpitch mono_perp = dev.dcm_perp mono_roll = dev.dcm_froll - mono_roll_value = 3.88 + mono_roll_value = 3.186 stripe_thresholds = {"silicon": 9000, "rhodium": 20000, "platinum": 40000} pitch_scan = {"halfwidth": 0.075, "steps": 20} @@ -29,9 +29,8 @@ class EnergyDefaults: class Calibration: """Calibration parameters for PXII optics""" - # pitch_calib = [-7.60082220e-19, 6.09893342e-14, -1.81702221e-09, 2.45457520e-05, -5.61275367e00] - - pitch_calib = [-1.84031410e-07,-6.76777668e-05, -5.59684124] + # pitch_calib = [-1.84031410e-07,-6.76777668e-05, -5.59684124] + pitch_calib = [-1.75794156e-07, -6.68437530e-05, -5.66961048e+00] # roll = np.array([2.28291039e-03, -2.41928101e01]) diff --git a/pxii_bec/macros/pxii_setup.py b/pxii_bec/macros/pxii_setup.py index a361a80..59dfb5d 100644 --- a/pxii_bec/macros/pxii_setup.py +++ b/pxii_bec/macros/pxii_setup.py @@ -263,7 +263,7 @@ def scan_ge(edge=11103.1, mode="coarse"): if mode == "coarse": scan_range, scanpoints = 100, 41 if mode == "medium": - scan_range, scanpoints = 40, 41 + scan_range, scanpoints = 40, 61 elif mode == "fine": scan_range, scanpoints = 15, 61 else: diff --git a/pxii_bec/macros/weekly.py b/pxii_bec/macros/weekly.py index 27f938b..85c76fb 100644 --- a/pxii_bec/macros/weekly.py +++ b/pxii_bec/macros/weekly.py @@ -105,15 +105,43 @@ def save_weekly_results(results): "energy_offset_eV": format_value(edge_result.get("energy_offset"), ".2f") } - exists = filename.exists() - with open(filename, "a", newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=list(row)) - if not exists: - writer.writeheader() - writer.writerow(row) - print(f"\nWeekly results saved to:\n{filename}") - return filename + fieldnames = list(row.keys()) + file_exists = filename.exists() + + # Check whether the existing CSV has the current schema + if file_exists: + with open(filename, "r", newline="", encoding="utf-8") as f: + reader = csv.reader(f) + existing_header = next(reader, None) + + if existing_header != fieldnames: + # Archive the old CSV rather than writing incompatible rows to it + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_filename = filename.with_name( + f"{filename.stem}_archive_{timestamp}{filename.suffix}" + ) + + filename.rename(archive_filename) + + print( + "⚠ Weekly beamline CSV schema has changed.\n" + f" Previous history archived as: {archive_filename.name}\n" + f" Starting new history file: {filename.name}" + ) + + file_exists = False + + with open(filename, "a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + + if not file_exists: + writer.writeheader() + + writer.writerow(row) + + print(f"✓ Weekly beamline history saved to {filename}") + return filename def send_week_to_scilog(filename=None): if filename is None: -- 2.54.0 From 48ed5ce27594805333a1bdce71c720ebe5b5b6f4 Mon Sep 17 00:00:00 2001 From: x10sa Date: Sun, 1 Nov 2026 11:18:08 +0100 Subject: [PATCH 6/6] Update APscripts and KB_Class with latest PXII changes --- pxii_bec/macros/APscripts.py | 87 ++++++++++++++++++++++++++++-------- pxii_bec/macros/KB_Class.py | 48 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/pxii_bec/macros/APscripts.py b/pxii_bec/macros/APscripts.py index ac89d3e..8df8dc1 100644 --- a/pxii_bec/macros/APscripts.py +++ b/pxii_bec/macros/APscripts.py @@ -625,7 +625,7 @@ def save_data(hindex: int, device_name: str, signal_name: str, isave=1): ######################################### ### just retrieve the saved data from csv ######################################## -def read_data_csv(filename: str): +def read_data_csv(filename: str, iplot=1): """ Get data stored in a CSV file @@ -638,8 +638,10 @@ def read_data_csv(filename: str): # read output to simple data file for later analysis: with open(filename, "r") as f: - combined_data = np.loadtxt(f, delimiter=",") # no header and fmt for load - + try: + combined_data = np.loadtxt(f, delimiter=",") # no header and fmt ? + except ValueError: # First row may be a header + combined_data = np.loadtxt(f, delimiter=",", skiprows=1) rows, cols = combined_data.shape print("No of Rows =", rows) @@ -664,19 +666,48 @@ def read_data_csv(filename: str): data_x = combined_data[:, ind1] data_y = combined_data[:, ind2] - plt.ion() - plt.figure() - plt.plot(data_x, data_y, ".") + if iplot: + plt.ion() + plt.figure() + plt.plot(data_x, data_y, ".") - plt.title(f"Scan from {filename}") - plt.xlabel(f"data column {ind1}") - plt.ylabel(f"data column {ind2}") + plt.title(f"Scan from {filename}") + plt.xlabel(f"data column {ind1}") + plt.ylabel(f"data column {ind2}") - plt.show() + plt.show() return combined_data +#################################### +### read and plot several files +#################################### + +def plot_several(filenames): + #filenames = [ + # "scan8.csv", + # "scan16.csv", + # "scan24.csv", + #] + + fig, ax = plt.subplots() # outside the loop -> in one plot + # if new fig or subplot within loop: several + for filename in filenames: + x, y = read_data_csv(filename, iplot=0) + + ax.plot(x, y, marker=".", label=filename) + + ax.set_xlabel("Parameter 1") + ax.set_ylabel("Parameter 2") + ax.legend() + ax.grid(True) + + plt.tight_layout() + plt.show() + + + #################################### ### save and plot the long gapscans #################################### @@ -1047,8 +1078,8 @@ def slitscan(device_location: str, direction: str, range=1, nsteps=50, centre=0) # FE slits ================================ if device_location in ["fe"]: - default_h = 1.0 ## 1.4 or 0.4 or ??? - default_v = 1.0 ## 1.2 + default_h = 0.6 ## 1.4 or 0.4 or ??? + default_v = 0.6 ## 1.2 det = dev.lu_bpmsum if direction in ["x", "h"]: mot = dev.fe_sl_xcen @@ -1208,7 +1239,7 @@ def slitscan(device_location: str, direction: str, range=1, nsteps=50, centre=0) print(f"opening slit to {s_open}") umv(size, s_open) - return + return res_center ##################### @@ -1283,12 +1314,30 @@ def kbfocus(sizex, sizey): return +######################### +### Status of the BEC +######################### + + +def bstatus(): + # + dock_area = bec.gui.new() + + dbrowser = dock_area.new("device_browser").new(bec.gui.available_widgets.DeviceBrowser) + + dock_area.new("queue").new(bec.gui.available_widgets.BECQueue) + # queue = dock_area.queue.BECQueue # give it a name + # text_box = dock_area.new().new(widget=bec.gui.available_widgets.TextBox) + # text_box.set_plain_text("Hello, World!") + # text_box.set_html_text("

Welcome to BEC Widgets

This is an example of displaying HTML text.

") + + ########################### ### move in the X-ray Eye ########################### -def detxeye_in(setdetzpos=200, setdetypos=151.6): +def detxeye_in(setdetzpos=200, setdetypos=151.6): # had been 213.6 """ moves in the Xray eye and the Diode below detector """ @@ -1298,7 +1347,7 @@ def detxeye_in(setdetzpos=200, setdetypos=151.6): setxrpos = 0.0 # setdetzpos = 200 # 200 better? - # Detector Y reference changed by 62 mm; previous position was 213.6 mm. + # setdetypos = 213.6 --> 151.6 detzpos = dev.det_z.user_readback.get() detypos = dev.det_y.user_readback.get() @@ -1335,7 +1384,7 @@ def detxeye_out(): moves det back and down """ setdetzpos = 500 - setdetypos = 0 + setdetypos = 0 ### 70 umv(dev.det_z, setdetzpos) detzpos = dev.det_z.user_readback.get() if detzpos > 300: @@ -1753,12 +1802,13 @@ def fluxcal(start_en=13, stop_en=14, step=1): fitpoly(dat_en, dat_quot_flbsum, 3) +# ############################# check flux measurements ################## def testflux(): import time, datetime energy = get_current_energy() count = 0 - stop = 1000 + stop = 1000 while count < stop: bpmsum_c = dev.bcu_bpmsum.read(cached=True)["bcu_bpmsum"]["value"] @@ -1767,7 +1817,7 @@ def testflux(): bpmsum = dev.bcu_bpmsum.read()["bcu_bpmsum"]["value"] transm = dev.transm.read()["transm"]["value"] - print(f'******************************************************************************') + print(f'******************************************************************************') print(f'bpmsum_cached {bpmsum_c}, transm_cached {transm_c}') bpm2flux(energy, bpmsum_c, transm_c) print(f' ') @@ -1779,3 +1829,4 @@ def testflux(): time.sleep(1) count += 1 return + diff --git a/pxii_bec/macros/KB_Class.py b/pxii_bec/macros/KB_Class.py index f90604a..1692692 100644 --- a/pxii_bec/macros/KB_Class.py +++ b/pxii_bec/macros/KB_Class.py @@ -1,6 +1,54 @@ import numpy as np +def readKB(): + """ + reads the focussing params from the KBs + """ + vfm_pitch = dev.vfm_pitch.user_readback.get() + vfm_bu = dev.vfm_bu.user_readback.get() + vfm_bd = dev.vfm_bd.user_readback.get() + + hfm_pitch = dev.hfm_pitch.user_readback.get() + hfm_bu = dev.hfm_bu.user_readback.get() + hfm_bd = dev.hfm_bd.user_readback.get() + + print( + f" KB values vertical: \n pitch = {vfm_pitch} , bend upstream = {vfm_bu} , bend downstream = {vfm_bd}" + ) + + print( + f" KB values horizontal: \n pitch = {hfm_pitch} , bend upstream = {hfm_bu} , bend downstream = {hfm_bd}" + ) + + return vfm_pitch, vfm_bu, vfm_bd, hfm_pitch, hfm_bu, hfm_bd + + +def read_focinput(dir="v"): + """ + reads the input from the User Panel + """ + import epics + + # channels: + # X10SA-ES-VFM:DEFOCUS.RBV + # X10SA-ES-VFM:USER_ASYM_SET + # X10SA-ES-VFM:PARAM_CURV --- calcrec + + str = "VFM" + if dir == "h": + str = "HFM" + + user_defoc = epics.caget(f"X10SA-ES-{str}:DEFOCUS.RBV") + user_asym_set = epics.caget(f"X10SA-ES-{str}:USER_ASYM_SET") + calc_rad = epics.caget(f"X10SA-ES-{str}:PARAM_CURV") + + print( + f"{str} User Set Values: Defoc Dist s = {user_defoc}, Additional asym_set = {user_asym_set}, Radius from input = {calc_rad} " + ) + return user_defoc, user_asym_set, calc_rad + + class KBMirror: def __init__(self, mirror_type="PXII_HFM"): self.mirror_type = mirror_type -- 2.54.0