Files
EsfRixsApps/ARESvis/ARESvis.py
2024-08-29 16:35:14 +02:00

601 lines
18 KiB
Python
Executable File

#!/usr/bin/env python
# *-----------------------------------------------------------------------*
# | |
# | Copyright (c) 2024 by Paul Scherrer Institute (http://www.psi.ch) |
# | |
# | Author Thierry Zamofing (thierry.zamofing@psi.ch) |
# *-----------------------------------------------------------------------*
"""
Furka ARES chamber visualization
For simulated motor IOC:
/home/zamofing_t/Documents/prj/SwissFEL/test_ioc/MotorSim/iocBoot/ARESvis/ARESvis.cmd
For motor ui:
caQtDM ~/Documents/prj/SwissFEL/test_ioc/MotorSim/iocBoot/ARESvis/ARESvis.ui&
self.pv_angles = [epics.PV("SATES30-ARES:MOT_SRY.RBV"), epics.PV("SATES30-ARES:MOT_DRY.RBV"), epics.PV("SATES30-ARES:MOT_2TRY.RBV")]
SATES30-RIXS:MOT_RY.RBV # sliding seal
SATES30-ARES:MOT_JFRY.RBV # jungfrau detector angle:
SATES30-ARES:MOT_2TRY.RBV # 2thetha angle: foc.mirror Diode2 Diode3
SATES30-ARES:MOT_DRY.RBV # detector angle: Diode1 Mirror
SATES30-ARES:MOT_SRY.RBV # sample rotation
bitmask for simulation:
0x01: EPICS motors
0x02:
0x04:
0x08:
0x10:
0x20:
0x40:
0x80:
"""
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QSlider, QLineEdit,\
QCheckBox, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, QComboBox
from PyQt5.QtGui import QPainter, QColor, QPen, QBrush, QPolygon, QTransform
from PyQt5.QtCore import QPoint, QPointF, Qt
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
import sys, logging, copy
import epics
_log=logging.getLogger(__name__)
class ARESdevice():
_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),
)
def __init__(self,name,**kwargs):
self._name=name
self._paint=p={
'ofs':(500, 500), # location of device
'rArm':3*62, # radius of chamber (RIXS arm)
'rJFr':3*52, # radius of Jungfrau
'r2Th':3*48, # radius 2Theta platfform
'rDet':3*23, # radius of detector table
'rTrg':3*20, # radius of target
'szArm':(20, 50),
'aArm':100, # angle RIXS arm
'aJFr':140, # angle detector
'a2Th':30, # angle 2thetha
'aDet':-20, # angle detector
'aTrg':10, # angle target
'mode':4, #difraction beam paint mode
'szG':(200, 5), # size VLS grating
'szD':(150, 5), # size detector
'sclTrf':(2**(6/2), 2**(0/2)), # scaling transfformation [angle, distance]
}
# 2thetha angle SATES30-ARES:MOT_2TRY
# detector angle SATES30-ARES:MOT_DRY
# sliding seal SATES30-RIXS:MOT_RY
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.setGeometry(g)
def setGeometry(self,geo):
self._geo=geo
p=self._paint
sclA,sclD=p['sclTrf']
p.update({
'r1':int(geo['r1']*sclD),
'r2':int(geo['r2']*sclD),
'aa':int((90-geo['aa'])*sclA),
'bb':int((90-geo['bb'])*sclA),
'cc':int(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=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
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 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, 2, QtCore.Qt.SolidLine)
penG=QPen(QtCore.Qt.green, 2, 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
ofs=p['ofs']
rArm=p['rArm']
rJFr=p['rJFr']
r2Th=p['r2Th']
rDet=p['rDet']
rTrg=p['rTrg']
aArm=p['aArm']
aJFr=p['aJFr']
a2Th=p['a2Th']
aDet=p['aDet']
aTrg=p['aTrg']
sclTrf=p['sclTrf']
# --- prepare transformations ---
# tf0: target not rotated
# tfArm: target center rotated angle aaArm
# tf2Th: target center rotated angle tf2Th
# tfDet: target center rotated angle aaDet
# tfTrg: target center rotated angle aaTrg
tf0=QTransform()
tf0.translate(ofs[0], ofs[1])
tf0.scale(sclTrf[1],sclTrf[1])
tfArm=copy.copy(tf0) # center
tfArm.rotate(-aArm)
tfJFr=copy.copy(tf0)
tfJFr.rotate(-aJFr)
tf2Th=copy.copy(tf0)
tf2Th.rotate(-a2Th)
tfDet=copy.copy(tf0)
tfDet.rotate(-aDet)
tfTrg=copy.copy(tf0)
tfTrg.rotate(-aTrg)
#tfd.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)
penBl=QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine)
penRd=QPen(QtCore.Qt.red, 1, QtCore.Qt.SolidLine)
# --- visualize ---
#qp.setRenderHints(QPainter.HighQualityAntialiasing)
# setup and plot dragable region
#pl=[QPoint(*tf0.map (-rArm , -rArm)),QPoint(*tf0.map (-rArm , +rArm+50)),QPoint(*tf0.map (+rArm+50+aaArm, +rArm+50+aa2Th)),QPoint(*tf0.map(+rArm+50+aaDet , -rArm-50)),]
self._polygon=QPolygon([
QPoint(*tf0.map(-rArm ,-rArm)),
QPoint(*tf0.map(-rArm ,+rArm+100)),
QPoint(*tf0.map(+rArm,+rArm+100)),
QPoint(*tf0.map(+rArm,-rArm)),])
qp.setBrush(QColor(0, 0, 0,64))
qp.drawPolygon(self._polygon)
# plot beam path
qp.setTransform(tf0)
qp.setPen(penBk)
qp.setBrush(QColor(128, 128, 128, 128)) #r,g,b,a
#circles of rtation
qp.drawEllipse(-rArm, -rArm, 2*rArm, 2*rArm) # ARES chamber
qp.drawEllipse(-rJFr, -rJFr, 2*rJFr, 2*rJFr) # Jungfrau
qp.drawEllipse(-r2Th, -r2Th, 2*r2Th, 2*r2Th) # 2theta
qp.drawEllipse(-rDet, -rDet, 2*rDet, 2*rDet) # detector
qp.drawEllipse(-rTrg, -rTrg, 2*rTrg, 2*rTrg) # target
#beam arrow
qp.setPen(QPen(QtCore.Qt.black, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,+rArm+100,0,rArm)
qp.drawPolygon(QPolygon([QPoint(0,rArm),QPoint(-5,rArm+20),QPoint(+5,rArm+20),]))
#self.plotOrig(qp)
#qp.setPen(penRd)
#qp.drawRect(-10, -10+rArm, 20, 20)
#RIXS-arm devices
qp.setTransform(tfArm)
qp.setPen(QPen(QtCore.Qt.red, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,-rArm,0,-rArm+10)
qp.setPen(penBk)
qp.setBrush(QColor(255,0,0,128))
qp.drawRect(-20, -50-rArm, 40, 50) # 2th mount sample
qp.drawRect(-100, -200-rArm, 200, 150) # 2th mount sample
#Jungfrau devices
qp.setTransform(tfJFr)
qp.setPen(QPen(QtCore.Qt.magenta, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,-rJFr,0,-rJFr+10)
qp.setPen(penBk)
qp.setBrush(QColor(255,0,255,128))
qp.drawRect(-50, -20-rJFr, 100, 15) # detector mount sample
#2-theta devices
qp.setTransform(tf2Th)
qp.setPen(QPen(QtCore.Qt.green, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,-r2Th,0,-r2Th+10)
qp.setPen(penBk)
qp.setBrush(QColor(0,255,0,128))
qp.drawRect(-15, -30-r2Th, 5, 3*29) # foc. mirror 1
qp.drawRect(+10, -30-r2Th, 5, 3*29) # foc. mirror 2
qp.rotate(20)
qp.drawRect(-10, -r2Th, 20, 20) # diode2
qp.setTransform(tf2Th);qp.rotate(120)
qp.drawRect(-10, -r2Th, 20, 20) # diode3
#detector devices
qp.setTransform(tfDet)
qp.setPen(QPen(QtCore.Qt.blue, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,-rDet,0,-rDet+10)
qp.setPen(penBk)
qp.setBrush(QColor(0,0,255,128))
qp.drawRect(-10, -rDet, 20, 10) # diode1
qp.rotate(30);qp.translate(0,-rDet);qp.rotate(45)
qp.drawRect(-10, 0, 30, 5) # mirror1
#target devices
qp.setTransform(tfTrg)
qp.setPen(QPen(QtCore.Qt.cyan, 3, QtCore.Qt.SolidLine))
qp.drawLine(0,-rTrg,0,-rTrg+10)
qp.setPen(penBk)
qp.setBrush(QColor(0,255,255,128))
qp.drawRect(-30, -20, 60, 20) # target mount sample
#parabola mirror
qp.setTransform(tf0)
qp.setPen(penBk)
qp.setBrush(QColor(80,80,80,128))
qp.drawPolygon(QPolygon([QPoint(-15,30),QPoint(-15,60),QPoint(15,60),]))
#qp.setPen(penBk)
#qp.drawRect(-10, -10+rArm, 20, 20)
#self.plotOrig(qp)
#qp.setTransform(tf2Th)
#qp.setPen(penYl)
#qp.drawRect(-10, -10+r2Th, 20, 20)
#self.plotOrig(qp)
#qp.setTransform(tfDet)
#qp.setPen(penBl)
#qp.drawRect(-10, -10+r2Th+20, 20, 20)
#qp.setTransform(tfTrg)
#qp.setPen(penBl)
#qp.drawRect(-10, -10+r2Th+20, 20, 20)
#self.plotOrig(qp)
#qp.setCompositionMode(QtGui.QPainter.CompositionMode_Lighten)
#qp.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
#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()
self.connectEPICS()
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 (
('aArm',(0,360,), 30), # angle ARES sliding seal
('aJFr' ,(0,360,), 30), # angle Jungfrau
('a2Th',(0,360,), 30), # angle 2thetha
('aDet',(0,360,), 30), # angle detector
('aTrg',(0,360,), 30), # angle target
('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 connectEPICS(self):
self._pv=pv=set()
for rec_name in (
'SATES30-RIXS:MOT_RY',
'SATES30-ARES:MOT_JFRY',
'SATES30-ARES:MOT_2TRY',
'SATES30-ARES:MOT_DRY',
'SATES30-ARES:MOT_SRY',):
m=epics.Motor(rec_name)
pv.add(m)
#_log.debug(m.get_position()) # this is the VAL field
#_log.debug(m.PV('RBV').get()) # this is the RBV field
#_log.debug(m.get('RBV')) # this is the RBV field
#m.add_callback('RBV', self.update_label)
#m.set_callback('RBV', self.emit_signals, {'source_field': 'RBV'})
#/home/zamofing_t/.local/lib/python3.8/site-packages/epics/motor.py
m.add_callback('RBV', self.OnChangedRBV)
#print(pv)
# def update_label(self, **kwargs):
# _log.info(kwargs)
# def emit_signals(self, **kw):
# _log.info(kw)
#def OnChangedRBV(self, **kw):
def OnChangedRBV(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"{kw['pvname']}:{kw['value']}")
_log.info(f"{pvname}:{value}")
if pvname=='SATES30-RIXS:MOT_RY.RBV':
self.sldChanged('aArm',value)
elif pvname=='SATES30-ARES:MOT_JFRY.RBV':
self.sldChanged('aJFr',value)
elif pvname=='SATES30-ARES:MOT_2TRY.RBV':
self.sldChanged('a2Th',value)
elif pvname=='SATES30-ARES:MOT_DRY.RBV':
self.sldChanged('aDet',value)
elif pvname=='SATES30-ARES:MOT_SRY.RBV':
self.sldChanged('aTrg',value)
else:
_log.warning(f"can't handle PV: {pvname}:{value}")
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))
print(p['sclTrf'])
dev.setGeometry(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):
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 OnEvent(self,*args,**kwargs):
#test event
print(f'OnEvent: {args} ,{kwargs}')
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
app._dev=dev=ARESdevice('Furka-ARES')
if args.mode&0x01:
app._wndVisualize=wnd=WndVisualize()
wnd.show()
sys.exit(app.exec_())
main()