updates, bug fixes etc
This commit is contained in:
1 parent
1cd9a87aa6
commit
d4fafdc178
23 files changed
+6208
-1487
No files matched your search
@@ -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
|
||||
|
||||
+292
-256
File diff suppressed because it is too large.
Load diff
+157
-41
@@ -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
|
||||
|
||||
##################################
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
+2353
-408
File diff suppressed because it is too large.
Load diff
@@ -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:
|
||||
|
||||
+31
-30
@@ -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
|
||||
@@ -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))
|
||||
+1957
-439
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
@@ -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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
+197
-68
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
Reference in new issue
Block a user