Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40309491b0 | |||
| 2f0265fff7 | |||
| 2d21eb90fe | |||
| 8ddf67e817 | |||
| 617cca71a5 | |||
| 7e20d46881 | |||
| 74ff173f98 | |||
| 87758710d9 | |||
| 8bc36ed6a2 | |||
| 78d58ad26f | |||
| 359ef0b6d7 | |||
| 262a0b6318 | |||
| 98d5c22667 | |||
| 0e77dd5679 | |||
| f8e5b5e073 | |||
| 70750d6aa1 | |||
| 94aca18a22 | |||
| ba82fd1715 | |||
| c998c06b41 | |||
| 8b8138ec05 | |||
| ef4c82262c | |||
| 5a54675f1e | |||
| 62582da1d9 | |||
| 6b5ff49b04 | |||
| bda7a688e1 | |||
| bee4562ab1 |
+1
-1
@@ -2,7 +2,7 @@
|
||||
# It is needed to track the repo template version, and editing may break things.
|
||||
# This file will be overwritten by copier on template updates.
|
||||
|
||||
_commit: v1.2.8
|
||||
_commit: v1.4.0
|
||||
_src_path: https://github.com/bec-project/plugin_copier_template.git
|
||||
make_commit: false
|
||||
project_name: debye_bec
|
||||
|
||||
@@ -17,16 +17,22 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
pip install copier PySide6
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Create virtualenv
|
||||
run: |
|
||||
python -m virtualenv .venv
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pip install copier PySide6 bec_lib
|
||||
|
||||
- name: Perform update
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
git config --global user.email "bec_ci_staging@psi.ch"
|
||||
git config --global user.name "BEC automated CI"
|
||||
|
||||
@@ -35,8 +41,10 @@ jobs:
|
||||
git checkout -b $branch
|
||||
|
||||
echo "Running copier update..."
|
||||
output="$(copier update --trust --defaults --conflict inline 2>&1)"
|
||||
echo "$output"
|
||||
copier update --trust --defaults --conflict inline 2>&1 | tee copier.log
|
||||
status=${PIPESTATUS[0]}
|
||||
output="$(cat copier.log)"
|
||||
echo $output
|
||||
msg="$(printf '%s\n' "$output" | head -n 1)"
|
||||
|
||||
if ! grep -q "make_commit: true" .copier-answers.yml ; then
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
import os
|
||||
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
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
def calc_positions(cfg):
|
||||
|
||||
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]
|
||||
|
||||
# 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}
|
||||
|
||||
## 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
|
||||
|
||||
# TRX
|
||||
try:
|
||||
index = bl.cm.surface.index(cfg['cm_stripe'])
|
||||
except:
|
||||
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}
|
||||
|
||||
# TRY
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
## 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':
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg = cfg['mo1_bragg']
|
||||
elif cfg['mo1_mode'] == 'Pinkbeam':
|
||||
# Align xtal surfaces parallel to beam
|
||||
bragg = 0
|
||||
else:
|
||||
raise Exception('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'])
|
||||
|
||||
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
|
||||
else:
|
||||
raise Exception('Monochromator mode not supported')
|
||||
|
||||
def csc(a):
|
||||
return 1/np.sin(a)
|
||||
|
||||
def cot(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}')
|
||||
|
||||
# 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':
|
||||
pass
|
||||
elif cfg['mo1_mode'] == 'Pinkbeam':
|
||||
heightCCM1real = heightCCM1real - 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}
|
||||
|
||||
# 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
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
except:
|
||||
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
|
||||
pos['mo1_trx'] = {'value': bl.mo1.xtalOffsetX[index]}
|
||||
else:
|
||||
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']))
|
||||
|
||||
## 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
|
||||
|
||||
## 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}
|
||||
|
||||
## Focusing Mirror
|
||||
p = bl.fm.center[1]
|
||||
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
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
|
||||
|
||||
# TRY
|
||||
if cfg['fm_stripe'] in 'Rh (toroid)':
|
||||
r = bl.fm.r[0]
|
||||
h_cyl = bl.fm.hToroid[0]
|
||||
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))
|
||||
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}
|
||||
|
||||
# TRX
|
||||
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}
|
||||
|
||||
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_beam_height = fm_height
|
||||
pos['fm_try'] = {'value': fm_height}
|
||||
|
||||
# TRX
|
||||
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}
|
||||
|
||||
else:
|
||||
raise Exception('FM Stripe selection not valid')
|
||||
|
||||
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
|
||||
|
||||
## 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}
|
||||
|
||||
## 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}
|
||||
|
||||
# Pitch
|
||||
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}
|
||||
|
||||
# ES0 exit window
|
||||
pos['es0wi_try'] = {'value': 5} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
|
||||
|
||||
return pos
|
||||
@@ -1,42 +0,0 @@
|
||||
import numpy as np
|
||||
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
|
||||
|
||||
def calc_sideview(cfg):
|
||||
|
||||
# Calculate height of beam after CM
|
||||
height = 2 * bl.cm.center[1] * np.tan(cfg['v_acc'])
|
||||
|
||||
# beam height (Y=height, Z=along beam)
|
||||
beam = {}
|
||||
beam['x'] = []
|
||||
beam['y'] = []
|
||||
beam['x'].append(0) # Source
|
||||
beam['y'].append(bl.sourceHeight)
|
||||
beam['x'].append(bl.cm.center[1]) # CM
|
||||
beam['y'].append(bl.sourceHeight)
|
||||
if cfg['mo1_mode'] in 'Monochromatic':
|
||||
diag = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg']) # Calculations for Mono
|
||||
dy = diag*np.sin(2*(cfg['cm_pitch']+cfg['mo1_bragg']))
|
||||
dz = diag*np.cos(2*(cfg['cm_pitch']+cfg['mo1_bragg']))
|
||||
beam['x'].append(bl.mo1.center[1]-dz/2) # Mono 1.1
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1]))
|
||||
beam['x'].append(bl.mo1.center[1]+dz/2) # Mono 1.2
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1])+dy)
|
||||
beam['x'].append(bl.fm.center[1]) # FM
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy)
|
||||
beam['x'].append(cfg['smpl']) # Experiment
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy+np.tan(2*(cfg['cm_pitch']-cfg['fm_rotx']))*(cfg['smpl']-bl.fm.center[1]))
|
||||
elif cfg['mo1_mode'] == 'Pinkbeam':
|
||||
beam['x'].append(bl.fm.center[1]) # FM
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]))
|
||||
beam['x'].append(cfg['smpl']) # Experiment
|
||||
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1])+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,131 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
os.environ["USE_XRT"] = "False"
|
||||
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
|
||||
|
||||
def calc_surfaces(cfg):
|
||||
|
||||
out = {
|
||||
'cm': {'x': [], 'y': []},
|
||||
'mo1_1': {'x': [], 'y': []},
|
||||
'mo1_2': {'x': [], 'y': []},
|
||||
'fm': {'x': [], 'y': []},
|
||||
}
|
||||
|
||||
# Collimating mirror
|
||||
l = 2 * bl.cm.center[1] * np.tan(cfg['v_acc'])/np.sin(cfg['cm_pitch'])
|
||||
|
||||
w1 = 2 * (bl.cm.center[1]-l/2) * np.tan(cfg['h_acc'])
|
||||
w2 = 2 * (bl.cm.center[1]+l/2) * np.tan(cfg['h_acc'])
|
||||
|
||||
index = bl.cm.surface.index(cfg['cm_stripe'])
|
||||
cen = (bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2
|
||||
|
||||
if cfg['cm_trx'] is not None:
|
||||
cen = cfg['cm_trx']
|
||||
|
||||
out['cm']['x'] = [cen-w1/2, cen-w2/2, cen+w2/2, cen+w1/2]
|
||||
out['cm']['y'] = [-l/2, l/2, l/2, -l/2]
|
||||
|
||||
|
||||
# Monochromator
|
||||
# calculate height of center of first crystal surface
|
||||
c = bl.mo1.heightOffset*1/np.sin(cfg['mo1_bragg'])-bl.mo1.rotOffset*1/np.tan(cfg['mo1_bragg'])
|
||||
e = bl.mo1.xtalGap[0]/np.tan(cfg['mo1_bragg'])-c
|
||||
|
||||
xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # 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]
|
||||
|
||||
widthBeam = 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'])
|
||||
|
||||
if cfg['mo1_mode'] in 'Monochromatic':
|
||||
out['mo1_1']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/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]
|
||||
out['mo1_2']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/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]
|
||||
else: # Pinkbeam
|
||||
out['mo1_1']['x'] = []
|
||||
out['mo1_1']['y'] = []
|
||||
out['mo1_2']['x'] = []
|
||||
out['mo1_2']['y'] = []
|
||||
|
||||
# Focusing mirror
|
||||
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
|
||||
surface = bl.fm.surfaceToroid
|
||||
stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip()
|
||||
index = surface.index(stripe)
|
||||
off = (bl.fm.limOptXToroid[0][index] + bl.fm.limOptXToroid[1][index]) / 2
|
||||
r = bl.fm.r[index]
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip()
|
||||
index = surface.index(stripe)
|
||||
off = (bl.fm.limOptXFlat[0][index] + bl.fm.limOptXFlat[1][index]) / 2
|
||||
r = bl.fm.r[index]
|
||||
if cfg['fm_trx'] is not None:
|
||||
off = cfg['fm_trx']
|
||||
|
||||
widthBeam = 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))
|
||||
h = r-(r*np.cos(alpha/2))
|
||||
z = h/np.tan(cfg['fm_rotx'])
|
||||
|
||||
x = [off-widthBeam/2, off-widthBeam/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.extend(xElipse)
|
||||
y.extend(yElipse)
|
||||
|
||||
x.extend([off+widthBeam/2, off+widthBeam/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.extend(xElipse)
|
||||
y.extend(yElipse)
|
||||
|
||||
out['fm']['x'] = x
|
||||
out['fm']['y'] = y
|
||||
|
||||
else: # flat surface, no toroid
|
||||
l = heightBeam/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'])
|
||||
|
||||
out['fm']['x'] = [off-w1/2, off+w1/2, off+w2/2, off-w2/2]
|
||||
out['fm']['y'] = [-l/2, -l/2, l/2, l/2]
|
||||
|
||||
return out
|
||||
@@ -1,206 +0,0 @@
|
||||
import re
|
||||
import numpy as np
|
||||
from scipy.interpolate import UnivariateSpline
|
||||
from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO
|
||||
from bec_lib import bec_logger
|
||||
|
||||
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
def sldi_gap_to_acc(sldi_gapx, sldi_gapy):
|
||||
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):
|
||||
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:
|
||||
cm_stripe = name
|
||||
return cm_stripe
|
||||
|
||||
def fm_trx_to_stripe(fm_trx):
|
||||
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:
|
||||
fm_stripe = name + ' (flat)'
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if low <= fm_trx <= high:
|
||||
fm_stripe = name + ' (toroid)'
|
||||
return fm_stripe
|
||||
|
||||
def mo1_energy_resolution(xtal, energy):
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
crystal = bl.mo1.material1[index]
|
||||
|
||||
dtheta = np.linspace(-30, 90, 601)
|
||||
theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6
|
||||
refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0])**2 # single crystal
|
||||
|
||||
refl2 = refl**2 # DCM with parallel crystals
|
||||
|
||||
# FWHM of the DCM curve
|
||||
spline = UnivariateSpline(dtheta, refl2 - refl2.max()/2, s=0)
|
||||
r1, r2 = spline.roots()
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
def cm_reflectivity(cm_stripe, cm_pitch, energy):
|
||||
index = bl.cm.surface.index(cm_stripe)
|
||||
rs, rp = 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):
|
||||
if fm_stripe in ('Rh (toroid)', 'Pt (toroid)'):
|
||||
surface = bl.fm.surfaceToroid
|
||||
material = bl.fm.materialToroid
|
||||
stripe = re.sub(r'\s*\(.*?\)', '', fm_stripe).strip()
|
||||
index = surface.index(stripe)
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
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]
|
||||
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
|
||||
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':
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg_angle_cor = ((2 * cm_pitch) + bragg_angle)
|
||||
elif mo_mode in 'Pinkbeam':
|
||||
# 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):
|
||||
p = bl.fm.center[1] # posFM
|
||||
q = smpl - bl.fm.center[1] # dist posFM to posEX
|
||||
if fm_focus in 'Defocused':
|
||||
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] # Beam height at focusing mirror (collimated beam)
|
||||
x = fm_focx
|
||||
y = fm_focy
|
||||
qx = q + x * p / a
|
||||
qy = q + y * p / b
|
||||
f = (p * qx) / (p + qx) # focal length
|
||||
else: # Calculate for focused beam on sample in "manual" and "focused" mode
|
||||
qy = None
|
||||
f = (p * q) / (p + q) # focal length
|
||||
pitch = 0
|
||||
if 'Rh' in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[0]/(2*f))# ideal pitch for FM
|
||||
if 'Pt' in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[1]/(2*f)) # ideal pitch for FM
|
||||
return pitch, qy
|
||||
|
||||
def cm_critical_angle(cm_stripe, energy):
|
||||
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!')
|
||||
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
|
||||
|
||||
|
||||
def mirror_surface_geometries(mirror):
|
||||
if mirror in "cm":
|
||||
surface = bl.cm.surface
|
||||
limOptX = bl.cm.limOptX
|
||||
limOptY = bl.cm.limOptY
|
||||
elif mirror in 'fm_toroid':
|
||||
surface = bl.fm.surfaceToroid
|
||||
limOptX = bl.fm.limOptXToroid
|
||||
limOptY = bl.fm.limOptYToroid
|
||||
elif mirror in 'fm_flat':
|
||||
surface = bl.fm.surfaceFlat
|
||||
limOptX = bl.fm.limOptXFlat
|
||||
limOptY = 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]):
|
||||
geom[sf] = (lx, ly, hx-lx, hy-ly)
|
||||
return geom
|
||||
|
||||
def mo_surface_geometries(mo, plane):
|
||||
if mo in 'mo1':
|
||||
xtal = bl.mo1.xtal
|
||||
xtal_width = bl.mo1.xtalWidth
|
||||
xtal_offset_x = bl.mo1.xtalOffsetX
|
||||
if plane == 0:
|
||||
xtal_length = bl.mo1.xtalLength1
|
||||
else:
|
||||
xtal_length = bl.mo1.xtalLength2
|
||||
else:
|
||||
raise ValueError(f'Requested mono {mo} not available!')
|
||||
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():
|
||||
geom = []
|
||||
for i, _ in enumerate(bl.walls.start):
|
||||
geom.append([
|
||||
bl.walls.start[i],
|
||||
bl.walls.height[i][0],
|
||||
bl.walls.end[i] - bl.walls.start[i],
|
||||
bl.walls.height[i][1] - bl.walls.height[i][0],
|
||||
])
|
||||
return geom
|
||||
|
||||
def pipe_geometries():
|
||||
pipes = []
|
||||
for i, _ in enumerate(bl.vacuum_pipes.center):
|
||||
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight
|
||||
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight
|
||||
pipes.append({
|
||||
'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
'y': np.array([top, top])
|
||||
})
|
||||
pipes.append({
|
||||
'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
'y': np.array([bottom, bottom])
|
||||
})
|
||||
return pipes
|
||||
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Calculates the positions of axes based on a beamline config
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
|
||||
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
xgap = trxw - trxr
|
||||
ygap = tryt - tryb
|
||||
|
||||
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
|
||||
|
||||
# TRX
|
||||
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}
|
||||
|
||||
# TRY
|
||||
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)
|
||||
|
||||
# Bending Radius
|
||||
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
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg = cfg["mo1_bragg"]
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
# Align xtal surfaces parallel to beam
|
||||
bragg = 0
|
||||
else:
|
||||
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.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"]))
|
||||
yver = yhor * np.tan(2.0 * cfg["cm_pitch"])
|
||||
|
||||
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 ValueError("Monochromator mode not supported")
|
||||
|
||||
def csc(a):
|
||||
return 1 / np.sin(a)
|
||||
|
||||
def cot(a):
|
||||
return 1 / np.tan(a)
|
||||
|
||||
# calculate height of center of first crystal surface
|
||||
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
|
||||
)
|
||||
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":
|
||||
height_mo1_real = (
|
||||
height_mo1_real - 13
|
||||
) # Move down to let beam pass between both crystal without touching copper cooler
|
||||
else:
|
||||
raise ValueError("Monochromator mode not supported")
|
||||
pos["mo1_try"] = {"value": height_mo1_real}
|
||||
|
||||
# TRX, Crystal selection
|
||||
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)
|
||||
else:
|
||||
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
|
||||
pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]}
|
||||
else:
|
||||
pos["mo1_trx"] = {"value": 0}
|
||||
|
||||
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"]) + 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
|
||||
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
|
||||
|
||||
# Bender radius
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
|
||||
# TRY
|
||||
if cfg["fm_stripe"] in "Rh (toroid)":
|
||||
r = bl.fm.r[0]
|
||||
h_cyl = bl.fm.hToroid[0]
|
||||
else: # PT toroid
|
||||
r = bl.fm.r[1]
|
||||
h_cyl = bl.fm.hToroid[1]
|
||||
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"]) + 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]
|
||||
else:
|
||||
x_cyl = -bl.fm.xToroid[1]
|
||||
pos["fm_trx"] = {"value": x_cyl}
|
||||
|
||||
elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"):
|
||||
|
||||
# TRY
|
||||
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}
|
||||
|
||||
# TRX
|
||||
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}
|
||||
|
||||
else:
|
||||
raise ValueError("FM Stripe selection not valid")
|
||||
|
||||
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
|
||||
|
||||
## 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}
|
||||
|
||||
## 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"]))
|
||||
pos["ot_try"] = {"value": ot_height}
|
||||
|
||||
# Pitch
|
||||
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}
|
||||
|
||||
# ES0 exit window
|
||||
pos["es0wi_try"] = {
|
||||
"value": 5
|
||||
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
|
||||
|
||||
return pos
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Calculates the sideview coordinates based on a beamline config.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, DataDict
|
||||
|
||||
|
||||
def calc_sideview(cfg: ConfigDict) -> DataDict:
|
||||
"""
|
||||
Calculates the sideview coordinates based on a beamline config.
|
||||
|
||||
Args:
|
||||
cfg(ConfigDict): Dictionary with beamline config
|
||||
|
||||
Returns:
|
||||
DataDict: Sideview data
|
||||
"""
|
||||
|
||||
beam: DataDict = {"x": [], "y": []}
|
||||
|
||||
beam["x"] = []
|
||||
beam["y"] = []
|
||||
beam["x"].append(0) # Source
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
beam["x"].append(bl.cm.center[1]) # CM
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
if cfg["mo1_mode"] in "Monochromatic":
|
||||
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
|
||||
dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
|
||||
)
|
||||
beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
|
||||
+ dy
|
||||
)
|
||||
beam["x"].append(bl.fm.center[1]) # FM
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
)
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
beam["x"].append(bl.fm.center[1]) # FM
|
||||
beam["y"].append(
|
||||
bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
)
|
||||
|
||||
return beam
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
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.digital_twin.x01da_parameters as bl
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, SurfaceDict
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
|
||||
"""
|
||||
Calculates the surface coordinates based on a beamline config.
|
||||
|
||||
Args:
|
||||
cfg(ConfigDict): Dictionary with beamline config
|
||||
|
||||
Returns:
|
||||
SurfaceDict: Surface data
|
||||
"""
|
||||
|
||||
out: SurfaceDict = {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
}
|
||||
|
||||
# Collimating mirror
|
||||
l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"])
|
||||
|
||||
w1 = 2 * (bl.cm.center[1] - l / 2) * np.tan(cfg["h_acc"])
|
||||
w2 = 2 * (bl.cm.center[1] + l / 2) * np.tan(cfg["h_acc"])
|
||||
|
||||
index = bl.cm.surface.index(cfg["cm_stripe"])
|
||||
|
||||
cen = -cfg["cm_trx"]
|
||||
|
||||
out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2]
|
||||
out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2]
|
||||
|
||||
# Monochromator
|
||||
# calculate height of center of first crystal surface
|
||||
c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan(
|
||||
cfg["mo1_bragg"]
|
||||
)
|
||||
e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c
|
||||
|
||||
xtal = cfg["mo1_xtal"].translate(
|
||||
str.maketrans("", "", "()")
|
||||
) # Remove brackets from xtal name to conform with parameters
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
|
||||
xtal_pos = bl.mo1.xtalOffsetX[index]
|
||||
xtal_length_1 = bl.mo1.xtalLength1[index]
|
||||
xtal_length_2 = bl.mo1.xtalLength2[index]
|
||||
|
||||
width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"])
|
||||
|
||||
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"] = [
|
||||
xtal_pos - width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos - width_beam / 2,
|
||||
]
|
||||
out["mo1_1"]["y"] = [
|
||||
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"] = [
|
||||
xtal_pos - width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos - width_beam / 2,
|
||||
]
|
||||
out["mo1_2"]["y"] = [
|
||||
-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"] = []
|
||||
out["mo1_1"]["y"] = []
|
||||
out["mo1_2"]["x"] = []
|
||||
out["mo1_2"]["y"] = []
|
||||
|
||||
# Focusing mirror
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
surface = bl.fm.surfaceToroid
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
|
||||
index = surface.index(stripe)
|
||||
r = bl.fm.r[index]
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
|
||||
index = surface.index(stripe)
|
||||
r = bl.fm.r[index]
|
||||
off = -cfg["fm_trx"]
|
||||
|
||||
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"])
|
||||
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
|
||||
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 - width_beam / 2, off - width_beam / 2]
|
||||
y = [l / 2 - z / 2, -l / 2 - z / 2]
|
||||
|
||||
res = 20
|
||||
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(x_elipse)
|
||||
y.extend(y_elipse)
|
||||
|
||||
x.extend([off + width_beam / 2, off + width_beam / 2])
|
||||
y.extend([-l / 2 - z / 2, l / 2 - z / 2])
|
||||
|
||||
res = 50
|
||||
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(x_elipse)
|
||||
y.extend(y_elipse)
|
||||
|
||||
out["fm"]["x"] = x
|
||||
out["fm"]["y"] = y
|
||||
|
||||
else: # flat surface, no toroid
|
||||
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"])
|
||||
|
||||
out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2]
|
||||
out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2]
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,418 @@
|
||||
"""
|
||||
Various calculations for the digital twin
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Literal, cast
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from scipy.interpolate import UnivariateSpline
|
||||
from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm
|
||||
|
||||
import debye_bec.bec_widgets.widgets.digital_twin.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: 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))
|
||||
return h_acc, v_acc
|
||||
|
||||
|
||||
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:
|
||||
cm_stripe = name
|
||||
return 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 None
|
||||
|
||||
|
||||
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:
|
||||
fm_stripe = name + " (flat)"
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if low <= fm_trx <= high:
|
||||
fm_stripe = name + " (toroid)"
|
||||
return 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 None
|
||||
|
||||
|
||||
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]
|
||||
|
||||
dtheta = np.linspace(-30, 90, 601)
|
||||
theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6
|
||||
refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal
|
||||
|
||||
refl2 = refl**2 # DCM with parallel crystals
|
||||
|
||||
# FWHM of the DCM curve
|
||||
spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0)
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
|
||||
refl = abs(rs) ** 2
|
||||
return refl
|
||||
|
||||
|
||||
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
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
|
||||
index = surface.index(stripe)
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
material = bl.fm.materialFlat
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
|
||||
index = surface.index(stripe)
|
||||
rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2]
|
||||
refl = abs(rs) ** 2
|
||||
return refl
|
||||
|
||||
|
||||
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 == "Monochromatic":
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg_angle_cor = (2 * cm_pitch) + bragg_angle
|
||||
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: 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]
|
||||
) # Beam height at focusing mirror (collimated beam)
|
||||
x = fm_focx
|
||||
y = fm_focy
|
||||
qx = q + x * p / a
|
||||
qy = q + y * p / b
|
||||
f = (p * qx) / (p + qx) # focal length
|
||||
else: # Calculate for focused beam on sample in "manual" and "focused" mode
|
||||
qy = None
|
||||
f = (p * q) / (p + q) # focal length
|
||||
pitch = 0
|
||||
if "Rh" in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM
|
||||
if "Pt" in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM
|
||||
return pitch, qy
|
||||
|
||||
|
||||
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
|
||||
else:
|
||||
stripe = bl.stripeRh
|
||||
w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m]
|
||||
f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy))
|
||||
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: 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
|
||||
lim_opt_x = bl.cm.limOptX
|
||||
lim_opt_y = bl.cm.limOptY
|
||||
elif mirror in "fm_toroid":
|
||||
surface = bl.fm.surfaceToroid
|
||||
lim_opt_x = bl.fm.limOptXToroid
|
||||
lim_opt_y = bl.fm.limOptYToroid
|
||||
elif mirror in "fm_flat":
|
||||
surface = bl.fm.surfaceFlat
|
||||
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, 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: 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
|
||||
xtal_offset_x = bl.mo1.xtalOffsetX
|
||||
if plane == 0:
|
||||
xtal_length = bl.mo1.xtalLength1
|
||||
else:
|
||||
xtal_length = bl.mo1.xtalLength2
|
||||
else:
|
||||
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() -> 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(
|
||||
[
|
||||
bl.walls.start[i],
|
||||
bl.walls.height[i][0],
|
||||
bl.walls.end[i] - bl.walls.start[i],
|
||||
bl.walls.height[i][1] - bl.walls.height[i][0],
|
||||
]
|
||||
)
|
||||
return geom
|
||||
|
||||
|
||||
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
|
||||
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
|
||||
pipes.append(
|
||||
{
|
||||
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
"y": np.array([top, top]),
|
||||
}
|
||||
)
|
||||
pipes.append(
|
||||
{
|
||||
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
"y": np.array([bottom, bottom]),
|
||||
}
|
||||
)
|
||||
return pipes
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Panel for user inputs of the digital twin widget
|
||||
"""
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
|
||||
Button,
|
||||
ComboBox,
|
||||
Group,
|
||||
InputNumberField,
|
||||
NumberIndicator,
|
||||
)
|
||||
|
||||
|
||||
class InputPanel(QWidget):
|
||||
"""Panel for user inputs of the digital twin widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
# Adapt to reality
|
||||
self.adapt_reality = Button(label_button="Adapt to reality", enabled=True)
|
||||
|
||||
# Energy
|
||||
self.energy = InputNumberField(
|
||||
"energy", "Energy", unit="eV", init=8979, decimals=0, single_step=100, ll=4000, hl=65000
|
||||
)
|
||||
|
||||
# FE Slits Acceptance
|
||||
self.sldi_hacc = InputNumberField(
|
||||
"h_acc",
|
||||
"Horizontal",
|
||||
unit="mrad",
|
||||
prefix="±",
|
||||
init=0.25,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-0.1,
|
||||
hl=0.9,
|
||||
)
|
||||
self.sldi_vacc = InputNumberField(
|
||||
"v_acc",
|
||||
"Vertical",
|
||||
unit="mrad",
|
||||
prefix="±",
|
||||
init=0.1,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-0.1,
|
||||
hl=0.5,
|
||||
)
|
||||
self.sldi_ass_group = Group("FE Slits Acceptance", [self.sldi_hacc, self.sldi_vacc])
|
||||
|
||||
# Collimating mirror
|
||||
self.cm_stripe = ComboBox("cm_stripe", "Stripe", ["Si", "Rh", "Pt"])
|
||||
self.cm_pitch = InputNumberField(
|
||||
"cm_pitch",
|
||||
"Pitch",
|
||||
unit="mrad",
|
||||
init=-2.391,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-4.6,
|
||||
hl=-1.2,
|
||||
)
|
||||
self.cm_pitch_critical = NumberIndicator("Critical Pitch", "mrad", decimals=3)
|
||||
self.cm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.cm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.cm_ass_group = Group(
|
||||
"Collimating Mirror",
|
||||
[
|
||||
self.cm_stripe,
|
||||
self.cm_pitch,
|
||||
self.cm_pitch_critical,
|
||||
self.cm_refl,
|
||||
self.cm_refl_harm,
|
||||
],
|
||||
)
|
||||
|
||||
# Monochromator
|
||||
self.mo1_mode = ComboBox("mo1_mode", "Mode", ["Monochromatic", "Pinkbeam"])
|
||||
self.mo1_xtal = ComboBox("mo1_xtal", "Crystal", ["Si(111)", "Si(311)"])
|
||||
self.mo1_bragg_angle = NumberIndicator("Bragg Angle", "deg", decimals=1)
|
||||
self.mo1_eres = NumberIndicator("Energy Resolution", "eV", decimals=2)
|
||||
self.mo1_ass_group = Group(
|
||||
"Monochromator", [self.mo1_mode, self.mo1_xtal, self.mo1_bragg_angle, self.mo1_eres]
|
||||
)
|
||||
|
||||
# Focusing Mirror
|
||||
self.fm_stripe = ComboBox(
|
||||
"fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"]
|
||||
)
|
||||
self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"])
|
||||
self.fm_rotx = InputNumberField(
|
||||
"fm_rotx",
|
||||
"Incidence Angle",
|
||||
unit="mrad",
|
||||
init=-2.391,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-10,
|
||||
hl=2,
|
||||
)
|
||||
self.fm_focx = InputNumberField(
|
||||
"fm_focx",
|
||||
"Beam Size Horizontal",
|
||||
unit="mm",
|
||||
init=1,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=0,
|
||||
hl=30,
|
||||
)
|
||||
self.fm_focy = InputNumberField(
|
||||
"fm_focy",
|
||||
"Beam Size Vertical",
|
||||
unit="mm",
|
||||
init=1,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=0,
|
||||
hl=10,
|
||||
)
|
||||
self.fm_rotx_ideal = NumberIndicator("Incidence Angle for focused beam", "mrad", decimals=3)
|
||||
self.fm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.fm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.fm_ass_group = Group(
|
||||
"Focusing Mirror",
|
||||
[
|
||||
self.fm_stripe,
|
||||
self.fm_focus,
|
||||
self.fm_rotx,
|
||||
self.fm_focx,
|
||||
self.fm_focy,
|
||||
self.fm_rotx_ideal,
|
||||
self.fm_refl,
|
||||
self.fm_refl_harm,
|
||||
],
|
||||
)
|
||||
|
||||
# Sample
|
||||
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
|
||||
self.smpl = InputNumberField(
|
||||
"smpl",
|
||||
"Sample Position",
|
||||
unit="mm",
|
||||
init=23511,
|
||||
decimals=0,
|
||||
single_step=100,
|
||||
ll=23000,
|
||||
hl=30000,
|
||||
)
|
||||
|
||||
# Assemble complete assitant group
|
||||
self.input_group = Group(
|
||||
"User Input",
|
||||
[
|
||||
self.adapt_reality,
|
||||
self.energy,
|
||||
self.sldi_ass_group,
|
||||
self.cm_ass_group,
|
||||
self.mo1_ass_group,
|
||||
self.fm_ass_group,
|
||||
self.cm_fm_harm_suppr,
|
||||
self.smpl,
|
||||
],
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
self._layout.addStretch()
|
||||
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
Panel to move an axis to a certain position
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import (
|
||||
AbsorberWidget,
|
||||
MoveWidget,
|
||||
)
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group
|
||||
|
||||
|
||||
class MoverPanel(QWidget):
|
||||
""" "Panel to move an axis to a certain position"""
|
||||
|
||||
def __init__(self, dev, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
self.mover_widgets = []
|
||||
|
||||
# FE Slits
|
||||
self.sldi_gapx = MoveWidget(
|
||||
dev=dev, motor="sldi_gapx", label="GAPX", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.sldi_gapx)
|
||||
|
||||
self.sldi_gapy = MoveWidget(
|
||||
dev=dev, motor="sldi_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.sldi_gapy)
|
||||
|
||||
self.sldi_mov_group = Group("FE Slits", [self.sldi_gapx, self.sldi_gapy])
|
||||
|
||||
# Absorber
|
||||
self.abs = AbsorberWidget(absorber=dev.abs, label="")
|
||||
|
||||
self.abs_group = Group("Absorber", [self.abs])
|
||||
|
||||
# Collimating mirror
|
||||
self.cm_trx = MoveWidget(
|
||||
dev=dev, motor="cm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.cm_trx)
|
||||
|
||||
self.cm_try = MoveWidget(
|
||||
dev=dev, motor="cm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.cm_try)
|
||||
|
||||
self.cm_bnd = MoveWidget(
|
||||
dev=dev, motor="cm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
|
||||
)
|
||||
self.mover_widgets.append(self.cm_bnd)
|
||||
|
||||
self.cm_rotx = MoveWidget(
|
||||
dev=dev, motor="cm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.cm_rotx)
|
||||
|
||||
self.cm_mov_group = Group(
|
||||
"Collimating Mirror", [self.cm_trx, self.cm_try, self.cm_bnd, self.cm_rotx]
|
||||
)
|
||||
|
||||
# Monochromator
|
||||
self.mo1_bragg_angle = MoveWidget(
|
||||
dev=dev,
|
||||
motor="mo1_bragg_angle",
|
||||
label="Bragg Angle",
|
||||
unit="deg",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_bragg_angle)
|
||||
|
||||
self.mo1_trx = MoveWidget(
|
||||
dev=dev, motor="mo1_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_trx)
|
||||
|
||||
self.mo1_try = MoveWidget(
|
||||
dev=dev, motor="mo1_try", label="TRY", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_try)
|
||||
|
||||
self.mo1_mov_group = Group(
|
||||
"Monochromator", [self.mo1_bragg_angle, self.mo1_trx, self.mo1_try]
|
||||
)
|
||||
|
||||
# OP Slits 1
|
||||
self.sl1_centery = MoveWidget(
|
||||
dev=dev, motor="sl1_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.sl1_centery)
|
||||
|
||||
self.sl1_gapy = MoveWidget(
|
||||
dev=dev, motor="sl1_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.sl1_gapy)
|
||||
|
||||
self.sl1_mov_group = Group("OP Slits 1", [self.sl1_centery, self.sl1_gapy])
|
||||
|
||||
# OP Beam Monitor 1
|
||||
self.bm1_try = MoveWidget(
|
||||
dev=dev, motor="bm1_try", label="TRY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.bm1_try)
|
||||
|
||||
self.bm1_mov_group = Group("OP Beam Monitor 1", [self.bm1_try])
|
||||
|
||||
# Focusing Mirror
|
||||
self.fm_trx = MoveWidget(
|
||||
dev=dev, motor="fm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.fm_trx)
|
||||
|
||||
self.fm_try = MoveWidget(
|
||||
dev=dev, motor="fm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.fm_try)
|
||||
|
||||
self.fm_bnd = MoveWidget(
|
||||
dev=dev, motor="fm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
|
||||
)
|
||||
self.mover_widgets.append(self.fm_bnd)
|
||||
|
||||
self.fm_rotx = MoveWidget(
|
||||
dev=dev, motor="fm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.fm_rotx)
|
||||
|
||||
self.fm_roty = MoveWidget(
|
||||
dev=dev, motor="fm_roty", label="YAW", unit="mrad", decimals=3, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.fm_roty)
|
||||
|
||||
self.fm_rotz = MoveWidget(
|
||||
dev=dev, motor="fm_rotz", label="ROLL", unit="mrad", decimals=3, deadband=0.01
|
||||
)
|
||||
self.mover_widgets.append(self.fm_rotz)
|
||||
|
||||
self.fm_mov_group = Group(
|
||||
"Focusing Mirror",
|
||||
[self.fm_trx, self.fm_try, self.fm_bnd, self.fm_rotx, self.fm_roty, self.fm_rotz],
|
||||
)
|
||||
|
||||
# OP Slits 2
|
||||
self.sl2_centery = MoveWidget(
|
||||
dev=dev, motor="sl2_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.sl2_centery)
|
||||
|
||||
self.sl2_gapy = MoveWidget(
|
||||
dev=dev, motor="sl2_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.sl2_gapy)
|
||||
|
||||
self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy])
|
||||
|
||||
# OP Beam Monitor 2
|
||||
self.bm2_try = MoveWidget(
|
||||
dev=dev, motor="bm2_try", label="TRY", unit="mm", decimals=2, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.bm2_try)
|
||||
|
||||
self.bm2_mov_group = Group("OP Beam Monitor 2", [self.bm2_try])
|
||||
|
||||
# Optical Table
|
||||
self.ot_try = MoveWidget(
|
||||
dev=dev, motor="ot_try", label="TRY", unit="mm", decimals=2, deadband=0.2
|
||||
)
|
||||
self.mover_widgets.append(self.ot_try)
|
||||
|
||||
self.ot_rotx = MoveWidget(
|
||||
dev=dev, motor="ot_rotx", label="ROTX", unit="mrad", decimals=3, deadband=0.05
|
||||
)
|
||||
self.mover_widgets.append(self.ot_rotx)
|
||||
|
||||
self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx])
|
||||
|
||||
# Experimental Station 0
|
||||
self.es0wi_try = MoveWidget(
|
||||
dev=dev, motor="es0wi_try", label="ES0 WI", unit="mm", decimals=0, deadband=0.1
|
||||
)
|
||||
self.mover_widgets.append(self.es0wi_try)
|
||||
|
||||
self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try])
|
||||
|
||||
# Experimental Station 1
|
||||
self.ot_es1_trz = MoveWidget(
|
||||
dev=dev, motor="ot_es1_trz", label="ES1 TRZ", unit="mm", decimals=0, deadband=5
|
||||
)
|
||||
self.mover_widgets.append(self.ot_es1_trz)
|
||||
|
||||
self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz])
|
||||
|
||||
# Assemble complete mover group
|
||||
self.mover_group = Group(
|
||||
"Mover",
|
||||
[
|
||||
self.sldi_mov_group,
|
||||
self.abs_group,
|
||||
self.cm_mov_group,
|
||||
self.mo1_mov_group,
|
||||
self.sl1_mov_group,
|
||||
self.bm1_mov_group,
|
||||
self.fm_mov_group,
|
||||
self.sl2_mov_group,
|
||||
self.bm2_mov_group,
|
||||
self.ot_mov_group,
|
||||
self.es0_mov_group,
|
||||
self.es1_mov_group,
|
||||
],
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.mover_group)
|
||||
self._layout.addStretch()
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
for widget in self.mover_widgets:
|
||||
widget.apply_theme(theme)
|
||||
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
Two plot classes to plot side-view and surface-view
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QBrush, QColor
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import (
|
||||
mirror_surface_geometries,
|
||||
mo_surface_geometries,
|
||||
pipe_geometries,
|
||||
wall_geometries,
|
||||
)
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict, SurfaceDict
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SurfacePlots(QWidget):
|
||||
"""Plot widget with two curves and legend."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._layout = QHBoxLayout(self)
|
||||
|
||||
self.surfaces: dict[str, SurfaceDict] = {
|
||||
"assistant": {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
},
|
||||
"reality": {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
},
|
||||
}
|
||||
|
||||
self.plots = {"fm": {}, "mo1_2": {}, "mo1_1": {}, "cm": {}}
|
||||
|
||||
self.color_impenetrable = (0, 0, 0)
|
||||
self.colors = [(255, 255, 0), (255, 0, 255)]
|
||||
self.text_color = (255, 255, 255)
|
||||
|
||||
# Create plot widgets
|
||||
for name, widget in self.plots.items():
|
||||
plot_widget = pg.PlotWidget()
|
||||
plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
|
||||
|
||||
plot_group = Group("Surface " + name, [plot_widget])
|
||||
|
||||
plot_widget.setLabel("left", "Z [mm]")
|
||||
plot_widget.setLabel("bottom", "X [mm]")
|
||||
plot_widget.setMouseEnabled(x=False, y=False)
|
||||
plot_widget.setMenuEnabled(False)
|
||||
plot_widget.hideButtons()
|
||||
|
||||
widget["widget"] = plot_widget
|
||||
self._layout.addWidget(plot_group)
|
||||
|
||||
# Create surfaces
|
||||
for idx, scene in enumerate(self.surfaces):
|
||||
for name, _ in self.surfaces[scene].items():
|
||||
if scene in "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(
|
||||
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
|
||||
)
|
||||
z_value = 2
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
|
||||
z_value = 1
|
||||
widget = self.plots[name]
|
||||
self.plots[name][scene] = widget["widget"].plot(
|
||||
[], [], pen=pen, name=scene, brush=brush, fillLevel=0
|
||||
)
|
||||
self.plots[name][scene].setZValue(z_value)
|
||||
|
||||
self.walls = []
|
||||
self.texts = []
|
||||
|
||||
self.plot_walls()
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
for _, plot in self.plots.items():
|
||||
# Background
|
||||
plot["widget"].setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = plot["widget"].getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if theme == "light":
|
||||
self.color_impenetrable = (30, 30, 30)
|
||||
self.colors = [(79, 163, 224), (240, 128, 60)]
|
||||
self.text_color = (255, 255, 255)
|
||||
else: # dark theme
|
||||
self.color_impenetrable = (180, 180, 180)
|
||||
self.colors = [(26, 111, 173), (212, 83, 10)]
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
for idx, scene in enumerate(self.surfaces):
|
||||
for name, _ in self.surfaces[scene].items():
|
||||
if scene in "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(
|
||||
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
|
||||
)
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0)
|
||||
self.plots[name][scene].setPen(pen)
|
||||
self.plots[name][scene].setBrush(brush)
|
||||
|
||||
for wall in self.walls:
|
||||
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
|
||||
for text in self.texts:
|
||||
text.setColor(self.text_color)
|
||||
|
||||
def plot_walls(self):
|
||||
"""Plot walls"""
|
||||
|
||||
def plot_surface(widget, surfaces):
|
||||
for name, surface in surfaces.items():
|
||||
rect = QGraphicsRectItem(*surface)
|
||||
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
widget.addItem(rect)
|
||||
text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5))
|
||||
widget.addItem(text)
|
||||
text.setPos(surface[0] + surface[2] / 2, surface[1] + surface[3] / 2)
|
||||
text.setZValue(10)
|
||||
self.walls.append(rect)
|
||||
self.texts.append(text)
|
||||
|
||||
for name, plot in self.plots.items():
|
||||
if name in "cm":
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("cm"))
|
||||
elif name in "mo1_1":
|
||||
plot_surface(plot["widget"], mo_surface_geometries("mo1", 0))
|
||||
elif name in "mo1_2":
|
||||
plot_surface(plot["widget"], mo_surface_geometries("mo1", 1))
|
||||
elif name in "fm":
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("fm_flat"))
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid"))
|
||||
else:
|
||||
raise ValueError(f"Plot {name} not found!")
|
||||
for name, plot in self.plots.items():
|
||||
plot["widget"].disableAutoRange()
|
||||
|
||||
def update_surfaces(self, scene: Literal["assistant", "reality"], data: SurfaceDict):
|
||||
"""Update the curves of the plot
|
||||
|
||||
Args:
|
||||
scene (str): The scene to update, either "assistant" or "reality".
|
||||
data (DataDict): The new data to plot, with keys "x" and "y",
|
||||
each containing a list of values.
|
||||
"""
|
||||
self.surfaces[scene] = data
|
||||
for name, device in self.surfaces[scene].items():
|
||||
device = cast(DataDict, device)
|
||||
plot = self.plots[name][scene]
|
||||
x = np.array(device["x"] + [device["x"][0]]) if len(device["x"]) != 0 else np.array([])
|
||||
y = np.array(device["y"] + [device["y"][0]]) if len(device["y"]) != 0 else np.array([])
|
||||
plot.setData(x=x, y=y)
|
||||
|
||||
|
||||
class SideviewPlot(QWidget):
|
||||
"""Plot widget with two curves and legend."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
|
||||
self.plot_widget.invertX(True)
|
||||
self.plot_widget.addLegend()
|
||||
|
||||
self.color_impenetrable = (0, 0, 0)
|
||||
self.colors = [(255, 255, 0), (255, 0, 255)]
|
||||
|
||||
self.data: dict[str, DataDict] = {
|
||||
"assistant": {"x": [0, 1000, 2000], "y": [0, 20, 30]},
|
||||
"reality": {"x": [0, 1000, 2000], "y": [0, 15, 50]},
|
||||
}
|
||||
|
||||
self.plots = {}
|
||||
|
||||
self.pipes = []
|
||||
self.walls = []
|
||||
|
||||
for idx, scene in enumerate(self.data.keys()):
|
||||
if scene in "assistant":
|
||||
pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine)
|
||||
z_value = 2
|
||||
else:
|
||||
pen = pg.mkPen(color=self.colors[idx], width=2)
|
||||
z_value = 1
|
||||
self.plots[scene] = self.plot_widget.plot([], [], pen=pen, name=scene)
|
||||
self.plots[scene].setZValue(z_value)
|
||||
|
||||
self.plot_group = Group("Side View", [self.plot_widget])
|
||||
|
||||
self.plot_widget.setLabel("left", "Height [mm]")
|
||||
self.plot_widget.setLabel("bottom", "Distance [mm]")
|
||||
self.plot_widget.setMouseEnabled(x=False, y=False)
|
||||
self.plot_widget.setXRange(0, 25000, 0.1) # pylint: disable=E1121 # type: ignore
|
||||
self.plot_widget.setYRange(-20, 120, 0.1) # pylint: disable=E1121 # type: ignore
|
||||
self.plot_widget.setMenuEnabled(False)
|
||||
self.plot_widget.hideButtons()
|
||||
|
||||
self._layout.addWidget(self.plot_group)
|
||||
self._layout.addStretch()
|
||||
|
||||
self.plot_vacuum_pipes()
|
||||
self.plot_walls()
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
# Background
|
||||
self.plot_widget.setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = self.plot_widget.getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if theme == "light":
|
||||
self.color_impenetrable = (30, 30, 30)
|
||||
self.colors = [(79, 163, 224), (240, 128, 60)]
|
||||
self.text_color = (255, 255, 255)
|
||||
else: # dark theme
|
||||
self.color_impenetrable = (180, 180, 180)
|
||||
self.colors = [(26, 111, 173), (212, 83, 10)]
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
for idx, scene in enumerate(self.data):
|
||||
if scene in "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.PenStyle.DashLine)
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3)
|
||||
self.plots[scene].setPen(pen)
|
||||
self.plots[scene].setBrush(brush)
|
||||
|
||||
for wall in self.walls:
|
||||
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
|
||||
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
|
||||
for pipe in self.pipes:
|
||||
pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
|
||||
|
||||
def plot_vacuum_pipes(self):
|
||||
"""Plot vacuum pipes"""
|
||||
pipes = pipe_geometries()
|
||||
for pipe in pipes:
|
||||
self.pipes.append(
|
||||
self.plot_widget.plot(
|
||||
x=pipe["x"], y=pipe["y"], pen=pg.mkPen(color=self.color_impenetrable, width=2)
|
||||
)
|
||||
)
|
||||
|
||||
def plot_walls(self):
|
||||
"""Plot walls"""
|
||||
walls = wall_geometries()
|
||||
for wall in walls:
|
||||
rect = QGraphicsRectItem(wall[0], wall[1], wall[2], wall[3])
|
||||
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
self.plot_widget.addItem(rect)
|
||||
self.walls.append(rect)
|
||||
|
||||
def update_curves(self, scene: Literal["assistant", "reality"], data: DataDict):
|
||||
"""Update the curves of the plot
|
||||
|
||||
Args:
|
||||
scene (str): The scene to update, either "assistant" or "reality".
|
||||
data (DataDict): The new data to plot, with keys "x" and "y",
|
||||
each containing a list of values.
|
||||
"""
|
||||
self.data[scene] = data
|
||||
plot = self.plots[scene]
|
||||
plot.setData(x=self.data[scene]["x"], y=self.data[scene]["y"])
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Settings panel for the digital twin widget
|
||||
"""
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
|
||||
Button,
|
||||
Group,
|
||||
TextIndicator,
|
||||
)
|
||||
|
||||
|
||||
class SettingsPanel(QWidget):
|
||||
"""Settings panel for the digital twin widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
# Reload offsets
|
||||
self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True)
|
||||
self.offsets_status = TextIndicator(label="Offsets")
|
||||
self.show_offsets = Button(label="Show Offsets", label_button="Show", enabled=True)
|
||||
|
||||
# Assemble complete offset group
|
||||
self.offset_group = Group(
|
||||
"Axes Offsets", [self.load_offsets, self.offsets_status, self.show_offsets]
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.offset_group)
|
||||
self._layout.addStretch()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""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.
|
||||
|
||||
Attributes:
|
||||
x (list[float]): List of x-axis values.
|
||||
y (list[float]): List of y-axis values.
|
||||
"""
|
||||
|
||||
x: list
|
||||
y: list
|
||||
|
||||
|
||||
class SurfaceDict(TypedDict):
|
||||
"""
|
||||
Typed dictionary representing the surfaces of a scene,
|
||||
grouping plot data by surface type.
|
||||
|
||||
Attributes:
|
||||
cm (DataDict): Data for the cm surface.
|
||||
mo1_1 (DataDict): Data for the mo1_1 surface.
|
||||
mo1_2 (DataDict): Data for the mo1_2 surface.
|
||||
fm (DataDict): Data for the fm surface.
|
||||
"""
|
||||
|
||||
cm: DataDict
|
||||
mo1_1: DataDict
|
||||
mo1_2: DataDict
|
||||
fm: DataDict
|
||||
+246
-163
@@ -1,28 +1,33 @@
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
"""Move widget to display an axis and also move it through BEC"""
|
||||
|
||||
# import qtawesome as qta
|
||||
import threading
|
||||
import time
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_lib import bec_logger
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Property # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import Signal # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread
|
||||
from qtpy.QtGui import QTransform
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget
|
||||
|
||||
from debye_bec.devices.absorber import STATUS as ABS_STATUS
|
||||
|
||||
from qtpy.QtCore import Qt, QThread, Signal, QObject, Property, QPropertyAnimation
|
||||
from qtpy.QtWidgets import (
|
||||
QGroupBox, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
|
||||
QDoubleSpinBox, QFrame, QWidget, QApplication
|
||||
)
|
||||
from qtpy.QtGui import QTransform
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class Status:
|
||||
IN_POSITION = "in_position" # green mdi.check-circle
|
||||
"""Status class for the axis"""
|
||||
|
||||
IN_POSITION = "in_position" # green mdi.check-circle
|
||||
NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle
|
||||
MOVING = "moving" # blue mdi.loading (spinning)
|
||||
ERROR = "error" # red mdi.alert-circle
|
||||
MOVING = "moving" # blue mdi.loading (spinning)
|
||||
ERROR = "error" # red mdi.alert-circle
|
||||
|
||||
|
||||
class StatusIcon(QWidget):
|
||||
"""
|
||||
@@ -33,10 +38,10 @@ class StatusIcon(QWidget):
|
||||
ICON_SIZE = 20
|
||||
|
||||
_ICON_MAP = {
|
||||
Status.IN_POSITION: ("check_circle", "#27ae60"),
|
||||
Status.IN_POSITION: ("check_circle", "#27ae60"),
|
||||
Status.NOT_IN_POSITION: ("cancel", "#e6d922"),
|
||||
Status.ERROR: ("warning", "#e74c3c"),
|
||||
Status.MOVING: ("cycle", "#2980b9"),
|
||||
Status.ERROR: ("warning", "#e74c3c"),
|
||||
Status.MOVING: ("cycle", "#2980b9"),
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -46,10 +51,10 @@ class StatusIcon(QWidget):
|
||||
|
||||
self._label = QLabel(self)
|
||||
self._label.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._label.setAlignment(Qt.AlignCenter)
|
||||
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
|
||||
|
||||
self._spin_anim = QPropertyAnimation(self, b"rotation")
|
||||
self._spin_anim = QPropertyAnimation(self, b"rotation") # type: ignore[call-arg]
|
||||
self._spin_anim.setStartValue(0)
|
||||
self._spin_anim.setEndValue(360)
|
||||
self._spin_anim.setDuration(1000)
|
||||
@@ -57,26 +62,56 @@ class StatusIcon(QWidget):
|
||||
|
||||
self.set_status(Status.NOT_IN_POSITION)
|
||||
|
||||
def get_rotation(self):
|
||||
def get_rotation(self) -> float:
|
||||
"""
|
||||
Return the current rotation angle in degrees.
|
||||
|
||||
Returns:
|
||||
float: Rotation angle in deg
|
||||
"""
|
||||
return self._rotation
|
||||
|
||||
def set_rotation(self, angle):
|
||||
def set_rotation(self, angle: float):
|
||||
"""
|
||||
Set the rotation angle and update the displayed pixmap.
|
||||
|
||||
Rotates the current base pixmap around its center point using a smooth
|
||||
transformation. Has no effect on the display if no base pixmap is set.
|
||||
|
||||
Args:
|
||||
angle (float): Rotation angle in degrees, clockwise.
|
||||
"""
|
||||
self._rotation = angle
|
||||
if self._current_pixmap_base is not None:
|
||||
cx = self._current_pixmap_base.width() / 2
|
||||
cy = self._current_pixmap_base.height() / 2
|
||||
t = QTransform().translate(cx, cy).rotate(angle).translate(-cx, -cy)
|
||||
self._label.setPixmap(self._current_pixmap_base.transformed(t, Qt.SmoothTransformation))
|
||||
self._label.setPixmap(
|
||||
self._current_pixmap_base.transformed(t, Qt.TransformationMode.SmoothTransformation)
|
||||
)
|
||||
|
||||
rotation = Property(float, get_rotation, set_rotation)
|
||||
rotation = Property(float, get_rotation, set_rotation) # type: ignore[call-arg]
|
||||
|
||||
def set_status(self, status: str):
|
||||
"""
|
||||
Update the widget's status and refresh the displayed icon accordingly.
|
||||
|
||||
Looks up the icon name and color associated with the given status from
|
||||
``_ICON_MAP``, renders a new pixmap, and starts or stops the spin
|
||||
animation depending on whether the status is ``Status.MOVING``. Returns
|
||||
early without any updates if the status has not changed.
|
||||
|
||||
Args:
|
||||
status (str): The new status value. Must be a key in ``_ICON_MAP``.
|
||||
"""
|
||||
if status == self._status:
|
||||
return
|
||||
self._status = status
|
||||
|
||||
icon_name, color = self._ICON_MAP[status]
|
||||
icon = material_icon(icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True)
|
||||
icon = material_icon(
|
||||
icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True
|
||||
)
|
||||
self._current_pixmap_base = icon
|
||||
|
||||
if status == Status.MOVING:
|
||||
@@ -85,14 +120,16 @@ class StatusIcon(QWidget):
|
||||
self._spin_anim.stop()
|
||||
self._label.setPixmap(icon)
|
||||
|
||||
|
||||
class MotionWorker(QObject):
|
||||
"""
|
||||
Executes motion on the specified motor and includes some safety during
|
||||
motion for certain motors.
|
||||
"""
|
||||
|
||||
position_changed = Signal(float)
|
||||
error = Signal(bool) # True = error
|
||||
finished = Signal(bool) # True = reached target, False = stopped
|
||||
error = Signal(bool) # True = error
|
||||
finished = Signal(bool) # True = reached target, False = stopped
|
||||
|
||||
def __init__(self, dev, motor, target_pos: float):
|
||||
super().__init__()
|
||||
@@ -102,103 +139,108 @@ class MotionWorker(QObject):
|
||||
self._stop_flag = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""Sets the stop flag"""
|
||||
self._stop_flag.set()
|
||||
|
||||
# def run(self):
|
||||
# logger.info(f'Would run motor {self.motor}')
|
||||
# simulated_run_time = 3
|
||||
# start = time.time()
|
||||
# while (time.time() - start) < simulated_run_time:
|
||||
# if self._stop_flag.is_set():
|
||||
# break
|
||||
# time.sleep(0.01)
|
||||
|
||||
# # self.motor.move(self._target, relative=False)
|
||||
# # while self.motor.motor_is_moving.get():
|
||||
# # if self._stop_flag.is_set():
|
||||
# # self.motor.motor_stop()
|
||||
# # self.position_changed.emit(self.motor.read[self.name]['value'])
|
||||
# # time.sleep(0.1)
|
||||
# self.finished.emit(True)
|
||||
|
||||
def run(self):
|
||||
"""Prepares the movement based on the axis (motor)"""
|
||||
match self.motor:
|
||||
case 'sldi_gapx' | 'sldi_gapy' | 'sldi_centerx' | 'sldi_centery':
|
||||
case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery":
|
||||
self.motion()
|
||||
case 'cm_trx':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['cm_roty'], 'abs_tol': 0.05}
|
||||
])
|
||||
case 'cm_roty':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['cm_trx'], 'abs_tol': 0.05}
|
||||
])
|
||||
case 'cm_try':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['cm_rotx'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['cm_rotz'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'cm_rotx':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['cm_try'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['cm_rotz'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'cm_rotz':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['cm_try'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['cm_rotx'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'cm_bnd':
|
||||
p1 = (1/(self.dev.cm_bnd_radius.read()['cm_bnd_radius']['value']*1e3) + 0.0284)/2e-6
|
||||
p2 = (1/(self._target*1e3) + 0.0284)/2e-6
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb=
|
||||
{'device': self.dev['cm_bnd_radius']}
|
||||
case "cm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case 'mo1_try' | 'mo1_trx' | 'mo1_roty':
|
||||
case "cm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "cm_try":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotz":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_bnd":
|
||||
p1 = (
|
||||
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
|
||||
) / 2e-6
|
||||
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
|
||||
case "mo1_try" | "mo1_trx" | "mo1_roty":
|
||||
self.motion(abs_closed=True)
|
||||
case 'mo1_bragg_angle':
|
||||
case "mo1_bragg_angle":
|
||||
self.motion()
|
||||
case 'sl1_centery' | 'sl1_gapy' | 'bm1_try':
|
||||
case "sl1_centery" | "sl1_gapy" | "bm1_try":
|
||||
self.motion()
|
||||
case 'fm_trx':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['fm_roty'], 'abs_tol': 0.05}
|
||||
])
|
||||
case 'fm_roty':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['fm_trx'], 'abs_tol': 0.05}
|
||||
])
|
||||
case 'fm_try':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['fm_rotx'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['fm_rotz'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'fm_rotx':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['fm_try'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['fm_rotz'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'fm_rotz':
|
||||
self.motion(abs_closed=True, surveyed_axes=[
|
||||
{'device': self.dev['fm_try'], 'abs_tol': 0.05},
|
||||
{'device': self.dev['fm_rotx'], 'abs_tol': 0.05},
|
||||
])
|
||||
case 'fm_bnd':
|
||||
p1 = (1/(self.dev.fm_bnd_radius.read()['fm_bnd_radius']['value']*1e3) + 4.28e-5)/1.84e-9
|
||||
p2 = (1/(self._target*1e3) + 4.28e-5)/1.84e-9
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb=
|
||||
{'device': self.dev['fm_bnd_radius']}
|
||||
case "fm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case 'sl2_centery' | 'sl2_gapy' | 'bm2_try':
|
||||
case "fm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "fm_try":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "fm_rotx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "fm_rotz":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "fm_bnd":
|
||||
p1 = (
|
||||
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3) + 4.28e-5
|
||||
) / 1.84e-9
|
||||
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
|
||||
case "sl2_centery" | "sl2_gapy" | "bm2_try":
|
||||
self.motion()
|
||||
case 'ot_try' | 'ot_rotx' | 'ot_es1_trz':
|
||||
case "ot_try" | "ot_rotx" | "ot_es1_trz":
|
||||
self.motion()
|
||||
case _:
|
||||
logger.warning(f'Motor {self.motor} not integrated in digital twin!')
|
||||
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
|
||||
|
||||
def motion(self, abs_closed=False, relative=False, rb=None, surveyed_axes = None):
|
||||
def motion(self, abs_closed: bool = False, relative: bool = False, rb=None, surveyed_axes=None):
|
||||
"""
|
||||
Moves an axis while surverying a set of axes (if set).
|
||||
Example surveyed_axes:
|
||||
@@ -210,34 +252,42 @@ class MotionWorker(QObject):
|
||||
if abs_closed:
|
||||
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
|
||||
status = self.dev.abs.close()
|
||||
# TODO Set timeout to 0.001 and check if it actually raises (it should not start motion).
|
||||
# TODO Set timeout to 0.001 and check if it actually raises
|
||||
# (it should not start motion).
|
||||
# Check of behavior of digital twin afterwards.
|
||||
status.wait(timeout=5)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
surv_ax['name'] = surv_ax['device'].dotted_name
|
||||
surv_ax['old_value'] = surv_ax['device'].read(cached=True)[surv_ax['name']]['value']
|
||||
surv_ax["name"] = surv_ax["device"].dotted_name
|
||||
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
|
||||
if rb is not None:
|
||||
rb['name'] = rb['device'].dotted_name
|
||||
self.dev[self.motor].move(self._target, relative=relative)
|
||||
time.sleep(0.5)
|
||||
while self.dev[self.motor].motor_is_moving.get():
|
||||
rb["name"] = rb["device"].dotted_name
|
||||
status = self.dev[self.motor].move(self._target, relative=relative)
|
||||
last_check = time.time()
|
||||
update_interval = 0.1
|
||||
while status.status == "RUNNING":
|
||||
now = time.time()
|
||||
if time.time() - last_check < update_interval:
|
||||
time.sleep(0.01)
|
||||
last_check = now
|
||||
if self._stop_flag.is_set():
|
||||
self.dev[self.motor].stop()
|
||||
self._stop_flag.clear()
|
||||
if rb is not None:
|
||||
self.position_changed.emit(rb['device'].read(cached=True)[rb['name']]['value'])
|
||||
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
|
||||
else:
|
||||
self.position_changed.emit(self.dev[self.motor].read(cached=True)[self.motor]['value'])
|
||||
self.position_changed.emit(
|
||||
self.dev[self.motor].read(cached=True)[self.motor]["value"]
|
||||
)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
fb = surv_ax['device'].read(cached=True)[surv_ax['name']]['value']
|
||||
if abs(fb - surv_ax['old_value']) > surv_ax['abs_tol']:
|
||||
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
|
||||
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
|
||||
self.dev[self.motor].stop()
|
||||
self.error.emit(1)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
self.finished.emit(True)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MoveWidget(QWidget):
|
||||
"""
|
||||
@@ -248,7 +298,7 @@ class MoveWidget(QWidget):
|
||||
- Start / Stop button
|
||||
"""
|
||||
|
||||
def __init__(self, dev, motor, label: str = '', unit=None, decimals=3, deadband=0.0):
|
||||
def __init__(self, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0):
|
||||
super().__init__()
|
||||
self.fb = 0.0
|
||||
self.target = 0
|
||||
@@ -276,12 +326,12 @@ class MoveWidget(QWidget):
|
||||
layout.addWidget(self.label)
|
||||
|
||||
# Target
|
||||
self.target_label = QLabel('-')
|
||||
self.target_label = QLabel("-")
|
||||
self.target_label.setFixedWidth(100)
|
||||
layout.addWidget(self.target_label)
|
||||
|
||||
# Feedback
|
||||
self.fb_label = QLabel('-')
|
||||
self.fb_label = QLabel("-")
|
||||
self.fb_label.setFixedWidth(100)
|
||||
layout.addWidget(self.fb_label)
|
||||
|
||||
@@ -297,66 +347,80 @@ class MoveWidget(QWidget):
|
||||
self.btn_action.setFixedHeight(20)
|
||||
self.btn_action.clicked.connect(self._on_button_clicked)
|
||||
layout.addWidget(self.btn_action)
|
||||
self.btn_mode = 'start'
|
||||
self.btn_mode = "start"
|
||||
|
||||
self._apply_button_style("start")
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme=None):
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
if theme == "light":
|
||||
self.text_color = {'target': (79, 163, 224), 'fb': (240, 128, 60)}
|
||||
else: # dark theme
|
||||
self.text_color = {'target': (26, 111, 173), 'fb': (212, 83, 10)}
|
||||
r, g, b = self.text_color['target']
|
||||
self.target_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}')
|
||||
r, g, b = self.text_color['fb']
|
||||
self.fb_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}')
|
||||
self.text_color = {"target": (79, 163, 224), "fb": (240, 128, 60)}
|
||||
else: # dark theme
|
||||
self.text_color = {"target": (26, 111, 173), "fb": (212, 83, 10)}
|
||||
r, g, b = self.text_color["target"]
|
||||
self.target_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
|
||||
r, g, b = self.text_color["fb"]
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
|
||||
|
||||
if self.btn_mode == 'start':
|
||||
if self.btn_mode == "start":
|
||||
self.btn_action.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
else:
|
||||
self.btn_action.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
|
||||
def set_target(self, target):
|
||||
"""Change the target value in the ui"""
|
||||
self.target = target
|
||||
text = f'{target:.{int(self.decimals)}f}'
|
||||
text = f"{target:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + ' ' + self.unit
|
||||
text = text + " " + self.unit
|
||||
self.target_label.setText(text)
|
||||
self._on_target_or_fb_changed()
|
||||
|
||||
def set_feedback(self, fb):
|
||||
"""Change the feedback value in the ui"""
|
||||
if self.status != Status.MOVING:
|
||||
self.fb = fb
|
||||
text = f'{fb:.{int(self.decimals)}f}'
|
||||
text = f"{fb:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + ' ' + self.unit
|
||||
text = text + " " + self.unit
|
||||
self.fb_label.setText(text)
|
||||
self._on_target_or_fb_changed()
|
||||
|
||||
def _apply_button_style(self, mode: str):
|
||||
"""Apply a button style depending on if the button shows start or stop"""
|
||||
self.btn_mode = mode
|
||||
if mode == "start":
|
||||
self.btn_action.setText("Move")
|
||||
self.btn_action.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
else: # stop
|
||||
self.btn_action.setText("Stop")
|
||||
self.btn_action.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
|
||||
def _set_status(self, status: str):
|
||||
"""Set the current status icon in the ui"""
|
||||
self.status = status
|
||||
self.status_icon.set_status(status)
|
||||
|
||||
@@ -370,12 +434,14 @@ class MoveWidget(QWidget):
|
||||
self._set_status(Status.NOT_IN_POSITION)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
"""Starts or stops motion depending on current situation"""
|
||||
if self._thread and self._thread.isRunning():
|
||||
self._stop_motion()
|
||||
else:
|
||||
self._start_motion()
|
||||
|
||||
def _start_motion(self):
|
||||
"""Start a motion"""
|
||||
target = self.target
|
||||
if abs(target - self.fb) <= self.deadband:
|
||||
self._set_status(Status.IN_POSITION)
|
||||
@@ -399,21 +465,25 @@ class MoveWidget(QWidget):
|
||||
self._thread.start()
|
||||
|
||||
def _on_error(self):
|
||||
"""Called when an error occurs"""
|
||||
self._set_status(Status.ERROR)
|
||||
self._apply_button_style("start")
|
||||
|
||||
def _stop_motion(self):
|
||||
"""Attempts to stop the motion"""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
|
||||
def _on_position_changed(self, pos: float):
|
||||
"""Change the feedback value in the ui"""
|
||||
self.fb = pos
|
||||
text = f'{pos:.{int(self.decimals)}f}'
|
||||
text = f"{pos:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + ' ' + self.unit
|
||||
text = text + " " + self.unit
|
||||
self.fb_label.setText(text)
|
||||
|
||||
def _on_motion_finished(self, reached: bool):
|
||||
def _on_motion_finished(self):
|
||||
"""Finished a movement"""
|
||||
target = self.target
|
||||
if self.status not in Status.ERROR:
|
||||
if abs(self.fb - target) <= self.deadband:
|
||||
@@ -423,6 +493,7 @@ class MoveWidget(QWidget):
|
||||
self._apply_button_style("start")
|
||||
|
||||
def _cleanup_thread(self):
|
||||
"""Cleaning up of the mover thread"""
|
||||
if self._thread:
|
||||
self._thread.deleteLater()
|
||||
self._thread = None
|
||||
@@ -431,18 +502,20 @@ class MoveWidget(QWidget):
|
||||
self._worker = None
|
||||
|
||||
def shutdown(self):
|
||||
"""Cleaning up of the mover when shutting down the application"""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
if self._thread:
|
||||
self._thread.quit()
|
||||
self._thread.wait(2000) # max 2 s grace period
|
||||
|
||||
|
||||
class AbsorberWidget(QWidget):
|
||||
"""
|
||||
Control of the frontend absorber (only open)
|
||||
"""
|
||||
|
||||
def __init__(self, absorber, label: str = 'Absorber'):
|
||||
def __init__(self, absorber, label: str = "Absorber"):
|
||||
super().__init__()
|
||||
self.absorber = absorber
|
||||
self.fb = False
|
||||
@@ -460,17 +533,17 @@ class AbsorberWidget(QWidget):
|
||||
layout.addWidget(self.label)
|
||||
|
||||
# Blank
|
||||
self.blank_label = QLabel('')
|
||||
self.blank_label = QLabel("")
|
||||
self.blank_label.setFixedWidth(100)
|
||||
layout.addWidget(self.blank_label)
|
||||
|
||||
# Feedback
|
||||
self.fb_label = QLabel('-')
|
||||
self.fb_label = QLabel("-")
|
||||
self.fb_label.setFixedWidth(100)
|
||||
layout.addWidget(self.fb_label)
|
||||
|
||||
# Blank icon
|
||||
self.blank_icon = QLabel('')
|
||||
self.blank_icon = QLabel("")
|
||||
self.blank_icon.setFixedWidth(30)
|
||||
self.blank_icon.setContentsMargins(0, 0, 10, 0)
|
||||
layout.addWidget(self.blank_icon)
|
||||
@@ -483,22 +556,31 @@ class AbsorberWidget(QWidget):
|
||||
layout.addWidget(self.btn_action)
|
||||
|
||||
def set_feedback(self, fb: bool):
|
||||
"""
|
||||
Displays the status of the absober in the ui
|
||||
|
||||
Args:
|
||||
fb (bool): True will set the button to Open, False to Closed
|
||||
"""
|
||||
self.fb = fb
|
||||
if fb:
|
||||
self.fb_label.setText('Open')
|
||||
self.fb_label.setStyleSheet(
|
||||
f"QLabel {{color: {get_accent_colors().success.name()}}}"
|
||||
)
|
||||
self.fb_label.setText("Open")
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().success.name()}}}")
|
||||
else:
|
||||
self.fb_label.setText('Closed')
|
||||
self.fb_label.setStyleSheet(
|
||||
f"QLabel {{color: {get_accent_colors().emergency.name()}}}"
|
||||
)
|
||||
self.fb_label.setText("Closed")
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().emergency.name()}}}")
|
||||
|
||||
def enable_open(self, enable: bool = False):
|
||||
"""
|
||||
Enable or disable the open/close button
|
||||
|
||||
Args:
|
||||
enable (bool): Enables and disables the button
|
||||
"""
|
||||
if enable:
|
||||
self.btn_action.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
self.btn_action.setEnabled(True)
|
||||
else: # disabled
|
||||
@@ -508,4 +590,5 @@ class AbsorberWidget(QWidget):
|
||||
self.btn_action.setDisabled(True)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
"""Open absorber"""
|
||||
self.absorber.open()
|
||||
+80
-46
@@ -1,24 +1,37 @@
|
||||
"""
|
||||
Universal Qt widgets
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QGroupBox, QComboBox, QApplication, QDoubleSpinBox
|
||||
)
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, widgets):
|
||||
super().__init__(label)
|
||||
self.layout = QVBoxLayout(self) # type: ignore
|
||||
self.layout = QVBoxLayout(self) # type: ignore
|
||||
for widget in widgets:
|
||||
self.layout.addWidget(widget) # type: ignore
|
||||
self.layout.addWidget(widget) # type: ignore
|
||||
|
||||
|
||||
class NumberIndicator(QWidget):
|
||||
def __init__(self, label='', unit=None, highlight=False, decimals=3):
|
||||
def __init__(self, label="", unit=None, highlight=False, decimals=3):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
@@ -28,8 +41,8 @@ class NumberIndicator(QWidget):
|
||||
self.label.setContentsMargins(0, 0, 10, 0)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.val = QLabel('-')
|
||||
self.val.setAlignment(Qt.AlignTop) # type: ignore
|
||||
self.val = QLabel("-")
|
||||
self.val.setAlignment(Qt.AlignTop) # type: ignore
|
||||
# self.val.setFixedWidth(140)
|
||||
layout.addWidget(self.val)
|
||||
self.unit = unit
|
||||
@@ -51,13 +64,25 @@ class NumberIndicator(QWidget):
|
||||
|
||||
def setValue(self, number):
|
||||
self.number = number
|
||||
text = f'{number:.{int(self.decimals)}f}'
|
||||
text = f"{number:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + ' ' + self.unit
|
||||
text = text + " " + self.unit
|
||||
self.val.setText(text)
|
||||
|
||||
|
||||
class InputNumberField(QWidget):
|
||||
def __init__(self, identifier='', label='', unit=None, prefix=None, init=0.0, decimals=1, single_step=0.1, ll=-1e6, hl=1e6):
|
||||
def __init__(
|
||||
self,
|
||||
identifier="",
|
||||
label="",
|
||||
unit=None,
|
||||
prefix=None,
|
||||
init=0.0,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=-1e6,
|
||||
hl=1e6,
|
||||
):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
@@ -74,9 +99,9 @@ class InputNumberField(QWidget):
|
||||
self.val.setSingleStep(single_step)
|
||||
self.val.setValue(init)
|
||||
if unit is not None:
|
||||
self.val.setSuffix(' ' + unit)
|
||||
self.val.setSuffix(" " + unit)
|
||||
if prefix is not None:
|
||||
self.val.setPrefix(prefix + ' ')
|
||||
self.val.setPrefix(prefix + " ")
|
||||
# self.val.setFixedWidth(140)
|
||||
layout.addWidget(self.val)
|
||||
|
||||
@@ -85,18 +110,21 @@ class InputNumberField(QWidget):
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return self.val.hasFocus()
|
||||
|
||||
|
||||
def value(self) -> float:
|
||||
return self.val.value()
|
||||
|
||||
def value_changed_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.val.valueChanged.connect(
|
||||
partial(func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value())
|
||||
partial(
|
||||
func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ComboBox(QWidget):
|
||||
def __init__(self, identifier='', label='', enums=[]):
|
||||
def __init__(self, identifier="", label="", enums=[]):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
@@ -124,14 +152,20 @@ class ComboBox(QWidget):
|
||||
def activated_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.activated.connect(
|
||||
partial(func, identifier=self.identifier, value_obj=self.value, value=lambda: self.value.currentText())
|
||||
partial(
|
||||
func,
|
||||
identifier=self.identifier,
|
||||
value_obj=self.value,
|
||||
value=lambda: self.value.currentText(),
|
||||
)
|
||||
)
|
||||
|
||||
def setDisabled(self, disable):
|
||||
self.value.setDisabled(disable)
|
||||
|
||||
|
||||
class Button(QWidget):
|
||||
def __init__(self, label=None, label_button:str='', enabled=False):
|
||||
def __init__(self, label=None, label_button: str = "", enabled=False):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
@@ -165,31 +199,31 @@ class Button(QWidget):
|
||||
def setText(self, text):
|
||||
self.button.setText(text)
|
||||
|
||||
# class TextIndicator(QWidget):
|
||||
# def __init__(self, label, unit=None, highlight=False):
|
||||
# super().__init__()
|
||||
# layout = QHBoxLayout(self)
|
||||
# layout.setContentsMargins(10, 0, 0, 0)
|
||||
# layout.setSpacing(0)
|
||||
# self.label = QLabel(label)
|
||||
# self.label.setFixedWidth(150)
|
||||
# layout.addWidget(self.label)
|
||||
# self.value = QLabel('-')
|
||||
# self.value.setFixedWidth(160)
|
||||
# layout.addWidget(self.value)
|
||||
# self.unit = unit
|
||||
# self.highlight = highlight
|
||||
# if highlight:
|
||||
# font = QFont()
|
||||
# font.setBold(True)
|
||||
# font.setPointSize(14)
|
||||
# self.label.setFont(font)
|
||||
# self.value.setFont(font)
|
||||
|
||||
# def set_text(self, text):
|
||||
# if self.unit is not None:
|
||||
# text = text + ' ' + self.unit
|
||||
# self.value.setText(text)
|
||||
class TextIndicator(QWidget):
|
||||
def __init__(self, label):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(140)
|
||||
self.label.setContentsMargins(0, 0, 10, 0)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.text = QLabel("-")
|
||||
self.text.setAlignment(Qt.AlignTop) # type: ignore
|
||||
layout.addWidget(self.text)
|
||||
|
||||
def setLabel(self, label) -> None:
|
||||
self.label.setText(label)
|
||||
|
||||
def setText(self, text):
|
||||
self.text.setText(text)
|
||||
|
||||
def setColor(self, color: str):
|
||||
self.text.setStyleSheet(f"QLabel {{color:{color}}}")
|
||||
|
||||
|
||||
# class Button(QWidget):
|
||||
# def __init__(self, label, label_button):
|
||||
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
X01DA / Debye Beamline Parameters.
|
||||
This file describes the parameter of each component of the Debye beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
import xrt.backends.raycing.materials as rm
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType]
|
||||
stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType]
|
||||
stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType]
|
||||
stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType]
|
||||
stripePyrex = rm.Material(
|
||||
"Si", rho=2.20
|
||||
) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterSi3N4 = rm.Material(
|
||||
["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate"
|
||||
) # pyright: ignore[reportArgumentType]
|
||||
filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
# General parameters
|
||||
sourceHeight = 0
|
||||
|
||||
# Synchrotron
|
||||
synchrotron = namedtuple(
|
||||
"synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"]
|
||||
)
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"])
|
||||
|
||||
sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4)
|
||||
|
||||
sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1)
|
||||
|
||||
sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5)
|
||||
|
||||
sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0)
|
||||
|
||||
# FE slits
|
||||
fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"])
|
||||
|
||||
feSlits = fe_slits(
|
||||
name="FE-SLITS",
|
||||
center=(0, 6117, sourceHeight),
|
||||
center1=(0, 5045, sourceHeight),
|
||||
center2=(0, 5289.5, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,
|
||||
)
|
||||
|
||||
# FE Window
|
||||
filt = namedtuple(
|
||||
"filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"]
|
||||
)
|
||||
|
||||
feWindow = filt(
|
||||
name="FE-WINDOW",
|
||||
center=(0.0, 7020, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3.0, 3.0),
|
||||
surface="None",
|
||||
material=filterDiamond,
|
||||
thickness=0.1,
|
||||
)
|
||||
feWindow = feWindow._replace(surface=f"CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m")
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple(
|
||||
"collimatingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surface",
|
||||
"material",
|
||||
"limPhysX",
|
||||
"limPhysY",
|
||||
"limOptX",
|
||||
"limOptY",
|
||||
"R",
|
||||
"pitch",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
cm = collimatingMirror(
|
||||
name="FE-CM",
|
||||
center=[0, 6890, sourceHeight],
|
||||
surface=("Si", "Pt", "Rh"),
|
||||
material=(stripeSi, stripePt, stripeRh),
|
||||
limPhysX=(-34, 34),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((-21, -7, 14), (-11, 11, 23)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, -0.0e-3],
|
||||
jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global)
|
||||
jack2=[-210.0, 8310.0, 0.0],
|
||||
jack3=[210.0, 8310.0, 0.0],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],
|
||||
) # X-Stage 2
|
||||
|
||||
apertures = namedtuple("apertures", ["name", "center", "opening"])
|
||||
|
||||
fePS = apertures(
|
||||
name="FE-PS", center=[0, 8815, sourceHeight], opening=[-20.0, 20.0, -20.0 + 12.5, 20.0 + 12.5]
|
||||
) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name="OP-WB-BS-BLOCK", center=[0.0, 13860, sourceHeight], opening=[-18.0, 18.0, 25, 85.5]
|
||||
) # left, right, bottom, top
|
||||
# opening=[-18., 18., 42, 76], # X10DA
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple(
|
||||
"monochromator",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"xtal",
|
||||
"material1",
|
||||
"material2",
|
||||
"xtalWidth",
|
||||
"xtalOffsetX",
|
||||
"xtalLength1",
|
||||
"xtalLength2",
|
||||
"xtalGap",
|
||||
"rotOffset",
|
||||
"heightOffset",
|
||||
"braggLim",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx",
|
||||
],
|
||||
)
|
||||
|
||||
mo1 = monochromator(
|
||||
name="OP-MO1",
|
||||
center=[0.0, 11750, sourceHeight],
|
||||
xtal=("Si311", "Si111"),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth=(24, 24),
|
||||
xtalOffsetX=(-21.2, 21.2),
|
||||
xtalLength1=(55, 55),
|
||||
xtalLength2=(105, 105),
|
||||
xtalGap=(8, 8),
|
||||
rotOffset=6,
|
||||
heightOffset=8.5,
|
||||
braggLim=[3.6, 33],
|
||||
jack1=[0.0, 11350.0, 0.0], # Tripod maybe not available!
|
||||
jack2=[-400.0, 12350.0, 0.0],
|
||||
jack3=[400.0, 12350.0, 0.0],
|
||||
tx=0.0,
|
||||
) # X-Stage [x]
|
||||
|
||||
mo2 = monochromator(
|
||||
name="OP-CCM2",
|
||||
center=[0.0, 13250, sourceHeight],
|
||||
xtal=("Si311", "Si111"),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth=(24, 24),
|
||||
xtalOffsetX=(-21, 21),
|
||||
xtalLength1=(55, 55),
|
||||
xtalLength2=(105, 105),
|
||||
xtalGap=(8, 8),
|
||||
rotOffset=6,
|
||||
heightOffset=8.5,
|
||||
braggLim=[3.6, 33],
|
||||
jack1=[0.0, 13350.0, 0.0], # Tripod maybe not available!
|
||||
jack2=[-400.0, 14350.0, 0.0],
|
||||
jack3=[400.0, 14350.0, 0.0],
|
||||
tx=0.0,
|
||||
) # X-Stage [x]
|
||||
|
||||
# OP Slits
|
||||
op_slits = namedtuple("op_slits", ["name", "center"])
|
||||
|
||||
opSlits1 = op_slits(name="OP-SLITS 1", center=(0, 14349.6, sourceHeight))
|
||||
|
||||
opSlits2 = op_slits(name="OP-SLITS 2", center=(0, 18134.8, sourceHeight))
|
||||
|
||||
# OP Beam Monitors
|
||||
op_bm = namedtuple("op_bm", ["name", "center"])
|
||||
|
||||
opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14599.6, sourceHeight))
|
||||
|
||||
opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 18384.8, sourceHeight))
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple(
|
||||
"focusingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surfaceToroid",
|
||||
"materialToroid",
|
||||
"surfaceFlat",
|
||||
"materialFlat",
|
||||
"limPhysXToroid",
|
||||
"limPhysYToroid",
|
||||
"limPhysXFlat",
|
||||
"limPhysYFlat",
|
||||
"limOptXToroid",
|
||||
"limOptYToroid",
|
||||
"limOptXFlat",
|
||||
"limOptYFlat",
|
||||
"R",
|
||||
"pitch",
|
||||
"r",
|
||||
"xToroid",
|
||||
"xFlat",
|
||||
"hToroid",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
fm = focusingMirror(
|
||||
name="OP-FM",
|
||||
center=[0.0, 15670, sourceHeight], # nominal height 58 mm above ring, SLS1!
|
||||
surfaceToroid=("Rh", "Pt"),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
surfaceFlat=("Rh", "Pt"),
|
||||
materialFlat=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-79.0, 79.0),
|
||||
limPhysYToroid=(-575.0, 575.0),
|
||||
limPhysXFlat=(-79.0, 79.0),
|
||||
limPhysYFlat=(-575.0, 575.0),
|
||||
limOptXToroid=((-38, 66), (-66, 31)),
|
||||
limOptYToroid=((-500.0, -500.0), (500.0, 500.0)),
|
||||
limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)),
|
||||
limOptYFlat=((-500.0, -500.0), (500.0, 500.0)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, 0e-3],
|
||||
r=[35.510, 24.986],
|
||||
xToroid=[-52, 48.5], # offset in local x
|
||||
xFlat=[-20.95, 8.55],
|
||||
hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[-130.0, 15535 - 538.0, 0.0],
|
||||
jack2=[130.0, 15535 + 538.0, 0.0],
|
||||
jack3=[0.0, 15535 + 538.0, 0.0],
|
||||
tx1=[0.0, -575.0], # X-Stage 1 [x, y]
|
||||
tx2=[0.0, 575.0],
|
||||
) # X-Stage 2 [x, y]
|
||||
|
||||
# EH Window
|
||||
ehWindow = filt(
|
||||
name="EH-WINDOW",
|
||||
center=(0.0, 19998.3, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-20.0, 20.0),
|
||||
limPhysY=(-4, 4),
|
||||
surface="None",
|
||||
material=filterSi3N4,
|
||||
thickness=0.002,
|
||||
)
|
||||
ehWindow = ehWindow._replace(surface=f"Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m")
|
||||
|
||||
# Sample
|
||||
sample = namedtuple("sample", ["name", "center"])
|
||||
|
||||
smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight])
|
||||
|
||||
smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight])
|
||||
|
||||
# Vacuum pipes
|
||||
# DN40CF ID = 35 mm oder 37 mm
|
||||
# DN50CF ID = 47.5 mm
|
||||
# DN63CF ID = 60.2 mm oder 66 mm
|
||||
# DN100CF ID = 97.4 mm oder 104 mm
|
||||
pipe = namedtuple("pipes", ["center", "diameter", "start", "end"])
|
||||
vacuum_pipes = pipe(
|
||||
center=[27.5, (37.5 + 27.5) / 2, 37.5, 62.5, 72.5],
|
||||
diameter=[97.4, 97.4, 97.4, 97.4, 97.4],
|
||||
start=[10952.88, 11750 + 250, mo2.center[1] + 250, 14000, fm.center[1]],
|
||||
end=[11750 - 250, mo2.center[1] - 250, 14000, fm.center[1], ehWindow.center[1]],
|
||||
)
|
||||
|
||||
Walls = namedtuple("walls", ["start", "end", "height"])
|
||||
walls = Walls(start=[13999.30], end=[13999 + 75.5 + 30], height=[[-20, 25]])
|
||||
@@ -1,311 +0,0 @@
|
||||
"""
|
||||
X01DA / Debye Beamline Parameters.
|
||||
This file describes the parameter of each component of the Debye beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
from collections import namedtuple
|
||||
|
||||
import xrt.backends.raycing.materials as rm
|
||||
|
||||
# if os.environ.get("USE_XRT", "True").lower() in ("1", "true", "yes"):
|
||||
# import xrt.backends.raycing.materials as rm # type: ignore
|
||||
# else:
|
||||
# class _DummyClass:
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# pass
|
||||
# class _DummyMaterials:
|
||||
# Material = _DummyClass
|
||||
# CrystalSi = _DummyClass
|
||||
# rm = _DummyMaterials()
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
|
||||
stripeSi = rm.Material('Si', rho=2.33) # pyright: ignore[reportArgumentType]
|
||||
stripePt = rm.Material('Pt', rho=21.45) # pyright: ignore[reportArgumentType]
|
||||
stripeRh = rm.Material('Rh', rho=12.41) # pyright: ignore[reportArgumentType]
|
||||
stripeCr = rm.Material('Cr', rho=7.14) # pyright: ignore[reportArgumentType]
|
||||
stripePyrex = rm.Material('Si', rho=2.20) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterBe = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterSi3N4 = rm.Material(['Si', 'N'], quantities=[3, 4], rho=3.44, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterAl = rm.Material('Al', rho=2.69, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
|
||||
# General parameters
|
||||
sourceHeight = 0
|
||||
|
||||
#Synchrotron
|
||||
synchrotron = namedtuple('synchrotron', ['eE', 'eI', 'eEspread',
|
||||
'eEpsilonX', 'eEpsilonZ', 'betaX', 'betaZ'])
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE = 2.4,
|
||||
eI = 0.4,
|
||||
eEspread=0.878e-3,
|
||||
eEpsilonX=5.63,
|
||||
eEpsilonZ=0.007,
|
||||
betaX=0.45,
|
||||
betaZ=14.4,
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7,
|
||||
eI=0.4,
|
||||
eEspread=1.147e-3,
|
||||
eEpsilonX=0.156,
|
||||
eEpsilonZ=0.01,
|
||||
betaX=0.18,
|
||||
betaZ=4.6,
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple('bendingMagnet', ['name', 'center', 'sync', 'B0'])
|
||||
|
||||
sls1_14t = bendingMagnet(
|
||||
name='FE-BM-SLS1-1.4T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls1,
|
||||
B0=1.4,)
|
||||
|
||||
sls2_21t = bendingMagnet(
|
||||
name='FE-BM-SLS2-2.1T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=2.1,)
|
||||
|
||||
sls2_35t = bendingMagnet(
|
||||
name='FE-BM-SLS2-3.5T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=3.5,)
|
||||
|
||||
sls2_50t = bendingMagnet(
|
||||
name='FE-BM-SLS2-5.0T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=5.0,)
|
||||
|
||||
# FE slits
|
||||
fe_slits = namedtuple('slits', ['name', 'center', 'center1', 'center2', 'maxDivH', 'maxDivV'])
|
||||
|
||||
feSlits = fe_slits(
|
||||
name='FE-SLITS',
|
||||
center=(0, 6117, sourceHeight),
|
||||
center1=(0, 5045, sourceHeight),
|
||||
center2=(0, 5289.5, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,)
|
||||
|
||||
# FE Window
|
||||
filt = namedtuple('filt', ['name', 'center', 'pitch', 'limPhysX', 'limPhysY', 'surface', 'material', 'thickness'])
|
||||
|
||||
feWindow = filt(
|
||||
name='FE-WINDOW',
|
||||
center=(0., 7020, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3., 3.),
|
||||
surface='None',
|
||||
material=filterDiamond,
|
||||
thickness=0.1,)
|
||||
feWindow = feWindow._replace(surface=f'CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m')
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple('collimatingMirror', ['name',
|
||||
'center', 'surface', 'material', 'limPhysX', 'limPhysY',
|
||||
'limOptX', 'limOptY', 'R', 'pitch', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
cm = collimatingMirror(
|
||||
name='FE-CM',
|
||||
center=[0, 6890, sourceHeight],
|
||||
surface=('Si','Pt','Rh'),
|
||||
material=(stripeSi, stripePt, stripeRh),
|
||||
limPhysX=(-34, 34),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((-21, -7, 14), (-11, 11, 23)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, -0.0e-3],
|
||||
jack1=[0., 7210., 0.], #Tripod X, Y, Z (global)
|
||||
jack2=[-210., 8310., 0.],
|
||||
jack3=[210., 8310., 0.],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],) # X-Stage 2
|
||||
|
||||
apertures = namedtuple('apertures', ['name', 'center', 'opening'])
|
||||
|
||||
fePS = apertures(
|
||||
name='FE-PS',
|
||||
center=[0, 8815, sourceHeight],
|
||||
opening=[-20., 20., -20.+12.5, 20.+12.5]) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name='OP-WB-BS-BLOCK',
|
||||
center=[0., 13860, sourceHeight],
|
||||
opening=[-18., 18., 25, 85.5]) # left, right, bottom, top
|
||||
# opening=[-18., 18., 42, 76], # X10DA
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple('monochromator', ['name', 'center',
|
||||
'xtal', 'material1', 'material2', 'xtalWidth', 'xtalOffsetX',
|
||||
'xtalLength1', 'xtalLength2', 'xtalGap', 'rotOffset',
|
||||
'heightOffset', 'braggLim', 'jack1', 'jack2', 'jack3', 'tx'])
|
||||
|
||||
mo1 = monochromator(
|
||||
name='OP-MO1',
|
||||
center=[0., 11750, sourceHeight],
|
||||
xtal=('Si311','Si111'),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth = (24, 24),
|
||||
xtalOffsetX=(-21.2, 21.2),
|
||||
xtalLength1 = (55, 55),
|
||||
xtalLength2 = (105, 105),
|
||||
xtalGap = (8, 8),
|
||||
rotOffset = 6,
|
||||
heightOffset = 8.5,
|
||||
braggLim = [3.6, 33],
|
||||
jack1=[0., 11350., 0.], #Tripod maybe not available!
|
||||
jack2=[-400., 12350., 0.],
|
||||
jack3=[400., 12350., 0.],
|
||||
tx=0.0,) # X-Stage [x]
|
||||
|
||||
mo2 = monochromator(
|
||||
name='OP-CCM2',
|
||||
center=[0., 13250, sourceHeight],
|
||||
xtal=('Si311','Si111'),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth = (24, 24),
|
||||
xtalOffsetX=(-21, 21),
|
||||
xtalLength1 = (55, 55),
|
||||
xtalLength2 = (105, 105),
|
||||
xtalGap = (8, 8),
|
||||
rotOffset = 6,
|
||||
heightOffset = 8.5,
|
||||
braggLim = [3.6, 33],
|
||||
jack1=[0., 13350., 0.], #Tripod maybe not available!
|
||||
jack2=[-400., 14350., 0.],
|
||||
jack3=[400., 14350., 0.],
|
||||
tx=0.0,) # X-Stage [x]
|
||||
|
||||
# OP Slits
|
||||
op_slits = namedtuple('op_slits', ['name', 'center'])
|
||||
|
||||
opSlits1 = op_slits(
|
||||
name='OP-SLITS 1',
|
||||
center=(0, 14349.6, sourceHeight),
|
||||
)
|
||||
|
||||
opSlits2 = op_slits(
|
||||
name='OP-SLITS 2',
|
||||
center=(0, 18134.8, sourceHeight),
|
||||
)
|
||||
|
||||
# OP Beam Monitors
|
||||
op_bm = namedtuple('op_bm', ['name', 'center'])
|
||||
|
||||
opBM1 = op_bm(
|
||||
name='OP Beam Monitor 1',
|
||||
center=(0, 14599.6, sourceHeight),
|
||||
)
|
||||
|
||||
opBM2 = op_bm(
|
||||
name='OP Beam Monitor 2',
|
||||
center=(0, 18384.8, sourceHeight),
|
||||
)
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple('focusingMirror', ['name', 'center',
|
||||
'surfaceToroid', 'materialToroid', 'surfaceFlat', 'materialFlat',
|
||||
'limPhysXToroid', 'limPhysYToroid', 'limPhysXFlat', 'limPhysYFlat',
|
||||
'limOptXToroid', 'limOptYToroid', 'limOptXFlat', 'limOptYFlat',
|
||||
'R', 'pitch', 'r', 'xToroid', 'xFlat', 'hToroid', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
fm = focusingMirror(
|
||||
name='OP-FM',
|
||||
center=[0., 15670, sourceHeight], # nominal height 58 mm above ring, SLS1!
|
||||
surfaceToroid=('Rh', 'Pt'),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
surfaceFlat=('Rh', 'Pt'),
|
||||
materialFlat=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-79., 79.),
|
||||
limPhysYToroid=(-575., 575.),
|
||||
limPhysXFlat=(-79., 79.),
|
||||
limPhysYFlat=(-575., 575.),
|
||||
limOptXToroid=((-38, 66), (-66, 31)),
|
||||
limOptYToroid=((-500., -500.), (500., 500.)),
|
||||
limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)),
|
||||
limOptYFlat=((-500., -500.), (500., 500.)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, 0e-3],
|
||||
r=[35.510, 24.986],
|
||||
xToroid=[-52, 48.5], # offset in local x
|
||||
xFlat = [-20.95, 8.55],
|
||||
hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[-130., 15535-538., 0.],
|
||||
jack2=[130., 15535+538., 0.],
|
||||
jack3=[0., 15535+538., 0.],
|
||||
tx1=[0., -575.], # X-Stage 1 [x, y]
|
||||
tx2=[0., 575.],) # X-Stage 2 [x, y]
|
||||
|
||||
# EH Window
|
||||
ehWindow = filt(
|
||||
name='EH-WINDOW',
|
||||
center=(0., 19998.3, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-20., 20.),
|
||||
limPhysY=(-4, 4),
|
||||
surface='None',
|
||||
material=filterSi3N4,
|
||||
thickness=0.002,)
|
||||
ehWindow = ehWindow._replace(surface=f'Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m')
|
||||
|
||||
# Sample
|
||||
sample = namedtuple('sample', ['name', 'center'])
|
||||
|
||||
smpl = sample(
|
||||
name='EH-SMPL',
|
||||
center=[0, 23365, sourceHeight],)
|
||||
|
||||
smpl2 = sample(
|
||||
name='EH-SMPL2',
|
||||
center=[0, 27500, sourceHeight],)
|
||||
|
||||
# Vacuum pipes
|
||||
# DN40CF ID = 35 mm oder 37 mm
|
||||
# DN50CF ID = 47.5 mm
|
||||
# DN63CF ID = 60.2 mm oder 66 mm
|
||||
# DN100CF ID = 97.4 mm oder 104 mm
|
||||
pipe = namedtuple('pipes', ['center', 'diameter', 'start', 'end'])
|
||||
vacuum_pipes = pipe(
|
||||
center= [27.5, (37.5+27.5)/2, 37.5, 62.5, 72.5],
|
||||
diameter=[97.4, 97.4, 97.4, 97.4, 97.4],
|
||||
start= [10952.88, 11750+250, mo2.center[1]+250, 14000, fm.center[1]],
|
||||
end= [11750-250, mo2.center[1]-250, 14000, fm.center[1], ehWindow.center[1]],
|
||||
)
|
||||
|
||||
Walls = namedtuple('walls', ['start', 'end', 'height'])
|
||||
walls = Walls(
|
||||
start= [13999.30],
|
||||
end= [13999+75.5+30],
|
||||
height= [[-20, 25]],
|
||||
)
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
import cv2
|
||||
from bec_lib.file_utils import get_full_path
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from ophyd_devices import DeviceStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
@@ -21,6 +24,7 @@ CAM_USERNAME = "camera_user"
|
||||
CAM_PASSWORD = "camera_user1"
|
||||
CAM_PORT = 554
|
||||
|
||||
|
||||
class HutchCam(PSIDeviceBase):
|
||||
"""Class for the Hutch Cameras"""
|
||||
|
||||
@@ -28,7 +32,7 @@ class HutchCam(PSIDeviceBase):
|
||||
|
||||
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
|
||||
super().__init__(name=name, scan_info=scan_info, **kwargs)
|
||||
|
||||
self.scan_parameters: ScanServerScanInfo = None
|
||||
self.hostname = prefix
|
||||
self.status = None
|
||||
|
||||
@@ -47,33 +51,36 @@ class HutchCam(PSIDeviceBase):
|
||||
|
||||
def on_stage(self) -> DeviceStatus:
|
||||
"""Called while staging the device."""
|
||||
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
file_path = get_full_path(scan_msg, name='hutch_cam_' + self.hostname).removesuffix('h5')
|
||||
self.scan_parameters = fetch_scan_info(self.scan_info)
|
||||
file_path = get_full_path(self.scan_info, name="hutch_cam_" + self.hostname).removesuffix(
|
||||
"h5"
|
||||
)
|
||||
|
||||
self.status = DeviceStatus(self)
|
||||
|
||||
thread = threading.Thread(target=self._save_picture, args=(file_path, self.status), daemon=True)
|
||||
thread = threading.Thread(
|
||||
target=self._save_picture, args=(file_path, self.status), daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return self.status
|
||||
|
||||
def _save_picture(self, file_path, status):
|
||||
try:
|
||||
logger.info(f'Capture from camera {self.hostname}')
|
||||
logger.info(f"Capture from camera {self.hostname}")
|
||||
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
|
||||
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
|
||||
if not cap.isOpened():
|
||||
logger.error("Connection Failed", "Could not connect to the camera stream.")
|
||||
return
|
||||
logger.info(f'Connection to camera {self.hostname} established')
|
||||
logger.info(f"Connection to camera {self.hostname} established")
|
||||
ret, frame = cap.readAsync()
|
||||
cap.release()
|
||||
if not ret:
|
||||
logger.error("Capture Failed", "Failed to capture image from camera.")
|
||||
return
|
||||
cv2.imwrite(file_path + 'png', frame)
|
||||
cv2.imwrite(file_path + "png", frame)
|
||||
status.set_finished()
|
||||
logger.info(f'Capture from camera {self.hostname} done')
|
||||
logger.info(f"Capture from camera {self.hostname} done")
|
||||
except Exception as e:
|
||||
status.set_exception(e)
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Literal
|
||||
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import DeviceStatus, StatusBase
|
||||
from ophyd.status import WaitTimeoutError
|
||||
@@ -33,6 +34,7 @@ from debye_bec.devices.mo1_bragg.mo1_bragg_enums import (
|
||||
TriggerControlSource,
|
||||
)
|
||||
from debye_bec.devices.mo1_bragg.mo1_bragg_utils import compute_spline
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
# Initialise logger
|
||||
logger = bec_logger.logger
|
||||
@@ -44,36 +46,6 @@ class Mo1BraggError(Exception):
|
||||
"""Exception for the Mo1 Bragg positioner"""
|
||||
|
||||
|
||||
########## Scan Parameter Model ##########
|
||||
|
||||
|
||||
class ScanParameter(BaseModel):
|
||||
"""Dataclass to store the scan parameters for the Mo1 Bragg positioner.
|
||||
This needs to be in sync with the kwargs of the MO1 Bragg scans from Debye, to
|
||||
ensure that the scan parameters are correctly set. Any changes in the scan kwargs,
|
||||
i.e. renaming or adding new parameters, need to be represented here as well."""
|
||||
|
||||
scan_time: float | None = Field(None, description="Scan time for a half oscillation")
|
||||
scan_duration: float | None = Field(None, description="Duration of the scan")
|
||||
break_enable_low: bool | None = Field(
|
||||
None, description="Break enabled for low, should be PV trig_ena_lo_enum"
|
||||
) # trig_enable_low: bool = None
|
||||
break_enable_high: bool | None = Field(
|
||||
None, description="Break enabled for high, should be PV trig_ena_hi_enum"
|
||||
) # trig_enable_high: bool = None
|
||||
break_time_low: float | None = Field(None, description="Break time low energy/angle")
|
||||
break_time_high: float | None = Field(None, description="Break time high energy/angle")
|
||||
cycle_low: int | None = Field(None, description="Cycle for low energy/angle")
|
||||
cycle_high: int | None = Field(None, description="Cycle for high energy/angle")
|
||||
exp_time: float | None = Field(None, description="XRD trigger period")
|
||||
n_of_trigger: int | None = Field(None, description="Amount of XRD triggers")
|
||||
start: float | None = Field(None, description="Start value for energy/angle")
|
||||
stop: float | None = Field(None, description="Stop value for energy/angle")
|
||||
p_kink: float | None = Field(None, description="P Kink")
|
||||
e_kink: float | None = Field(None, description="Energy Kink")
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
########### Mo1 Bragg Motor Class ###########
|
||||
|
||||
|
||||
@@ -85,7 +57,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
|
||||
|
||||
progress_signal = Cpt(ProgressSignal, name="progress_signal")
|
||||
|
||||
USER_ACCESS = ["set_advanced_xas_settings", "set_xtal"]
|
||||
USER_ACCESS = ["set_advanced_xas_settings", "set_xtal", "convert_angle_energy"]
|
||||
|
||||
def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): # type: ignore
|
||||
"""
|
||||
@@ -96,8 +68,15 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
|
||||
scan_info (ScanInfo): The scan info to use.
|
||||
"""
|
||||
super().__init__(name=name, scan_info=scan_info, prefix=prefix, **kwargs)
|
||||
self.scan_parameter = ScanParameter()
|
||||
self.scan_parameters: ScanServerScanInfo = None
|
||||
self.timeout_for_pvwait = 7.5
|
||||
self.valid_scan_names = [
|
||||
"xas_simple_scan",
|
||||
"xas_simple_scan_with_xrd",
|
||||
"xas_advanced_scan",
|
||||
"xas_advanced_scan_with_xrd",
|
||||
"nidaq_continuous_scan",
|
||||
]
|
||||
|
||||
########################################
|
||||
# Beamline Specific Implementations #
|
||||
@@ -124,94 +103,187 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
|
||||
|
||||
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
|
||||
"""
|
||||
self.scan_parameters = fetch_scan_info(self.scan_info)
|
||||
if self.scan_control.scan_msg.get() != ScanControlLoadMessage.PENDING:
|
||||
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING)
|
||||
self.cancel_on_stop(status)
|
||||
self.scan_control.scan_val_reset.put(1)
|
||||
status.wait(timeout=self.timeout_for_pvwait)
|
||||
|
||||
scan_name = self.scan_info.msg.scan_name
|
||||
self._update_scan_parameter()
|
||||
if scan_name == "xas_simple_scan":
|
||||
self.set_xas_settings(
|
||||
low=self.scan_parameter.start,
|
||||
high=self.scan_parameter.stop,
|
||||
scan_time=self.scan_parameter.scan_time,
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=False,
|
||||
enable_high=False,
|
||||
break_time_low=0,
|
||||
break_time_high=0,
|
||||
cycle_low=0,
|
||||
cycle_high=0,
|
||||
exp_time=0,
|
||||
n_of_trigger=0,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration
|
||||
)
|
||||
elif scan_name == "xas_simple_scan_with_xrd":
|
||||
self.set_xas_settings(
|
||||
low=self.scan_parameter.start,
|
||||
high=self.scan_parameter.stop,
|
||||
scan_time=self.scan_parameter.scan_time,
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=self.scan_parameter.break_enable_low,
|
||||
enable_high=self.scan_parameter.break_enable_high,
|
||||
break_time_low=self.scan_parameter.break_time_low,
|
||||
break_time_high=self.scan_parameter.break_time_high,
|
||||
cycle_low=self.scan_parameter.cycle_low,
|
||||
cycle_high=self.scan_parameter.cycle_high,
|
||||
exp_time=self.scan_parameter.exp_time,
|
||||
n_of_trigger=self.scan_parameter.n_of_trigger,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration
|
||||
)
|
||||
elif scan_name == "xas_advanced_scan":
|
||||
self.set_advanced_xas_settings(
|
||||
low=self.scan_parameter.start,
|
||||
high=self.scan_parameter.stop,
|
||||
scan_time=self.scan_parameter.scan_time,
|
||||
p_kink=self.scan_parameter.p_kink,
|
||||
e_kink=self.scan_parameter.e_kink,
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=False,
|
||||
enable_high=False,
|
||||
break_time_low=0,
|
||||
break_time_high=0,
|
||||
cycle_low=0,
|
||||
cycle_high=0,
|
||||
exp_time=0,
|
||||
n_of_trigger=0,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration
|
||||
)
|
||||
elif scan_name == "xas_advanced_scan_with_xrd":
|
||||
self.set_advanced_xas_settings(
|
||||
low=self.scan_parameter.start,
|
||||
high=self.scan_parameter.stop,
|
||||
scan_time=self.scan_parameter.scan_time,
|
||||
p_kink=self.scan_parameter.p_kink,
|
||||
e_kink=self.scan_parameter.e_kink,
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=self.scan_parameter.break_enable_low,
|
||||
enable_high=self.scan_parameter.break_enable_high,
|
||||
break_time_low=self.scan_parameter.break_time_low,
|
||||
break_time_high=self.scan_parameter.break_time_high,
|
||||
cycle_low=self.scan_parameter.cycle_low,
|
||||
cycle_high=self.scan_parameter.cycle_high,
|
||||
exp_time=self.scan_parameter.exp_time,
|
||||
n_of_trigger=self.scan_parameter.n_of_trigger,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration
|
||||
scan_name = self.scan_parameters.scan_name
|
||||
if self._check_if_scan_name_is_valid(self.scan_parameters):
|
||||
if self.scan_parameters.positions is not None:
|
||||
start, stop = (
|
||||
self.scan_parameters.positions
|
||||
if len(self.scan_parameters.positions) == 2
|
||||
else (None, None)
|
||||
)
|
||||
else:
|
||||
start, stop = (None, None)
|
||||
scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None)
|
||||
scan_duration = self.scan_parameters.additional_scan_parameters.get(
|
||||
"scan_duration", None
|
||||
)
|
||||
if scan_name == "xas_simple_scan":
|
||||
if any(param is None for param in [start, stop, scan_time, scan_duration]):
|
||||
raise Mo1BraggError(
|
||||
f"Missing scan parameters for xas_simple_scan. Required parameters: start, stop, scan_time, scan_duration in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
|
||||
)
|
||||
self.set_xas_settings(low=start, high=stop, scan_time=scan_time)
|
||||
self.set_trig_settings(
|
||||
enable_low=False,
|
||||
enable_high=False,
|
||||
break_time_low=0,
|
||||
break_time_high=0,
|
||||
cycle_low=0,
|
||||
cycle_high=0,
|
||||
exp_time=0,
|
||||
n_of_trigger=0,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.SIMPLE, scan_duration=scan_duration
|
||||
)
|
||||
elif scan_name == "xas_simple_scan_with_xrd":
|
||||
break_enable_low = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_low", None
|
||||
)
|
||||
break_enable_high = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_high", None
|
||||
)
|
||||
break_time_low = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_time_low", None
|
||||
)
|
||||
break_time_high = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_time_high", None
|
||||
)
|
||||
cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None)
|
||||
cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None)
|
||||
exp_time = self.scan_parameters.exp_time
|
||||
n_of_trigger = self.scan_parameters.additional_scan_parameters.get(
|
||||
"n_of_trigger", None
|
||||
)
|
||||
if any(
|
||||
param is None
|
||||
for param in [
|
||||
start,
|
||||
stop,
|
||||
scan_time,
|
||||
scan_duration,
|
||||
break_enable_low,
|
||||
break_enable_high,
|
||||
break_time_low,
|
||||
break_time_high,
|
||||
cycle_low,
|
||||
cycle_high,
|
||||
exp_time,
|
||||
n_of_trigger,
|
||||
]
|
||||
):
|
||||
raise Mo1BraggError(
|
||||
f"Missing scan parameters for xas_simple_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
|
||||
)
|
||||
self.set_xas_settings(low=start, high=stop, scan_time=scan_time)
|
||||
self.set_trig_settings(
|
||||
enable_low=break_enable_low,
|
||||
enable_high=break_enable_high,
|
||||
break_time_low=break_time_low,
|
||||
break_time_high=break_time_high,
|
||||
cycle_low=cycle_low,
|
||||
cycle_high=cycle_high,
|
||||
exp_time=exp_time,
|
||||
n_of_trigger=n_of_trigger,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.SIMPLE, scan_duration=scan_duration
|
||||
)
|
||||
elif scan_name == "xas_advanced_scan":
|
||||
p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None)
|
||||
e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None)
|
||||
if any(
|
||||
param is None
|
||||
for param in [start, stop, scan_time, scan_duration, p_kink, e_kink]
|
||||
):
|
||||
raise Mo1BraggError(
|
||||
f"Missing scan parameters for xas_advanced_scan. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
|
||||
)
|
||||
self.set_advanced_xas_settings(
|
||||
low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=False,
|
||||
enable_high=False,
|
||||
break_time_low=0,
|
||||
break_time_high=0,
|
||||
cycle_low=0,
|
||||
cycle_high=0,
|
||||
exp_time=0,
|
||||
n_of_trigger=0,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.ADVANCED, scan_duration=scan_duration
|
||||
)
|
||||
elif scan_name == "xas_advanced_scan_with_xrd":
|
||||
p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None)
|
||||
e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None)
|
||||
break_enable_low = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_low", None
|
||||
)
|
||||
break_enable_high = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_high", None
|
||||
)
|
||||
break_time_low = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_time_low", None
|
||||
)
|
||||
break_time_high = self.scan_parameters.additional_scan_parameters.get(
|
||||
"break_time_high", None
|
||||
)
|
||||
cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None)
|
||||
cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None)
|
||||
exp_time = self.scan_parameters.exp_time
|
||||
n_of_trigger = self.scan_parameters.additional_scan_parameters.get(
|
||||
"n_of_trigger", None
|
||||
)
|
||||
if any(
|
||||
param is None
|
||||
for param in [
|
||||
start,
|
||||
stop,
|
||||
scan_time,
|
||||
scan_duration,
|
||||
p_kink,
|
||||
e_kink,
|
||||
break_enable_low,
|
||||
break_enable_high,
|
||||
break_time_low,
|
||||
break_time_high,
|
||||
cycle_low,
|
||||
cycle_high,
|
||||
exp_time,
|
||||
n_of_trigger,
|
||||
]
|
||||
):
|
||||
raise Mo1BraggError(
|
||||
f"Missing scan parameters for xas_advanced_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
|
||||
)
|
||||
|
||||
self.set_advanced_xas_settings(
|
||||
low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink
|
||||
)
|
||||
self.set_trig_settings(
|
||||
enable_low=break_enable_low,
|
||||
enable_high=break_enable_high,
|
||||
break_time_low=break_time_low,
|
||||
break_time_high=break_time_high,
|
||||
cycle_low=cycle_low,
|
||||
cycle_high=cycle_high,
|
||||
exp_time=exp_time,
|
||||
n_of_trigger=n_of_trigger,
|
||||
)
|
||||
self.set_scan_control_settings(
|
||||
mode=ScanControlMode.ADVANCED, scan_duration=scan_duration
|
||||
)
|
||||
else:
|
||||
return # Should never happen.
|
||||
else:
|
||||
return
|
||||
# Setting scan duration seems to lag behind slightly in the backend, include small sleep
|
||||
@@ -291,6 +363,13 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
|
||||
self.stopped = True # Needs to be set to stop motion
|
||||
|
||||
######### Utility Methods #########
|
||||
|
||||
def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool:
|
||||
"""Check if the scan is within the list of scans for which the backend is working"""
|
||||
if scan_parameters.scan_name in self.valid_scan_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Callback method to update the scan progress, runs a callback
|
||||
to SUB_PROGRESS subscribers, i.e. BEC.
|
||||
@@ -468,12 +547,3 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
|
||||
|
||||
for s in status_list:
|
||||
s.wait(timeout=self.timeout_for_pvwait)
|
||||
|
||||
def _update_scan_parameter(self):
|
||||
"""Get the scan_info parameters for the scan."""
|
||||
for key, value in self.scan_info.msg.request_inputs["inputs"].items():
|
||||
if hasattr(self.scan_parameter, key):
|
||||
setattr(self.scan_parameter, key, value)
|
||||
for key, value in self.scan_info.msg.request_inputs["kwargs"].items():
|
||||
if hasattr(self.scan_parameter, key):
|
||||
setattr(self.scan_parameter, key, value)
|
||||
|
||||
@@ -182,7 +182,7 @@ class Mo1TriggerSettings(Device):
|
||||
class Mo1BraggCalculator(Device):
|
||||
"""Mo1 Bragg PVs to convert angle to energy or vice-versa."""
|
||||
|
||||
calc_reset = Cpt(EpicsSignal, suffix="calc_reset", kind="config", put_complete=True)
|
||||
calc_reset = Cpt(EpicsSignalWithRBV, suffix="calc_reset", kind="config", put_complete=True)
|
||||
calc_done = Cpt(EpicsSignalRO, suffix="calc_done_RBV", kind="config")
|
||||
calc_energy = Cpt(EpicsSignalWithRBV, suffix="calc_energy", kind="config")
|
||||
calc_angle = Cpt(EpicsSignalWithRBV, suffix="calc_angle", kind="config")
|
||||
|
||||
+117
-418
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Kind, StatusBase
|
||||
from ophyd.status import WaitTimeoutError
|
||||
@@ -18,6 +19,7 @@ from debye_bec.devices.nidaq.nidaq_enums import (
|
||||
ScanRates,
|
||||
ScanType,
|
||||
)
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
@@ -32,46 +34,23 @@ class NidaqError(Exception):
|
||||
class NidaqControl(Device):
|
||||
"""Nidaq control class with all PVs"""
|
||||
|
||||
# fmt: off
|
||||
### Readback PVs for EpicsEmitter ###
|
||||
energy = Cpt(SetableSignal, value=0, kind=Kind.normal)
|
||||
|
||||
smpl_abs = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption"
|
||||
)
|
||||
smpl_fluo = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence"
|
||||
)
|
||||
ref_abs = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption"
|
||||
)
|
||||
cisum = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum"
|
||||
)
|
||||
smpl_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption")
|
||||
smpl_fluo = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence")
|
||||
ref_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption")
|
||||
cisum = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum")
|
||||
|
||||
ai0_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN"
|
||||
)
|
||||
ai1_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN"
|
||||
)
|
||||
ai2_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN"
|
||||
)
|
||||
ai3_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN"
|
||||
)
|
||||
ai4_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN"
|
||||
)
|
||||
ai5_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN"
|
||||
)
|
||||
ai6_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN"
|
||||
)
|
||||
ai7_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN"
|
||||
)
|
||||
ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN")
|
||||
ai1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN")
|
||||
ai2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN")
|
||||
ai3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN")
|
||||
ai4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN")
|
||||
ai5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN")
|
||||
ai6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN")
|
||||
ai7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN")
|
||||
|
||||
di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX")
|
||||
di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX")
|
||||
@@ -79,377 +58,91 @@ class NidaqControl(Device):
|
||||
di3_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 3, MAX")
|
||||
di4_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 4, MAX")
|
||||
|
||||
ci0_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN"
|
||||
)
|
||||
ci1_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN"
|
||||
)
|
||||
ci2_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN"
|
||||
)
|
||||
ci3_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN"
|
||||
)
|
||||
ci4_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN"
|
||||
)
|
||||
ci5_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN"
|
||||
)
|
||||
ci6_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN"
|
||||
)
|
||||
ci7_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN"
|
||||
)
|
||||
ci8_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN"
|
||||
)
|
||||
ci9_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN"
|
||||
)
|
||||
ci10_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN"
|
||||
)
|
||||
ci11_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN"
|
||||
)
|
||||
ci12_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN"
|
||||
)
|
||||
ci13_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN"
|
||||
)
|
||||
ci14_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN"
|
||||
)
|
||||
ci15_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN"
|
||||
)
|
||||
ci16_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN"
|
||||
)
|
||||
ci17_mean = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN"
|
||||
)
|
||||
ci0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN")
|
||||
ci1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN")
|
||||
ci2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN")
|
||||
ci3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN")
|
||||
ci4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN")
|
||||
ci5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN")
|
||||
ci6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN")
|
||||
ci7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN")
|
||||
ci8_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN")
|
||||
ci9_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN")
|
||||
ci10_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN")
|
||||
ci11_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN")
|
||||
ci12_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN")
|
||||
ci13_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN")
|
||||
ci14_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN")
|
||||
ci15_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN")
|
||||
ci16_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN")
|
||||
ci17_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN")
|
||||
|
||||
ai0 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI0",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 0",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai1 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI1",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 1",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai2 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI2",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 2",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai3 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI3",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 3",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai4 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI4",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 4",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai5 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI5",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 5",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai6 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI6",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 6",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai7 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-AI7",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS analog input 7",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ai0 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.normal, doc="EPICS analog input 0", auto_monitor=True)
|
||||
ai1 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.normal, doc="EPICS analog input 1", auto_monitor=True)
|
||||
ai2 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.normal, doc="EPICS analog input 2", auto_monitor=True)
|
||||
ai3 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.normal, doc="EPICS analog input 3", auto_monitor=True)
|
||||
ai4 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.normal, doc="EPICS analog input 4", auto_monitor=True)
|
||||
ai5 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.normal, doc="EPICS analog input 5", auto_monitor=True)
|
||||
ai6 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.normal, doc="EPICS analog input 6", auto_monitor=True)
|
||||
ai7 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.normal, doc="EPICS analog input 7", auto_monitor=True)
|
||||
|
||||
ci0 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI0",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 0",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci1 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI1",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 1",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci2 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI2",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 2",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci3 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI3",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 3",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci4 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI4",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 4",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci5 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI5",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 5",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci6 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI6",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 6",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci7 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI7",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 7",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci8 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI8",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 8",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci9 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI9",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 9",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci10 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI10",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 0",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci11 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI11",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 1",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci12 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI12",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 2",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci13 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI13",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 3",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci14 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI14",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 4",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci15 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI15",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 5",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci16 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI16",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 6",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci17 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-CI17",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS counter input 7",
|
||||
auto_monitor=True,
|
||||
)
|
||||
ci0 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True)
|
||||
ci1 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True)
|
||||
ci2 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True)
|
||||
ci3 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True)
|
||||
ci4 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True)
|
||||
ci5 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True)
|
||||
ci6 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True)
|
||||
ci7 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True)
|
||||
ci8 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.normal, doc="EPICS counter input 8", auto_monitor=True)
|
||||
ci9 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.normal, doc="EPICS counter input 9", auto_monitor=True)
|
||||
ci10 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True)
|
||||
ci11 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True)
|
||||
ci12 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True)
|
||||
ci13 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True)
|
||||
ci14 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True)
|
||||
ci15 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True)
|
||||
ci16 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True)
|
||||
ci17 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True)
|
||||
|
||||
di0 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-DI0",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS digital input 0",
|
||||
auto_monitor=True,
|
||||
)
|
||||
di1 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-DI1",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS digital input 1",
|
||||
auto_monitor=True,
|
||||
)
|
||||
di2 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-DI2",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS digital input 2",
|
||||
auto_monitor=True,
|
||||
)
|
||||
di3 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-DI3",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS digital input 3",
|
||||
auto_monitor=True,
|
||||
)
|
||||
di4 = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-DI4",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS digital input 4",
|
||||
auto_monitor=True,
|
||||
)
|
||||
di0 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.normal, doc="EPICS digital input 0", auto_monitor=True)
|
||||
di1 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.normal, doc="EPICS digital input 1", auto_monitor=True)
|
||||
di2 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.normal, doc="EPICS digital input 2", auto_monitor=True)
|
||||
di3 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.normal, doc="EPICS digital input 3", auto_monitor=True)
|
||||
di4 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.normal, doc="EPICS digital input 4", auto_monitor=True)
|
||||
|
||||
enc_epics = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-ENC",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS Encoder reading",
|
||||
auto_monitor=True,
|
||||
)
|
||||
enc_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENC", kind=Kind.normal, doc="EPICS Encoder reading", auto_monitor=True)
|
||||
|
||||
energy_epics = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="NIDAQ-ENERGY",
|
||||
kind=Kind.normal,
|
||||
doc="EPICS Energy reading",
|
||||
auto_monitor=True,
|
||||
)
|
||||
energy_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENERGY", kind=Kind.normal, doc="EPICS Energy reading", auto_monitor=True)
|
||||
|
||||
### Readback for BEC emitter ###
|
||||
ai0_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD"
|
||||
)
|
||||
ai1_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, STD"
|
||||
)
|
||||
ai2_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, STD"
|
||||
)
|
||||
ai3_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, STD"
|
||||
)
|
||||
ai4_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, STD"
|
||||
)
|
||||
ai5_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, STD"
|
||||
)
|
||||
ai6_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, STD"
|
||||
)
|
||||
ai7_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD"
|
||||
)
|
||||
ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD")
|
||||
ai1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, STD")
|
||||
ai2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, STD")
|
||||
ai3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, STD")
|
||||
ai4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, STD")
|
||||
ai5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, STD")
|
||||
ai6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, STD")
|
||||
ai7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD")
|
||||
|
||||
ci0_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD"
|
||||
)
|
||||
ci1_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1. STD"
|
||||
)
|
||||
ci2_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2. STD"
|
||||
)
|
||||
ci3_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3. STD"
|
||||
)
|
||||
ci4_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4. STD"
|
||||
)
|
||||
ci5_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5. STD"
|
||||
)
|
||||
ci6_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6. STD"
|
||||
)
|
||||
ci7_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD"
|
||||
)
|
||||
ci8_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD"
|
||||
)
|
||||
ci9_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD"
|
||||
)
|
||||
ci10_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD"
|
||||
)
|
||||
ci11_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD"
|
||||
)
|
||||
ci12_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD"
|
||||
)
|
||||
ci13_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD"
|
||||
)
|
||||
ci14_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD"
|
||||
)
|
||||
ci15_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD"
|
||||
)
|
||||
ci16_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD"
|
||||
)
|
||||
ci17_std_dev = Cpt(
|
||||
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD"
|
||||
)
|
||||
ci0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD")
|
||||
ci1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1. STD")
|
||||
ci2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2. STD")
|
||||
ci3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3. STD")
|
||||
ci4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4. STD")
|
||||
ci5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5. STD")
|
||||
ci6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6. STD")
|
||||
ci7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD")
|
||||
ci8_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD")
|
||||
ci9_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD")
|
||||
ci10_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD")
|
||||
ci11_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD")
|
||||
ci12_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD")
|
||||
ci13_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD")
|
||||
ci14_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD")
|
||||
ci15_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD")
|
||||
ci16_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD")
|
||||
ci17_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD")
|
||||
|
||||
xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp")
|
||||
xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp")
|
||||
@@ -510,6 +203,8 @@ class NidaqControl(Device):
|
||||
ref_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, auto_monitor=True)
|
||||
ref_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, string=True, auto_monitor=True)
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
"""NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05
|
||||
@@ -526,9 +221,11 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
|
||||
def __init__(self, prefix: str = "", *, name: str, scan_info: ScanInfo = None, **kwargs):
|
||||
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
|
||||
self.scan_info: ScanInfo
|
||||
self.scan_parameters: ScanServerScanInfo = None
|
||||
self.timeout_wait_for_signal = 5 # put 5s firsts
|
||||
self._timeout_wait_for_pv = 5 # 5s timeout for pv calls. editted due to timeout issues persisting
|
||||
self._timeout_wait_for_pv = (
|
||||
5 # 5s timeout for pv calls. editted due to timeout issues persisting
|
||||
)
|
||||
self.valid_scan_names = [
|
||||
"xas_simple_scan",
|
||||
"xas_simple_scan_with_xrd",
|
||||
@@ -541,10 +238,9 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
# Beamline Methods #
|
||||
########################################
|
||||
|
||||
def _check_if_scan_name_is_valid(self) -> bool:
|
||||
def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool:
|
||||
"""Check if the scan is within the list of scans for which the backend is working"""
|
||||
scan_name = self.scan_info.msg.scan_name
|
||||
if scan_name in self.valid_scan_names:
|
||||
if scan_parameters.scan_name in self.valid_scan_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -701,7 +397,8 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
|
||||
If the upcoming scan is not in the list of valid scans, return immediately.
|
||||
"""
|
||||
if not self._check_if_scan_name_is_valid():
|
||||
self.scan_parameters = fetch_scan_info(self.scan_info)
|
||||
if not self._check_if_scan_name_is_valid(self.scan_parameters):
|
||||
return None
|
||||
|
||||
if self.state.get() != NidaqState.STANDBY:
|
||||
@@ -711,18 +408,18 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
status.wait(timeout=self.timeout_wait_for_signal)
|
||||
|
||||
# If scan is not part of the valid_scan_names,
|
||||
if self.scan_info.msg.scan_name != "nidaq_continuous_scan":
|
||||
if self.scan_parameters.scan_name != "nidaq_continuous_scan": # what is the new v4 scan
|
||||
self.scan_type.set(ScanType.TRIGGERED).wait(timeout=self._timeout_wait_for_pv)
|
||||
self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv)
|
||||
self.enable_compression.set(1).wait(timeout=self._timeout_wait_for_pv)
|
||||
else:
|
||||
self.scan_type.set(ScanType.CONTINUOUS).wait(timeout=self._timeout_wait_for_pv)
|
||||
self.scan_duration.set(self.scan_info.msg.scan_parameters["scan_duration"]).wait(
|
||||
timeout=self._timeout_wait_for_pv
|
||||
)
|
||||
self.enable_compression.set(self.scan_info.msg.scan_parameters["compression"]).wait(
|
||||
timeout=self._timeout_wait_for_pv
|
||||
)
|
||||
self.scan_duration.set(
|
||||
self.scan_parameters.additional_scan_parameters["scan_duration"]
|
||||
).wait(timeout=self._timeout_wait_for_pv)
|
||||
self.enable_compression.set(
|
||||
self.scan_parameters.additional_scan_parameters["compression"]
|
||||
).wait(timeout=self._timeout_wait_for_pv)
|
||||
|
||||
# Stage call to IOC
|
||||
status = CompareStatus(self.state, NidaqState.STAGE)
|
||||
@@ -733,7 +430,7 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
# self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
|
||||
self.stage_call.put(1)
|
||||
status.wait(timeout=self.timeout_wait_for_signal)
|
||||
if self.scan_info.msg.scan_name != "nidaq_continuous_scan":
|
||||
if self.scan_parameters.scan_name != "nidaq_continuous_scan":
|
||||
status = self.on_kickoff()
|
||||
self.cancel_on_stop(status)
|
||||
status.wait(timeout=self._timeout_wait_for_pv)
|
||||
@@ -764,10 +461,10 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
before the motor starts its oscillation. This is needed for being properly homed.
|
||||
The NIDAQ should go into Acquiring mode.
|
||||
"""
|
||||
if not self._check_if_scan_name_is_valid():
|
||||
if not self._check_if_scan_name_is_valid(self.scan_parameters):
|
||||
return None
|
||||
|
||||
if self.scan_info.msg.scan_name == "nidaq_continuous_scan":
|
||||
if self.scan_parameters.scan_name == "nidaq_continuous_scan":
|
||||
logger.info(f"Device {self.name} ready to be kicked off for nidaq_continuous_scan")
|
||||
return None
|
||||
|
||||
@@ -788,12 +485,12 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
For the NIDAQ we use this method to stop the backend since it
|
||||
would not stop by itself in its current implementation since the number of points are not predefined.
|
||||
"""
|
||||
if not self._check_if_scan_name_is_valid():
|
||||
if not self._check_if_scan_name_is_valid(self.scan_parameters):
|
||||
return None
|
||||
|
||||
status = CompareStatus(self.state, NidaqState.STANDBY)
|
||||
self.cancel_on_stop(status)
|
||||
if self.scan_info.msg.scan_name != "nidaq_continuous_scan":
|
||||
if self.scan_parameters.scan_name != "nidaq_continuous_scan":
|
||||
self.on_stop()
|
||||
return status
|
||||
|
||||
@@ -804,7 +501,9 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
Args:
|
||||
value (int) : current progress value
|
||||
"""
|
||||
scan_duration = self.scan_info.msg.scan_parameters.get("scan_duration", None)
|
||||
if self.scan_parameters is None:
|
||||
return
|
||||
scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None)
|
||||
if not isinstance(scan_duration, (int, float)):
|
||||
return
|
||||
value = scan_duration - value
|
||||
|
||||
@@ -11,16 +11,26 @@ from typing import TYPE_CHECKING, Tuple
|
||||
import numpy as np
|
||||
from bec_lib.file_utils import get_full_path
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.areadetector.cam import ADBase, PilatusDetectorCam
|
||||
from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin
|
||||
from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin
|
||||
from ophyd.status import WaitTimeoutError
|
||||
from ophyd_devices import AndStatus, CompareStatus, DeviceStatus, FileEventSignal, PreviewSignal
|
||||
from ophyd_devices import (
|
||||
AndStatus,
|
||||
CompareStatus,
|
||||
DeviceStatus,
|
||||
ExceptionStatus,
|
||||
FileEventSignal,
|
||||
PreviewSignal,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage
|
||||
@@ -145,17 +155,17 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
|
||||
# USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||
|
||||
cam_gain_menu_string = Cpt(EpicsSignalRO, suffix='cam1:GainMenu', string=True)
|
||||
cam_gain_menu_string = Cpt(EpicsSignalRO, suffix="cam1:GainMenu", string=True)
|
||||
|
||||
_default_configuration_attrs = [
|
||||
'cam.threshold_energy',
|
||||
'cam.threshold_auto_apply',
|
||||
'cam.gain_menu',
|
||||
'cam_gain_menu_string',
|
||||
'cam.pixel_cut_off',
|
||||
'cam.acquire_time',
|
||||
'cam.num_exposures',
|
||||
'cam.model',
|
||||
"cam.threshold_energy",
|
||||
"cam.threshold_auto_apply",
|
||||
"cam.gain_menu",
|
||||
"cam_gain_menu_string",
|
||||
"cam.pixel_cut_off",
|
||||
"cam.acquire_time",
|
||||
"cam.num_exposures",
|
||||
"cam.model",
|
||||
]
|
||||
|
||||
cam = Cpt(PilatusDetectorCam, "cam1:")
|
||||
@@ -233,7 +243,6 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self.scan_parameter = ScanParameter()
|
||||
self.device_manager = device_manager
|
||||
self._readout_time = PILATUS_READOUT_TIME
|
||||
self._full_path = ""
|
||||
@@ -251,6 +260,7 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
# self._live_mode_run_event = threading.Event()
|
||||
# self._live_mode_stopped_event = threading.Event()
|
||||
# self._live_mode_stopped_event.set() # Initial state is stopped
|
||||
self.scan_parameters: ScanServerScanInfo = None
|
||||
|
||||
########################################
|
||||
# Custom Beamline Methods #
|
||||
@@ -368,19 +378,22 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
status = status_acquire & status_writing & status_cam_server
|
||||
return status
|
||||
|
||||
def _calculate_trigger(self, scan_msg: ScanStatusMessage) -> Tuple[float, float]:
|
||||
self._update_scan_parameter()
|
||||
def _calculate_trigger(self, scan_parameters: ScanServerScanInfo) -> Tuple[float, float]:
|
||||
total_osc = 0
|
||||
calc_duration = 0
|
||||
total_trig_lo = 0
|
||||
total_trig_hi = 0
|
||||
# Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy
|
||||
loc_break_enable_low = self.scan_parameter.break_enable_high
|
||||
loc_break_time_low = self.scan_parameter.break_time_high
|
||||
loc_cycle_low = self.scan_parameter.cycle_high
|
||||
loc_break_enable_high = self.scan_parameter.break_enable_low
|
||||
loc_break_time_high = self.scan_parameter.break_time_low
|
||||
loc_cycle_high = self.scan_parameter.cycle_low
|
||||
loc_break_enable_low = scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_high", False
|
||||
)
|
||||
loc_break_time_low = scan_parameters.additional_scan_parameters.get("break_time_high", 0)
|
||||
loc_cycle_low = scan_parameters.additional_scan_parameters.get("cycle_high", 1)
|
||||
loc_break_enable_high = scan_parameters.additional_scan_parameters.get(
|
||||
"break_enable_low", False
|
||||
)
|
||||
loc_break_time_high = scan_parameters.additional_scan_parameters.get("break_time_low", 0)
|
||||
loc_cycle_high = scan_parameters.additional_scan_parameters.get("cycle_low", 1)
|
||||
|
||||
if not loc_break_enable_low:
|
||||
loc_break_time_low = 0
|
||||
@@ -389,28 +402,36 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
loc_break_time_high = 0
|
||||
loc_cycle_high = 1
|
||||
|
||||
total_osc = self.scan_parameter.scan_duration / (
|
||||
self.scan_parameter.scan_time +
|
||||
loc_break_time_low / (2 * loc_cycle_low) +
|
||||
loc_break_time_high / (2 * loc_cycle_high)
|
||||
total_osc = scan_parameters.additional_scan_parameters.get("scan_duration", 0) / (
|
||||
scan_parameters.additional_scan_parameters.get("scan_time", 0)
|
||||
+ loc_break_time_low / (2 * loc_cycle_low)
|
||||
+ loc_break_time_high / (2 * loc_cycle_high)
|
||||
)
|
||||
total_osc = np.ceil(total_osc)
|
||||
total_osc = total_osc + total_osc % 2 # round up to the next even number
|
||||
total_osc = total_osc + total_osc % 2 # round up to the next even number
|
||||
|
||||
if loc_break_enable_low:
|
||||
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
|
||||
if loc_break_enable_high:
|
||||
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
|
||||
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
|
||||
|
||||
if calc_duration < self.scan_parameter.scan_duration:
|
||||
calc_duration = (
|
||||
total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0)
|
||||
+ total_trig_lo * loc_break_time_low
|
||||
+ total_trig_hi * loc_break_time_high
|
||||
)
|
||||
|
||||
if calc_duration < scan_parameters.additional_scan_parameters.get("scan_duration", 0):
|
||||
# Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers
|
||||
total_osc = total_osc + 2
|
||||
if loc_break_enable_low:
|
||||
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
|
||||
if loc_break_enable_high:
|
||||
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
|
||||
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
|
||||
calc_duration = (
|
||||
total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0)
|
||||
+ total_trig_lo * loc_break_time_low
|
||||
+ total_trig_hi * loc_break_time_high
|
||||
)
|
||||
|
||||
return total_trig_lo, total_trig_hi
|
||||
|
||||
@@ -464,6 +485,7 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
(self.scan_info.msg) object.
|
||||
"""
|
||||
# self.stop_live_mode() # Make sure that live mode is stopped if scan runs
|
||||
self.scan_parameters = fetch_scan_info(self.scan_info)
|
||||
|
||||
# If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition
|
||||
if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value:
|
||||
@@ -473,25 +495,29 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
|
||||
status_cam.wait(timeout=5)
|
||||
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
if scan_msg.scan_name in self.xas_xrd_scan_names:
|
||||
self._update_scan_parameter()
|
||||
if self.scan_parameters.scan_name in self.xas_xrd_scan_names:
|
||||
# Compute number of triggers
|
||||
total_trig_lo, total_trig_hi = self._calculate_trigger(scan_msg)
|
||||
total_trig_lo, total_trig_hi = self._calculate_trigger(self.scan_parameters)
|
||||
# Set the number of images, we may also set this to a higher values if preferred and stop the acquisition
|
||||
# TODO This logic is prone to errors, as we rely on the scans to nicely resolve to n_images. We should
|
||||
# use here instead a way of settings the n_images independently of the scan parameters to avoid running out of sync
|
||||
# with the complete method. Ideally we comput them in the scan itself.. This is much safer IMO!
|
||||
self.n_images = (total_trig_lo + total_trig_hi) * self.scan_parameter.n_of_trigger
|
||||
exp_time = self.scan_parameter.exp_time
|
||||
self.n_images = (
|
||||
total_trig_lo + total_trig_hi
|
||||
) * self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1)
|
||||
exp_time = self.scan_parameters.exp_time
|
||||
self.trigger_source.set(MONOTRIGGERSOURCE.INPOS).wait(5)
|
||||
self.trigger_n_of.set(self.scan_parameter.n_of_trigger).wait(5)
|
||||
|
||||
elif scan_msg.scan_type == "step":
|
||||
self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get(
|
||||
"frames_per_trigger", 1
|
||||
self.trigger_n_of.set(
|
||||
self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1)
|
||||
).wait(5)
|
||||
# TODO migrate logic to v4 once old scans are deprecated,
|
||||
# TODO if num_points=None and no logic from scan_name applies, can't measure with this detector..
|
||||
elif self.scan_parameters.scan_type == "software_triggered":
|
||||
self.n_images = (
|
||||
self.scan_parameters.num_monitored_readouts
|
||||
* self.scan_parameters.frames_per_trigger
|
||||
)
|
||||
exp_time = scan_msg.scan_parameters.get("exp_time")
|
||||
exp_time = self.scan_parameters.exp_time
|
||||
self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5)
|
||||
self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition
|
||||
else:
|
||||
@@ -512,7 +538,7 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
)
|
||||
)
|
||||
detector_exp_time = exp_time - self._readout_time
|
||||
self._full_path = get_full_path(scan_msg, name="pilatus")
|
||||
self._full_path = get_full_path(self.scan_info.msg, name="pilatus")
|
||||
file_path = "/".join(self._full_path.split("/")[:-1])
|
||||
file_name = self._full_path.split("/")[-1]
|
||||
# Prepare detector and backend
|
||||
@@ -544,9 +570,9 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
if (
|
||||
scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step"
|
||||
self.scan_parameters.scan_name in self.xas_xrd_scan_names
|
||||
or self.scan_parameters.scan_type == "software_triggered"
|
||||
): # TODO how to deal with fly scans?
|
||||
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value)
|
||||
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value)
|
||||
@@ -561,8 +587,7 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | None:
|
||||
"""Called when the device is triggered."""
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
if not scan_msg.scan_type == "step":
|
||||
if not self.scan_parameters.scan_type == "software_triggered":
|
||||
return None
|
||||
start_time = time.time()
|
||||
img_counter = self.hdf.num_captured.get()
|
||||
@@ -575,9 +600,9 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
|
||||
def _complete_callback(self, status: DeviceStatus):
|
||||
"""Callback for when the device completes a scan."""
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
if (
|
||||
scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step"
|
||||
self.scan_parameters.scan_name in self.xas_xrd_scan_names
|
||||
or self.scan_parameters.scan_type == "software_triggered"
|
||||
): # TODO how to deal with fly scans?
|
||||
if status.success:
|
||||
self.file_event.put(
|
||||
@@ -598,14 +623,15 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
|
||||
def on_complete(self) -> DeviceStatus | None:
|
||||
"""Called to inquire if a device has completed a scans."""
|
||||
scan_msg: ScanStatusMessage = self.scan_info.msg
|
||||
if (
|
||||
scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step"
|
||||
self.scan_parameters.scan_name in self.xas_xrd_scan_names
|
||||
or self.scan_parameters.scan_type == "software_triggered"
|
||||
): # TODO how to deal with fly scans?
|
||||
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
|
||||
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
|
||||
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
|
||||
if self.scan_info.msg.scan_name in self.xas_xrd_scan_names:
|
||||
# status_write_error = ExceptionStatus(self.hdf.write_status, 0, operation="!=")
|
||||
if self.scan_parameters.scan_name in self.xas_xrd_scan_names:
|
||||
# For long scans, it can be that the mono will execute one cycle more,
|
||||
# meaning a few more XRD triggers will be sent
|
||||
status_img_written = CompareStatus(
|
||||
@@ -614,7 +640,9 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
else:
|
||||
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
|
||||
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
|
||||
status = status_hdf & status_cam & status_img_written & status_cam_server
|
||||
status = (
|
||||
status_hdf & status_cam & status_img_written & status_cam_server
|
||||
) # & status_write_error
|
||||
status.add_callback(self._complete_callback) # Callback that writing was successful
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
@@ -635,15 +663,6 @@ class Pilatus(PSIDeviceBase, ADBase):
|
||||
# TODO do we need to clean the poll thread ourselves?
|
||||
self.on_stop()
|
||||
|
||||
def _update_scan_parameter(self):
|
||||
"""Get the scan_info parameters for the scan."""
|
||||
for key, value in self.scan_info.msg.request_inputs["inputs"].items():
|
||||
if hasattr(self.scan_parameter, key):
|
||||
setattr(self.scan_parameter, key, value)
|
||||
for key, value in self.scan_info.msg.request_inputs["kwargs"].items():
|
||||
if hasattr(self.scan_parameter, key):
|
||||
setattr(self.scan_parameter, key, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Utility functions for the devices."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from pydantic import ValidationError
|
||||
|
||||
|
||||
def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo:
|
||||
"""Fetch the scan parameters from the scan_info object and return them as a ScanServerScanInfo object."""
|
||||
info = scan_info.msg.info
|
||||
if isinstance(info["positions"], list):
|
||||
info["positions"] = np.array(info["positions"])
|
||||
try:
|
||||
msg = ScanServerScanInfo.model_validate(info)
|
||||
except ValidationError: # This means we have an old scan_info object.
|
||||
info = deepcopy(info)
|
||||
# We need to convert a few parameters manually.
|
||||
info["scan_type"] = (
|
||||
"hardware_triggered" if info["scan_type"] == "fly" else "software_triggered"
|
||||
)
|
||||
msg = ScanServerScanInfo.model_validate(info)
|
||||
|
||||
return msg
|
||||
@@ -1,6 +1,7 @@
|
||||
from bec_server.file_writer.default_writer import DefaultFormat
|
||||
|
||||
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
|
||||
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
|
||||
|
||||
|
||||
class DebyeNexusStructure(DefaultFormat):
|
||||
"""Nexus Structure for Debye"""
|
||||
@@ -31,8 +32,7 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
|
||||
if "curr" in self.device_manager.devices:
|
||||
ring_current = source.create_soft_link(
|
||||
name="ring_current",
|
||||
target="/entry/collection/devices/curr/curr/value",
|
||||
name="ring_current", target="/entry/collection/devices/curr/curr/value"
|
||||
)
|
||||
ring_current.attrs["NX_class"] = "NX_FLOAT"
|
||||
ring_current.attrs["units"] = "mA"
|
||||
@@ -57,12 +57,12 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
name="reflection",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value",
|
||||
)
|
||||
reflection.attrs["NX_class"] = "NX_CHAR"
|
||||
reflection.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
# Create a softlink
|
||||
d_spacing = crystal.create_soft_link(
|
||||
name="d_spacing",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value",
|
||||
name="d_spacing",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value",
|
||||
)
|
||||
d_spacing.attrs["NX_class"] = "NX_FLOAT"
|
||||
d_spacing.attrs["units"] = "angstrom"
|
||||
@@ -71,40 +71,40 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
name="bragg_offset",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_bragg_off/value",
|
||||
)
|
||||
bragg_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
bragg_offset.attrs["units"] = "degree"
|
||||
bragg_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
bragg_offset.attrs["units"] = "degree"
|
||||
|
||||
phi_offset = crystal.create_soft_link(
|
||||
name="phi_offset",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_phi_off/value",
|
||||
)
|
||||
phi_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
phi_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
phi_offset.attrs["units"] = "degree"
|
||||
|
||||
## Logic if device exist
|
||||
if "mo1_roty" in self.device_manager.devices:
|
||||
if "mo1_roty" in self.device_manager.devices:
|
||||
|
||||
# Create a softlink
|
||||
azimuthal_angle = crystal.create_soft_link(
|
||||
name="azimuthal_angle",
|
||||
target="/entry/collection/devices/mo1_roty/mo1_roty/value",
|
||||
name="azimuthal_angle",
|
||||
target="/entry/collection/devices/mo1_roty/mo1_roty/value",
|
||||
)
|
||||
azimuthal_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
azimuthal_angle.attrs["units"] = "degree"
|
||||
|
||||
azimuthal_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
azimuthal_angle.attrs["units"] = "degree"
|
||||
|
||||
azm_offset = crystal.create_soft_link(
|
||||
name="azm_offset",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_azm_off/value",
|
||||
)
|
||||
azm_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
azm_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
azm_offset.attrs["units"] = "degree"
|
||||
|
||||
miscut = crystal.create_soft_link(
|
||||
name="miscut",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_miscut/value",
|
||||
)
|
||||
miscut.attrs["NX_class"] = "NX_FLOAT"
|
||||
miscut.attrs["units"] = "degree"
|
||||
miscut.attrs["NX_class"] = "NX_FLOAT"
|
||||
miscut.attrs["units"] = "degree"
|
||||
|
||||
###################
|
||||
### cm mirror specific information
|
||||
@@ -118,7 +118,7 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
)
|
||||
cm_substrate_material.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
#previous error due to space in name field
|
||||
# previous error due to space in name field
|
||||
|
||||
if "cm_bnd_radius" in self.device_manager.devices:
|
||||
cm_bending_radius = collimating_mirror.create_soft_link(
|
||||
@@ -149,15 +149,15 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
cm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
cm_roll_angle.attrs["units"] = "mrad"
|
||||
|
||||
if 'cm_trx' in self.device_manager.devices:
|
||||
cm_trx = - self.device_manager.devices.cm_trx.read(cached=True).get('cm_trx').get('value')
|
||||
stripe = 'Unknown'
|
||||
if "cm_trx" in self.device_manager.devices:
|
||||
cm_trx = (
|
||||
-self.device_manager.devices.cm_trx.read(cached=True).get("cm_trx").get("value")
|
||||
)
|
||||
stripe = "Unknown"
|
||||
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
|
||||
if low <= cm_trx <= high:
|
||||
stripe = name
|
||||
cm_stripe = collimating_mirror.create_dataset(
|
||||
name="stripe", data=stripe
|
||||
)
|
||||
cm_stripe = collimating_mirror.create_dataset(name="stripe", data=stripe)
|
||||
cm_stripe.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
###################
|
||||
@@ -167,9 +167,7 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
focusing_mirror = instrument.create_group(name="focusing_mirror")
|
||||
focusing_mirror.attrs["NX_class"] = "NXmirror"
|
||||
|
||||
fm_substrate_material = focusing_mirror.create_dataset(
|
||||
name="substrate_material", data="Si"
|
||||
)
|
||||
fm_substrate_material = focusing_mirror.create_dataset(name="substrate_material", data="Si")
|
||||
fm_substrate_material.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if "fm_bnd_radius" in self.device_manager.devices:
|
||||
@@ -201,18 +199,22 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
fm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
fm_roll_angle.attrs["units"] = "mrad"
|
||||
|
||||
if 'fm_trx' in self.device_manager.devices:
|
||||
fm_trx = - self.device_manager.devices.fm_trx.read(cached=True).get('fm_trx').get('value')
|
||||
stripe = 'Unknown'
|
||||
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + ' (flat)'
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + ' (toroid)'
|
||||
fm_stripe = focusing_mirror.create_dataset(
|
||||
name="stripe", data=stripe
|
||||
if "fm_trx" in self.device_manager.devices:
|
||||
fm_trx = (
|
||||
-self.device_manager.devices.fm_trx.read(cached=True).get("fm_trx").get("value")
|
||||
)
|
||||
stripe = "Unknown"
|
||||
for name, low, high in zip(
|
||||
bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]
|
||||
):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + " (flat)"
|
||||
for name, low, high in zip(
|
||||
bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]
|
||||
):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + " (toroid)"
|
||||
fm_stripe = focusing_mirror.create_dataset(name="stripe", data=stripe)
|
||||
fm_stripe.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
###################
|
||||
@@ -220,46 +222,71 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
###################
|
||||
|
||||
## Logic if device exist
|
||||
if "nidaq" in self.device_manager.devices:
|
||||
|
||||
#ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value")
|
||||
ai_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value")
|
||||
ci_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value")
|
||||
#add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value")
|
||||
add_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
|
||||
if "nidaq" in self.device_manager.devices:
|
||||
|
||||
# ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value")
|
||||
ai_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value")
|
||||
)
|
||||
ci_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value")
|
||||
)
|
||||
# add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value")
|
||||
add_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
|
||||
)
|
||||
|
||||
rle = self.configuration.get("nidaq", {}).get("nidaq_rle", {}).get("value")
|
||||
|
||||
measurement_mode = entry.create_group(name="mode")
|
||||
measurement_mode.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if (int(ci_chans_bits) & 0x7F) != 0:
|
||||
# Create a dataset
|
||||
rayspec_sdd_active = measurement_mode.create_group(name="Multi_Element_Partial_Fluorescence_Yield")
|
||||
me_sdd = rayspec_sdd_active.create_dataset(name="Detector", data="Rayspec 7 element Silicon Drift Detector")
|
||||
me_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
if ci_chans_bits is not None:
|
||||
if (int(ci_chans_bits) & 0x7F) != 0:
|
||||
# Create a dataset
|
||||
rayspec_sdd_active = measurement_mode.create_group(
|
||||
name="Multi_Element_Partial_Fluorescence_Yield"
|
||||
)
|
||||
me_sdd = rayspec_sdd_active.create_dataset(
|
||||
name="Detector", data="Rayspec 7 element Silicon Drift Detector"
|
||||
)
|
||||
me_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if (int(ci_chans_bits) & (1<<8)) != 0:
|
||||
# Create a dataset
|
||||
ketek_sdd_active = measurement_mode.create_group(name="Single_Element_Partial_Fluorescence_Yield")
|
||||
se_sdd = ketek_sdd_active.create_dataset(name="Detector", data="Ketex mini single element Silicon Drift Detector")
|
||||
se_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
if (int(ci_chans_bits) & (1 << 8)) != 0:
|
||||
# Create a dataset
|
||||
ketek_sdd_active = measurement_mode.create_group(
|
||||
name="Single_Element_Partial_Fluorescence_Yield"
|
||||
)
|
||||
se_sdd = ketek_sdd_active.create_dataset(
|
||||
name="Detector", data="Ketex mini single element Silicon Drift Detector"
|
||||
)
|
||||
se_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<6)) != 0):
|
||||
# Create a dataset
|
||||
pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield")
|
||||
tfy = pips_active.create_dataset(name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector")
|
||||
tfy.attrs["NX_class"] = "NX_CHAR"
|
||||
if ai_chans_bits is not None:
|
||||
if (int(ai_chans_bits) & (1 << 6)) != 0:
|
||||
# Create a dataset
|
||||
pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield")
|
||||
tfy = pips_active.create_dataset(
|
||||
name="Detector",
|
||||
data="Mirion Technologies Partially Depeleted PIPS Detector",
|
||||
)
|
||||
tfy.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<0)) != 0) & ((int(ai_chans_bits) & (1<<2)) != 0):
|
||||
# Create a dataset
|
||||
ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission")
|
||||
sam_trans = ai0ai2_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers")
|
||||
sam_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0):
|
||||
# Create a dataset
|
||||
ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission")
|
||||
sam_trans = ai0ai2_active.create_dataset(
|
||||
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
|
||||
)
|
||||
sam_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<2)) != 0) & ((int(ai_chans_bits) & (1<<4)) != 0):
|
||||
# Create a dataset
|
||||
ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission")
|
||||
ref_trans = ai2ai4_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers")
|
||||
ref_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0):
|
||||
# Create a dataset
|
||||
ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission")
|
||||
ref_trans = ai2ai4_active.create_dataset(
|
||||
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
|
||||
)
|
||||
ref_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
main_data = entry.create_group(name="data")
|
||||
main_data.attrs["NX_class"] = "NXdata"
|
||||
@@ -267,45 +294,60 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
##################
|
||||
## energy, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
|
||||
energy = main_data.create_group(name="energy")
|
||||
energy.attrs["NX_class"] = "NXdata"
|
||||
energy.attrs["units"] = "eV"
|
||||
|
||||
main_data.create_soft_link(name="energy", target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value")
|
||||
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="energy",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## i0
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<0)) !=0:
|
||||
|
||||
if (int(ai_chans_bits) & (1 << 0)) != 0:
|
||||
i0 = main_data.create_group(name="i0")
|
||||
i0.attrs["NX_class"] = "NXdata"
|
||||
i0.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i0", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value")
|
||||
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0/value"
|
||||
main_data.create_soft_link(name="i0", target=target)
|
||||
|
||||
##################
|
||||
## i1
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<2)) !=0:
|
||||
|
||||
if (int(ai_chans_bits) & (1 << 2)) != 0:
|
||||
i1 = main_data.create_group(name="i1")
|
||||
i1.attrs["NX_class"] = "NXdata"
|
||||
i1.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i1", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value")
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2/value"
|
||||
main_data.create_soft_link(name="i1", target=target)
|
||||
|
||||
##################
|
||||
## i2
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<4)) !=0:
|
||||
if (int(ai_chans_bits) & (1 << 4)) != 0:
|
||||
i2 = main_data.create_group(name="i2")
|
||||
i2.attrs["NX_class"] = "NXdata"
|
||||
i2.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i2", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value")
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4/value"
|
||||
main_data.create_soft_link(name="i2", target=target)
|
||||
|
||||
##################
|
||||
## ci sum
|
||||
@@ -316,38 +358,46 @@ class DebyeNexusStructure(DefaultFormat):
|
||||
ci_sum.attrs["NX_class"] = "NXdata"
|
||||
ci_sum.attrs["units"] = "counts"
|
||||
|
||||
main_data.create_soft_link(name="Fluorescence_Sum", target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value")
|
||||
main_data.create_soft_link(
|
||||
name="Fluorescence_Sum",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## mu sample, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<0)) !=0:
|
||||
if (int(add_chans_bits) & (1 << 0)) != 0:
|
||||
mu_sample = main_data.create_group(name="mu_sample")
|
||||
mu_sample.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="mu_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value")
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="mu_sample",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## fluo sample, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<1)) !=0:
|
||||
if (int(add_chans_bits) & (1 << 1)) != 0:
|
||||
mu_sample = main_data.create_group(name="fluo_sample")
|
||||
mu_sample.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="fluo_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value")
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="fluo_sample",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## mu reference, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<2)) !=0:
|
||||
|
||||
if (int(add_chans_bits) & (1 << 2)) != 0:
|
||||
mu_reference = main_data.create_group(name="mu_reference")
|
||||
mu_reference.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="mu_reference", target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value")
|
||||
|
||||
|
||||
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="mu_reference",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .mono_bragg_scans import (
|
||||
XASAdvancedScan,
|
||||
XASAdvancedScanWithXRD,
|
||||
XASSimpleScan,
|
||||
XASSimpleScanWithXRD,
|
||||
from .nidaq_continuous_scan import NidaqContinuousScan
|
||||
from .xas_simple_scan import (
|
||||
XasAdvancedScan,
|
||||
XasAdvancedScanWithXrd,
|
||||
XasSimpleScan,
|
||||
XasSimpleScanWithXrd,
|
||||
)
|
||||
from .nidaq_cont_scan import NIDAQContinuousScan
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
"""This module contains the scan classes for the mono bragg motor of the Debye beamline."""
|
||||
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.device import DeviceBase
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class XASSimpleScan(AsyncFlyScanBase):
|
||||
"""Class for the XAS simple scan"""
|
||||
|
||||
scan_name = "xas_simple_scan"
|
||||
scan_type = "fly"
|
||||
scan_report_hint = "device_progress"
|
||||
required_kwargs = []
|
||||
use_scan_progress_report = False
|
||||
pre_move = False
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: float,
|
||||
stop: float,
|
||||
scan_time: float,
|
||||
scan_duration: float,
|
||||
motor: DeviceBase = "mo1_bragg",
|
||||
**kwargs,
|
||||
):
|
||||
"""The xas_simple_scan is used to start a simple oscillating scan on the mono bragg motor.
|
||||
Start and Stop define the energy range for the scan, scan_time is the time for one scan
|
||||
cycle and scan_duration is the duration of the scan. If scan duration is set to 0, the
|
||||
scan will run infinitely.
|
||||
|
||||
Args:
|
||||
start (float): Start energy for the scan.
|
||||
stop (float): Stop energy for the scan.
|
||||
scan_time (float): Time for one scan cycle.
|
||||
scan_duration (float): Duration of the scan.
|
||||
motor (DeviceBase, optional): Motor device to be used for the scan.
|
||||
Defaults to "mo1_bragg".
|
||||
Examples:
|
||||
>>> scans.xas_simple_scan(start=8000, stop=9000, scan_time=1, scan_duration=10)
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.motor = motor
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
self.scan_time = scan_time
|
||||
self.scan_duration = scan_duration
|
||||
self.primary_readout_cycle = 1
|
||||
|
||||
def update_readout_priority(self):
|
||||
"""Ensure that NIDAQ is not monitored for any quick EXAFS."""
|
||||
super().update_readout_priority()
|
||||
self.readout_priority["async"].append("nidaq")
|
||||
|
||||
def prepare_positions(self):
|
||||
"""Prepare the positions for the scan.
|
||||
|
||||
Use here only start and end energy defining the range for the scan.
|
||||
"""
|
||||
self.positions = np.array([self.start, self.stop], dtype=float)
|
||||
self.num_pos = None
|
||||
yield None
|
||||
|
||||
def pre_scan(self):
|
||||
"""Pre Scan action."""
|
||||
|
||||
self._check_limits()
|
||||
# Ensure parent class pre_scan actions to be called.
|
||||
yield from super().pre_scan()
|
||||
|
||||
def scan_report_instructions(self):
|
||||
"""
|
||||
Return the instructions for the scan report.
|
||||
"""
|
||||
yield from self.stubs.scan_report_instruction({"device_progress": [self.motor]})
|
||||
|
||||
def scan_core(self):
|
||||
"""Run the scan core.
|
||||
Kickoff the oscillation on the Bragg motor and wait for the completion of the motion.
|
||||
"""
|
||||
# Start the oscillation on the Bragg motor.
|
||||
yield from self.stubs.kickoff(device=self.motor)
|
||||
complete_status = yield from self.stubs.complete(device=self.motor, wait=False)
|
||||
|
||||
while not complete_status.done:
|
||||
# Readout monitored devices
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
time.sleep(self.primary_readout_cycle)
|
||||
self.point_id += 1
|
||||
|
||||
self.num_pos = self.point_id
|
||||
|
||||
|
||||
class XASSimpleScanWithXRD(XASSimpleScan):
|
||||
"""Class for the XAS simple scan with XRD"""
|
||||
|
||||
scan_name = "xas_simple_scan_with_xrd"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration"],
|
||||
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
|
||||
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
|
||||
"XRD Triggers": ["exp_time", "n_of_trigger"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: float,
|
||||
stop: float,
|
||||
scan_time: float,
|
||||
scan_duration: float,
|
||||
break_enable_low: bool,
|
||||
break_time_low: float,
|
||||
cycle_low: int,
|
||||
break_enable_high: bool,
|
||||
break_time_high: float,
|
||||
cycle_high: float,
|
||||
exp_time: float,
|
||||
n_of_trigger: int,
|
||||
motor: DeviceBase = "mo1_bragg",
|
||||
**kwargs,
|
||||
):
|
||||
"""The xas_simple_scan_with_xrd is an oscillation motion on the mono motor
|
||||
with XRD triggering at low and high energy ranges.
|
||||
If scan duration is set to 0, the scan will run infinitely.
|
||||
|
||||
Args:
|
||||
start (float): Start energy for the scan.
|
||||
stop (float): Stop energy for the scan.
|
||||
scan_time (float): Time for one oscillation .
|
||||
scan_duration (float): Total duration of the scan.
|
||||
break_enable_low (bool): Enable breaks for the low energy range.
|
||||
break_time_low (float): Break time for the low energy range.
|
||||
cycle_low (int): Specify how often the triggers should be considered,
|
||||
every nth cycle for low
|
||||
break_enable_high (bool): Enable breaks for the high energy range.
|
||||
break_time_high (float): Break time for the high energy range.
|
||||
cycle_high (int): Specify how often the triggers should be considered,
|
||||
every nth cycle for high
|
||||
exp_time (float): Length of 1 trigger period in seconds
|
||||
n_of_trigger (int): Amount of triggers to be fired during break
|
||||
motor (DeviceBase, optional): Motor device to be used for the scan.
|
||||
Defaults to "mo1_bragg".
|
||||
|
||||
Examples:
|
||||
>>> scans.xas_simple_scan_with_xrd(start=8000, stop=9000, scan_time=1, scan_duration=10, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000)
|
||||
"""
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
motor=motor,
|
||||
**kwargs,
|
||||
)
|
||||
self.break_enable_low = break_enable_low
|
||||
self.break_time_low = break_time_low
|
||||
self.cycle_low = cycle_low
|
||||
self.break_enable_high = break_enable_high
|
||||
self.break_time_high = break_time_high
|
||||
self.cycle_high = cycle_high
|
||||
self.exp_time = exp_time
|
||||
self.n_of_trigger = n_of_trigger
|
||||
|
||||
|
||||
class XASAdvancedScan(XASSimpleScan):
|
||||
"""Class for the XAS advanced scan"""
|
||||
|
||||
scan_name = "xas_advanced_scan"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration"],
|
||||
"Spline Parameters": ["p_kink", "e_kink"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: float,
|
||||
stop: float,
|
||||
scan_time: float,
|
||||
scan_duration: float,
|
||||
p_kink: float,
|
||||
e_kink: float,
|
||||
motor: DeviceBase = "mo1_bragg",
|
||||
**kwargs,
|
||||
):
|
||||
"""The xas_advanced_scan is an oscillation motion on the mono motor.
|
||||
Start and Stop define the energy range for the scan, scan_time is the
|
||||
time for one scan cycle and scan_duration is the duration of the scan.
|
||||
If scan duration is set to 0, the scan will run infinitely.
|
||||
p_kink and e_kink add a kink to the motion profile to slow down in the
|
||||
exafs region of the scan.
|
||||
|
||||
Args:
|
||||
start (float): Start angle for the scan.
|
||||
stop (float): Stop angle for the scan.
|
||||
scan_time (float): Time for one oscillation .
|
||||
scan_duration (float): Total duration of the scan.
|
||||
p_kink (float): Position of the kink.
|
||||
e_kink (float): Energy of the kink.
|
||||
motor (DeviceBase, optional): Motor device to be used for the scan.
|
||||
Defaults to "mo1_bragg".
|
||||
|
||||
Examples:
|
||||
>>> scans.xas_advanced_scan(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500)
|
||||
"""
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
motor=motor,
|
||||
**kwargs,
|
||||
)
|
||||
self.p_kink = p_kink
|
||||
self.e_kink = e_kink
|
||||
|
||||
|
||||
class XASAdvancedScanWithXRD(XASAdvancedScan):
|
||||
"""Class for the XAS advanced scan with XRD"""
|
||||
|
||||
scan_name = "xas_advanced_scan_with_xrd"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration"],
|
||||
"Spline Parameters": ["p_kink", "e_kink"],
|
||||
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
|
||||
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
|
||||
"XRD Triggers": ["exp_time", "n_of_trigger"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: float,
|
||||
stop: float,
|
||||
scan_time: float,
|
||||
scan_duration: float,
|
||||
p_kink: float,
|
||||
e_kink: float,
|
||||
break_enable_low: bool,
|
||||
break_time_low: float,
|
||||
cycle_low: int,
|
||||
break_enable_high: bool,
|
||||
break_time_high: float,
|
||||
cycle_high: float,
|
||||
exp_time: float,
|
||||
n_of_trigger: int,
|
||||
motor: DeviceBase = "mo1_bragg",
|
||||
**kwargs,
|
||||
):
|
||||
"""The xas_advanced_scan is an oscillation motion on the mono motor
|
||||
with XRD triggering at low and high energy ranges.
|
||||
Start and Stop define the energy range for the scan, scan_time is the time for
|
||||
one scan cycle and scan_duration is the duration of the scan. If scan duration
|
||||
is set to 0, the scan will run infinitely. p_kink and e_kink add a kink to the
|
||||
motion profile to slow down in the exafs region of the scan.
|
||||
|
||||
Args:
|
||||
start (float): Start angle for the scan.
|
||||
stop (float): Stop angle for the scan.
|
||||
scan_time (float): Time for one oscillation .
|
||||
scan_duration (float): Total duration of the scan.
|
||||
p_kink (float): Position of kink.
|
||||
e_kink (float): Energy of the kink.
|
||||
break_enable_low (bool): Enable breaks for the low energy range.
|
||||
break_time_low (float): Break time for the low energy range.
|
||||
cycle_low (int): Specify how often the triggers should be considered,
|
||||
every nth cycle for low
|
||||
break_enable_high (bool): Enable breaks for the high energy range.
|
||||
break_time_high (float): Break time for the high energy range.
|
||||
cycle_high (int): Specify how often the triggers should be considered,
|
||||
every nth cycle for high
|
||||
exp_time (float): Length of 1 trigger period in seconds
|
||||
n_of_trigger (int): Amount of triggers to be fired during break
|
||||
motor (DeviceBase, optional): Motor device to be used for the scan.
|
||||
Defaults to "mo1_bragg".
|
||||
|
||||
Examples:
|
||||
>>> scans.xas_advanced_scan_with_xrd(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000)
|
||||
"""
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
p_kink=p_kink,
|
||||
e_kink=e_kink,
|
||||
motor=motor,
|
||||
**kwargs,
|
||||
)
|
||||
self.p_kink = p_kink
|
||||
self.e_kink = e_kink
|
||||
self.break_enable_low = break_enable_low
|
||||
self.break_time_low = break_time_low
|
||||
self.cycle_low = cycle_low
|
||||
self.break_enable_high = break_enable_high
|
||||
self.break_time_high = break_time_high
|
||||
self.cycle_high = cycle_high
|
||||
self.exp_time = exp_time
|
||||
self.n_of_trigger = n_of_trigger
|
||||
@@ -1,84 +0,0 @@
|
||||
"""This module contains the scan class for the nidaq of the Debye beamline for use in continuous mode."""
|
||||
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.device import DeviceBase
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class NIDAQContinuousScan(AsyncFlyScanBase):
|
||||
"""Class for the nidaq continuous scan (without mono)"""
|
||||
|
||||
scan_name = "nidaq_continuous_scan"
|
||||
scan_type = "fly"
|
||||
scan_report_hint = "device_progress"
|
||||
required_kwargs = []
|
||||
use_scan_progress_report = False
|
||||
pre_move = False
|
||||
gui_config = {"Scan Parameters": ["scan_duration"], "Data Compression": ["compression"]}
|
||||
|
||||
def __init__(
|
||||
self, scan_duration: float, daq: DeviceBase = "nidaq", compression: bool = False, **kwargs
|
||||
):
|
||||
"""The NIDAQ continuous scan is used to measure with the NIDAQ without moving the
|
||||
monochromator or any other motor. The NIDAQ thus runs in continuous mode, with a
|
||||
set scan_duration.
|
||||
|
||||
Args:
|
||||
scan_duration (float): Duration of the scan.
|
||||
daq (DeviceBase, optional): DAQ device to be used for the scan.
|
||||
Defaults to "nidaq".
|
||||
Examples:
|
||||
>>> scans.nidaq_continuous_scan(scan_duration=10)
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.scan_duration = scan_duration
|
||||
self.daq = daq
|
||||
self.start_time = 0
|
||||
self.primary_readout_cycle = 1
|
||||
self.scan_parameters["scan_duration"] = scan_duration
|
||||
self.scan_parameters["compression"] = compression
|
||||
|
||||
def update_readout_priority(self):
|
||||
"""Ensure that NIDAQ is not monitored for any quick EXAFS."""
|
||||
super().update_readout_priority()
|
||||
self.readout_priority["async"].append("nidaq")
|
||||
|
||||
def prepare_positions(self):
|
||||
"""Prepare the positions for the scan."""
|
||||
yield None
|
||||
|
||||
def pre_scan(self):
|
||||
"""Pre Scan action."""
|
||||
|
||||
self.start_time = time.time()
|
||||
# Ensure parent class pre_scan actions to be called.
|
||||
yield from super().pre_scan()
|
||||
|
||||
def scan_report_instructions(self):
|
||||
"""
|
||||
Return the instructions for the scan report.
|
||||
"""
|
||||
yield from self.stubs.scan_report_instruction({"device_progress": [self.daq]})
|
||||
|
||||
def scan_core(self):
|
||||
"""Run the scan core.
|
||||
Kickoff the acquisition of the NIDAQ wait for the completion of the scan.
|
||||
"""
|
||||
kickoff_status = yield from self.stubs.kickoff(device=self.daq)
|
||||
kickoff_status.wait(timeout=5) # wait for proper kickoff of device
|
||||
|
||||
complete_status = yield from self.stubs.complete(device=self.daq, wait=False)
|
||||
|
||||
while not complete_status.done:
|
||||
# Readout monitored devices
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
time.sleep(self.primary_readout_cycle)
|
||||
self.point_id += 1
|
||||
|
||||
self.num_pos = self.point_id
|
||||
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
The NIDAQ continuous scan is used to measure with the NIDAQ without moving the monochromator or any other motor.
|
||||
|
||||
Scan procedure:
|
||||
- prepare_scan
|
||||
- open_scan
|
||||
- stage
|
||||
- pre_scan
|
||||
- scan_core
|
||||
- at_each_point (optionally called by scan_core)
|
||||
- post_scan
|
||||
- unstage
|
||||
- close_scan
|
||||
- on_exception (called if any exception is raised during the scan)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Annotated
|
||||
|
||||
from bec_lib.device import DeviceBase
|
||||
from bec_lib.scan_args import ScanArgument, Units
|
||||
from bec_server.scan_server.scans.scan_base import ScanBase, ScanType
|
||||
from bec_server.scan_server.scans.scan_modifier import scan_hook
|
||||
|
||||
|
||||
class NidaqContinuousScan(ScanBase):
|
||||
# Scan Type: Hardware triggered or software triggered?
|
||||
# If the main trigger and readout logic is done within the at_each_point method in scan_core, choose SOFTWARE_TRIGGERED.
|
||||
# If the main trigger and readout logic is implemented on a device that is simply kicked off in this scan, choose HARDWARE_TRIGGERED.
|
||||
# This primarily serves as information for devices: The device may need to react differently if a software trigger is expected
|
||||
# for every point.
|
||||
scan_type = ScanType.HARDWARE_TRIGGERED
|
||||
|
||||
# Scan name: This is the name of the scan, e.g. "line_scan". This is used for display purposes and to identify the scan type in user interfaces.
|
||||
# Choose a descriptive name that does not conflict with existing scan names.
|
||||
# It must be a valid Python identifier, that is, it can only contain letters, numbers, and underscores, and must not start with a number.
|
||||
scan_name = "nidaq_continuous_scan"
|
||||
|
||||
gui_config = {"Scan Parameters": ["scan_duration", "daq", "compression"]}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
#fmt: off
|
||||
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Duration of the scan", units=Units.s)],
|
||||
daq: Annotated[DeviceBase | None, ScanArgument(display_name="Daq", description="DAQ device to be used for the scan")] = None,
|
||||
compression: Annotated[bool, ScanArgument(display_name="Compression", description="Whether to compress the data")]= False,
|
||||
#fmt: on
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
The NIDAQ continuous scan is used to measure with the NIDAQ without moving the
|
||||
monochromator or any other motor. The NIDAQ thus runs in continuous mode, with a
|
||||
set scan_duration.
|
||||
|
||||
Args:
|
||||
scan_duration (float): Duration of the scan
|
||||
daq (DeviceBase): DAQ device to be used for the scan
|
||||
compression (bool): Whether to compress the data
|
||||
|
||||
Returns:
|
||||
ScanReport
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._baseline_readout_status = None
|
||||
self.scan_duration = scan_duration
|
||||
self.daq = daq or self.dev["nidaq"]
|
||||
self.compression = compression
|
||||
self.monitored_readout_cycle = 1.0 # seconds
|
||||
self.motors = [self.daq]
|
||||
|
||||
self.update_scan_info(scan_duration=scan_duration, compression=compression)
|
||||
self.actions.set_device_readout_priority([self.daq], priority="async")
|
||||
|
||||
@scan_hook
|
||||
def prepare_scan(self):
|
||||
"""
|
||||
Prepare the scan. This can include any steps that need to be executed
|
||||
before the scan is opened, such as preparing the positions (if not done already)
|
||||
or setting up the devices.
|
||||
"""
|
||||
|
||||
self.actions.add_scan_report_instruction_device_progress(self.daq)
|
||||
self._baseline_readout_status = self.actions.read_baseline_devices(wait=False)
|
||||
|
||||
@scan_hook
|
||||
def open_scan(self):
|
||||
"""
|
||||
Open the scan.
|
||||
This step must call self.actions.open_scan() to ensure that a new scan is
|
||||
opened. Make sure to prepare the scan metadata before, either in
|
||||
prepare_scan() or in open_scan() itself and call self.update_scan_info(...)
|
||||
to update the scan metadata if needed.
|
||||
"""
|
||||
self.actions.open_scan()
|
||||
|
||||
@scan_hook
|
||||
def stage(self):
|
||||
"""
|
||||
Stage the devices for the upcoming scan. The stage logic is typically
|
||||
implemented on the device itself (i.e. by the device's stage method).
|
||||
However, if there are any additional steps that need to be executed before
|
||||
staging the devices, they can be implemented here.
|
||||
"""
|
||||
self.actions.stage_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def pre_scan(self):
|
||||
"""
|
||||
Pre-scan steps to be executed before the main scan logic.
|
||||
This is typically the last chance to prepare the devices before the core scan
|
||||
logic is executed. For example, this is a good place to initialize time-criticial
|
||||
devices, e.g. devices that have a short timeout.
|
||||
The pre-scan logic is typically implemented on the device itself.
|
||||
"""
|
||||
self.actions.pre_scan_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def scan_core(self):
|
||||
"""
|
||||
Core scan logic to be executed during the scan.
|
||||
This is where the main scan logic should be implemented.
|
||||
"""
|
||||
|
||||
kickoff_status = self.actions.kickoff(device=self.daq, wait=False)
|
||||
kickoff_status.wait(timeout=5) # wait for proper kickoff of device
|
||||
|
||||
complete_status = self.actions.complete(device=self.daq, wait=False)
|
||||
|
||||
while not complete_status.done:
|
||||
self.at_each_point()
|
||||
time.sleep(self.monitored_readout_cycle)
|
||||
|
||||
@scan_hook
|
||||
def at_each_point(self):
|
||||
"""
|
||||
Logic to be executed at each acquisition point during the scan.
|
||||
"""
|
||||
self.actions.read_monitored_devices()
|
||||
|
||||
@scan_hook
|
||||
def post_scan(self):
|
||||
"""
|
||||
Post-scan steps to be executed after the main scan logic.
|
||||
"""
|
||||
self.actions.complete_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def unstage(self):
|
||||
"""Unstage the scan by executing post-scan steps."""
|
||||
self.actions.unstage_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def close_scan(self):
|
||||
"""Close the scan."""
|
||||
if self._baseline_readout_status is not None:
|
||||
self._baseline_readout_status.wait()
|
||||
self.actions.close_scan()
|
||||
self.actions.check_for_unchecked_statuses()
|
||||
|
||||
@scan_hook
|
||||
def on_exception(self, exception: Exception):
|
||||
"""
|
||||
Handle exceptions that occur during the scan.
|
||||
This is a good place to implement any cleanup logic that needs to be executed in case of an exception,
|
||||
such as returning the devices to a safe state or moving the motors back to their starting position.
|
||||
"""
|
||||
|
||||
#######################################################
|
||||
######### Helper methods for the scan logic ###########
|
||||
#######################################################
|
||||
|
||||
# Implement scan-specific helper methods below.
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Scan components for debye_bec.
|
||||
|
||||
The scan components module allows you to define custom components that can be used in your scans.
|
||||
These components can be used to encapsulate reusable logic, interact with devices, or perform specific actions during the scan lifecycle.
|
||||
"""
|
||||
|
||||
from bec_server.scan_server.scans.scan_components import ScanComponents
|
||||
|
||||
|
||||
class DebyeBecScanComponents(ScanComponents):
|
||||
"""Scan components for debye_bec."""
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Scan modifier plugin for debye_bec.
|
||||
|
||||
The scan modifier allows you to modify the scan lifecycle and run custom actions before or after the scan hook or replace the scan hook entirely.
|
||||
Note that the scan_modifier module must be registered as a plugin in the pyproject.toml file for it to be recognized by the BEC framework and that
|
||||
there can only be one scan_modifier plugin registered at a time. If you need to run multiple scan modifiers, you can create a single scan
|
||||
modifier plugin that runs multiple actions in sequence with conditional logic to determine which actions to run based on the scan context.
|
||||
"""
|
||||
|
||||
from bec_server.scan_server.scans.scan_modifier import ScanModifier, scan_hook_impl
|
||||
|
||||
|
||||
class DebyeBecScanModifier(ScanModifier):
|
||||
"""
|
||||
Scan modifier for debye_bec.
|
||||
|
||||
By inheriting from the ScanModifier base class, you get access to currently running scan (self.scan), the devices (self.dev), the scan info (self.scan_info),
|
||||
the scan components (self.components) and the scan actions (self.actions).
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the scan modifier."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Example of running code before the scan stage for a specific scan
|
||||
# @scan_hook_impl("stage", "before")
|
||||
# def before_stage(self):
|
||||
# """Run before the stage hook."""
|
||||
# self.actions.send_client_info("Custom stage logic executed by ScanModifier.")
|
||||
# if self.scan_info.scan_name == "example_scan":
|
||||
# self.dev.samx.set(20)
|
||||
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
V4 implementation of the Debye XAS simple scan.
|
||||
|
||||
Scan procedure:
|
||||
- prepare_scan
|
||||
- open_scan
|
||||
- stage
|
||||
- pre_scan
|
||||
- scan_core
|
||||
- at_each_point (optionally called by scan_core)
|
||||
- post_scan
|
||||
- unstage
|
||||
- close_scan
|
||||
- on_exception (called if any exception is raised during the scan)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Annotated
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.device import DeviceBase
|
||||
from bec_lib.scan_args import ScanArgument, Units
|
||||
from bec_server.scan_server.scans.scan_base import ScanBase, ScanType
|
||||
from bec_server.scan_server.scans.scan_modifier import scan_hook
|
||||
|
||||
|
||||
class XasSimpleScan(ScanBase):
|
||||
scan_type = ScanType.HARDWARE_TRIGGERED
|
||||
scan_name = "xas_simple_scan"
|
||||
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# fmt: off
|
||||
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV, ge=4500, le=64000)],
|
||||
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV, ge=4500, le=64000)],
|
||||
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0.05)],
|
||||
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0.05)],
|
||||
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
|
||||
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
|
||||
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0)] = 1,
|
||||
# fmt: on
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Start a simple oscillating scan on the mono bragg motor.
|
||||
|
||||
Args:
|
||||
start (float): Start energy.
|
||||
stop (float): Stop energy.
|
||||
scan_time (float): Time for one scan cycle.
|
||||
scan_duration (float): Total scan duration.
|
||||
motor (DeviceBase | None): Bragg motor device.
|
||||
daq (DeviceBase | None): NIDAQ device.
|
||||
monitored_readout_cycle (float): Delay between monitored readouts.
|
||||
|
||||
Returns:
|
||||
ScanReport
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
self.scan_time = scan_time
|
||||
self.scan_duration = scan_duration
|
||||
self.motor = motor if motor is not None else self.dev["mo1_bragg"]
|
||||
self.daq = daq if daq is not None else self.dev["nidaq"]
|
||||
self.monitored_readout_cycle = monitored_readout_cycle
|
||||
self.positions = np.array([self.start, self.stop], dtype=float)
|
||||
|
||||
# We pass on the arguments as "additional_scan_parameters" in the scan info
|
||||
self.update_scan_info(
|
||||
positions=self.positions,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
monitored_readout_cycle=monitored_readout_cycle,
|
||||
)
|
||||
self.actions.set_device_readout_priority([self.daq], priority="async")
|
||||
|
||||
@scan_hook
|
||||
def prepare_scan(self):
|
||||
"""
|
||||
Prepare the scan. This can include any steps that need to be executed
|
||||
before the scan is opened, such as preparing the positions (if not done already)
|
||||
or setting up the devices.
|
||||
"""
|
||||
self.actions.add_scan_report_instruction_device_progress(self.motor)
|
||||
self._baseline_readout_status = self.actions.read_baseline_devices(wait=False)
|
||||
|
||||
@scan_hook
|
||||
def open_scan(self):
|
||||
"""
|
||||
Open the scan.
|
||||
This step must call self.actions.open_scan() to ensure that a new scan is
|
||||
opened. Make sure to prepare the scan metadata before, either in
|
||||
prepare_scan() or in open_scan() itself and call self.update_scan_info(...)
|
||||
to update the scan metadata if needed.
|
||||
"""
|
||||
self.actions.open_scan()
|
||||
|
||||
@scan_hook
|
||||
def stage(self):
|
||||
"""
|
||||
Stage the devices for the upcoming scan. The stage logic is typically
|
||||
implemented on the device itself (i.e. by the device's stage method).
|
||||
However, if there are any additional steps that need to be executed before
|
||||
staging the devices, they can be implemented here.
|
||||
"""
|
||||
self.actions.stage_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def pre_scan(self):
|
||||
"""
|
||||
Pre-scan steps to be executed before the main scan logic.
|
||||
This is typically the last chance to prepare the devices before the core scan
|
||||
logic is executed. For example, this is a good place to initialize time-criticial
|
||||
devices, e.g. devices that have a short timeout.
|
||||
The pre-scan logic is typically implemented on the device itself.
|
||||
"""
|
||||
self.actions.pre_scan_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def scan_core(self):
|
||||
"""
|
||||
Core scan logic to be executed during the scan.
|
||||
This is where the main scan logic should be implemented.
|
||||
"""
|
||||
self.actions.kickoff(self.motor)
|
||||
completion_status = self.actions.complete(self.motor, wait=False)
|
||||
|
||||
while not completion_status.done:
|
||||
self.at_each_point()
|
||||
|
||||
@scan_hook
|
||||
def at_each_point(self):
|
||||
"""
|
||||
Logic to be executed at each acquisition point during the scan.
|
||||
"""
|
||||
self.actions.read_monitored_devices()
|
||||
time.sleep(self.monitored_readout_cycle)
|
||||
|
||||
@scan_hook
|
||||
def post_scan(self):
|
||||
"""
|
||||
Post-scan steps to be executed after the main scan logic.
|
||||
"""
|
||||
self.actions.complete_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def unstage(self):
|
||||
"""Unstage the scan by executing post-scan steps."""
|
||||
self.actions.unstage_all_devices()
|
||||
|
||||
@scan_hook
|
||||
def close_scan(self):
|
||||
"""Close the scan."""
|
||||
if self._baseline_readout_status is not None:
|
||||
self._baseline_readout_status.wait()
|
||||
self.actions.close_scan()
|
||||
self.actions.check_for_unchecked_statuses()
|
||||
|
||||
@scan_hook
|
||||
def on_exception(self, exception: Exception):
|
||||
"""
|
||||
Handle exceptions that occur during the scan.
|
||||
This is a good place to implement any cleanup logic that needs to be executed in case of an exception,
|
||||
such as returning the devices to a safe state or moving the motors back to their starting position.
|
||||
"""
|
||||
self.actions.complete_all_devices(wait=False)
|
||||
|
||||
|
||||
class XasSimpleScanWithXrd(XasSimpleScan):
|
||||
scan_name = "xas_simple_scan_with_xrd"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
|
||||
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
|
||||
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
|
||||
"XRD Triggers": ["exp_time", "n_of_trigger"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# fmt: off
|
||||
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
|
||||
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
|
||||
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
|
||||
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
|
||||
break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")],
|
||||
break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)],
|
||||
cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)],
|
||||
break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")],
|
||||
break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)],
|
||||
cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)],
|
||||
exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)],
|
||||
n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)],
|
||||
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
|
||||
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
|
||||
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
|
||||
**kwargs,
|
||||
# fmt: on
|
||||
):
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
motor=motor,
|
||||
daq=daq,
|
||||
monitored_readout_cycle=monitored_readout_cycle,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# We pass on the arguments as "additional_scan_parameters" in the scan info
|
||||
self.update_scan_info(
|
||||
break_enable_low=break_enable_low,
|
||||
break_time_low=break_time_low,
|
||||
cycle_low=cycle_low,
|
||||
break_enable_high=break_enable_high,
|
||||
break_time_high=break_time_high,
|
||||
cycle_high=cycle_high,
|
||||
exp_time=exp_time,
|
||||
n_of_trigger=n_of_trigger,
|
||||
)
|
||||
|
||||
|
||||
class XasAdvancedScan(XasSimpleScan):
|
||||
scan_name = "xas_advanced_scan"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
|
||||
"Spline Parameters": ["p_kink", "e_kink"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# fmt: off
|
||||
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
|
||||
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
|
||||
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
|
||||
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
|
||||
p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)],
|
||||
e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)],
|
||||
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
|
||||
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
|
||||
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
|
||||
**kwargs,
|
||||
# fmt: on
|
||||
):
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
motor=motor,
|
||||
daq=daq,
|
||||
monitored_readout_cycle=monitored_readout_cycle,
|
||||
**kwargs,
|
||||
)
|
||||
# We pass on the arguments as "additional_scan_parameters" in the scan info
|
||||
self.update_scan_info(p_kink=p_kink, e_kink=e_kink)
|
||||
|
||||
|
||||
class XasAdvancedScanWithXrd(XasAdvancedScan):
|
||||
scan_name = "xas_advanced_scan_with_xrd"
|
||||
gui_config = {
|
||||
"Movement Parameters": ["start", "stop"],
|
||||
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
|
||||
"Spline Parameters": ["p_kink", "e_kink"],
|
||||
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
|
||||
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
|
||||
"XRD Triggers": ["exp_time", "n_of_trigger"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# fmt: off
|
||||
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
|
||||
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
|
||||
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
|
||||
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
|
||||
p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)],
|
||||
e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)],
|
||||
break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")],
|
||||
break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)],
|
||||
cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)],
|
||||
break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")],
|
||||
break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)],
|
||||
cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)],
|
||||
exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)],
|
||||
n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)],
|
||||
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
|
||||
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
|
||||
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
|
||||
**kwargs,
|
||||
# fmt: on
|
||||
):
|
||||
super().__init__(
|
||||
start=start,
|
||||
stop=stop,
|
||||
scan_time=scan_time,
|
||||
scan_duration=scan_duration,
|
||||
p_kink=p_kink,
|
||||
e_kink=e_kink,
|
||||
motor=motor,
|
||||
daq=daq,
|
||||
monitored_readout_cycle=monitored_readout_cycle,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# We pass on the arguments as "additional_scan_parameters" in the scan info
|
||||
self.update_scan_info(
|
||||
break_enable_low=break_enable_low,
|
||||
break_time_low=break_time_low,
|
||||
cycle_low=cycle_low,
|
||||
break_enable_high=break_enable_high,
|
||||
break_time_high=break_time_high,
|
||||
cycle_high=cycle_high,
|
||||
exp_time=exp_time,
|
||||
n_of_trigger=n_of_trigger,
|
||||
)
|
||||
@@ -46,6 +46,9 @@ plugin_file_writer = "debye_bec.file_writer"
|
||||
[project.entry-points."bec.scans"]
|
||||
plugin_scans = "debye_bec.scans"
|
||||
|
||||
[project.entry-points."bec.scans.scan_modifier"]
|
||||
plugin_scan_modifier = "debye_bec.scans.scan_customization.scan_modifier"
|
||||
|
||||
[project.entry-points."bec.scans.metadata_schema"]
|
||||
plugin_metadata_schema = "debye_bec.scans.metadata_schema"
|
||||
|
||||
|
||||
@@ -159,60 +159,6 @@ def test_set_control_settings(mock_bragg):
|
||||
assert dev.scan_control.scan_duration.get() == 5
|
||||
|
||||
|
||||
def test_update_scan_parameters(mock_bragg):
|
||||
dev = mock_bragg
|
||||
msg = ScanStatusMessage(
|
||||
scan_id="my_scan_id",
|
||||
status="closed",
|
||||
request_inputs={
|
||||
"inputs": {},
|
||||
"kwargs": {
|
||||
"start": 0,
|
||||
"stop": 5,
|
||||
"scan_time": 1,
|
||||
"scan_duration": 10,
|
||||
"xrd_enable_low": True,
|
||||
"xrd_enable_high": False,
|
||||
"num_trigger_low": 1,
|
||||
"num_trigger_high": 7,
|
||||
"exp_time_low": 1,
|
||||
"exp_time_high": 3,
|
||||
"cycle_low": 1,
|
||||
"cycle_high": 5,
|
||||
"p_kink": 50,
|
||||
"e_kink": 8000,
|
||||
},
|
||||
},
|
||||
info={
|
||||
"kwargs": {
|
||||
"start": 0,
|
||||
"stop": 5,
|
||||
"scan_time": 1,
|
||||
"scan_duration": 10,
|
||||
"xrd_enable_low": True,
|
||||
"xrd_enable_high": False,
|
||||
"num_trigger_low": 1,
|
||||
"num_trigger_high": 7,
|
||||
"exp_time_low": 1,
|
||||
"exp_time_high": 3,
|
||||
"cycle_low": 1,
|
||||
"cycle_high": 5,
|
||||
"p_kink": 50,
|
||||
"e_kink": 8000,
|
||||
}
|
||||
},
|
||||
metadata={},
|
||||
)
|
||||
mock_bragg.scan_info.msg = msg
|
||||
scan_param = dev.scan_parameter.model_dump()
|
||||
for _, v in scan_param.items():
|
||||
assert v == None
|
||||
dev._update_scan_parameter()
|
||||
scan_param = dev.scan_parameter.model_dump()
|
||||
for k, v in scan_param.items():
|
||||
assert v == msg.content["request_inputs"]["kwargs"].get(k, None)
|
||||
|
||||
|
||||
def test_kickoff_scan(mock_bragg):
|
||||
dev = mock_bragg
|
||||
dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.READY
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest import mock
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_server.scan_server.scan_worker import ScanWorker
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from ophyd.status import WaitTimeoutError
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError
|
||||
from ophyd_devices.tests.utils import MockPV
|
||||
@@ -15,6 +16,13 @@ from debye_bec.devices.nidaq.nidaq import Nidaq, NidaqError
|
||||
|
||||
# TODO move this function to ophyd_devices, it is duplicated in csaxs_bec and needed for other pluging repositories
|
||||
from debye_bec.devices.test_utils.utils import patch_dual_pvs
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def scan_info_mock():
|
||||
"""Fixture for the ScanInfo object."""
|
||||
return ScanServerScanInfo(scan_name="xas_simple_scan", scan_id="test")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@@ -52,13 +60,17 @@ def test_init(mock_nidaq):
|
||||
]
|
||||
|
||||
|
||||
def test_check_if_scan_name_is_valid(mock_nidaq):
|
||||
def test_check_if_scan_name_is_valid(mock_nidaq, scan_info_mock):
|
||||
"""Test the check_if_scan_name_is_valid method."""
|
||||
dev = mock_nidaq
|
||||
dev.scan_info.msg.scan_name = "xas_simple_scan"
|
||||
assert dev._check_if_scan_name_is_valid()
|
||||
dev.scan_info.msg.scan_name = "invalid_scan_name"
|
||||
assert not dev._check_if_scan_name_is_valid()
|
||||
scan_info_mock.scan_name = "xas_simple_scan"
|
||||
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
|
||||
scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
assert dev._check_if_scan_name_is_valid(scan_parameters)
|
||||
scan_info_mock.scan_name = "invalid_scan_name"
|
||||
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
|
||||
scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
assert not dev._check_if_scan_name_is_valid(scan_parameters)
|
||||
|
||||
|
||||
def test_set_config(mock_nidaq):
|
||||
@@ -120,11 +132,13 @@ def test_on_unstage(mock_nidaq):
|
||||
("nidaq_continuous_scan", False, 0),
|
||||
],
|
||||
)
|
||||
def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state):
|
||||
def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state, scan_info_mock):
|
||||
"""Test the on_pre_scan method of the Nidaq device."""
|
||||
dev = mock_nidaq
|
||||
dev.state.put(nidaq_state)
|
||||
dev.scan_info.msg.scan_name = scan_name
|
||||
scan_info_mock.scan_name = scan_name
|
||||
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
|
||||
dev.scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
dev._timeout_wait_for_pv = 0.1 # Set a short timeout for testing
|
||||
if not raise_error:
|
||||
dev.pre_scan()
|
||||
@@ -133,11 +147,13 @@ def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state):
|
||||
dev.pre_scan()
|
||||
|
||||
|
||||
def test_on_complete(mock_nidaq):
|
||||
def test_on_complete(mock_nidaq, scan_info_mock):
|
||||
"""Test the on_complete method of the Nidaq device."""
|
||||
dev = mock_nidaq
|
||||
scan_info_mock.scan_name = "nidaq_continuous_scan"
|
||||
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
|
||||
dev.scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
# Check for nidaq_continuous_scan
|
||||
dev.scan_info.msg.scan_name = "nidaq_continuous_scan"
|
||||
dev.state.put(0) # Set state to DISABLED
|
||||
status = dev.complete()
|
||||
assert status.done is False
|
||||
@@ -147,7 +163,9 @@ def test_on_complete(mock_nidaq):
|
||||
assert status.done is True
|
||||
|
||||
# Check for XAS simple scan
|
||||
dev.scan_info.msg.scan_name = "xas_simple_scan"
|
||||
scan_info_mock.scan_name = "xas_simple_scan"
|
||||
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
|
||||
dev.scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
dev.state.put(0) # Set state to ACQUIRE
|
||||
dev.stop_call.put(0)
|
||||
dev._timeout_wait_for_pv = 5
|
||||
|
||||
@@ -7,6 +7,9 @@ import ophyd
|
||||
import pytest
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
from bec_server.scan_server.scan_worker import ScanWorker
|
||||
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
|
||||
from bec_server.scan_server.tests.scan_fixtures import *
|
||||
from bec_server.scan_server.tests.scan_fixtures import _MockDevice
|
||||
from ophyd_devices import CompareStatus, DeviceStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError
|
||||
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
|
||||
@@ -20,6 +23,7 @@ from debye_bec.devices.pilatus.pilatus import (
|
||||
TRIGGERMODE,
|
||||
Pilatus,
|
||||
)
|
||||
from debye_bec.devices.utils.utils import fetch_scan_info
|
||||
|
||||
if TYPE_CHECKING: # pragma no cover
|
||||
from bec_lib.messages import FileMessage
|
||||
@@ -34,32 +38,38 @@ if TYPE_CHECKING: # pragma no cover
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
(0.1, 1, 1, "line_scan", "step"),
|
||||
(0.2, 2, 2, "time_scan", "step"),
|
||||
(0.5, 5, 5, "xas_advanced_scan", "fly"),
|
||||
(("samx", 0.1, 1, 5, "samy", 0, 1, 5), {"relative": True}, "_v4_hexagonal_scan"),
|
||||
((1, 0.2), {}, "_v4_time_scan"),
|
||||
((9000, 10000, 1, 20, 0.1, 9500), {}, "xas_advanced_scan"),
|
||||
],
|
||||
)
|
||||
def mock_scan_info(request, tmpdir):
|
||||
exp_time, frames_per_trigger, num_points, scan_name, scan_type = request.param
|
||||
scan_info = ScanStatusMessage(
|
||||
scan_id="test_id",
|
||||
status="open",
|
||||
scan_type=scan_type,
|
||||
scan_number=1,
|
||||
scan_parameters={
|
||||
"exp_time": exp_time,
|
||||
"frames_per_trigger": frames_per_trigger,
|
||||
"system_config": {},
|
||||
},
|
||||
info={"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")},
|
||||
num_points=num_points,
|
||||
scan_name=scan_name,
|
||||
)
|
||||
yield scan_info
|
||||
def mock_scan_info(request, tmpdir, v4_scan_assembler, device_manager):
|
||||
args, kwargs, scan_name = request.param
|
||||
mo1_bragg = _MockDevice(name="mo1_bragg")
|
||||
nidaq = _MockDevice(name="nidaq")
|
||||
device_manager.add_device(mo1_bragg)
|
||||
device_manager.add_device(nidaq)
|
||||
scan = v4_scan_assembler(scan_name, *args, **kwargs)
|
||||
yield scan.scan_info
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]:
|
||||
def mock_scan_status_message(mock_scan_info, tmpdir) -> ScanStatusMessage:
|
||||
info = mock_scan_info.model_dump()
|
||||
info.update({"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")})
|
||||
return ScanStatusMessage(
|
||||
scan_id=mock_scan_info.scan_id,
|
||||
status="open",
|
||||
scan_number=1,
|
||||
scan_name=mock_scan_info.scan_name,
|
||||
scan_type="fly" if mock_scan_info.scan_type == "hardware_triggered" else "step",
|
||||
num_points=mock_scan_info.num_points,
|
||||
info=info,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pilatus(mock_scan_status_message) -> Generator[Pilatus, None, None]:
|
||||
name = "pilatus"
|
||||
prefix = "X01DA-OP-MO1:PILATUS:"
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
@@ -70,8 +80,9 @@ def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]:
|
||||
# dev.image1 = mock.MagicMock()
|
||||
# with mock.patch.object(dev, "image1"):
|
||||
with mock.patch.object(dev, "task_handler"):
|
||||
dev.scan_info.msg = mock_scan_info
|
||||
dev.scan_info.msg = mock_scan_status_message
|
||||
try:
|
||||
dev.scan_parameters = fetch_scan_info(dev.scan_info)
|
||||
yield dev
|
||||
finally:
|
||||
try:
|
||||
@@ -177,7 +188,6 @@ def test_pilatus_on_trigger_cancel_on_stop(pilatus):
|
||||
|
||||
def test_pilatus_on_complete(pilatus: Pilatus):
|
||||
"""Test the on_complete logic of the Pilatus detector."""
|
||||
|
||||
if pilatus.scan_info.msg.scan_name.startswith("xas"):
|
||||
# TODO add test cases for xas scans
|
||||
# status = pilatus.complete()
|
||||
@@ -196,8 +206,9 @@ def test_pilatus_on_complete(pilatus: Pilatus):
|
||||
pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value
|
||||
pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value
|
||||
pilatus.cam.armed._read_pv.mock_data = DETECTORSTATE.ARMED.value
|
||||
num_images = pilatus.scan_info.msg.num_points * pilatus.scan_info.msg.scan_parameters.get(
|
||||
"frames_per_trigger", 1
|
||||
num_images = (
|
||||
pilatus.scan_parameters.num_points
|
||||
* pilatus.scan_parameters.additional_scan_parameters.get("frames_per_trigger", 1)
|
||||
)
|
||||
pilatus.hdf.num_captured._read_pv.mock_data = num_images - 1
|
||||
# Call on complete
|
||||
@@ -275,9 +286,12 @@ def test_pilatus_on_complete(pilatus: Pilatus):
|
||||
|
||||
def test_pilatus_on_stage_raises_low_exp_time(pilatus):
|
||||
"""Test that on_stage raises a ValueError if the exposure time is too low."""
|
||||
pilatus.scan_info.msg.scan_parameters["exp_time"] = 0.09
|
||||
scan_msg = pilatus.scan_info.msg
|
||||
if scan_msg.scan_type != "step" and scan_msg.scan_name not in pilatus.xas_xrd_scan_names:
|
||||
pilatus.scan_info.msg.info["exp_time"] = 0.09
|
||||
pilatus.scan_parameters = fetch_scan_info(pilatus.scan_info)
|
||||
if (
|
||||
pilatus.scan_parameters.scan_type != "software_triggered"
|
||||
and pilatus.scan_parameters.scan_name not in pilatus.xas_xrd_scan_names
|
||||
):
|
||||
return
|
||||
with pytest.raises(ValueError):
|
||||
pilatus.on_stage()
|
||||
|
||||
@@ -3,10 +3,10 @@ from functools import partial
|
||||
|
||||
import pytest
|
||||
from bec_server.device_server.tests.utils import DeviceMockType, DMMock
|
||||
from bec_server.scan_server.tests.fixtures import (
|
||||
ScanStubStatusMock,
|
||||
connector_mock,
|
||||
instruction_handler_mock,
|
||||
from bec_server.scan_server.tests.scan_fixtures import (
|
||||
nth_done_status_mock,
|
||||
readout_priority,
|
||||
v4_scan_assembler,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
# pylint: skip-file
|
||||
from unittest import mock
|
||||
|
||||
from bec_lib.messages import DeviceInstructionMessage
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
|
||||
from debye_bec.scans import (
|
||||
XASAdvancedScan,
|
||||
XASAdvancedScanWithXRD,
|
||||
XASSimpleScan,
|
||||
XASSimpleScanWithXRD,
|
||||
)
|
||||
|
||||
|
||||
def get_instructions(request, ScanStubStatusMock):
|
||||
request.metadata["RID"] = "my_test_request_id"
|
||||
|
||||
def fake_done():
|
||||
"""
|
||||
Fake done function for ScanStubStatusMock. Upon each call, it returns the next value from the generator.
|
||||
This is used to simulate the completion of the scan.
|
||||
"""
|
||||
yield False
|
||||
yield False
|
||||
yield True
|
||||
|
||||
def fake_complete(*args, **kwargs):
|
||||
yield "fake_complete"
|
||||
return ScanStubStatusMock(done_func=fake_done)
|
||||
|
||||
with (
|
||||
mock.patch.object(request.stubs, "complete", side_effect=fake_complete),
|
||||
mock.patch.object(request.stubs, "_get_result_from_status", return_value=None),
|
||||
):
|
||||
reference_commands = list(request.run())
|
||||
|
||||
for cmd in reference_commands:
|
||||
if not cmd or isinstance(cmd, str):
|
||||
continue
|
||||
if "RID" in cmd.metadata:
|
||||
cmd.metadata["RID"] = "my_test_request_id"
|
||||
if "rpc_id" in cmd.parameter:
|
||||
cmd.parameter["rpc_id"] = "my_test_rpc_id"
|
||||
cmd.metadata.pop("device_instr_id", None)
|
||||
|
||||
return reference_commands
|
||||
|
||||
|
||||
def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
|
||||
|
||||
request = scan_assembler(XASSimpleScan, start=0, stop=5, scan_time=1, scan_duration=10)
|
||||
request.device_manager.add_device("nidaq")
|
||||
reference_commands = get_instructions(request, ScanStubStatusMock)
|
||||
|
||||
assert reference_commands == [
|
||||
None,
|
||||
None,
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="scan_report_instruction",
|
||||
parameter={"device_progress": ["mo1_bragg"]},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="open_scan",
|
||||
parameter={
|
||||
"readout_priority": {
|
||||
"monitored": [],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": ["nidaq"],
|
||||
},
|
||||
"num_points": None,
|
||||
"positions": [0.0, 5.0],
|
||||
"scan_name": "xas_simple_scan",
|
||||
"scan_type": "fly",
|
||||
},
|
||||
),
|
||||
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
|
||||
action="stage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "baseline", "RID": "my_test_request_id"},
|
||||
device=["samx"],
|
||||
action="read",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="pre_scan",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device="mo1_bragg",
|
||||
action="kickoff",
|
||||
parameter={"configure": {}},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="unstage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="close_scan",
|
||||
parameter={},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
|
||||
|
||||
request = scan_assembler(
|
||||
XASSimpleScanWithXRD,
|
||||
start=0,
|
||||
stop=5,
|
||||
scan_time=1,
|
||||
scan_duration=10,
|
||||
break_enable_low=True,
|
||||
break_time_low=1,
|
||||
cycle_low=1,
|
||||
break_enable_high=True,
|
||||
break_time_high=2,
|
||||
exp_time=1,
|
||||
n_of_trigger=1,
|
||||
cycle_high=4,
|
||||
)
|
||||
request.device_manager.add_device("nidaq")
|
||||
reference_commands = get_instructions(request, ScanStubStatusMock)
|
||||
# TODO #64 based on creating this ScanStatusMessage, we should test the logic of stage/kickoff/complete/unstage in Pilatus and mo1Bragg
|
||||
|
||||
assert reference_commands == [
|
||||
None,
|
||||
None,
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="scan_report_instruction",
|
||||
parameter={"device_progress": ["mo1_bragg"]},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="open_scan",
|
||||
parameter={
|
||||
"readout_priority": {
|
||||
"monitored": [],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": ["nidaq"],
|
||||
},
|
||||
"num_points": None,
|
||||
"positions": [0.0, 5.0],
|
||||
"scan_name": "xas_simple_scan_with_xrd",
|
||||
"scan_type": "fly",
|
||||
},
|
||||
),
|
||||
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
|
||||
action="stage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "baseline", "RID": "my_test_request_id"},
|
||||
device=["samx"],
|
||||
action="read",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="pre_scan",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device="mo1_bragg",
|
||||
action="kickoff",
|
||||
parameter={"configure": {}},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="unstage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="close_scan",
|
||||
parameter={},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
|
||||
|
||||
request = scan_assembler(
|
||||
XASAdvancedScan,
|
||||
start=8000,
|
||||
stop=9000,
|
||||
scan_time=1,
|
||||
scan_duration=10,
|
||||
p_kink=50,
|
||||
e_kink=8500,
|
||||
)
|
||||
request.device_manager.add_device("nidaq")
|
||||
reference_commands = get_instructions(request, ScanStubStatusMock)
|
||||
|
||||
assert reference_commands == [
|
||||
None,
|
||||
None,
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="scan_report_instruction",
|
||||
parameter={"device_progress": ["mo1_bragg"]},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="open_scan",
|
||||
parameter={
|
||||
"readout_priority": {
|
||||
"monitored": [],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": ["nidaq"],
|
||||
},
|
||||
"num_points": None,
|
||||
"positions": [8000.0, 9000.0],
|
||||
"scan_name": "xas_advanced_scan",
|
||||
"scan_type": "fly",
|
||||
},
|
||||
),
|
||||
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
|
||||
action="stage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "baseline", "RID": "my_test_request_id"},
|
||||
device=["samx"],
|
||||
action="read",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="pre_scan",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device="mo1_bragg",
|
||||
action="kickoff",
|
||||
parameter={"configure": {}},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="unstage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="close_scan",
|
||||
parameter={},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
|
||||
|
||||
request = scan_assembler(
|
||||
XASAdvancedScanWithXRD,
|
||||
start=8000,
|
||||
stop=9000,
|
||||
scan_time=1,
|
||||
scan_duration=10,
|
||||
p_kink=50,
|
||||
e_kink=8500,
|
||||
break_enable_low=True,
|
||||
break_time_low=1,
|
||||
cycle_low=1,
|
||||
break_enable_high=True,
|
||||
break_time_high=2,
|
||||
exp_time=1,
|
||||
n_of_trigger=1,
|
||||
cycle_high=4,
|
||||
)
|
||||
request.device_manager.add_device("nidaq")
|
||||
reference_commands = get_instructions(request, ScanStubStatusMock)
|
||||
|
||||
assert reference_commands == [
|
||||
None,
|
||||
None,
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="scan_report_instruction",
|
||||
parameter={"device_progress": ["mo1_bragg"]},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="open_scan",
|
||||
parameter={
|
||||
"readout_priority": {
|
||||
"monitored": [],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": ["nidaq"],
|
||||
},
|
||||
"num_points": None,
|
||||
"positions": [8000.0, 9000.0],
|
||||
"scan_name": "xas_advanced_scan_with_xrd",
|
||||
"scan_type": "fly",
|
||||
},
|
||||
),
|
||||
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
|
||||
action="stage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "baseline", "RID": "my_test_request_id"},
|
||||
device=["samx"],
|
||||
action="read",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="pre_scan",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device="mo1_bragg",
|
||||
action="kickoff",
|
||||
parameter={"configure": {}},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="unstage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="close_scan",
|
||||
parameter={},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,152 @@
|
||||
# pylint: skip-file
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_server.scan_server.tests.scan_fixtures import *
|
||||
from bec_server.scan_server.tests.scan_hook_tests import *
|
||||
|
||||
XAS_SIMPLE_SCAN_DEFAULT_HOOK_TESTS = [
|
||||
("prepare_scan", [assert_prepare_scan_reads_baseline_devices]),
|
||||
("open_scan", [assert_scan_open_called]),
|
||||
("stage", [assert_stage_all_devices_called]),
|
||||
("pre_scan", [assert_pre_scan_called]),
|
||||
("unstage", [assert_unstage_all_devices_called]),
|
||||
("close_scan", [assert_close_scan_waits_for_baseline_and_closes]),
|
||||
]
|
||||
|
||||
|
||||
def _assemble_xas_simple_scan(v4_scan_assembler, **overrides):
|
||||
params = {
|
||||
"start": 8000.0,
|
||||
"stop": 9000.0,
|
||||
"scan_time": 1.0,
|
||||
"scan_duration": 10.0,
|
||||
"motor": "mo1_bragg",
|
||||
"daq": "nidaq",
|
||||
"monitored_readout_cycle": 1.0,
|
||||
}
|
||||
params.update(overrides)
|
||||
return v4_scan_assembler("xas_simple_scan", **params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("hook_name", "hook_tests"), XAS_SIMPLE_SCAN_DEFAULT_HOOK_TESTS)
|
||||
def test_xas_simple_scan_v4_default_hooks(
|
||||
v4_scan_assembler, nth_done_status_mock, hook_name, hook_tests
|
||||
):
|
||||
scan = _assemble_xas_simple_scan(v4_scan_assembler)
|
||||
|
||||
run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock)
|
||||
|
||||
|
||||
def test_xas_simple_scan_v4_prepare_scan_updates_metadata(v4_scan_assembler):
|
||||
scan = _assemble_xas_simple_scan(v4_scan_assembler)
|
||||
scan.actions.add_scan_report_instruction_device_progress = mock.MagicMock()
|
||||
baseline_status = mock.MagicMock()
|
||||
scan.actions.read_baseline_devices = mock.MagicMock(return_value=baseline_status)
|
||||
|
||||
scan.prepare_scan()
|
||||
|
||||
scan.actions._build_scan_status_message("open")
|
||||
|
||||
np.testing.assert_array_equal(scan.scan_info.positions, np.array([8000.0, 9000.0]))
|
||||
assert scan.scan_info.additional_scan_parameters["scan_time"] == 1.0
|
||||
assert scan.scan_info.additional_scan_parameters["scan_duration"] == 10.0
|
||||
assert scan.scan_info.readout_priority_modification["async"] == ["nidaq"]
|
||||
scan.actions.add_scan_report_instruction_device_progress.assert_called_once_with(scan.motor)
|
||||
scan.actions.read_baseline_devices.assert_called_once_with(wait=False)
|
||||
assert scan._baseline_readout_status is baseline_status
|
||||
|
||||
|
||||
def test_xas_simple_scan_v4_scan_core_reads_until_complete(v4_scan_assembler, nth_done_status_mock):
|
||||
scan = _assemble_xas_simple_scan(v4_scan_assembler)
|
||||
completion_status = nth_done_status_mock(resolve_after=3)
|
||||
scan.actions.kickoff = mock.MagicMock()
|
||||
scan.actions.complete = mock.MagicMock(return_value=completion_status)
|
||||
scan.actions.read_monitored_devices = mock.MagicMock()
|
||||
|
||||
with mock.patch("debye_bec.scans.xas_simple_scan.time.sleep"):
|
||||
scan.scan_core()
|
||||
|
||||
scan.actions.kickoff.assert_called_once_with(scan.motor)
|
||||
scan.actions.complete.assert_called_once_with(scan.motor, wait=False)
|
||||
assert scan.actions.read_monitored_devices.call_count == 2
|
||||
|
||||
|
||||
def test_xas_simple_scan_v4_post_scan_completes_all_devices(v4_scan_assembler):
|
||||
scan = _assemble_xas_simple_scan(v4_scan_assembler)
|
||||
scan.actions.complete_all_devices = mock.MagicMock()
|
||||
|
||||
scan.post_scan()
|
||||
|
||||
scan.actions.complete_all_devices.assert_called_once_with()
|
||||
|
||||
|
||||
def test_xas_simple_scan_with_xrd_v4_updates_xrd_metadata(v4_scan_assembler):
|
||||
scan = v4_scan_assembler(
|
||||
"xas_simple_scan_with_xrd",
|
||||
start=8000.0,
|
||||
stop=9000.0,
|
||||
scan_time=1.0,
|
||||
scan_duration=10.0,
|
||||
break_enable_low=True,
|
||||
break_time_low=1.0,
|
||||
cycle_low=2,
|
||||
break_enable_high=False,
|
||||
break_time_high=3.0,
|
||||
cycle_high=4,
|
||||
exp_time=0.5,
|
||||
n_of_trigger=6,
|
||||
motor="mo1_bragg",
|
||||
daq="nidaq",
|
||||
)
|
||||
|
||||
assert scan.scan_name == "xas_simple_scan_with_xrd"
|
||||
assert scan.scan_info.additional_scan_parameters["break_enable_low"] is True
|
||||
assert scan.scan_info.additional_scan_parameters["cycle_high"] == 4
|
||||
assert scan.scan_info.additional_scan_parameters["n_of_trigger"] == 6
|
||||
|
||||
|
||||
def test_xas_advanced_scan_v4_updates_spline_metadata(v4_scan_assembler):
|
||||
scan = v4_scan_assembler(
|
||||
"xas_advanced_scan",
|
||||
start=8000.0,
|
||||
stop=9000.0,
|
||||
scan_time=1.0,
|
||||
scan_duration=10.0,
|
||||
p_kink=50.0,
|
||||
e_kink=8500.0,
|
||||
motor="mo1_bragg",
|
||||
daq="nidaq",
|
||||
)
|
||||
|
||||
assert scan.scan_name == "xas_advanced_scan"
|
||||
assert scan.scan_info.additional_scan_parameters["p_kink"] == 50.0
|
||||
assert scan.scan_info.additional_scan_parameters["e_kink"] == 8500.0
|
||||
|
||||
|
||||
def test_xas_advanced_scan_with_xrd_v4_updates_all_metadata(v4_scan_assembler):
|
||||
scan = v4_scan_assembler(
|
||||
"xas_advanced_scan_with_xrd",
|
||||
start=8000.0,
|
||||
stop=9000.0,
|
||||
scan_time=1.0,
|
||||
scan_duration=10.0,
|
||||
p_kink=55.0,
|
||||
e_kink=8450.0,
|
||||
break_enable_low=True,
|
||||
break_time_low=1.5,
|
||||
cycle_low=2,
|
||||
break_enable_high=True,
|
||||
break_time_high=2.5,
|
||||
cycle_high=3,
|
||||
exp_time=0.25,
|
||||
n_of_trigger=8,
|
||||
motor="mo1_bragg",
|
||||
daq="nidaq",
|
||||
)
|
||||
|
||||
assert scan.scan_name == "xas_advanced_scan_with_xrd"
|
||||
assert scan.scan_info.additional_scan_parameters["p_kink"] == 55.0
|
||||
assert scan.scan_info.additional_scan_parameters["break_enable_high"] is True
|
||||
assert scan.scan_info.exp_time == 0.25
|
||||
@@ -1,126 +1,75 @@
|
||||
# pylint: skip-file
|
||||
from unittest import mock
|
||||
|
||||
from bec_lib.messages import DeviceInstructionMessage
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
import pytest
|
||||
from bec_server.scan_server.tests.scan_fixtures import *
|
||||
from bec_server.scan_server.tests.scan_hook_tests import *
|
||||
|
||||
from debye_bec.scans import NIDAQContinuousScan
|
||||
NIDAQ_CONTINUOUS_SCAN_DEFAULT_HOOK_TESTS = [
|
||||
("prepare_scan", [assert_prepare_scan_reads_baseline_devices]),
|
||||
("open_scan", [assert_scan_open_called]),
|
||||
("stage", [assert_stage_all_devices_called]),
|
||||
("pre_scan", [assert_pre_scan_called]),
|
||||
("unstage", [assert_unstage_all_devices_called]),
|
||||
("close_scan", [assert_close_scan_waits_for_baseline_and_closes]),
|
||||
]
|
||||
|
||||
|
||||
def get_instructions(request, ScanStubStatusMock):
|
||||
request.metadata["RID"] = "my_test_request_id"
|
||||
|
||||
def fake_done():
|
||||
"""
|
||||
Fake done function for ScanStubStatusMock. Upon each call, it returns the next value from the generator.
|
||||
This is used to simulate the completion of the scan.
|
||||
"""
|
||||
yield False
|
||||
yield False
|
||||
yield True
|
||||
|
||||
def fake_complete(*args, **kwargs):
|
||||
yield "fake_complete"
|
||||
return ScanStubStatusMock(done_func=fake_done)
|
||||
|
||||
with (
|
||||
mock.patch.object(request.stubs, "complete", side_effect=fake_complete),
|
||||
mock.patch.object(request.stubs, "_get_result_from_status", return_value=None),
|
||||
):
|
||||
reference_commands = list(request.run())
|
||||
|
||||
for cmd in reference_commands:
|
||||
if not cmd or isinstance(cmd, str):
|
||||
continue
|
||||
if "RID" in cmd.metadata:
|
||||
cmd.metadata["RID"] = "my_test_request_id"
|
||||
if "rpc_id" in cmd.parameter:
|
||||
cmd.parameter["rpc_id"] = "my_test_rpc_id"
|
||||
cmd.metadata.pop("device_instr_id", None)
|
||||
|
||||
return reference_commands
|
||||
def _assemble_nidaq_continuous_scan(v4_scan_assembler, **overrides):
|
||||
params = {"scan_duration": 10.0, "daq": "nidaq", "compression": False}
|
||||
params.update(overrides)
|
||||
return v4_scan_assembler("nidaq_continuous_scan", **params)
|
||||
|
||||
|
||||
def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
|
||||
@pytest.mark.parametrize(("hook_name", "hook_tests"), NIDAQ_CONTINUOUS_SCAN_DEFAULT_HOOK_TESTS)
|
||||
def test_nidaq_continuous_scan_v4_default_hooks(
|
||||
v4_scan_assembler, nth_done_status_mock, hook_name, hook_tests
|
||||
):
|
||||
scan = _assemble_nidaq_continuous_scan(v4_scan_assembler)
|
||||
|
||||
request = scan_assembler(NIDAQContinuousScan, scan_duration=10)
|
||||
request.device_manager.add_device("nidaq")
|
||||
reference_commands = get_instructions(request, ScanStubStatusMock)
|
||||
assert reference_commands == [
|
||||
None,
|
||||
None,
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="scan_report_instruction",
|
||||
parameter={"device_progress": ["nidaq"]},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="open_scan",
|
||||
parameter={
|
||||
"readout_priority": {
|
||||
"monitored": [],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": ["nidaq"],
|
||||
},
|
||||
"num_points": 0,
|
||||
"positions": [],
|
||||
"scan_name": "nidaq_continuous_scan",
|
||||
"scan_type": "fly",
|
||||
},
|
||||
),
|
||||
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
|
||||
action="stage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "baseline", "RID": "my_test_request_id"},
|
||||
device=["samx"],
|
||||
action="read",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="pre_scan",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device="nidaq",
|
||||
action="kickoff",
|
||||
parameter={"configure": {}},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1},
|
||||
device=["bpm4i", "eiger", "mo1_bragg"],
|
||||
action="read",
|
||||
parameter={"group": "monitored"},
|
||||
),
|
||||
"fake_complete",
|
||||
DeviceInstructionMessage(
|
||||
metadata={},
|
||||
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
|
||||
action="unstage",
|
||||
parameter={},
|
||||
),
|
||||
DeviceInstructionMessage(
|
||||
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
|
||||
device=None,
|
||||
action="close_scan",
|
||||
parameter={},
|
||||
),
|
||||
]
|
||||
run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock)
|
||||
|
||||
|
||||
def test_nidaq_continuous_scan_v4_prepare_scan_updates_metadata(v4_scan_assembler):
|
||||
scan = _assemble_nidaq_continuous_scan(v4_scan_assembler)
|
||||
scan.actions.add_scan_report_instruction_device_progress = mock.MagicMock()
|
||||
baseline_status = mock.MagicMock()
|
||||
scan.actions.read_baseline_devices = mock.MagicMock(return_value=baseline_status)
|
||||
|
||||
scan.prepare_scan()
|
||||
scan.actions._build_scan_status_message("open")
|
||||
|
||||
assert scan.scan_info.additional_scan_parameters["scan_duration"] == 10.0
|
||||
assert scan.scan_info.additional_scan_parameters["compression"] is False
|
||||
assert scan.scan_info.readout_priority_modification["async"] == ["nidaq"]
|
||||
scan.actions.add_scan_report_instruction_device_progress.assert_called_once_with(scan.daq)
|
||||
scan.actions.read_baseline_devices.assert_called_once_with(wait=False)
|
||||
assert scan._baseline_readout_status is baseline_status
|
||||
|
||||
|
||||
def test_nidaq_continuous_scan_v4_scan_core_reads_until_complete(
|
||||
v4_scan_assembler, nth_done_status_mock
|
||||
):
|
||||
scan = _assemble_nidaq_continuous_scan(v4_scan_assembler)
|
||||
kickoff_status = mock.MagicMock()
|
||||
completion_status = nth_done_status_mock(resolve_after=3)
|
||||
scan.actions.kickoff = mock.MagicMock(return_value=kickoff_status)
|
||||
scan.actions.complete = mock.MagicMock(return_value=completion_status)
|
||||
scan.actions.read_monitored_devices = mock.MagicMock()
|
||||
|
||||
with mock.patch("debye_bec.scans.nidaq_continuous_scan.time.sleep"):
|
||||
scan.scan_core()
|
||||
|
||||
scan.actions.kickoff.assert_called_once_with(device=scan.daq, wait=False)
|
||||
kickoff_status.wait.assert_called_once_with(timeout=5)
|
||||
scan.actions.complete.assert_called_once_with(device=scan.daq, wait=False)
|
||||
assert scan.actions.read_monitored_devices.call_count == 2
|
||||
|
||||
|
||||
def test_nidaq_continuous_scan_v4_post_scan_completes_all_devices(v4_scan_assembler):
|
||||
scan = _assemble_nidaq_continuous_scan(v4_scan_assembler)
|
||||
scan.actions.complete_all_devices = mock.MagicMock()
|
||||
|
||||
scan.post_scan()
|
||||
|
||||
scan.actions.complete_all_devices.assert_called_once_with()
|
||||
|
||||
Reference in New Issue
Block a user