Files
EsfRixsApps/ESF_RIXS/ESF_RIXS.py
2024-09-17 10:36:47 +02:00

1238 lines
35 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# *-----------------------------------------------------------------------*
# | |
# | Copyright (c) 2024 by Paul Scherrer Institute (http://www.psi.ch) |
# | |
# | Author Thierry Zamofing (thierry.zamofing@psi.ch) |
# *-----------------------------------------------------------------------*
"""
SwissFEL Athos spectrometer
EPICS simulation motor PVs:
~/Documents/prj/SwissFEL/test_ioc/MotorSim/iocBoot/SATES30-RIXS/SATES30-RIXS.cmd
caQtDM -attach ~/Documents/prj/SwissFEL/test_ioc/MotorSim/iocBoot/SATES30-RIXS/SATES30-RIXS.ui
bitmask for modes:
0x01:
0x02:
0x04:
0x08:
0x10:
0x20:
0x40:
0x80:
"""
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QSlider, QLineEdit,\
QCheckBox, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, QComboBox, QMessageBox
from PyQt5.QtGui import QPainter, QColor, QPen, QBrush, QPolygon, QTransform
from PyQt5.QtCore import QPoint, QPointF, Qt
from numpy.matlib import empty
from pyqtgraph.Qt import QtCore, QtGui
import PyQt5.QtGui as QtGui
import PyQt5.QtCore as QtCore
import PyQt5.QtWidgets as QtW
from PyQt5.uic import loadUiType
import numpy as np
from RIXSgeometry import VLSgrating
from RIXScfg import Config
from RIXSgirder import WndGirderRIXS
import sys, logging, copy, re
import epics
if sys.version_info[0] < 3 or sys.version_info[1] < 6:
print(f"Must be using Python 3.6 or newer. Try e.g. /opt/gfa/python-3.8/latest/bin/python /sf/furka/bin/RIXSconfig")
_log=logging.getLogger(__name__)
def obj_info(obj,p=''):
print(f"{p}obj_info:{obj}")
try:
pos=obj.pos()
print(f"{p}pos:({pos.x():.6g},{pos.y():.6g})") # in coordinate value on the scene (no change by zooming)
except AttributeError:
pass
try:
sz=obj.size()
print(f"{p}size:({sz.x():.6g},{sz.y():.6g})") # in coordinate value on the scene (no change by zooming)
except AttributeError:
pass
try:
for k, v in (('Viewport', obj.viewport()), ('Window', obj.window())):
print(
f"{p}{k} (x,y)(w,h):({v.x():.6g},{v.y():.6g})({v.width():.6g},{v.height():.6g})") # in coordinate value on the scene (no change by zooming)
except AttributeError:
pass
try:
scnPos=obj.scenePos()
print(f"{p}scenePos:({scnPos.x():.6g},{scnPos.y():.6g})") # in pixel on the scene (changes by zooming)
except AttributeError:
pass
try:
if type(obj)==QTransform:
t=obj
else:
t=obj.transform()
print(f"{p}QTransform:{t.m11():8.5g} {t.m12():8.5g} {t.m13():8.5g}")
print(f"{p} {t.m21():8.5g} {t.m22():8.5g} {t.m23():8.5g}")
print(f"{p} {t.m31():8.5g} {t.m32():8.5g} {t.m33():8.5g}")
except AttributeError:
pass
#Ui_MainWindow, QMainWindow = loadUiType("graphEx1.ui")
#class RIXSgrating(QMainWindow, Ui_MainWindow):
# def __init__(self, ):
# super(RIXSgrating, self).__init__()
# self.setupUi(self)
class WndMainRIXS(QWidget):
def __init__(self):
super().__init__()
self._param={
'ofs':(500, 500), # location of drawings
'difrBeamPaint':4, # mode to plot the difracted beam
}
self.initUI()
def initUI(self):
self.setGeometry(100, 100, 350, 350)
app=QApplication.instance()
dev=app._dev
self.setWindowTitle(f'{dev._name} main')
self._wDevs=w=QWidget(self)
w.move(10, 10)
lv=QVBoxLayout(w)
#lv.addWidget(w)
for pb in ('Visualize','Vars'):
w=QPushButton(pb)
#w.clicked.connect(self.OnOpenChildWnd
w.clicked.connect(lambda dummy,pb=pb: self.OnOpenChildWnd(pb))
lv.addWidget(w)
w=QGroupBox(dev._name, self,objectName=dev._name)
lv.addWidget(w)
lg=QGridLayout(w)
row=0
t='energy'
w=QLabel(t);
lg.addWidget(w, row, 0)
w=QLineEdit(t, objectName=t);
lg.addWidget(w, row, 1)
w.returnPressed.connect(lambda w=w:self.OnEnergyChanged(w))
w=QLabel('out of\nspecs',objectName='oos');w.setStyleSheet("background-color:yellow;color:red;font-weight: bold;font-size: small;")
#https://stackoverflow.com/questions/10794532/how-to-make-a-qt-widget-invisible-without-changing-the-position-of-the-other-qt
sp=w.sizePolicy();sp.setRetainSizeWhenHidden(True);w.setSizePolicy(sp)#avoid resizing if not visible
w.setVisible(False)
lg.addWidget(w, row, 2)
row+=1;
key, rng, tk, pos=('energy', (200, 2000), 50, 60)
w=QSlider(QtCore.Qt.Horizontal, objectName=key)
w.setFixedWidth(200);
w.setMinimum(rng[0]);
w.setMaximum(rng[1])
w.setTickPosition(QSlider.TicksBelow);
w.setTickInterval(tk)
#lv.addWidget(w)
lg.addWidget(w, row, 0, 1, 2)
w.valueChanged.connect(lambda val, w=w:self.OnEnergyChanged(w, val))
row+=1
t='probe'
w=QLabel(t);
lg.addWidget(w, row, 0)
w=QComboBox(objectName=t);
lg.addWidget(w, row, 1)
for i, tx in enumerate(tuple(dev._probes.keys())):
w.insertItem(i, tx)
w.currentIndexChanged.connect(lambda val, w=w:self.OnEnergyChanged(w,val))
w=QCheckBox('fix', objectName=t)
w.clicked.connect(lambda val, w=w:self.OnEnergyChanged(w, val))
lg.addWidget(w, row, 2)
row+=1
w=QPushButton('sync motors')
w.clicked.connect(self.btnSyncMotors)
lv.addWidget(w)
self.show()
def btnSyncMotors(self):
#reads the epics motor and updates the vizualization
_log.info('')
app=QApplication.instance()
dev=app._dev
geo=dev._geo
pv_rbv=dev._pvDict['rbv']
wGrp=self.findChild(QGroupBox,dev._name)
wLe=wGrp.findChild(QLineEdit, 'energy')
#wLe=self.findChild(QLineEdit, 'energy')
wLe.blockSignals(True)
wLe.setText('<unknown>')
wLe.blockSignals(False)
m=dict()
for k,v in pv_rbv.items():
if v.connected:
m[k]=v.get()
else:
_log.warning(f'pv {k} not connected')
break
dev.motor2geometry(m)
geo.update(m)
dev.setGeo2Paint(dev._geo)
try:
wndCld=app._wndVisualize
except AttributeError as e:
pass
else:
wndCld.updateDevice(dev)
try:
wndCld=app._wndVars
except AttributeError as e:
pass
else:
wndCld.updateDevice(dev)
def OnOpenChildWnd(self,wnd):
app=QApplication.instance()
if wnd=='Visualize':
try:
wnd=app._wndVisualize
except AttributeError as e:
app._wndVisualize=WndVisualize()
else:
wnd.show()
elif wnd=='Vars':
try:
wnd=app._wndVars
except AttributeError as e:
app._wndVars=WndVars()
else:
wnd.show()
else:
_log.error(f'unknown window{wnd}')
def OnEnergyChanged(self,w,*args):
#_log.debug(f'OnEnergyChanged: {w} {args}')
wGrp=w.parent()
app=QApplication.instance()
#wCb=self._wdGrpEnergy.findChild(QComboBox, 'Grating')
wLeEnergy=wGrp.findChild(QLineEdit, 'energy')
wSlEnergy=wGrp.findChild(QSlider, 'energy')
wFix=wGrp.findChild(QCheckBox, 'probe')
t=type(w)
if t==QLineEdit:
try:
e=float(w.text())
except ValueError as e:
return
wSlEnergy.blockSignals(True)
wSlEnergy.setValue(int(e))
wSlEnergy.blockSignals(False)
elif t==QComboBox:
_log.info(f'cb:{args}')
app=QApplication.instance()
try:
e=float(wLeEnergy.text())
except ValueError as e:
return
elif t==QSlider:
e=args[0]
wLeEnergy.setText(f'{e:.4g}')
elif t==QCheckBox:
_log.info(f'fix:{args}')
else:
raise(TypeError(f'wrong type: {t}'))
try: e # is there an energy to set?
except UnboundLocalError as er: pass
else:
self.setEnergy(e,wFix.isChecked(),wGrp)
def setEnergy(self,energy,fix,wGrp):
app=QApplication.instance()
dev=app._dev
vlsg=app._vlsg
wcbProbe=wGrp.findChild(QComboBox, 'probe')
wLbOOS=wGrp.findChild(QLabel, 'oos')
k=wcbProbe.currentText()
probe=dev._probes[k]
try:
eMin,eMax=Config._lutProbes[probe['probe']]['e']
except KeyError as e:
eMin=eMax=0
if fix:
wLbOOS.setVisible(energy<eMin or energy>eMax)
else:
if energy<eMin or energy>eMax:
probe=None
_log.info('try to find a better probe')
for idx,p in enumerate(dev._probes.values()):
try:
eMin, eMax=Config._lutProbes[p['probe']]['e']
except KeyError as e:
#_log.error(f'no energy range for probe: {i}')
continue
if energy>=eMin and energy<=eMax:
wcbProbe.blockSignals(True)
wcbProbe.setCurrentIndex(idx)
wcbProbe.blockSignals(False)
probe=p
break
if probe is None:
wLbOOS.setVisible(True)
_log.warning(f'found no probe for energy {energy}')
return
else:
wLbOOS.setVisible(False)
try:
vlsg.setup(probe['probe'])
except KeyError as e:
_log.error(f'can not find probe: {repr(e)}')
return
geo=vlsg.energy2geometry(energy,probe)
dev.setGeo2Paint(geo)
try:
dev.geometry2motor()
except ValueError as e:
_log.warning(f'{e}')
try:
wndCld=app._wndVisualize
except AttributeError as e:
pass
else:
wndCld.updateDevice(dev)
try:
wndCld=app._wndVars
except AttributeError as e:
pass
else:
wndCld.updateDevice(dev)
class RIXSdevice():
_lutDifrBeamPaint=( # (number of difr beam,draw mode,alpha,width)
(4, 2, 190, 0),
(4, 1, 200, 0),
(32, 1, 120, 0),
(8, 1, 196, 0),
(32, 0, 120, 3),
(32, 2, 255, 0),
)
_prefix='SATES30-RIXS:'
_motors=(
('mt', 'Mask translation'),
('gtz', 'Grating translation along Z-axis (along beam)'),
('gty1', 'Grating translation along Y-axis (height) Wedge leveller 1'),
('gty2', 'Grating translation along Y-axis (height) Wedge leveller 2'),
('grx', 'Grating rotation around X-axis'),
('gtx', 'Grating translation along X-axis'),
('dtz', 'Detector Translation along Z-axis'),
('dty1', 'Detector Translation along Y-axis'),
('dty2', 'Detector Translation along Y-axis'),
('drx', 'Detector Rotation around X-axis (ɣ) '),
)
def __init__(self,name,**kwargs):
self._name=name
self._paint=p={
'mode':4, #difraction beam paint mode
'ofs':(500, 500), # location of vlsg
'szG':(200, 5), # size VLS grating
'szD':(150, 5), # size detector
'sclTrf':(2**(6/2), 2**(-6/2)), # scaling transfformation [angle, distance]
}
p.update(kwargs)
self._geo=g={
'r1':2000, # distance probe grating
'r2':3500, # distance grating detector
'aa':88, # grating angle
'bb':87, # reflection angle
'cc':22, # detector angle
}
self.setGeo2Paint(g)
self.connectEPICS()
def connectEPICS(self):
_log.info('connect PVs')
self._pvDict=pvd={'rbv':dict()}
pv_rbv=pvd['rbv']
self._pvConnected=0
for m,v in self._motors:
pvn=self._prefix+'MOT_'+m.upper()+'.RBV'
pv=epics.get_pv(pvn,connection_callback=self.OnConnectionChange,callback=self.OnValueChange)
#pv=epics.get_pv(pvn)
pv_rbv[m]=pv
_log.info(f'{pv}')
print(len(pv_rbv))
#for pv in pv_rbv.values(): #need to do it is 2 steps.
# pv.connection_callbacks.append(self.OnConnectionChange)
# idx=pv.add_callback(self.OnValueChange)
# pv.run_callback(idx)
def OnConnectionChange(self, pvname=None, conn=None, **kws):
pvc=self._pvConnected
if conn:
pvc+=1
else:
if pvc>0: pvc-=1
pv_rbv=self._pvDict['rbv']
_log.info(f'PV connection {pvc}/{len(pv_rbv)}: {pvname} {conn}')
self._pvConnected=pvc
app=QApplication.instance()
#wGrp=app._wndVisualize._wdGrpDraw
#key=self._pv2key[pvname]
#wLb=wGrp.findChild(QLabel, key)
#if not conn:
# v=f"<font color='#a00000'>{key}</font>"
# wLb.setText(v)
#else:
# v=f"<font color='#00a000'>{key}</font>"
# wLb.setText(v)
def OnValueChange(self, pvname, value, **kw):
#_log.info(kw) #{
# 'pvname': 'SATES30-ARES:MOT_2TRY.RBV', 'value': 102.507, 'char_value': '102.5070', 'status': 0, 'ftype': 20,
# 'chid': 26100744, 'host': 'localhost:5064', 'count': 1, 'access': 'read-only', 'write_access': False, 'read_access': True,
# 'severity': 0, 'timestamp': 1724915455.46745, 'posixseconds': 1724915455.0, 'nanoseconds': 467450100, 'precision': 4,
# 'units': 'deg', 'enum_strs': None, 'upper_disp_limit': 0.0, 'lower_disp_limit': 0.0, 'upper_alarm_limit': nan,
#'lower_alarm_limit': nan, 'lower_warning_limit': nan, 'upper_warning_limit': nan, 'upper_ctrl_limit': 0.0,
#'lower_ctrl_limit': 0.0, 'nelm': 1, 'type': 'time_double', 'typefull': 'time_double',
#'cb_info': (1, <PV 'SATES30-ARES:MOT_2TRY.RBV', count=1, type=time_double, access=read-only>)
#}
_log.info(f"PV val:{pvname}:{value}")
app=QApplication.instance()
self.motor2geometry()
self.setGeo2Paint(self._geo)
try:
wndCld=app._wndVisualize
except AttributeError as e:
pass
else:
wndCld.updateDevice(self)
try:
wndCld=app._wndVars
except AttributeError as e:
pass
else:
#wnd.update_label(pvname, value, **kw) # would not update the geometry
wndCld.updateDevice(self)
#pv.add_callback('RBV', self.update_label)
#_log.warning(f"can't handle PV: {pvname}:{value}")
# return
#self.vis_update(key,value,'008000')
def setGeo2Paint(self,geo):
self._geo=geo
p=self._paint
sclA,sclD=p['sclTrf']
p.update({
'r1':geo['r1']*sclD,
'r2':geo['r2']*sclD,
'aa':(90-geo['aa'])*sclA,
'bb':(90-geo['bb'])*sclA,
'cc':geo['cc'],
})
def geometry2motor(self):
# returns raw motor positions
# offset detector plane to deflected beam: 34deg
geo=self._geo
r1,r2,aa,bb,cc=geo['r1'],geo['r2'],geo['aa'],geo['bb'],geo['cc']
mt=gtz=gty1=gty2=grx=gtx=dtz=dty1=dty2=drx=None
degArm=90-aa+90-bb
radArm=np.deg2rad(degArm)
gtz=r1
#grx=np.sin(np.deg2rad(90-aa))*200 # TODO: check angle to distance conversion
grx=aa-90
dtz=r1+np.cos(radArm)*r2
dty1=dty2=np.sin(radArm)*r2
#drx=np.sin(np.deg2rad(90-aa+90-bb+cc-34))*200 # TODO: check angle to distance conversion
drx=90-aa+90-bb+cc
dd=cc-34 # angle of bellow to detector
geo.update({
'mt':mt,
'gtz':gtz,
'gty1':gty1,
'gty2': gty2,
'grx':grx,
'gtx':gtx,
'dtz':dtz,
'dty1':dty1,
'dty2':dty2,
'drx':drx})
if degArm>10:
raise(ValueError('angle arm > 10°'))
elif degArm<1:
raise(ValueError('angle arm < 1°'))
elif abs(dd)>15:
raise(ValueError('angle bellow to detector > 15°'))
def motor2geometry(self,m=None):
# reads the motor position from the pv and updates the geometry
# IT DOES NOT touch the motor fields in _geometry !
if m is None: #dictionary is empty
pv_rbv=self._pvDict['rbv']
if len(pv_rbv)<10:
_log.info('not yet populated')
return
m=dict()
for k, v in pv_rbv.items():
if v.connected and v._args['value'] is not None:
# v._args['value'] is not None is needed as connected sometimes is not save enough
m[k]=v.get()
else:
_log.warning(f'pv {k} not connected or no value')
return
def mot2geo(mt,gtz,gty1,gty2,grx,gtx,dtz,dty1,dty2,drx):
geo=self._geo
#degArm=90-aa+90-bb
#radArm=np.deg2rad(degArm)
#gtz=r1
#grx=90-aa
#dtz=np.cos(radArm)*r2
#dty1=dty2=np.sin(radArm)*r2
#drx=90-aa+90-bb+cc-34
#dd=cc-34 # angle of bellow to detector
r1=gtz
x=(dtz-gtz)
y=(dty1+dty2)/2
r2=(x**2+y**2)**.5
radArm=np.arctan2(y,x)
degArm=np.rad2deg(radArm)
#aa=90-np.rad2deg(np.arcsin(grx/200)) # TODO: check angle to distance conversion
aa=90+grx
#degArm=90-aa+90-bb
bb=90-aa+90-degArm
#drx=90-aa+90-bb+cc-34
#cc=drx-90+aa-90+bb+34
#cc=np.rad2deg(np.arcsin(drx/200))-degArm+34 # TODO: check angle to distance conversion
cc=drx-degArm
geo.update({
'r1':r1,
'r2':r2,
'aa':aa,
'bb':bb,
'cc':cc,
})
mot2geo(**m)
def containsPoint(self,point):
try:
pg=self._polygon
except AttributeError:
return False
return pg.containsPoint(point,Qt.OddEvenFill)
@staticmethod
def plotOrig(qp):
penR=QPen(QtCore.Qt.red, 5, QtCore.Qt.SolidLine)
penG=QPen(QtCore.Qt.green, 5, QtCore.Qt.SolidLine)
pOrig=qp.pen()
qp.setPen(penR)
qp.drawLine(-20, 0, 20, 0)
qp.drawLine( 20, 0, 16, 2)
qp.setPen(penG)
qp.drawLine(0, -20, 0, 20)
qp.drawLine(0, 20, 2, 16)
qp.setPen(pOrig)
def paint(self,qp):
# qp QPainter to paint on
# ofs caanter to draw
# scl scaling for x and y translation of coordinate systems
# paintMode: mode how to paint the diffraction beam
p=self._paint
r1=p['r1']
r2=p['r2']
aa=p['aa']
bb=p['bb']
cc=p['cc']
szG=p['szG']
szD=p['szD']
ofs=p['ofs']
w=int(max(szG[0], szD[0])/2)
ofs=(max(ofs[0], w+r1), max(w+r2*np.sin((aa+bb)*np.pi/180), ofs[1]))
# --- prepare transformations ---
# tf0: grating center not rotated
# tfa: grating center rotated angle -aa
# tfab: grating center rotated angle -aa-bb
# tfc: detector center rotated angle -cc
tf0=QTransform()
tf0.translate(ofs[0], ofs[1])
tfa=copy.copy(tf0) # center
tfa.rotate(-aa)
tfab=copy.copy(tf0) # center
tfab.rotate(-aa-bb)
tfc=copy.copy(tfab)
tfc.translate(r2,0).rotate(-cc)
penBk=QPen(QtCore.Qt.black, 0, QtCore.Qt.SolidLine)
penWt=QPen(QtCore.Qt.white, 1, QtCore.Qt.SolidLine)
penYl=QPen(QtCore.Qt.yellow, 1, QtCore.Qt.SolidLine)
penRd=QPen(QtCore.Qt.red, 1, QtCore.Qt.SolidLine)
# --- visualize ---
#qp.setRenderHints(QPainter.HighQualityAntialiasing)
pl=[
QPoint(*tf0.map (-r1-w, -w)),
QPoint(*tf0.map (-r1-w, +w)),
QPoint(*tf0.map ( 0, +w)),
QPoint(*tfab.map( 0, +w)),
QPoint(*tfab.map( r2+w, +w)),
QPoint(*tfab.map( r2+w, -w)),
QPoint(*tf0.map ( -w,-w)),
]
self._polygon=QPolygon(pl)
qp.setBrush(QColor(0, 0, 0))
# plot black background
qp.setTransform(tf0)
qp.drawRect(-r1-w, -w, r1+w, 2*w)
qp.drawEllipse(-w, -w, 2*w, 2*w)
qp.setTransform(tfab)
qp.drawRect(0, -w, r2+w, 2*w)
# plot beam path
qp.setTransform(tf0)
qp.setCompositionMode(QtGui.QPainter.CompositionMode_Lighten)
qp.setPen(penWt)
qp.setBrush(QColor(255, 80, 0, 128))
qp.drawEllipse(-r1-20, -10, 20, 20) # target
qp.drawLine(-r1, 0, 0, 0) # central beam1
qp.rotate(-aa)
tf1=qp.transform()
qp.drawRect(int(-szG[0]/2), 0, szG[0], szG[1]) # grating
qp.rotate(-bb)
tf2=qp.transform()
qp.drawLine(0, 0, int(r2+szD[0]/2), 0) # central beam2
qp.translate(r2, 0)
qp.rotate(-cc)
tf3=qp.transform()
qp.drawRect(int(-szD[0]/2), 0, szD[0], szD[1]) # detector
# beam target to vlsg
qp.setTransform(QtGui.QTransform())
p0=QPointF(*tf0.map(-r1-10, 10))
p1=QPointF(*tf1.map(-szG[0]/2, 0))
qp.drawLine(p0, p1)
p0=QPointF(*tf0.map(-r1-10, -10))
p1=QPointF(*tf1.map(szG[0]/2, 0))
qp.drawLine(p0, p1)
# beam vlsg to detector
# n=32;mode=0;alpha=120;width=3
n, mode, alpha, width=self._lutDifrBeamPaint[p['mode']]
scl=256/n
p0=QPointF(*tf1.map(-szG[0]/2, 0))
p1=QPointF(*tf1.map(szG[0]/2, 0))
if mode==0:
pen=QtGui.QPen(QtCore.Qt.black, width, QtCore.Qt.SolidLine)
col=QtGui.QColor()
for i in range(n):
p2=QPointF(*tf3.map(szD[0]/2-szD[0]*i/(n-1), 0))
col.setHsv(int(i*scl), 255, 255, alpha)
pen.setColor(col)
qp.setPen(pen)
qp.drawLine(p0, p2)
qp.drawLine(p1, p2)
if mode==1:
col=QtGui.QColor()
qp.setPen(penBk)
for i in range(n):
p2=QPointF(*tf3.map(szD[0]/2-szD[0]*i/(n-1), 0))
col.setHsv(int(i*scl), 255, 255, alpha)
qp.setBrush(col)
qp.drawPolygon(p0, p1, p2)
if mode==2:
col=QtGui.QColor()
qp.setPen(penBk)
for i in range(n):
p2=QPointF(*tf3.map(szD[0]/2-szD[0]*(i+1)/n, 0))
p3=QPointF(*tf3.map(szD[0]/2-szD[0]*i/n, 0))
col.setHsv(int(i*scl), 255, 255, alpha)
qp.setBrush(col)
qp.drawPolygon(p0, p1, p2, p3)
qp.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
# draw central beam (if needed)
if mode!=0:
qp.setTransform(tf0)
qp.setPen(QtGui.QPen(QtGui.QColor(0x757780), 2, QtCore.Qt.SolidLine))
qp.drawLine(-r1, 0, 0, 0) # central beam1
qp.setTransform(tf2)
qp.drawLine(0, 0, int(r2+szD[0]/2), 0) # central beam2
# draw dimensions
g=self._geo
vr1=g['r1']
vr2=g['r2']
vaa=90-g['aa']
vbb=90-g['bb']
vcc=g['cc']
qp.setTransform(QtGui.QTransform())
qp.setPen(penRd)
p0=QPointF(*tf0.map(-r1-10, +10))
p1=QPointF(*tf0.map(-r1-10, 0+w))
qp.drawLine(p0, p1)
p2=QPointF(*tf0.map(0, 0))
p3=QPointF(*tf0.map(0, 0+w))
qp.drawLine(p2, p3)
p4=(p1+p3)/2 # text pos
p5=QPointF(*tf2.map(0, 0+w))
qp.drawLine(p2, p5)
p6=QPointF(*tf2.map(r2, 0))
p7=QPointF(*tf2.map(r2, 0+w))
qp.drawLine(p6, p7)
p8=(p5+p7)/2 # text pos
p9=QPointF(*tf1.map(0, -w))
qp.drawLine(p2, p9)
s=50
qp.drawArc(int(p2.x())-s, int(p2.y())-s, 2*s, 2*s, 180*16, int(aa*16))
s1=s*np.cos(aa*np.pi/180);
s2=s*np.sin(aa*np.pi/180)
qp.drawArc(int(p2.x())-s, int(p2.y())-s, 2*s, 2*s, int(aa*16), int(bb*16))
qp.drawArc(int(p6.x())-s, int(p6.y())-s, 2*s, 2*s, int((aa+bb)*16), int(cc*16))
p10=QPointF(*tf2.map(r2+20, 0)) # text pos
qp.setPen(penYl)
sclTrf=p['sclTrf']
qp.drawText(p0+QPointF(20-w, 10-w), f'scl(ang;{sclTrf[0]:0.2g},dst:{sclTrf[1]:0.2g})')
qp.drawText(p4+QPointF(-40, -10), f'R1={vr1:.5g}mm')
# qp.setTransform(QtGui.QTransform())
qp.translate(p8);
qp.rotate(-aa-bb)
qp.drawText(-40, -10, f'R2={vr2:.5g}mm')
qp.setTransform(QtGui.QTransform())
qp.translate(p2);
qp.rotate(-aa)
qp.drawText(20-int(szG[0]/2), 20, f'aa={vaa:.5g}°')
qp.drawText(20, 20, f'bb={vbb:.5g}°')
qp.setTransform(QtGui.QTransform())
qp.translate(p10);
qp.rotate(-aa-bb)
qp.drawText(0, 20, f'cc={vcc:.5g}°')
#mouse move polygon
#qp.setTransform(QTransform())
#qp.setPen(penRd)
#qp.drawPolygon(self._polygon)
#origin crosses
#for tf in (tf0,tfa,tfab,tfc):#,tfg,tfs):#,tfc,tfs):
# qp.setTransform(tf);self.plotOrig(qp)
class WndVisualize(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(560, 100, 1300, 800)
self.setWindowTitle('Visualize')
app=QApplication.instance()
dev=app._dev
self._wdGrpDraw=w=QGroupBox(dev._name,self)
w.move(10,10)
row=0
lg=QGridLayout(w)
pDev=dev._paint
for key, rng, tk in (
('r1', (50, 800), 50),
('r2', (50, 800), 50),
('aa', (0, 90), 5),
('bb', (0, 90), 5),
('cc', (0, 180), 5),
('mode',(0,len(RIXSdevice._lutDifrBeamPaint)-1),1),
('sclA', ( -8, 8), 1),
('sclD', (-8, 8), 1),):
wLb=QLabel(key)
wSl=QSlider(QtCore.Qt.Horizontal,objectName=key)
wSl.setFixedWidth(200);wSl.setMinimum(rng[0]);wSl.setMaximum(rng[1])
if key.startswith('scl'):
if key[-1]=='A':
v=pDev['sclTrf'][0]
else:
v=pDev['sclTrf'][1]
v=int(round(np.log2(v)*2))
else:
v=pDev[key]
wSl.setValue(v)
wSl.setTickPosition(QSlider.TicksBelow);wSl.setTickInterval(tk)
wSl.valueChanged.connect(lambda val,key=key: self.sldChanged(key,val))
lg.addWidget(wLb, row, 0)
lg.addWidget(wSl, row, 1);row+=1
self.show()
def destroy(self, destroyWindow, destroySubWindows): #overloaded function
_log.info('destroy')
def closeEvent(self, event): #overloaded function
_log.info('closeEvent')
def sldChanged(self,key,val,*args,**kwargs):
app=QApplication.instance()
dev=app._dev
p=dev._paint
if key.startswith('scl'):
wGrp=self._wdGrpDraw
if key[-1]=='A':
p['sclTrf']=(2**(val/2),p['sclTrf'][1])
else:
p['sclTrf']=(p['sclTrf'][0],2**(val/2))
dev.setGeo2Paint(dev._geo)
else:
p[key]=val
g=dev._geo
if key in ('r1','r2'):
sclD=p['sclTrf'][1]
g[key]=val/sclD
elif key in ('aa', 'bb'):
sclA=p['sclTrf'][0]
g[key]=90-val/sclA
else:
g[key]=val
self.update()
def mouseReleaseEvent(self, a0):
try:
del self._mouseDrag
except AttributeError:
pass
def mousePressEvent(self, a0):
app=QApplication.instance()
mousePos=a0.pos()
print(a0.type)
if a0.type()!=QtGui.QMouseEvent.MouseButtonPress:
return
wGrp=self._wdGrpDraw
#if wGrp.underMouse(): #draging sliders?
if wGrp.geometry().contains(mousePos):
self._mouseDrag={'obj':wGrp, 'start':mousePos}
return
dev=app._dev
if dev.containsPoint(mousePos):
self._devSel=dev
wGrp=self._wdGrpDraw
wGrp.setTitle(dev._name)
devP=dev._paint
for wSl in wGrp.findChildren(QSlider):
#_log.info(wSl)
key=wSl.objectName()
if key.startswith('scl'):
if key[-1]=='A':
v=devP['sclTrf'][0]
else:
v=devP['sclTrf'][1]
v=int(round(np.log2(v)*2))
else:
v=devP[key]
wSl.setValue(v)
self._mouseDrag={'obj':dev,'start':(mousePos,dev._paint['ofs'])}
try:
_log.info(f'{self._mouseDrag}')
except AttributeError:
_log.info(f'no object to drag')
def mouseMoveEvent(self, a0):
try:
md=self._mouseDrag
except AttributeError:
return
obj=md['obj']
s=md['start']
if obj==self._wdGrpDraw:
p=a0.pos()
md['start']=p
p=obj.geometry().topLeft()+p-s
obj.move(p)
return
p=a0.pos()
ofs=QPoint(*s[1])+p-s[0]
_log.info(f'{p} {ofs}')
obj._paint['ofs']=(ofs.x(),ofs.y())
self.update()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
qp.setRenderHints(QPainter.HighQualityAntialiasing)
app=QApplication.instance()
dev=app._dev
app._dev.paint(qp)
qp.end()
def updateDevice(self, dev):
#_log.info(f'WndVis')
self._devSel=dev
devP=dev._paint
wGrp=self._wdGrpDraw
wGrp.setTitle(dev._name)
for wSl in wGrp.findChildren(QSlider):
# _log.info(wSl)
key=wSl.objectName()
if key.startswith('scl'):
if key[-1]=='A':
v=devP['sclTrf'][0]
else:
v=devP['sclTrf'][1]
v=int(round(np.log2(v)*2))
else:
v=devP[key]
wSl.blockSignals(True)
wSl.setValue(v)
wSl.blockSignals(False)
self.update()
def OnEnergyChanged(self,w,*args):
#_log.debug(f'OnEnergyChanged: {w} {args}')
app=QApplication.instance()
#wCb=self._wdGrpEnergy.findChild(QComboBox, 'Grating')
wLe=self._wdGrpEnergy.findChild(QLineEdit, 'Energy')
wSl=self._wdGrpEnergy.findChild(QSlider, 'energy')
t=type(w)
if t==QLineEdit:
try:
e=float(w.text())
except ValueError as e:
return
wSl.blockSignals(True)
wSl.setValue(int(e))
wSl.blockSignals(False)
elif t==QComboBox:
app=QApplication.instance()
vlsg=app._vlsg
vlsg.setup(w.currentText())
try:
e=float(wLe.text())
except ValueError as e:
return
elif t==QSlider:
e=args[0]
wLe.setText(f'{e:.4g}')
else:
raise(TypeError(f'wrong type: {t}'))
self.setEnergy(e)
def OnEvent(self,*args,**kwargs):
#test event
print(f'OnEvent: {args} ,{kwargs}')
def setEnergy(self,val):
app=QApplication.instance()
wGrating=self._wdGrpEnergy.findChild(QComboBox, 'Grating')
wEnergy=self._wdGrpEnergy.findChild(QLineEdit, 'Energy')
wEnergy.setText(f'{val:.4g}')
vlsg=app._vlsg
vlsg.setup(wGrating.currentText())
self._vlsg_geo=geo=vlsg.energy2geometry(val)
grp=self._wdGrpGeometry
for k,v in geo.items():
w=grp.findChild(QLineEdit, k)
w.setText(f'{v:.6g}')
self._vlsg_raw=raw,err=vlsg.geometry2raw(**geo)
if err is not None:
_log.error(str(err))
self._vlsg_raw=raw
grp=self._wdGrpRaw
for k,v in raw.items():
w=grp.findChild(QLineEdit, k)
if v is None:
w.setText('(null)')
else:
w.setText(f'{v:.6g}')
self.update()
class WndVars(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
app=QApplication.instance()
dev=app._dev
self.setGeometry(100, 500, 550, 550)
self.setWindowTitle('Vars')
self._wdGrpGeometry=w=QGroupBox("Geometry",self)
w.move(10,10)
l=QGridLayout(w)
lut={'aa':'\u03B1', 'bb':'\u03B2', 'cc':'\u03B3'}
for i,t in enumerate(('r1','r2','aa','bb','cc',)):
tl=lut.get(t,t)
w=QLabel(tl)
l.addWidget(w, i,0)
w=QLineEdit(t,objectName=t)
l.addWidget(w, i,1)
self._wdGrpMotor=w=QGroupBox("Motors",self)
w.move(200,10)
#w.setFixedSize(400,42+32*14)
w.setFixedWidth(330)
l=QGridLayout(w)
prefix=RIXSdevice._prefix
motors=RIXSdevice._motors
pv_rbv=dev._pvDict['rbv']
for i,v in enumerate(motors):
m,desc=v
wCb=QCheckBox(m,objectName=m)
l.addWidget(wCb, i,0)
pv=pv_rbv[m]
if pv.connected:
v=pv.get(as_string=True)
else:
v=m
w=QLineEdit(v,objectName=m)
w.setToolTip(desc)
l.addWidget(w, i,1)
v=f"<font color='#808080'>{m}</font>"
w=QLabel(v,objectName=m)
l.addWidget(w, i, 2)
#pv=pvd[rcNm]
w=QPushButton('move motors')
w.clicked.connect(self.btnMoveMotors)
l.addWidget(w, i+1, 1)
self.show()
def updateDevice(self, dev):
grpGeo=self._wdGrpGeometry
grpMot=self._wdGrpMotor
geo=dev._geo
for k, v in geo.items():
wLe=grpGeo.findChild(QLineEdit, k)
if wLe is not None:
wLe.setText(f'{v:.6g}')
else:
self.update_label(k,v)
self.update()
def update_label(self,k,v,**kwargs):
#_log.info(f'{k}:{v}:{kwargs}')
app=QApplication.instance()
dev=app._dev
if kwargs:
rbv=v
key=re.search('_(.*)\.',k).group(1).lower()
val=dev._geo.get(key,None)
#_log.debug(f'monitoring: {key}:{val}:{rbv}')
else:
key=k
val=v
pv_rbv=dev._pvDict['rbv']
pv=pv_rbv[k]
if pv.connected:
rbv=pv.get()
else:
rbv=None
#_log.debug(f'change: {key}:{val}:{rbv}')
grpMot=self._wdGrpMotor
wLe=grpMot.findChild(QLineEdit, key)
if wLe is not None:
wCb=grpMot.findChild(QCheckBox, key)
wLb=grpMot.findChild(QLabel, key)
#TODO:
if val is None:
v=f"<font color='#606060'>{rbv}</font>"
else:
wLe.setText(f'{val:.3f}') # {v:.6g}
if rbv is None:
v=f"<font color='#606060'>{rbv}</font>"
else:
d=abs(val-rbv)
b=bool(d>0.001)
wCb.setChecked(b)
if b:
c='ff8080'
else:
c='60a060'
v=f"<font color='#{c}'>{rbv:.5g} ({d:+.5g})</font>"
wLb.setText(v)
def btnMoveMotors(self):
app=QApplication.instance()
dev=app._dev
grpMot=self._wdGrpMotor
lg=grpMot.layout()
row=lg.rowCount()
cnt=0
for r in range(row-1): # -1 because last one is a button
wCb=lg.itemAtPosition(r, 0)
if wCb is None: continue
if wCb.widget().isChecked(): cnt+=1
if cnt==0: return
btn=QMessageBox.question(
self,
"Motion",
f"Really want to move {cnt} motors ?",
buttons=QMessageBox.Yes|QMessageBox.No,
defaultButton=QMessageBox.No,
)
if btn!=QMessageBox.Yes: return
_log.info('for safty reason the program will not move')
return
geo=dev._geo
try:
pv_val=dev._pvDict['val']
except KeyError as e:
pv_val=dev._pvDict['val']=dict()
for r in range(row-1): # -1 because last one is a button
wCb=lg.itemAtPosition(r, 0)
if wCb is None:
continue
wCb=wCb.widget()
if wCb.isChecked():
m=wCb.objectName()
try:
pv=pv_val[m]
except KeyError as e:
pvn=RIXSdevice._prefix+'MOT_'+m.upper()+'.VAL'
pv=pv_val[m]=epics.get_pv(pvn)#,connection_callback=self.OnConnectionChange,callback=self.OnValueChange)
pv.put(geo[m])
# ->>>> https://pyepics.github.io/pyepics/devices.html >>> USE THIS
#[satesf-cons-03 ~]$ /opt/gfa/python-3.8/latest/bin/ipython3
#import CaChannel
#import epics
#capv=CaChannel.CaChannel('SATOP11-PSAS079:MOT_DDF_X.VAL') -> THIS IS REALLY BASIC...
#capv.state()
#m=epics.Motor('SATOP11-PSAS079:MOT_DDF_X')
#pv=pvm.PV('RBV')
#dev=epics.Device('SATOP11-PSAS079:MOT_DDF_X.', attrs=('VAL', 'RBV', 'DESC'))
#dev.RBV
class SimEpicsDevice(): #behaves similar to epics.Device
def __init__(self, prefix='', attrs=None,
nonpvs=None, delim='', timeout=None,
mutable=True, aliases=None, with_poll=True):
self._prefix=prefix
self._pvs={}
for k in attrs:
self._pvs[k]=0
def __getattr__(self, attr):
#if self._pvs:
if '_pvs' in self.__dict__:
return self._pvs[attr]
else:
_log.debug(attr)
raise AttributeError
def __setattr__(self, attr, val):
if attr in ('_pvs','_prefix'):
self.__dict__[attr]=val
elif attr in self._pvs:
self._pvs[attr]= val
if attr=='VAL' and 'RBV' in self._pvs:
self._pvs['RBV']=val
else:
self.__dict__[attr]=val
def __repr__(self):
"string representation"
attr=[]
for k,v in self._pvs.items():
attr.append(f'{k}: {v}')
s=f"<Device: {self._prefix} ( "+', '.join(attr)+' )'
return s
# if attr in self._pvs:
# return self.get(attr)
# elif attr in self.__dict__:
# return self.__dict__[attr]
if __name__ == '__main__':
import argparse
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(module)s:%(lineno)d:%(funcName)s:%(message)s ')
def main():
epilog=__doc__ # +'\nExamples:'+''.join(map(lambda s:cmd+s, exampleCmd))+'\n'
parser=argparse.ArgumentParser(epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--mode', '-m', type=lambda x:int(x, 0), help='mode (see bitmasks) default=0x%(default)x', default=1)
parser.add_argument("--sim", "-s", type=lambda x: int(x,0), help="simulate devices (see bitmasks) default=0x%(default)x", default=0x01)
args=parser.parse_args()
_log.info('Arguments:{}'.format(args.__dict__))
app=QApplication(sys.argv)
app._args=args
#devUS=RIXSdevice('RIXS us', ofs=(750, 200), g=-100, s=-1500, sy=-20, cy=70)
#devDS=RIXSdevice('RIXS ds', ofs=(930, 630))
sCfg=Config._spectrometer
pCfg=Config._lutProbes
app._dev=dev=RIXSdevice('Furka-RIXS',**sCfg['vis'])
dev._probes=sCfg['probes']
vlsg=VLSgrating()
app._vlsg=vlsg
k=pCfg.keys()
for v in dev._probes.values():
try:
p=v['probe']
except:
pass
else:
if p not in k:
_log.error(f'probe:{p} not found in {tuple(k)}')
if args.mode&0x01:
app._wndRIXS=WndMainRIXS()
if args.mode&0x02:
app._wndGirder=WndGirderRIXS()
sys.exit(app.exec_())
main()