refactoring
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user