refactoring
CI for debye_bec / test (push) Successful in 1m16s
CI for debye_bec / test (pull_request) Successful in 1m14s

This commit is contained in:
x01da
2026-05-19 08:42:14 +02:00
parent 62582da1d9
commit 5a54675f1e
6 changed files with 592 additions and 272 deletions
@@ -1,242 +1,259 @@
import os
"""
Calculates the positions of axes based on a beamline config
"""
import numpy as np
from bec_lib import bec_logger
os.environ["USE_XRT"] = "False"
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
logger = bec_logger.logger
def calc_positions(cfg):
def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
"""
Calculates the positions of axes based on a beamline config.
Args:
cfg(ConfigDict): Dictionary with beamline config
Returns:
dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries
containing a "value" key with the corresponding float value (position).
"""
pos = {}
## FE slits
trxr = -np.arctan(cfg['h_acc'])*bl.feSlits.center1[1]
trxw = (np.arctan(cfg['h_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1]
tryb = -np.arctan(cfg['v_acc'])*bl.feSlits.center1[1]
tryt = (np.arctan(cfg['v_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1]
trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]
trxw = (
(np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1])
/ bl.feSlits.center1[1]
* bl.feSlits.center2[1]
)
tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]
tryt = (
(np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1])
/ bl.feSlits.center1[1]
* bl.feSlits.center2[1]
)
# trxw_proj = trxw/bl.feSlits.center2[1]*bl.feSlits.center1[1]
# tryt_proj = tryt/bl.feSlits.center2[1]*bl.feSlits.center1[1]
# xcen = (trxr + trxw) / 2
# ycen = (tryb + tryt) / 2
xgap = trxw - trxr
ygap = tryt - tryb
pos['sldi_gapx'] = {'value': xgap}
pos['sldi_gapy'] = {'value': ygap}
pos["sldi_gapx"] = {"value": xgap}
pos["sldi_gapy"] = {"value": ygap}
## Collimating Mirror
obj_dist = bl.cm.center[1] # object distance
beam_vs = 2 * obj_dist * np.tan(cfg['v_acc']) # vertical size of beam after CM
obj_dist = bl.cm.center[1] # object distance
beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM
# TRX
try:
index = bl.cm.surface.index(cfg['cm_stripe'])
except:
if cfg["cm_stripe"] in bl.cm.surface:
index = bl.cm.surface.index(cfg["cm_stripe"])
else:
raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!")
cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2
pos['cm_trx'] = {'value': cm_trx}
pos["cm_trx"] = {"value": cm_trx}
# TRY
height = obj_dist * np.tan(cfg['v_acc'])**2 * 1 / np.tan(cfg['cm_pitch'])
pos['cm_try'] = {'value': height}
height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"])
pos["cm_try"] = {"value": height}
# Pitch
pos['cm_rotx'] = {'value': -cfg["cm_pitch"]*1e3} # invert and convert to mrad (same as EGU of rotx axis)
pos["cm_rotx"] = {
"value": -cfg["cm_pitch"] * 1e3
} # invert and convert to mrad (same as EGU of rotx axis)
# Bending Radius
radius = 2. * obj_dist / np.sin(cfg['cm_pitch']) # Elements of modern X-ray Physics, page 108 ff.
pos['cm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km
radius = (
2.0 * obj_dist / np.sin(cfg["cm_pitch"])
) # Elements of modern X-ray Physics, page 108 ff.
pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
## Monochromator
# Bragg Angle
# if cfg['mo1_mode'] == 'Monochromatic':
# # Add 2x CM pitch to the bragg angle
# bragg = ((2 * cfg['cm_pitch']) + cfg['mo1_bragg']) / np.pi * 180
# elif cfg['mo1_mode'] == 'Pinkbeam':
# # Align xtal surfaces parallel to beam
# bragg = (2 * cfg['cm_pitch']) / np.pi * 180
# else:
# raise Exception('Monochromator mode not supported')
if cfg['mo1_mode'] == 'Monochromatic':
if cfg["mo1_mode"] == "Monochromatic":
# Add 2x CM pitch to the bragg angle
bragg = cfg['mo1_bragg']
elif cfg['mo1_mode'] == 'Pinkbeam':
bragg = cfg["mo1_bragg"]
elif cfg["mo1_mode"] == "Pinkbeam":
# Align xtal surfaces parallel to beam
bragg = 0
bragg = 0
else:
raise Exception('Monochromator mode not supported')
pos['mo1_bragg_angle'] = {'value': bragg/np.pi*180} # Bragg angle in deg
raise ValueError("Monochromator mode not supported")
pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg
# TRY, Height
l = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg'])
yhor = l*np.cos(2.*(cfg['mo1_bragg']+cfg['cm_pitch']))
yver = yhor*np.tan(2.*cfg['cm_pitch'])
l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"])
yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"]))
yver = yhor * np.tan(2.0 * cfg["cm_pitch"])
if cfg['mo1_mode'] == 'Monochromatic':
beamOffsetCCM = l*np.sin(2.*(cfg['mo1_bragg']+cfg['cm_pitch']))-yver # Resultat ist korrekt!
elif cfg['mo1_mode'] == 'Pinkbeam':
beamOffsetCCM = 0
if cfg["mo1_mode"] == "Monochromatic":
beam_offset_mo1 = (
l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver
) # Resultat ist korrekt!
elif cfg["mo1_mode"] == "Pinkbeam":
beam_offset_mo1 = 0
else:
raise Exception('Monochromator mode not supported')
raise ValueError("Monochromator mode not supported")
def csc(a):
return 1/np.sin(a)
return 1 / np.sin(a)
def cot(a):
return 1/np.tan(a)
return 1 / np.tan(a)
# calculate height of center of first crystal surface
f = bl.mo1.rotOffset # rotation offset, mm
# logger.info(f'f = {f}')
d = bl.mo1.heightOffset # xtal height offset, mm
# logger.info(f'd = {d}')
c = d*csc(cfg['mo1_bragg'])-f*cot(cfg['mo1_bragg'])
# logger.info(f'c = {c}')
f = bl.mo1.rotOffset # rotation offset, mm
d = bl.mo1.heightOffset # xtal height offset, mm
c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"])
# Calculate height of center of rotation
b = np.sqrt(d**2*csc(cfg['mo1_bragg'])**2-2*d*f*cot(cfg['mo1_bragg'])*csc(cfg['mo1_bragg'])+f**2*cot(cfg['mo1_bragg'])**2+f**2)
# logger.info(f'b = {b}')
h = np.cos(np.pi/2-np.arctan(f/c)-cfg['mo1_bragg']-2*cfg['cm_pitch'])*b
# logger.info(f'h = {h}')
h2 = ((bl.mo1.center[1] - bl.cm.center[1])-np.sqrt(b**2-h**2))*np.tan(2*cfg['cm_pitch'])
# logger.info(f'mo1 = {bl.mo1.center[1]}')
# logger.info(f'cm = {bl.cm.center[1]}')
# logger.info(f'pitch = {cfg["cm_pitch"]}')
# logger.info(f'h2 = {h2}')
#TODO Mono height not exactly the same as in raytracing
heightCCM1real = h + h2 # per design, the height should not change if the pitch of the CM is not changed!
# heightCCM1real = heightCCM1real - 30 # Zero position of stage is at 1430 mm from ground.
if cfg['mo1_mode'] == 'Monochromatic':
b = np.sqrt(
d**2 * csc(cfg["mo1_bragg"]) ** 2
- 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"])
+ f**2 * cot(cfg["mo1_bragg"]) ** 2
+ f**2
)
h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b
h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan(2 * cfg["cm_pitch"])
height_mo1_real = (
h + h2
) # per design, the height should not change if the pitch of the CM is not changed!
if cfg["mo1_mode"] == "Monochromatic":
pass
elif cfg['mo1_mode'] == 'Pinkbeam':
heightCCM1real = heightCCM1real - 13 # Move down to let beam pass between both crystal without touching copper cooler
elif cfg["mo1_mode"] == "Pinkbeam":
height_mo1_real = (
height_mo1_real - 13
) # Move down to let beam pass between both crystal without touching copper cooler
else:
raise Exception('Monochromator mode not supported')
pos['mo1_try'] = {'value': heightCCM1real}
raise ValueError("Monochromator mode not supported")
pos["mo1_try"] = {"value": height_mo1_real}
# TRX, Crystal selection
if cfg['mo1_mode'] == 'Monochromatic':
try:
xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters
if cfg["mo1_mode"] == "Monochromatic":
xtal = cfg["mo1_xtal"].translate(
str.maketrans("", "", "()")
) # Remove brackets from xtal name to conform with parameters
if xtal in bl.mo1.xtal:
index = bl.mo1.xtal.index(xtal)
except:
else:
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
pos['mo1_trx'] = {'value': bl.mo1.xtalOffsetX[index]}
pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]}
else:
pos['mo1_trx'] = {'value': 0}
pos["mo1_trx"] = {"value": 0}
#TODO move to mono, calc for beam Z-movement between crystal surfaces
diag = bl.mo1.xtalGap[0] / np.sin(cfg['mo1_bragg']) # Calculations for Mono
dz = diag * np.cos(2 * (cfg['cm_pitch'] + cfg['mo1_bragg']))
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
## Slits 1
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
sl1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM
pos['sl1_centery'] = {'value': sl1_beam_height}
pos['sl1_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam
sl1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
pos["sl1_centery"] = {"value": sl1_beam_height}
pos["sl1_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 1
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
# logger.info(f'distance: {d}')
# logger.info(f'cm pitch: {cfg["cm_pitch"]}')
# logger.info(f'mono offset: {beamOffsetCCM}')
bm1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM
pos['bm1_try'] = {'value': bm1_beam_height}
bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
pos["bm1_try"] = {"value": bm1_beam_height}
## Focusing Mirror
p = bl.fm.center[1]
q = cfg['smpl'] - bl.fm.center[1]
f = (p*q)/(p+q) # focal length
q = cfg["smpl"] - bl.fm.center[1]
f = (p * q) / (p + q) # focal length
# Bender radius
if cfg['fm_qy'] is None:
radius = 2 * q / np.sin(cfg['fm_rotx']) # ideal bending radius for focused beam
if cfg["fm_qy"] is None:
radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam
else:
radius = 2 * cfg['fm_qy'] / np.sin(cfg['fm_rotx']) # ideal bending radius for unfocused beam
pos['fm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km
radius = (
2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"])
) # ideal bending radius for unfocused beam
pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
# Pitch
d = bl.fm.center[1] - bl.cm.center[1] - dz
fm_rotx = 2 * cfg['cm_pitch'] - cfg['fm_rotx'] # calculate pitch in absolute values (according to horizontal plane)
pos['fm_rotx'] = {'value': -fm_rotx * 1e3} # invert and convert to mrad (same as EGU of rotx axis)
fm_rotx = (
2 * cfg["cm_pitch"] - cfg["fm_rotx"]
) # calculate pitch in absolute values (according to horizontal plane)
pos["fm_rotx"] = {
"value": -fm_rotx * 1e3
} # invert and convert to mrad (same as EGU of rotx axis)
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
# TRY
if cfg['fm_stripe'] in 'Rh (toroid)':
if cfg["fm_stripe"] in "Rh (toroid)":
r = bl.fm.r[0]
h_cyl = bl.fm.hToroid[0]
else: # PT toroid
else: # PT toroid
r = bl.fm.r[1]
h_cyl = bl.fm.hToroid[1]
widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc'] * 1e-3)
alpha = np.arccos(1 - widthBeam**2 / (2 * r**2))
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3)
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
h = r - (r * np.cos(alpha / 2))
fm_beam_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height']
fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - h_cyl + h / 2) * cfg['fm_gain_height']
pos['fm_try'] = {'value': fm_height}
fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[
"fm_gain_height"
]
pos["fm_try"] = {"value": fm_height}
# TRX
if cfg['fm_stripe'] in 'Rh (toroid)':
x_cyl = - bl.fm.xToroid[0]
if cfg["fm_stripe"] in "Rh (toroid)":
x_cyl = -bl.fm.xToroid[0]
else:
x_cyl = - bl.fm.xToroid[1]
pos['fm_trx'] = {'value': x_cyl}
x_cyl = -bl.fm.xToroid[1]
pos["fm_trx"] = {"value": x_cyl}
elif cfg['fm_stripe'] in ('Rh (flat)', 'Pt (flat)'):
elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"):
# TRY
fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height']
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
fm_beam_height = fm_height
pos['fm_try'] = {'value': fm_height}
pos["fm_try"] = {"value": fm_height}
# TRX
if cfg['fm_stripe'] in 'Rh (flat)':
x_flat = - bl.fm.xFlat[0]
if cfg["fm_stripe"] in "Rh (flat)":
x_flat = -bl.fm.xFlat[0]
else:
x_flat = - bl.fm.xFlat[1]
pos['fm_trx'] = {'value': x_flat}
x_flat = -bl.fm.xFlat[1]
pos["fm_trx"] = {"value": x_flat}
else:
raise Exception('FM Stripe selection not valid')
raise ValueError("FM Stripe selection not valid")
pos['fm_roty'] = {'value': 0}
pos['fm_rotz'] = {'value': 0}
pos["fm_roty"] = {"value": 0}
pos["fm_rotz"] = {"value": 0}
## Slits 2
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
pos['sl2_centery'] = {'value': sl2_beam_height}
pos['sl2_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["sl2_centery"] = {"value": sl2_beam_height}
pos["sl2_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 2
d = bl.opBM2.center[1] - bl.fm.center[1]
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
pos['bm2_try'] = {'value': bm2_beam_height}
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["bm2_try"] = {"value": bm2_beam_height}
## Optical Table
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
ot_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
# logger.info(fm_height)
# logger.info(d * np.tan((2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])))
pos['ot_try'] = {'value': ot_height}
ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["ot_try"] = {"value": ot_height}
# Pitch
ot_pitch = - (2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])
pos['ot_rotx'] = {'value': ot_pitch * 1e3}
ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
# TRZ ES1
ot_es1_trz = cfg['smpl']
pos['ot_es1_trz'] = {'value': ot_es1_trz}
ot_es1_trz = cfg["smpl"]
pos["ot_es1_trz"] = {"value": ot_es1_trz}
# ES0 exit window
pos['es0wi_try'] = {'value': 5} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
pos["es0wi_try"] = {
"value": 5
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
return pos
@@ -1,15 +1,23 @@
"""
Calculates the sideview coordinates based on a beamline config.
"""
import numpy as np
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, DataDict
def calc_sideview(cfg):
def calc_sideview(cfg: ConfigDict) -> DataDict:
"""
Calculates the sideview coordinates based on a beamline config.
# Calculate height of beam after CM
# height = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"])
Args:
cfg(ConfigDict): Dictionary with beamline config
# beam height (Y=height, Z=along beam)
Returns:
DataDict: Sideview data
"""
beam: DataDict = {"x": [], "y": []}
@@ -59,11 +67,4 @@ def calc_sideview(cfg):
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
)
dy_fm_ex = beam["y"][-1] - beam["y"][-2]
dz_fm_ex = beam["x"][-1] - beam["x"][-2]
dz_fm_win = bl.ehWindow.center[1] - beam["x"][-2]
h_at_win = beam["y"][-2] + dy_fm_ex / dz_fm_ex * dz_fm_win
# beam["heightWindow"] = h_at_win
return beam
@@ -1,17 +1,28 @@
import os
"""
Calculates the surface coordinates based on a beamline config.
"""
import re
import numpy as np
from bec_lib import bec_logger
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, SurfaceDict
logger = bec_logger.logger
os.environ["USE_XRT"] = "False"
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import SurfaceDict
def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
"""
Calculates the surface coordinates based on a beamline config.
def calc_surfaces(cfg):
Args:
cfg(ConfigDict): Dictionary with beamline config
Returns:
SurfaceDict: Surface data
"""
out: SurfaceDict = {
"cm": {"x": [], "y": []},
@@ -45,39 +56,39 @@ def calc_surfaces(cfg):
) # Remove brackets from xtal name to conform with parameters
index = bl.mo1.xtal.index(xtal)
xtalPos = bl.mo1.xtalOffsetX[index]
xtalLength1 = bl.mo1.xtalLength1[index]
xtalLength2 = bl.mo1.xtalLength2[index]
xtal_pos = bl.mo1.xtalOffsetX[index]
xtal_length_1 = bl.mo1.xtalLength1[index]
xtal_length_2 = bl.mo1.xtalLength2[index]
widthBeam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"])
width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"])
heightBeam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"])
w = heightBeam / np.sin(cfg["mo1_bragg"])
height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"])
w = height_beam / np.sin(cfg["mo1_bragg"])
if cfg["mo1_mode"] in "Monochromatic":
out["mo1_1"]["x"] = [
xtalPos - widthBeam / 2,
xtalPos + widthBeam / 2,
xtalPos + widthBeam / 2,
xtalPos - widthBeam / 2,
xtal_pos - width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos - width_beam / 2,
]
out["mo1_1"]["y"] = [
xtalLength1 / 2 - c - w / 2,
xtalLength1 / 2 - c - w / 2,
xtalLength1 / 2 - c + w / 2,
xtalLength1 / 2 - c + w / 2,
xtal_length_1 / 2 - c - w / 2,
xtal_length_1 / 2 - c - w / 2,
xtal_length_1 / 2 - c + w / 2,
xtal_length_1 / 2 - c + w / 2,
]
out["mo1_2"]["x"] = [
xtalPos - widthBeam / 2,
xtalPos + widthBeam / 2,
xtalPos + widthBeam / 2,
xtalPos - widthBeam / 2,
xtal_pos - width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos - width_beam / 2,
]
out["mo1_2"]["y"] = [
-xtalLength2 / 2 + e - w / 2,
-xtalLength2 / 2 + e - w / 2,
-xtalLength2 / 2 + e + w / 2,
-xtalLength2 / 2 + e + w / 2,
-xtal_length_2 / 2 + e - w / 2,
-xtal_length_2 / 2 + e - w / 2,
-xtal_length_2 / 2 + e + w / 2,
-xtal_length_2 / 2 + e + w / 2,
]
else: # Pinkbeam
out["mo1_1"]["x"] = []
@@ -98,50 +109,44 @@ def calc_surfaces(cfg):
r = bl.fm.r[index]
off = -cfg["fm_trx"]
widthBeam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"])
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"])
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
l = heightBeam / np.sin(cfg["fm_rotx"])
alpha = np.arccos(1 - widthBeam**2 / (2 * r**2))
l = height_beam / np.sin(cfg["fm_rotx"])
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
h = r - (r * np.cos(alpha / 2))
z = h / np.tan(cfg["fm_rotx"])
x = [off - widthBeam / 2, off - widthBeam / 2]
x = [off - width_beam / 2, off - width_beam / 2]
y = [l / 2 - z / 2, -l / 2 - z / 2]
# logger.info(f'stripe: {cfg["fm_stripe"]}')
# logger.info(f'fm_rotx: {cfg["fm_rotx"]}')
# logger.info(f'h: {h}')
# logger.info(f'z: {z}')
# logger.info(f'r: {r}')
res = 20
xElipse = np.linspace(0, np.pi, res)
yElipse = np.linspace(0, np.pi, res)
xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse]
yElipse = [widthBeam * np.sin(i) * z / widthBeam - l / 2 - z / 2 for i in yElipse]
x_elipse = np.linspace(0, np.pi, res)
y_elipse = np.linspace(0, np.pi, res)
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse]
x.extend(xElipse)
y.extend(yElipse)
x.extend(x_elipse)
y.extend(y_elipse)
x.extend([off + widthBeam / 2, off + widthBeam / 2])
x.extend([off + width_beam / 2, off + width_beam / 2])
y.extend([-l / 2 - z / 2, l / 2 - z / 2])
res = 50
xElipse = np.linspace(np.pi, 0, res)
yElipse = np.linspace(np.pi, 0, res)
xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse]
yElipse = [widthBeam * np.sin(i) * z / widthBeam + l / 2 - z / 2 for i in yElipse]
x_elipse = np.linspace(np.pi, 0, res)
y_elipse = np.linspace(np.pi, 0, res)
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse]
x.extend(xElipse)
y.extend(yElipse)
x.extend(x_elipse)
y.extend(y_elipse)
out["fm"]["x"] = x
out["fm"]["y"] = y
else: # flat surface, no toroid
l = heightBeam / np.sin(cfg["fm_rotx"])
l = height_beam / np.sin(cfg["fm_rotx"])
w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"])
w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"])
@@ -1,4 +1,9 @@
"""
Various calculations for the digital twin
"""
import re
from typing import Literal, cast
import numpy as np
from bec_lib import bec_logger
@@ -9,19 +14,41 @@ import debye_bec.bec_widgets.widgets.x01da_parameters as bl
logger = bec_logger.logger
H = 6.62606957e-34
E = 1.602176634e-19
C = 299792458
RE = 2.8179e-15
def sldi_gap_to_acc(sldi_gapx, sldi_gapy):
def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]:
"""
Calculate the slits acceptance based on the gap values
Args:
sldi_gapx(float): GAPX value of the slits in mm
sldi_gapy(float): GAPY value of the slits in mm
Returns:
tuple[float, float]: Horizontal and vertical acceptance in rad
"""
d1 = bl.feSlits.center1[1]
d2 = bl.feSlits.center2[1]
h_acc = np.tan(sldi_gapx / (d2 + d1))
v_acc = np.tan(sldi_gapy / (d2 + d1))
# h_acc = np.tan(sldi_gapx / (2 * d1))
# v_acc = np.tan(sldi_gapy / (2 * d1))
return h_acc, v_acc
def cm_trx_to_stripe(cm_trx):
def cm_trx_to_stripe(cm_trx: float) -> str | None:
"""
Based on the trx value of the collimating mirror, return
the correct stripe
Args:
cm_trx(float): Collimating mirror trx value
Returns
str | None: Stripe of the mirror, None if not found
"""
cm_stripe = None
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
if low <= cm_trx <= high:
@@ -29,14 +56,34 @@ def cm_trx_to_stripe(cm_trx):
return cm_stripe
def cm_stripe_to_trx(cm_stripe):
def cm_stripe_to_trx(cm_stripe: str) -> float | None:
"""
Based on the stripe of the collimating mirror, return
the trx value
Args:
cm_stripe(str): Stripe of the collimating mirror
Returns:
float | None: TRX value of the stripe. None if not found
"""
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
if cm_stripe == name:
return -(low + high) / 2
return 0
return None
def fm_trx_to_stripe(fm_trx):
def fm_trx_to_stripe(fm_trx: float) -> str | None:
"""
Based on the trx value of the focusing mirror, return
the correct stripe
Args:
fm_trx(float): focusing mirror trx value
Returns
str | None: Stripe of the mirror, None if not found
"""
fm_stripe = None
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if low <= fm_trx <= high:
@@ -47,17 +94,37 @@ def fm_trx_to_stripe(fm_trx):
return fm_stripe
def fm_stripe_to_trx(fm_stripe):
def fm_stripe_to_trx(fm_stripe: str) -> float | None:
"""
Based on the stripe of the focusing mirror, return
the trx value
Args:
fm_stripe(str): Stripe of the focusing mirror
Returns:
float | None: TRX value of the stripe. None if not found
"""
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if fm_stripe == name + " (flat)":
return (low + high) / 2
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
if fm_stripe == name + " (toroid)":
return -(low + high) / 2
return 0
return None
def mo1_energy_resolution(xtal, energy):
def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float:
"""
Calculate the energy resolution of the monochromator
Args:
xtal(str): Xtal name. "Si111" or "Si311"
energy(float): Energy in eV
Returns:
float: Energy resolution in eV
"""
index = bl.mo1.xtal.index(xtal)
crystal = bl.mo1.material1[index]
@@ -69,29 +136,54 @@ def mo1_energy_resolution(xtal, energy):
# FWHM of the DCM curve
spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0)
r1, r2 = spline.roots()
roots = cast(np.ndarray, spline.roots())
r1, r2 = float(roots[0]), float(roots[1])
fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad
# Energy resolution
theta_B = crystal.get_Bragg_angle(energy)
dE_over_E = fwhm_rad / np.tan(theta_B)
dE = dE_over_E * energy
theta_b = crystal.get_Bragg_angle(energy)
de_over_e = fwhm_rad / np.tan(theta_b)
de = de_over_e * energy
# logger.info(f"DCM FWHM : {r2-r1:.2f} µrad")
# logger.info(f"ΔE/E : {dE_over_E:.2e}")
# logger.info(f"ΔE : {dE:.3f} eV at {E} eV")
return dE
return de
def cm_reflectivity(cm_stripe, cm_pitch, energy):
def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float:
"""
Calculate the reflectivity of the mirror stripe based
on the pitch and energy.
Args:
cm_stripe(str): Mirror stripe
cm_pitch(float): Pitch of the mirror (beam incidence angle)
energy(float): Energy of the beam in eV
Returns:
float: Reflectivity [0-1]
"""
index = bl.cm.surface.index(cm_stripe)
rs, rp = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
rs, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
refl = abs(rs) ** 2
return refl
def fm_reflectivity(fm_stripe, fm_pitch, energy):
def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float:
"""
Calculate the reflectivity of the mirror stripe based
on the pitch and energy.
Args:
cm_stripe(str): Mirror stripe
cm_pitch(float): Pitch of the mirror (beam incidence angle)
energy(float): Energy of the beam in eV
Returns:
float: Reflectivity [0-1]
"""
if fm_stripe in ("Rh (toroid)", "Pt (toroid)"):
surface = bl.fm.surfaceToroid
material = bl.fm.materialToroid
@@ -102,35 +194,75 @@ def fm_reflectivity(fm_stripe, fm_pitch, energy):
material = bl.fm.materialFlat
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
index = surface.index(stripe)
rs, rp = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2]
rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2]
refl = abs(rs) ** 2
return refl
def mo1_bragg_angle(mo_mode, d_spacing, energy, cm_pitch):
H = 6.62606957e-34
E = 1.602176634e-19
C = 299792458
def mo1_bragg_angle(
mo_mode: Literal["Monochromatic", "Pinkbeam"], d_spacing: float, energy: float, cm_pitch: float
) -> tuple[float, float]:
"""
Calculate the bragg angle of the monochromator.
Corrects for the collimating mirror pitch.
Args:
mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam"
d_spacing(float): D-spacing of the crystal in Angstrom
energy(float): Energy of the beam in eV
cm_pitch(float): Pitch of collimating mirror in rad
Returns:
tuple[float, float]: Bragg angle and corrected bragg angle
"""
wl = C * H / (E * energy)
val = wl / (2 * d_spacing * 1e-10)
bragg_angle = 0
if val > -1 and val < 1:
bragg_angle = np.asin(val)
if mo_mode in "Monochromatic":
if mo_mode == "Monochromatic":
# Add 2x CM pitch to the bragg angle
bragg_angle_cor = (2 * cm_pitch) + bragg_angle
elif mo_mode in "Pinkbeam":
else:
# Align xtal surfaces parallel to beam
bragg_angle_cor = 2 * cm_pitch
return bragg_angle, bragg_angle_cor
def fm_ideal_pitch(
fm_focus, fm_stripe, smpl, sldi_hacc=None, sldi_vacc=None, fm_focx=None, fm_focy=None
):
fm_focus: Literal["Defocused", "Focused", "Manual"],
fm_stripe: str,
smpl: float,
sldi_hacc: float | None = None,
sldi_vacc: float | None = None,
fm_focx: float | None = None,
fm_focy: float | None = None,
) -> tuple[float, float | None]:
"""
Calculates the ideal pitch for the focusing mirror depending on the
focusing strategy.
If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy
must be provided.
Args:
fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual
fm_stripe(str): Mirror stripe
smpl(float): Sample position in mm from source
sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None
sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None
fm_focx(float): Requested horizontal spot size in mm. Defaults to None
fm_focy(float): Requested vertical spot size in mm. Defaults to None
Returns:
tuple[float, float | None]: Pitch of mirror in rad, qy in mm
"""
p = bl.fm.center[1] # posFM
q = smpl - bl.fm.center[1] # dist posFM to posEX
if fm_focus in "Defocused":
assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode"
assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode"
assert fm_focx is not None, "fm_focx must be provided for Defocused mode"
assert fm_focy is not None, "fm_focy must be provided for Defocused mode"
a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror
b = (
2 * np.tan(sldi_vacc) * bl.cm.center[1]
@@ -151,45 +283,77 @@ def fm_ideal_pitch(
return pitch, qy
def cm_critical_angle(cm_stripe, energy):
def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float:
"""
Calculate the critical angle of the mirror stripe
Args:
cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh"
energy(float): Energy in eV
Returns:
float: Critical angle in rad
"""
if cm_stripe in "Si":
stripe = bl.stripeSi
elif cm_stripe in "Pt":
stripe = bl.stripePt
elif cm_stripe in "Rh":
stripe = bl.stripeRh
else:
raise Exception(f"Stripe {stripe} not found in beamline parameters!")
stripe = bl.stripeRh
w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m]
# Calculate critical angle for mirror
f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy))
numberDensity = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3)
criticalAngle = np.sqrt(numberDensity * 2.8179e-15 * w**2 * f1 / np.pi)
return criticalAngle
number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3)
critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi)
return critical_angle
def mirror_surface_geometries(mirror):
def mirror_surface_geometries(
mirror: Literal["cm", "fm_toroid", "fm_flat"],
) -> dict[str, tuple[float, float, float, float]]:
"""
Return the mirror stripe geometries
Args:
mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat"
Returns:
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
names to tuples of (x, y, width, height).
"""
if mirror in "cm":
surface = bl.cm.surface
limOptX = bl.cm.limOptX
limOptY = bl.cm.limOptY
lim_opt_x = bl.cm.limOptX
lim_opt_y = bl.cm.limOptY
elif mirror in "fm_toroid":
surface = bl.fm.surfaceToroid
limOptX = bl.fm.limOptXToroid
limOptY = bl.fm.limOptYToroid
lim_opt_x = bl.fm.limOptXToroid
lim_opt_y = bl.fm.limOptYToroid
elif mirror in "fm_flat":
surface = bl.fm.surfaceFlat
limOptX = bl.fm.limOptXFlat
limOptY = bl.fm.limOptYFlat
lim_opt_x = bl.fm.limOptXFlat
lim_opt_y = bl.fm.limOptYFlat
else:
raise ValueError(f"Requested mirror {mirror} not available!")
geom = {}
for sf, lx, hx, ly, hy in zip(surface, limOptX[0], limOptX[1], limOptY[0], limOptY[1]):
for sf, lx, hx, ly, hy in zip(surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1]):
geom[sf] = (lx, ly, hx - lx, hy - ly)
return geom
def mo_surface_geometries(mo, plane):
def mo_surface_geometries(
mo: Literal["mo1"], plane: Literal[0, 1]
) -> dict[str, tuple[float, float, float, float]]:
"""
Return the monochromator xtal geometries
Args:
mo(str): Monochromator. Only "mo1" implemented
plane(int): Surface of xtal. 0 and 1 (First and second)
Returns:
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
names to tuples of (x, y, width, height).
"""
if mo in "mo1":
xtal = bl.mo1.xtal
xtal_width = bl.mo1.xtalWidth
@@ -199,14 +363,20 @@ def mo_surface_geometries(mo, plane):
else:
xtal_length = bl.mo1.xtalLength2
else:
raise ValueError(f"Requested mono {mo} not available!")
return {}
geom = {}
for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length):
geom[sf] = (offx - w / 2, -length / 2, w, length)
return geom
def wall_geometries():
def wall_geometries() -> list[list[float]]:
"""
Return the wall geometries
Returns:
list[list[float]]: List of [x, y, width, height] geometry values for each wall.
"""
geom = []
for i, _ in enumerate(bl.walls.start):
geom.append(
@@ -220,7 +390,15 @@ def wall_geometries():
return geom
def pipe_geometries():
def pipe_geometries() -> list[dict[str, np.ndarray]]:
"""
Return the wall geometries
Returns:
list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y",
each containing a numpy array of two float values representing
the start and end coordinates of the pipe top and bottom edges.
"""
pipes = []
for i, _ in enumerate(bl.vacuum_pipes.center):
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
@@ -4,7 +4,7 @@ Digital Twin: Custom BEC widget to support the beamline alignment.
import sys
from pathlib import Path
from typing import Literal
from typing import Literal, cast
import numpy as np
import yaml
@@ -48,6 +48,7 @@ from debye_bec.bec_widgets.widgets.digital_twin.input_panel import InputPanel
from debye_bec.bec_widgets.widgets.digital_twin.mover_panel import MoverPanel
from debye_bec.bec_widgets.widgets.digital_twin.plots import SideviewPlot, SurfacePlots
from debye_bec.bec_widgets.widgets.digital_twin.settings_panel import SettingsPanel
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
logger = bec_logger.logger
@@ -62,7 +63,7 @@ class DigitalTwin(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "lightbulb"
def __init__(self, parent=None, *arg, **kwargs):
def __init__(self, *arg, parent=None, **kwargs):
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
self.get_bec_shortcuts()
@@ -115,15 +116,15 @@ class DigitalTwin(BECWidget, QWidget):
self.settings.reload_offsets.clicked_connect(self.load_offsets)
self.settings.unload_offsets.clicked_connect(self.unload_offsets)
self.bragg_angle = 0
self.qy = 0
self.bragg_angle = 0.0
self.qy = 0.0
self.offsets = {}
# Initialize all values
self.load_offsets(recalculate=False)
self.calc_assistant(identifier="init")
# Timer: update plot every 1 second
# Timer: update plots every 1 second
self._timer = QTimer(self)
self._timer.setInterval(100)
self._timer.timeout.connect(self.calc_reality)
@@ -142,6 +143,12 @@ class DigitalTwin(BECWidget, QWidget):
@SafeSlot()
def check_config(self, *args):
"""
Checks the BEC config and opens a window if not all necessary
devices are loaded in the config. If called from a slot from
BEC dispatcher whenever there is a config update, stop the timer
that updates the plot in the background.
"""
reload = (args[0] if args else {}).get("action") == "reload"
if reload:
self._timer.stop()
@@ -221,14 +228,21 @@ class DigitalTwin(BECWidget, QWidget):
dialog.show()
info.setMinimumHeight(info.heightForWidth(info.width()))
if dialog.exec_() == QDialog.DialogCode.Rejected:
app = QApplication.instance()
if app is not None:
app.exit(0)
running_app = QApplication.instance()
if running_app is not None:
running_app.exit(0)
if reload:
self._timer.start()
@SafeSlot()
def calc_assistant(self, *args, **kwargs):
def calc_assistant(self, *_, **kwargs):
"""
Calculates various values for the assistant.
If called from a qt slot, the identifier represents
the button pressed / value changed. Based on the identifier,
calculate different values.
Note: identifier=init calculates all values
"""
identifier = kwargs["identifier"]
match identifier:
case "init":
@@ -281,6 +295,13 @@ class DigitalTwin(BECWidget, QWidget):
self.calc_assistant_surfaces()
def get_assistant_config(self, apply_offset: bool = False):
"""
Assembles the digital twin config from the assistants input.
Args:
apply_offset(bool): Applies the offset values to the config.
Defaults to False
"""
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
fm_rotx = self.input.fm_rotx.value()
@@ -297,7 +318,10 @@ class DigitalTwin(BECWidget, QWidget):
fm_stripe = self.input.fm_stripe.currentText()
fm_trx = fm_stripe_to_trx(fm_stripe)
config = {
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
config: ConfigDict = {
"energy": self.input.energy.value(),
"h_acc": self.input.sldi_hacc.value(),
"v_acc": self.input.sldi_vacc.value(),
@@ -320,16 +344,10 @@ class DigitalTwin(BECWidget, QWidget):
for axis, _ in config.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
logger.info(f"Axis: {axis}")
if "modifier" in axis_offsets and "offset" in axis_offsets:
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
logger.info(f"rng: {rng}")
logger.info(f'value: {config[axis_offsets["modifier"]["axis"]]}')
if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]:
logger.info(f'offset: {axis_offsets["offset"][idx]}')
# logger.info(f"axis_data before: {axis_data}")
config[axis] += axis_offsets["offset"][idx]
# logger.info(f"axis_data after: {axis_data}")
break
elif "offset" in axis_offsets:
config[axis] += axis_offsets["offset"]
@@ -344,6 +362,9 @@ class DigitalTwin(BECWidget, QWidget):
return config
def get_reality_config(self):
"""
Assembles the digital twin config based on the real axis positions.
"""
mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
if abs(mo1_trx) > 5:
mo1_mode = "Monochromatic"
@@ -361,7 +382,7 @@ class DigitalTwin(BECWidget, QWidget):
fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
fm_rotx_real = 2 * cm_pitch - fm_rotx
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
config = { # Config in SI units!
raw = { # Config in SI units!
"energy": mo1_bragg["mo1_bragg"]["value"],
"h_acc": h_acc,
"v_acc": v_acc,
@@ -374,9 +395,11 @@ class DigitalTwin(BECWidget, QWidget):
"fm_rotx": -fm_rotx_real * 1e-3,
"fm_stripe": fm_stripe,
"fm_trx": fm_trx,
"fm_qy": None,
"fm_gain_height": 1,
"smpl": smpl,
}
config = cast(ConfigDict, raw)
# logger.info(f'Config created: {config}')
abs_open = self.dev.abs.read(cached=True)["abs_status_string"]["value"] == "OPEN"
@@ -430,7 +453,12 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.abs.set_feedback(abs_open)
return config
def adapt_reality(self, *args):
@SafeSlot()
def adapt_reality(self, *_):
"""
Based on the real axis positions, adjust the assistant to reflect
the reality.
"""
pos = {}
pos["sldi_gapx"] = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"]
pos["sldi_gapy"] = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"]
@@ -474,7 +502,15 @@ class DigitalTwin(BECWidget, QWidget):
self.input.smpl.set_number(pos["ot_es1_trz"])
self.calc_assistant(identifier="init")
def load_offsets(self, recalculate=True, *args):
@SafeSlot()
def load_offsets(self, *_, recalculate: bool = True):
"""
Loads the offsets from the file
Args:
recalculate(bool): Recalculates the assistant values.
Defaults to True
"""
file = Path(OFFSET_FILE)
if not file.exists():
raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}")
@@ -490,11 +526,19 @@ class DigitalTwin(BECWidget, QWidget):
if recalculate:
self.calc_assistant(identifier="init")
def unload_offsets(self, *args):
@SafeSlot()
def unload_offsets(self, *_):
"""
Removes the offsets and recalculates the assistant values.
"""
self.offsets = {}
self.calc_assistant(identifier="init")
def update_fm_mode(self):
"""
Updates the focusing mirror input group based on the
selection of the focus strategy.
"""
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
self.input.fm_rotx.setVisible(True)
@@ -515,22 +559,32 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_focy.setVisible(True)
self.input.fm_rotx_ideal.setLabel("Incidence Angle for defocused beam")
@SafeSlot()
def calc_reality(self):
"""
Updates the plots for the reality scene
"""
config = self.get_reality_config()
data = calc_sideview(config)
self.sideview_plot.update_curves("reality", data=data)
# logger.info('Calc reality surfaces')
surfaces = calc_surfaces(config)
self.surface_plots.update_surfaces(scene="reality", data=surfaces)
def calc_mo1_energy_resolution(self):
"""
Calculates the energy resolution of the monochromator
"""
xtal = self.input.mo1_xtal.currentText().translate(
str.maketrans("", "", "()")
) # Remove brackets from xtal name to conform with parameters
xtal = cast(Literal["Si111", "Si311"], xtal)
energy = self.input.energy.value()
self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy))
def calc_cm_reflectivity(self):
"""
Calculates the collimating mirror reflectivity
"""
cm_stripe = self.input.cm_stripe.currentText()
cm_pitch = -self.input.cm_pitch.value() * 1e-3
energy = self.input.energy.value()
@@ -540,6 +594,9 @@ class DigitalTwin(BECWidget, QWidget):
self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
def calc_fm_reflectivity(self):
"""
Calculates the focusing mirror reflectivity
"""
fm_stripe = self.input.fm_stripe.currentText()
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
@@ -553,6 +610,9 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
def calc_cm_fm_harm_suppr(self):
"""
Calculates the combined harmonics suppression of both mirrors
"""
harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (
self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value()
)
@@ -562,16 +622,24 @@ class DigitalTwin(BECWidget, QWidget):
)
def calc_assistant_sideview(self):
"""
Updates the sideview plot based on the assistant values
"""
config = self.get_assistant_config(apply_offset=True)
data = calc_sideview(config)
self.sideview_plot.update_curves("assistant", data)
def calc_assistant_surfaces(self):
# logger.info('Calc assistant surfaces')
"""
Updates the surface plot based on the assistant values
"""
surfaces = calc_surfaces(self.get_assistant_config())
self.surface_plots.update_surfaces(scene="assistant", data=surfaces)
def calc_positions(self):
"""
Calculates the positions for the axes based on the assistant values
"""
out = calc_positions(self.get_assistant_config())
# Apply offsets
@@ -628,13 +696,17 @@ class DigitalTwin(BECWidget, QWidget):
else:
raise ValueError(f"Invalid xtal selection: {xtal}")
cm_pitch = -self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] * 1e-3
mo1_mode = self.input.mo1_mode.currentText()
mo1_mode = cast(Literal["Monochromatic", "Pinkbeam"], self.input.mo1_mode.currentText())
energy = self.input.energy.value()
theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch)
self.bragg_angle = theta
self.input.mo1_bragg_angle.setValue(theta / np.pi * 180)
def update_mo1_mode(self):
"""
Updates the monochromator input group based on the
selection of the mode.
"""
if self.input.mo1_mode.currentText() in "Monochromatic":
self.input.mo1_xtal.setVisible(True)
self.input.mo1_bragg_angle.setVisible(True)
@@ -645,7 +717,12 @@ class DigitalTwin(BECWidget, QWidget):
self.input.mo1_eres.setVisible(False)
def calc_fm_ideal_pitch(self):
fm_focus = self.input.fm_focus.currentText()
"""
Calculate the ideal pitch for the focusing mirror.
"""
fm_focus = cast(
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
)
fm_stripe = self.input.fm_stripe.currentText()
smpl = self.input.smpl.value()
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
@@ -659,7 +736,10 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3)
def calc_cm_crit_pitch(self):
cm_stripe = self.input.cm_stripe.currentText()
"""
Calculate the critical pitch for the collimating mirror
"""
cm_stripe = cast(Literal["Si", "Pt", "Rh"], self.input.cm_stripe.currentText())
energy = self.input.energy.value()
self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3)
@@ -1,8 +1,47 @@
"""Types used for plotting data"""
"""Types used for the beamline config and for plotting data"""
from typing import TypedDict
class ConfigDict(TypedDict):
"""
Typed dictionary representing the beamline configuration.
Attributes:
energy (float): Beam energy.
h_acc (float): Horizontal acceptance.
v_acc (float): Vertical acceptance.
cm_pitch (float): CM pitch angle.
cm_stripe (str): CM stripe name.
cm_trx (float): CM translation x.
mo1_mode (str): MO1 mode.
mo1_xtal (str): MO1 crystal.
mo1_bragg (float): MO1 Bragg angle.
fm_rotx (float): FM rotation x.
fm_stripe (str): FM stripe name.
fm_trx (float): FM translation x.
fm_qy (float): FM qy value.
fm_gain_height (int): FM gain height.
smpl (float): Sample value.
"""
energy: float
h_acc: float
v_acc: float
cm_pitch: float
cm_stripe: str
cm_trx: float
mo1_mode: str
mo1_xtal: str
mo1_bragg: float
fm_rotx: float
fm_stripe: str
fm_trx: float
fm_qy: None | float
fm_gain_height: int
smpl: float
class DataDict(TypedDict):
"""
Typed dictionary representing plot data.