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
+
+
+

+
+ """
+ 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 |
+ Status |
+ Duration |
+
+
+ {task_rows_html}
+
+
+ | Total duration |
+ |
+ {total_duration_text} |
+
+
+
+
+
+ Results
+
+
+
+
+ {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"]