wip
This commit is contained in:
405
epics_widgets/MotorTweak.py
Normal file
405
epics_widgets/MotorTweak.py
Normal file
@@ -0,0 +1,405 @@
|
||||
import logging
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
import math
|
||||
from time import sleep
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QDoubleValidator
|
||||
from PyQt5.QtWidgets import QMenu, QInputDialog, QAction
|
||||
from PyQt5.uic import loadUiType
|
||||
from epics import Motor
|
||||
|
||||
#from app_utils import assert_tweaker_positions
|
||||
|
||||
Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui')
|
||||
SPMG_STOP = 0
|
||||
SPMG_PAUSE = 1
|
||||
SPMG_MOVE = 2
|
||||
SPMG_GO = 3
|
||||
|
||||
class MotorTweak(QWidget, Ui_MotorTweak):
|
||||
event_val = pyqtSignal(str, dict)
|
||||
event_rbv = pyqtSignal(str, float, dict)
|
||||
event_soft_limit = pyqtSignal(str, dict)
|
||||
event_high_hard_limit = pyqtSignal(str, dict)
|
||||
event_low_hard_limit = pyqtSignal(str, dict)
|
||||
event_axis_fault = pyqtSignal(str, dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(MotorTweak, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self._wheel_tweaks = True
|
||||
self._auto_labels = True
|
||||
self._locked = False
|
||||
self._label_style = 'basic'
|
||||
self._templates_source = {
|
||||
'basic': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'small': '<small>{short_name} <font size="small" color="#080">{{rbv:.{precision}f}} {units}</font><small>',
|
||||
'2 lines': '<b>{short_name}</b><br><font size="small" color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'busy': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>'
|
||||
}
|
||||
self._templates = {}
|
||||
|
||||
|
||||
def connect_motor(self, motor_base, short_name=None, **kwargs):
|
||||
# TODO: DO NOT USE Motor base class, but reduce to the only really needed PV: s.a. class QopticZoom(object)
|
||||
# TODO: have a own motor Class as class SimMotor:
|
||||
m = Motor(motor_base)
|
||||
m.get_position()
|
||||
self._ignore_limits = m.HLM == m.LLM # if both high/low limits are equal they are meaningless
|
||||
self._motor = m
|
||||
if not short_name:
|
||||
short_name = m.description
|
||||
self.short_name = short_name
|
||||
self._pvname = motor_base
|
||||
|
||||
for attr in ['RTYP', 'JVEL', 'HLS', 'LLS', 'TWV', 'RBV', 'VAL', 'LVIO', 'HLM', 'LLM']:
|
||||
# _log.debug('connecting to field {}'.format(attr))
|
||||
m.PV(attr, connect=True)
|
||||
|
||||
self.set_motor_validator()
|
||||
self._drive_val.setText(m.get('VAL', as_string=True))
|
||||
self._drive_val.returnPressed.connect(self.move_abs)
|
||||
|
||||
tweak_min = kwargs.get("tweak_min", 0.0001)
|
||||
tweak_max = kwargs.get("tweak_max", 10.0)
|
||||
self._tweak_val.setText(m.get('TWV', as_string=True))
|
||||
self._tweak_val.setValidator(QDoubleValidator(tweak_min, tweak_max, 4, self._tweak_val))
|
||||
self._tweak_val.editingFinished.connect(lambda m=m: m.PV('TWV').put(self._tweak_val.text()))
|
||||
|
||||
self.jog_reverse.hide()
|
||||
self.jog_forward.hide()
|
||||
# self.jog_forward.pressed.connect(lambda: self.jog('forward', True))
|
||||
# self.jog_reverse.pressed.connect(lambda: self.jog('reverse', True))
|
||||
# self.jog_forward.released.connect(lambda: self.jog('forward', False))
|
||||
# self.jog_reverse.released.connect(lambda: self.jog('reverse', False))
|
||||
|
||||
self.tweak_forward.clicked.connect(lambda x: m.tweak('forward'))
|
||||
self.tweak_reverse.clicked.connect(lambda x: m.tweak('reverse'))
|
||||
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
m.add_callback('VAL', self.set_val)
|
||||
m.add_callback('RBV', self.update_label)
|
||||
m.set_callback('HLS', self.update_label)
|
||||
m.set_callback('LLS', self.update_label)
|
||||
|
||||
m.set_callback('VAL', self.emit_signals, {'source_field': 'VAL'})
|
||||
m.set_callback('RBV', self.emit_signals, {'source_field': 'RBV'})
|
||||
m.set_callback('HLS', self.emit_signals, {'source_field': 'HLS'})
|
||||
m.set_callback('LLS', self.emit_signals, {'source_field': 'LLS'})
|
||||
m.set_callback('LVIO', self.emit_signals, {'source_field': 'LVIO'})
|
||||
m.set_callback('STAT', self.emit_signals, {'source_field': 'STAT'})
|
||||
|
||||
m.set_callback('HLM', self.set_motor_validator)
|
||||
|
||||
|
||||
def set_val(self, **kw):
|
||||
v = kw['char_value']
|
||||
_log.debug('updating VAL = {}'.format(v))
|
||||
self._drive_val.setText(v)
|
||||
|
||||
|
||||
def set_motor_validator(self, **kwargs):
|
||||
m = self._motor
|
||||
lineedit = self._drive_val
|
||||
min, max = m.PV('LLM').get(), m.PV('HLM').get()
|
||||
if min == max:
|
||||
min = -1e6
|
||||
max = 1e6
|
||||
lineedit.setValidator(QDoubleValidator(min, max, m.PV('PREC').get(), lineedit))
|
||||
|
||||
|
||||
def move_rel(self, dist):
|
||||
try:
|
||||
self._val+=dist
|
||||
except AttributeError:
|
||||
pass
|
||||
self._motor.move(dist, ignore_limits=True, relative=True)
|
||||
|
||||
def is_done(self):
|
||||
m=self._motor
|
||||
m.refresh()
|
||||
return 1 == m.done_moving
|
||||
|
||||
def get_rbv(self):
|
||||
#return self._motor.get('RBV')
|
||||
m=self._motor
|
||||
return m.get_position(readback=True)
|
||||
|
||||
def get_val(self):
|
||||
#return self._motor.get('VAL')
|
||||
try:
|
||||
v=self._val
|
||||
except AttributeError:
|
||||
m=self._motor
|
||||
self._val=v=m.get('VAL')
|
||||
return v
|
||||
|
||||
def move_abs(self, drive=None, wait=False, assert_position=False):
|
||||
if drive is None:
|
||||
drive = float(self._drive_val.text())
|
||||
if assert_position:
|
||||
wait=True
|
||||
try:
|
||||
self._val=drive
|
||||
except AttributeError:
|
||||
pass
|
||||
m=self._motor
|
||||
m.move(drive, wait=wait, ignore_limits=True, relative=False)
|
||||
if assert_position:
|
||||
assert_tweaker_positions([(self, drive, 0.1)], timeout=1)
|
||||
|
||||
def emit_signals(self, **kw):
|
||||
'''
|
||||
:param kw: info about the channel {
|
||||
'access': 'read-only',
|
||||
'char_value': '-0.105',
|
||||
'chid': 36984304,
|
||||
'count': 1,
|
||||
'enum_strs': None,
|
||||
'ftype': 20,
|
||||
'host': 'SAR-CPPM-EXPMX1.psi.ch:5064',
|
||||
'lower_alarm_limit': None,
|
||||
'lower_ctrl_limit': 0.0,
|
||||
'lower_disp_limit': 0.0,
|
||||
'lower_warning_limit': None,
|
||||
'motor_field': 'RBV',
|
||||
'nelm': 1,
|
||||
'precision': 3,
|
||||
'pvname': 'SAR-EXPMX:MOT_ROT_Y.RBV',
|
||||
'read_access': True,
|
||||
'severity': 0,
|
||||
'source_field': 'RBV',
|
||||
'status': 0,
|
||||
'timestamp': 1522314072.878331,
|
||||
'type': 'time_double',
|
||||
'typefull': 'time_double',
|
||||
'units': b'deg',
|
||||
'upper_alarm_limit': None,
|
||||
'upper_ctrl_limit': 0.0,
|
||||
'upper_disp_limit': 0.0,
|
||||
'upper_warning_limit': None,
|
||||
'value': -0.105,
|
||||
'write_access': False}
|
||||
|
||||
|
||||
:return:
|
||||
'''
|
||||
field = kw['motor_field']
|
||||
src = kw['source_field']
|
||||
kw['alias'] = self.short_name
|
||||
if field != src:
|
||||
return
|
||||
if field == 'VAL':
|
||||
self.event_val.emit(self._pvname, kw)
|
||||
elif field == 'RBV':
|
||||
self.event_rbv.emit(kw['alias'], kw['value'], kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_soft_limit.emit(self._pvname, kw)
|
||||
elif field == 'HLS':
|
||||
self.event_high_hard_limit.emit(self._pvname, kw)
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_low_hard_limit.emit(self._pvname, kw)
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
elif field == 'STAT':
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
|
||||
def update_label(self, **kwargs):
|
||||
m = self._motor
|
||||
self.label.setText(self._templates[self._label_style].format(rbv=m.readback))
|
||||
self.jog_forward.setToolTip('jog forward at {:.3f} {}/s'.format(m.jog_speed, m.units))
|
||||
self.jog_reverse.setToolTip('jog reverse at {:.3f} {}/s'.format(m.jog_speed, m.units))
|
||||
self.tweak_forward.setToolTip('tweak forward by {:.3f} {}'.format(m.tweak_val, m.units))
|
||||
self.tweak_reverse.setToolTip('tweak reverse by {:.3f} {}'.format(m.tweak_val, m.units))
|
||||
|
||||
def update_jog_speed(self, event):
|
||||
m = self._motor
|
||||
speed = m.jog_speed
|
||||
sign = math.copysign(1, event.angleDelta().y())
|
||||
m.jog_speed = speed + (sign * 0.1 * speed)
|
||||
self.jog_forward.setToolTip('jog forward at {:.3f} {}/s'.format(m.jog_speed, m.units))
|
||||
self.jog_reverse.setToolTip('jog reverse at {:.3f} {}/s'.format(m.jog_speed, m.units))
|
||||
|
||||
def tweak_event(self, event):
|
||||
m = self._motor
|
||||
sign = event.angleDelta().y()
|
||||
if sign < 0:
|
||||
m.tweak_reverse = 1
|
||||
else:
|
||||
m.tweak_forward = 1
|
||||
|
||||
def bind_wheel(self):
|
||||
self.tweak_forward.wheelEvent = self.tweak_event
|
||||
self.tweak_reverse.wheelEvent = self.tweak_event
|
||||
|
||||
def jog(self, direction, enable):
|
||||
m = self._motor
|
||||
if 'forw' in direction:
|
||||
m.jog_forward = int(enable)
|
||||
else:
|
||||
m.jog_reverse = int(enable)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
m = self._motor
|
||||
menu = QMenu(self)
|
||||
menu.setTitle(self.short_name)
|
||||
|
||||
lockmotor = QAction('lock motor', menu, checkable=True)
|
||||
lockmotor.setChecked(self._locked)
|
||||
menu.addAction(lockmotor)
|
||||
|
||||
autolabelsAction = QAction('auto', menu, checkable=True)
|
||||
autolabelsAction.setChecked(self._auto_labels)
|
||||
menu.addAction(autolabelsAction)
|
||||
|
||||
wheel_tweaks = QAction('Mouse wheel tweaks motor', menu, checkable=True)
|
||||
wheel_tweaks.setChecked(self._wheel_tweaks)
|
||||
menu.addAction(wheel_tweaks)
|
||||
|
||||
stopmotorAction = QAction('Stopped', menu, checkable=True)
|
||||
stopmotorAction.setChecked(SPMG_STOP == m.stop_go)
|
||||
menu.addAction(stopmotorAction)
|
||||
changeprecAction = menu.addAction("Change Precision")
|
||||
changejogspeedAction = menu.addAction("Jog speed {:.3f} {}/s".format(m.jog_speed, m.units))
|
||||
changetweakstepAction = menu.addAction("Tweak step {:.3f} {}".format(m.tweak_val, m.units))
|
||||
|
||||
tozeroAction = menu.addAction("move to Zero")
|
||||
|
||||
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
if action == lockmotor:
|
||||
self._locked = not self._locked
|
||||
if self._locked:
|
||||
self._controlbox.setDisabled(True)
|
||||
else:
|
||||
self._controlbox.setDisabled(False)
|
||||
elif action == changeprecAction:
|
||||
name = m.NAME
|
||||
prec = m.PREC
|
||||
msg = 'Precision for motor {}'.format(name)
|
||||
_log.debug('prec before %d', prec)
|
||||
prec, ok = QInputDialog.getInt(self, msg, msg, prec, 0, 10)
|
||||
_log.debug('prec after (%d) %d', ok, prec)
|
||||
if ok:
|
||||
m.put('PREC', prec, wait=True)
|
||||
|
||||
elif action == changejogspeedAction:
|
||||
name = m.NAME
|
||||
speed = m.jog_speed
|
||||
msg = 'Jog speed for motor {}'.format(name)
|
||||
speed, ok = QInputDialog.getDouble(self, msg, msg, speed, 0.01, m.slew_speed, m.precision)
|
||||
if ok:
|
||||
m.put('JVEL', speed, wait=True)
|
||||
|
||||
elif action == changetweakstepAction:
|
||||
name = m.NAME
|
||||
tv = m.tweak_val
|
||||
msg = 'Tweak step for motor {} at {} {}'.format(name, tv, m.units)
|
||||
tv, ok = QInputDialog.getDouble(self, msg, msg, tv, pow(10, -m.precision), m.slew_speed, m.precision)
|
||||
if ok:
|
||||
m.put('TWV', tv, wait=True)
|
||||
|
||||
elif action == tozeroAction:
|
||||
m.move(0.0, ignore_limits=True)
|
||||
|
||||
elif action == stopmotorAction:
|
||||
m.stop_go = SPMG_STOP if m.stop_go == SPMG_GO else SPMG_GO
|
||||
|
||||
elif action == autolabelsAction:
|
||||
self._auto_labels = not self._auto_labels
|
||||
|
||||
elif action == wheel_tweaks:
|
||||
self._wheel_tweaks = not self._wheel_tweaks
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
return # FIXME disable for the time being
|
||||
if not self._auto_labels:
|
||||
return
|
||||
|
||||
w, h = event.size().width(), event.size().height()
|
||||
if w < 400:
|
||||
self.jog_reverse.hide()
|
||||
self.jog_forward.hide()
|
||||
else:
|
||||
self.jog_reverse.show()
|
||||
self.jog_forward.show()
|
||||
self.update_label()
|
||||
|
||||
def update_label_template(self):
|
||||
m = self._motor
|
||||
source = self._templates_source
|
||||
target = self._templates
|
||||
|
||||
for k in source:
|
||||
target[k] = source[k].format(
|
||||
short_name=self.short_name,
|
||||
precision=m.PREC,
|
||||
units=m.units)
|
||||
try:
|
||||
self.label.setText(target[self._label_style].format(rbv=m.readback))
|
||||
except ValueError as e:
|
||||
_log.error(e)
|
||||
print(self._label_style)
|
||||
print(m.readback)
|
||||
print(m)
|
||||
|
||||
def paintEvent(self, e):
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
qp.setRenderHint(QPainter.Antialiasing)
|
||||
self._draw_limits(qp)
|
||||
qp.end()
|
||||
|
||||
def _draw_limits(self, qp):
|
||||
m = self._motor
|
||||
width, height = self.size().width(), self.size().height()
|
||||
pad = 5
|
||||
rounding = 2
|
||||
size = 10
|
||||
if m.HLS:
|
||||
x, y, w, h = width - size, pad, size, height - 2 * pad
|
||||
elif m.LLS:
|
||||
x, y, w, h = 0, pad, size, height - 2 * pad
|
||||
else:
|
||||
return
|
||||
color = QColor('indianred')
|
||||
qp.setBrush(QBrush(color, Qt.SolidPattern))
|
||||
qp.setPen(QPen(color))
|
||||
path = QPainterPath()
|
||||
path.setFillRule(Qt.WindingFill)
|
||||
path.addRoundedRect(x, y, w, h, rounding, rounding)
|
||||
qp.drawPath(path)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(module)s:%(lineno)d:%(funcName)s:%(message)s ')
|
||||
_log.debug('start')
|
||||
def cb_rbv(*args,**kwargs):
|
||||
_log.debug(f'RBV{args} {kwargs}')
|
||||
|
||||
|
||||
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
|
||||
app=QApplication(sys.argv)
|
||||
mot_widget=MotorTweak()
|
||||
|
||||
#mot_widget.event_val.connect(cb_rbv)
|
||||
mot_widget.event_rbv.connect(cb_rbv)
|
||||
|
||||
pfx='SAR-EXPMX'
|
||||
mot_widget.connect_motor(f"{pfx}:MOT_FY", alias="fast_y", label="fast Y")
|
||||
mot_widget.setGeometry(50, 50, 520, 100)
|
||||
mot_widget.setWindowTitle("PyQt5 Example")
|
||||
mot_widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
|
||||
172
epics_widgets/MotorTweak.ui
Normal file
172
epics_widgets/MotorTweak.ui
Normal file
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Frame</class>
|
||||
<widget class="QFrame" name="Frame">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>650</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Frame</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="_controlbox" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="_drive_val">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="tweak_reverse">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>↶</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="tweak_forward">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>↷</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="_tweak_val">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="jog_reverse">
|
||||
<property name="text">
|
||||
<string>◀</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="jog_forward">
|
||||
<property name="text">
|
||||
<string>▶</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
325
epics_widgets/SimMotorTweak.py
Normal file
325
epics_widgets/SimMotorTweak.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python
|
||||
# *-----------------------------------------------------------------------*
|
||||
# | |
|
||||
# | Copyright (c) 2022 by Paul Scherrer Institute (http://www.psi.ch) |
|
||||
# | Based on Zac great first implementation |
|
||||
# | Author Thierry Zamofing (thierry.zamofing@psi.ch) |
|
||||
# *-----------------------------------------------------------------------*
|
||||
|
||||
# simulated motor tweaks ui:
|
||||
# same interface as MotorTweak and SmaractMotorTweaks, but mimes just a simulated motor
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QDoubleValidator
|
||||
from PyQt5.QtWidgets import QMenu, QInputDialog, QAction
|
||||
from PyQt5.uic import loadUiType
|
||||
from epics.ca import pend_event
|
||||
|
||||
Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui')
|
||||
_log = logging.getLogger(__name__)
|
||||
#logger.setLevel(logging.INFO)
|
||||
|
||||
class SimMotor:
|
||||
def __init__(self,rec_name, short_name):
|
||||
self._llm = -10
|
||||
self._hlm = 10
|
||||
self._prec = 5
|
||||
self._twv = 0.1
|
||||
self._units = 'mm'
|
||||
self._pos = 3.1415
|
||||
self._short_name=short_name
|
||||
self._rec_name=rec_name
|
||||
|
||||
class SimMotorTweak(QWidget, Ui_MotorTweak):
|
||||
event_val = pyqtSignal(str, dict)
|
||||
event_rbv = pyqtSignal(str, dict)
|
||||
event_soft_limit = pyqtSignal(str, dict)
|
||||
event_high_hard_limit = pyqtSignal(str, dict)
|
||||
event_low_hard_limit = pyqtSignal(str, dict)
|
||||
event_axis_fault = pyqtSignal(str, dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(SimMotorTweak, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self._wheel_tweaks = True
|
||||
self._auto_labels = True
|
||||
self._locked = False
|
||||
self._label_style = 'basic'
|
||||
self._templates_source = {
|
||||
'basic': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'small': '<small>{short_name} <font size="small" color="#080">{{rbv:.{precision}f}} {units}</font><small>',
|
||||
'2 lines': '<b>{short_name}</b><br><font size="small" color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'busy': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>'
|
||||
}
|
||||
self._templates = {}
|
||||
|
||||
|
||||
def connect_motor(self, rec_name, short_name=None, *args, **kwargs):
|
||||
self.label.setToolTip('{} => {}'.format(rec_name, short_name))
|
||||
self._motor=m=SimMotor(rec_name, short_name)
|
||||
self.set_motor_validator()
|
||||
self._drive_val.setText(str(m._pos))
|
||||
self._drive_val.returnPressed.connect(self.move_abs)
|
||||
|
||||
self.jog_forward.hide()
|
||||
self.jog_reverse.hide()
|
||||
|
||||
tweak_min = kwargs.get("tweak_min", 0.0001)
|
||||
tweak_max = kwargs.get("tweak_max", 20.0)
|
||||
self._tweak_val.setText(str(m._twv))
|
||||
self._tweak_val.setValidator(QDoubleValidator(tweak_min, tweak_max, 4, self._tweak_val))
|
||||
#self._tweak_val.editingFinished.connect(lambda m=self._pv_tweak_val: m.put(self._tweak_val.text()))
|
||||
self.tweak_forward.clicked.connect(lambda m: self.move_rel(float(self._tweak_val.text())))
|
||||
self.tweak_reverse.clicked.connect(lambda m: self.move_rel(-float(self._tweak_val.text())))
|
||||
#self.tweak_forward.clicked.connect(lambda m: self._pv_tweak_f.put(1))
|
||||
#self.tweak_reverse.clicked.connect(lambda m: self._pv_tweak_r.put(1))
|
||||
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
|
||||
#self._pv_readback.add_callback(self.update_label)
|
||||
#self._pv_status.add_callback(self.set_val_on_status_change)
|
||||
|
||||
# self._pv_drive.add_callback(self.set_val)
|
||||
#self._pv_drive.add_callback(self.emit_signals, source_field='VAL')
|
||||
#self._pv_readback.add_callback(self.emit_signals, source_field='RBV')
|
||||
#self._pv_hlm.add_callback(self.emit_signals, source_field='HLS')
|
||||
#self._pv_llm.add_callback(self.emit_signals, source_field='LLS')
|
||||
|
||||
# m.set_callback('HLM', self.set_motor_validator)
|
||||
|
||||
|
||||
def set_motor_validator(self, **kwargs):
|
||||
m=self._motor
|
||||
lineedit = self._drive_val
|
||||
min, max = m._llm, m._hlm
|
||||
if min == max:
|
||||
min = -1e6
|
||||
max = 1e6
|
||||
lineedit.setValidator(QDoubleValidator(min, max, 5, lineedit))
|
||||
|
||||
|
||||
def move_rel(self, dist, delay=None):
|
||||
m=self._motor
|
||||
m._pos += dist
|
||||
if delay:
|
||||
sleep(delay)
|
||||
_log.debug('{} rel move => {}'.format(m._short_name, dist))
|
||||
self.update_label()
|
||||
self.emit_signals(source_field='VAL')
|
||||
|
||||
def get_rbv(self):
|
||||
return self._motor._pos
|
||||
|
||||
def get_val(self):
|
||||
return self._motor._pos
|
||||
|
||||
def is_homed(self):
|
||||
return True
|
||||
|
||||
def get_status(self, as_string=False):
|
||||
"""
|
||||
States:
|
||||
0 Stopped
|
||||
1 Stepping
|
||||
2 Scanning
|
||||
3 Holding
|
||||
4 Targeting
|
||||
5 Move Delay
|
||||
6 Calibrating
|
||||
7 Finding Ref
|
||||
8 Locked
|
||||
:return:
|
||||
"""
|
||||
return 0
|
||||
|
||||
def wait(self):
|
||||
pend_event(.01)
|
||||
while not self.is_done():
|
||||
pend_event(0.2)
|
||||
|
||||
def is_done(self):
|
||||
return True # in (0, 3)
|
||||
|
||||
def move_abs(self, drive=None, wait=False, assert_position=False):
|
||||
m=self._motor
|
||||
if assert_position:
|
||||
wait=True
|
||||
if drive is None:
|
||||
_log.debug('{} abs target from widget'.format(m._short_name))
|
||||
drive = float(self._drive_val.text())
|
||||
_log.debug('{} abs move => {}'.format(m._short_name, drive))
|
||||
m._pos=drive
|
||||
self.update_label()
|
||||
self.emit_signals(source_field='VAL')
|
||||
|
||||
def emit_signals(self, **kw):
|
||||
m=self._motor
|
||||
field = kw['source_field']
|
||||
if field == 'VAL':
|
||||
self.event_val.emit(m._rec_name, kw)
|
||||
elif field == 'RBV':
|
||||
self.event_rbv.emit(m._rec_name, kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_soft_limit.emit(m._rec_name, kw)
|
||||
elif field == 'HLS':
|
||||
self.event_high_hard_limit.emit(m._rec_name, kw)
|
||||
self.event_axis_fault.emit(m._rec_name, kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_low_hard_limit.emit(m._rec_name, kw)
|
||||
self.event_axis_fault.emit(m._rec_name, kw)
|
||||
elif field == 'STAT':
|
||||
self.event_axis_fault.emit(m._rec_name, kw)
|
||||
|
||||
|
||||
def set_val_on_status_change(self, **kw):
|
||||
'''
|
||||
0 Stopped
|
||||
1 Stepping
|
||||
2 Scanning
|
||||
3 Holding
|
||||
4 Targeting
|
||||
5 Move Delay
|
||||
6 Calibrating
|
||||
7 Finding Ref
|
||||
8 Locked
|
||||
:param kw:
|
||||
:return:
|
||||
'''
|
||||
status = kw['char_value']
|
||||
if status in ('Holding', 'Stopped'):
|
||||
v = self._pv_readback.get(as_string=True)
|
||||
_log.debug('updating VAL on status change to holding/stopped = {}'.format(v))
|
||||
self._drive_val.setText(v)
|
||||
|
||||
def set_val(self, **kw):
|
||||
v = kw['char_value']
|
||||
_log.debug('updating VAL = {}'.format(v))
|
||||
self._drive_val.setText(v)
|
||||
|
||||
def update_label(self, **kwargs):
|
||||
m=self._motor
|
||||
self.label.setText(self._templates[self._label_style].format(rbv=m._pos))
|
||||
self._drive_val.setText(f'{m._pos:.3f}')
|
||||
twv=float(self._tweak_val.text())
|
||||
self.tweak_forward.setToolTip('tweak forward by {:.3f} {}'.format(twv, m._units))
|
||||
self.tweak_reverse.setToolTip('tweak reverse by {:.3f} {}'.format(twv, m._units))
|
||||
|
||||
def tweak_event(self, event):
|
||||
sign = event.angleDelta().y()
|
||||
if sign < 0:
|
||||
self._pv_tweak_r.put(1)
|
||||
else:
|
||||
self._pv_tweak_f.put(1)
|
||||
|
||||
def bind_wheel(self):
|
||||
self.tweak_forward.wheelEvent = self.tweak_event
|
||||
self.tweak_reverse.wheelEvent = self.tweak_event
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
name = self._pv_name.get()
|
||||
unit = self._units
|
||||
prec = self._prec
|
||||
|
||||
menu = QMenu(self)
|
||||
menu.setTitle(self.short_name)
|
||||
|
||||
lockmotor = QAction('lock motor', menu, checkable=True)
|
||||
lockmotor.setChecked(self._locked)
|
||||
menu.addAction(lockmotor)
|
||||
|
||||
autolabelsAction = QAction('auto', menu, checkable=True)
|
||||
autolabelsAction.setChecked(self._auto_labels)
|
||||
menu.addAction(autolabelsAction)
|
||||
|
||||
wheel_tweaks = QAction('Mouse wheel tweaks motor', menu, checkable=True)
|
||||
wheel_tweaks.setChecked(self._wheel_tweaks)
|
||||
menu.addAction(wheel_tweaks)
|
||||
|
||||
# stopmotorAction = QAction('Stopped', menu, checkable=True)
|
||||
# stopmotorAction.setChecked(SPMG_STOP == m.stop_go)
|
||||
# menu.addAction(stopmotorAction)
|
||||
changeprecAction = menu.addAction("Change Precision")
|
||||
changetweakstepAction = menu.addAction("Tweak step {:.3f} {}".format(self._pv_tweak_val.get(), unit))
|
||||
|
||||
tozeroAction = menu.addAction("move to Zero")
|
||||
|
||||
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
if action == lockmotor:
|
||||
self._locked = not self._locked
|
||||
if self._locked:
|
||||
self._controlbox.setDisabled(True)
|
||||
else:
|
||||
self._controlbox.setDisabled(False)
|
||||
elif action == changeprecAction:
|
||||
msg = 'Precision for motor {}'.format(name)
|
||||
_log.debug('prec before %d', prec)
|
||||
prec, ok = QInputDialog.getInt(self, msg, msg, prec, 0, 10)
|
||||
_log.debug('prec after (%d) %d', ok, prec)
|
||||
if ok:
|
||||
self._prec = prec
|
||||
|
||||
elif action == changetweakstepAction:
|
||||
tv = self._pv_tweak_val.get()
|
||||
msg = 'Tweak step for motor {} at {} {}'.format(name, tv, unit)
|
||||
tv, ok = QInputDialog.getDouble(self, msg, msg, tv, pow(10, -prec), 10.0, prec)
|
||||
if ok:
|
||||
self._pv_tweak_val.put(tv)
|
||||
|
||||
elif action == tozeroAction:
|
||||
self._pv_drive.put(0.0)
|
||||
|
||||
elif action == autolabelsAction:
|
||||
self._auto_labels = not self._auto_labels
|
||||
|
||||
elif action == wheel_tweaks:
|
||||
self._wheel_tweaks = not self._wheel_tweaks
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
pass
|
||||
|
||||
def update_label_template(self):
|
||||
m=self._motor
|
||||
source = self._templates_source
|
||||
target = self._templates
|
||||
|
||||
for k in source:
|
||||
target[k] = source[k].format(
|
||||
short_name=m._short_name,
|
||||
precision=m._prec,
|
||||
units=m._units)
|
||||
self.label.setText(target[self._label_style].format(rbv=m._pos))
|
||||
|
||||
def paintEvent(self, e):
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
qp.setRenderHint(QPainter.Antialiasing)
|
||||
self._draw_limits(qp)
|
||||
qp.end()
|
||||
|
||||
def _draw_limits(self, qp):
|
||||
pass
|
||||
# width, height = self.size().width(), self.size().height()
|
||||
# pad = 5
|
||||
# rounding = 2
|
||||
# size = 10
|
||||
# if m.HLS:
|
||||
# x, y, w, h = width - size, pad, size, height - 2 * pad
|
||||
# elif m.LLS:
|
||||
# x, y, w, h = 0, pad, size, height - 2 * pad
|
||||
# else:
|
||||
# return
|
||||
# color = QColor('indianred')
|
||||
# qp.setBrush(QBrush(color, Qt.SolidPattern))
|
||||
# qp.setPen(QPen(color))
|
||||
# path = QPainterPath()
|
||||
# path.setFillRule(Qt.WindingFill)
|
||||
# path.addRoundedRect(x, y, w, h, rounding, rounding)
|
||||
# qp.drawPath(path)
|
||||
342
epics_widgets/SmaractMotorTweak.py
Normal file
342
epics_widgets/SmaractMotorTweak.py
Normal file
@@ -0,0 +1,342 @@
|
||||
import math
|
||||
import logging
|
||||
from time import sleep
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QDoubleValidator
|
||||
from PyQt5.QtWidgets import QMenu, QInputDialog, QAction
|
||||
from PyQt5.uic import loadUiType
|
||||
from epics import PV
|
||||
from epics.ca import pend_event
|
||||
#from app_utils import assert_tweaker_positions
|
||||
|
||||
Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui')
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
SPMG_STOP = 0
|
||||
SPMG_PAUSE = 1
|
||||
SPMG_MOVE = 2
|
||||
SPMG_GO = 3
|
||||
|
||||
|
||||
|
||||
class SmaractMotorTweak(QWidget, Ui_MotorTweak):
|
||||
event_val = pyqtSignal(str, dict)
|
||||
event_rbv = pyqtSignal(str, dict)
|
||||
event_soft_limit = pyqtSignal(str, dict)
|
||||
event_high_hard_limit = pyqtSignal(str, dict)
|
||||
event_low_hard_limit = pyqtSignal(str, dict)
|
||||
event_axis_fault = pyqtSignal(str, dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(SmaractMotorTweak, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self._wheel_tweaks = True
|
||||
self._auto_labels = True
|
||||
self._locked = False
|
||||
self._label_style = 'basic'
|
||||
self._templates_source = {
|
||||
'basic': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'small': '<small>{short_name} <font size="small" color="#080">{{rbv:.{precision}f}} {units}</font><small>',
|
||||
'2 lines': '<b>{short_name}</b><br><font size="small" color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'busy': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>'
|
||||
}
|
||||
self._templates = {}
|
||||
|
||||
|
||||
def connect_motor(self, motor_base, short_name=None, *args, **kwargs):
|
||||
# TODO: DO NOT USE so many PVs, but reduce to the only really needed PV: s.a. class QopticZoom(object)
|
||||
self._pvname = motor_base+':DRIVE'
|
||||
self._pv_name = PV(motor_base+':NAME')
|
||||
self._pv_drive = PV(motor_base+':DRIVE')
|
||||
self._pv_readback = PV(motor_base+':MOTRBV')
|
||||
self._pv_tweak_r = PV(motor_base+':TWR.PROC')
|
||||
self._pv_tweak_f = PV(motor_base+':TWF.PROC')
|
||||
self._pv_tweak_val = PV(motor_base+':TWV')
|
||||
self._pv_status = PV(motor_base + ':STATUS')
|
||||
self._pv_home_f = PV(motor_base + ':FRM_FORW.PROC')
|
||||
self._pv_home_b = PV(motor_base + ':FRM_BACK.PROC')
|
||||
self._pv_is_homed = PV(motor_base + ':GET_HOMED')
|
||||
self._pv_llm = PV(motor_base + ':LLM')
|
||||
self._pv_hlm = PV(motor_base + ':HLM')
|
||||
|
||||
self.label.setToolTip('{} => {}'.format(motor_base, self._pv_name.get()))
|
||||
|
||||
try:
|
||||
self._prec = kwargs['prec']
|
||||
except:
|
||||
self._prec = 5
|
||||
|
||||
try:
|
||||
self._units = kwargs['units']
|
||||
except:
|
||||
self._units = 'mm'
|
||||
|
||||
self._ignore_limits = self._pv_llm.get() == self._pv_hlm.get() # if both high/low limits are equal they are meaningless
|
||||
if not short_name:
|
||||
short_name = self._pv_name.value
|
||||
self.short_name = short_name
|
||||
|
||||
self.set_motor_validator()
|
||||
self._drive_val.setText(self._pv_drive.get(as_string=True))
|
||||
self._drive_val.returnPressed.connect(self.move_abs)
|
||||
|
||||
self.jog_forward.hide()
|
||||
self.jog_reverse.hide()
|
||||
|
||||
tweak_min = kwargs.get("tweak_min", 0.0001)
|
||||
tweak_max = kwargs.get("tweak_max", 20.0)
|
||||
self._tweak_val.setText(self._pv_tweak_val.get(as_string=True))
|
||||
self._tweak_val.setValidator(QDoubleValidator(tweak_min, tweak_max, 4, self._tweak_val))
|
||||
self._tweak_val.editingFinished.connect(lambda m=self._pv_tweak_val: m.put(self._tweak_val.text()))
|
||||
|
||||
self.tweak_forward.clicked.connect(lambda m: self._pv_tweak_f.put(1))
|
||||
self.tweak_reverse.clicked.connect(lambda m: self._pv_tweak_r.put(1))
|
||||
|
||||
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
|
||||
self._pv_readback.add_callback(self.update_label)
|
||||
self._pv_status.add_callback(self.set_val_on_status_change)
|
||||
|
||||
# self._pv_drive.add_callback(self.set_val)
|
||||
self._pv_drive.add_callback(self.emit_signals, source_field='VAL')
|
||||
self._pv_readback.add_callback(self.emit_signals, source_field='RBV')
|
||||
self._pv_hlm.add_callback(self.emit_signals, source_field='HLS')
|
||||
self._pv_llm.add_callback(self.emit_signals, source_field='LLS')
|
||||
|
||||
# m.set_callback('HLM', self.set_motor_validator)
|
||||
|
||||
|
||||
def set_motor_validator(self, **kwargs):
|
||||
lineedit = self._drive_val
|
||||
min, max = self._pv_llm.get(), self._pv_hlm.get()
|
||||
if min == max:
|
||||
min = -1e6
|
||||
max = 1e6
|
||||
lineedit.setValidator(QDoubleValidator(min, max, 5, lineedit))
|
||||
|
||||
|
||||
def move_rel(self, dist, delay=None):
|
||||
cur = self._pv_readback.get()
|
||||
target = cur + dist
|
||||
if delay:
|
||||
sleep(delay)
|
||||
self._pv_drive.put(target)
|
||||
|
||||
def get_rbv(self):
|
||||
return self._pv_readback.get(use_monitor=False)
|
||||
|
||||
def get_val(self):
|
||||
# return self._motor.get('VAL')
|
||||
try:
|
||||
v=self._val
|
||||
except AttributeError:
|
||||
self._val=v=self._pv_drive.get()
|
||||
return v
|
||||
|
||||
def is_homed(self):
|
||||
pend_event()
|
||||
return 1 == self._pv_is_homed.get(use_monitor=False)
|
||||
|
||||
def get_status(self, as_string=False):
|
||||
"""
|
||||
States:
|
||||
0 Stopped
|
||||
1 Stepping
|
||||
2 Scanning
|
||||
3 Holding
|
||||
4 Targeting
|
||||
5 Move Delay
|
||||
6 Calibrating
|
||||
7 Finding Ref
|
||||
8 Locked
|
||||
:return:
|
||||
"""
|
||||
return self._pv_status.get(as_string=as_string, use_monitor=False)
|
||||
|
||||
def wait(self):
|
||||
pend_event(.01)
|
||||
while not self.is_done():
|
||||
pend_event(0.2)
|
||||
|
||||
def is_done(self):
|
||||
return self._pv_status.get() in (0, 3)
|
||||
|
||||
def move_abs(self, drive=None, wait=False, assert_position=False):
|
||||
if assert_position:
|
||||
wait=True
|
||||
if drive is None:
|
||||
logger.debug('{} abs target from widget'.format(self.short_name))
|
||||
drive = float(self._drive_val.text())
|
||||
logger.debug('{} abs move => {}'.format(self.short_name, drive))
|
||||
self._pv_drive.put(drive)
|
||||
if wait:
|
||||
self.wait()
|
||||
if assert_position:
|
||||
assert_tweaker_positions([(self, drive, 0.05)], timeout=10.)
|
||||
|
||||
def emit_signals(self, **kw):
|
||||
field = kw['source_field']
|
||||
if field == 'VAL':
|
||||
self.event_val.emit(self._pvname, kw)
|
||||
elif field == 'RBV':
|
||||
self.event_rbv.emit(self._pvname, kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_soft_limit.emit(self._pvname, kw)
|
||||
elif field == 'HLS':
|
||||
self.event_high_hard_limit.emit(self._pvname, kw)
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
elif field == 'LVIO':
|
||||
self.event_low_hard_limit.emit(self._pvname, kw)
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
elif field == 'STAT':
|
||||
self.event_axis_fault.emit(self._pvname, kw)
|
||||
|
||||
|
||||
def set_val_on_status_change(self, **kw):
|
||||
'''
|
||||
0 Stopped
|
||||
1 Stepping
|
||||
2 Scanning
|
||||
3 Holding
|
||||
4 Targeting
|
||||
5 Move Delay
|
||||
6 Calibrating
|
||||
7 Finding Ref
|
||||
8 Locked
|
||||
:param kw:
|
||||
:return:
|
||||
'''
|
||||
status = kw['char_value']
|
||||
if status in ('Holding', 'Stopped'):
|
||||
v = self._pv_readback.get(as_string=True)
|
||||
logger.debug('updating VAL on status change to holding/stopped = {}'.format(v))
|
||||
self._drive_val.setText(v)
|
||||
|
||||
def set_val(self, **kw):
|
||||
v = kw['char_value']
|
||||
logger.debug('updating VAL = {}'.format(v))
|
||||
self._drive_val.setText(v)
|
||||
|
||||
def update_label(self, **kwargs):
|
||||
self.label.setText(self._templates[self._label_style].format(rbv=self._pv_readback.get()))
|
||||
self.tweak_forward.setToolTip('tweak forward by {:.3f} {}'.format(self._pv_tweak_val.get(), self._units))
|
||||
self.tweak_reverse.setToolTip('tweak reverse by {:.3f} {}'.format(self._pv_tweak_val.get(), self._units))
|
||||
|
||||
def tweak_event(self, event):
|
||||
sign = event.angleDelta().y()
|
||||
if sign < 0:
|
||||
self._pv_tweak_r.put(1)
|
||||
else:
|
||||
self._pv_tweak_f.put(1)
|
||||
|
||||
def bind_wheel(self):
|
||||
self.tweak_forward.wheelEvent = self.tweak_event
|
||||
self.tweak_reverse.wheelEvent = self.tweak_event
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
name = self._pv_name.get()
|
||||
unit = self._units
|
||||
prec = self._prec
|
||||
|
||||
menu = QMenu(self)
|
||||
menu.setTitle(self.short_name)
|
||||
|
||||
lockmotor = QAction('lock motor', menu, checkable=True)
|
||||
lockmotor.setChecked(self._locked)
|
||||
menu.addAction(lockmotor)
|
||||
|
||||
autolabelsAction = QAction('auto', menu, checkable=True)
|
||||
autolabelsAction.setChecked(self._auto_labels)
|
||||
menu.addAction(autolabelsAction)
|
||||
|
||||
wheel_tweaks = QAction('Mouse wheel tweaks motor', menu, checkable=True)
|
||||
wheel_tweaks.setChecked(self._wheel_tweaks)
|
||||
menu.addAction(wheel_tweaks)
|
||||
|
||||
# stopmotorAction = QAction('Stopped', menu, checkable=True)
|
||||
# stopmotorAction.setChecked(SPMG_STOP == m.stop_go)
|
||||
# menu.addAction(stopmotorAction)
|
||||
changeprecAction = menu.addAction("Change Precision")
|
||||
changetweakstepAction = menu.addAction("Tweak step {:.3f} {}".format(self._pv_tweak_val.get(), unit))
|
||||
|
||||
tozeroAction = menu.addAction("move to Zero")
|
||||
|
||||
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
if action == lockmotor:
|
||||
self._locked = not self._locked
|
||||
if self._locked:
|
||||
self._controlbox.setDisabled(True)
|
||||
else:
|
||||
self._controlbox.setDisabled(False)
|
||||
elif action == changeprecAction:
|
||||
msg = 'Precision for motor {}'.format(name)
|
||||
logger.debug('prec before %d', prec)
|
||||
prec, ok = QInputDialog.getInt(self, msg, msg, prec, 0, 10)
|
||||
logger.debug('prec after (%d) %d', ok, prec)
|
||||
if ok:
|
||||
self._prec = prec
|
||||
|
||||
elif action == changetweakstepAction:
|
||||
tv = self._pv_tweak_val.get()
|
||||
msg = 'Tweak step for motor {} at {} {}'.format(name, tv, unit)
|
||||
tv, ok = QInputDialog.getDouble(self, msg, msg, tv, pow(10, -prec), 10.0, prec)
|
||||
if ok:
|
||||
self._pv_tweak_val.put(tv)
|
||||
|
||||
elif action == tozeroAction:
|
||||
self._pv_drive.put(0.0)
|
||||
|
||||
elif action == autolabelsAction:
|
||||
self._auto_labels = not self._auto_labels
|
||||
|
||||
elif action == wheel_tweaks:
|
||||
self._wheel_tweaks = not self._wheel_tweaks
|
||||
self.bind_wheel()
|
||||
|
||||
self.update_label_template()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
pass
|
||||
|
||||
def update_label_template(self):
|
||||
source = self._templates_source
|
||||
target = self._templates
|
||||
|
||||
for k in source:
|
||||
target[k] = source[k].format(
|
||||
short_name=self.short_name,
|
||||
precision=self._prec,
|
||||
units=self._units)
|
||||
self.label.setText(target[self._label_style].format(rbv=self._pv_readback.get()))
|
||||
|
||||
def paintEvent(self, e):
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
qp.setRenderHint(QPainter.Antialiasing)
|
||||
self._draw_limits(qp)
|
||||
qp.end()
|
||||
|
||||
def _draw_limits(self, qp):
|
||||
pass
|
||||
# width, height = self.size().width(), self.size().height()
|
||||
# pad = 5
|
||||
# rounding = 2
|
||||
# size = 10
|
||||
# if m.HLS:
|
||||
# x, y, w, h = width - size, pad, size, height - 2 * pad
|
||||
# elif m.LLS:
|
||||
# x, y, w, h = 0, pad, size, height - 2 * pad
|
||||
# else:
|
||||
# return
|
||||
# color = QColor('indianred')
|
||||
# qp.setBrush(QBrush(color, Qt.SolidPattern))
|
||||
# qp.setPen(QPen(color))
|
||||
# path = QPainterPath()
|
||||
# path.setFillRule(Qt.WindingFill)
|
||||
# path.addRoundedRect(x, y, w, h, rounding, rounding)
|
||||
# qp.drawPath(path)
|
||||
0
epics_widgets/__init__.py
Normal file
0
epics_widgets/__init__.py
Normal file
113
furkaRIXS.py
113
furkaRIXS.py
@@ -59,9 +59,9 @@ from PyQt5.QtWidgets import (
|
||||
QMessageBox, QPlainTextEdit, QProgressBar, QProgressDialog, QPushButton, QShortcut, QSizePolicy, QSpinBox,
|
||||
QSplashScreen, QTextBrowser, QToolBox, QVBoxLayout, QWidget,)
|
||||
ts.log('Import part 4/8:')
|
||||
#from epics_widgets.MotorTweak import MotorTweak
|
||||
#from epics_widgets.SmaractMotorTweak import SmaractMotorTweak
|
||||
#from epics_widgets.SimMotorTweak import SimMotorTweak
|
||||
from epics_widgets.MotorTweak import MotorTweak
|
||||
from epics_widgets.SmaractMotorTweak import SmaractMotorTweak
|
||||
from epics_widgets.SimMotorTweak import SimMotorTweak
|
||||
ts.log('Import part 5/8:')
|
||||
import numpy as np
|
||||
np.set_printoptions(suppress=True,linewidth=196)
|
||||
@@ -88,6 +88,17 @@ def sigint_handler(*args):
|
||||
app=QApplication.instance()
|
||||
app.quit()
|
||||
|
||||
def add_widgets_to_toolbox(toolbox, label, widget_list=[]):
|
||||
block = QWidget()
|
||||
block.setAccessibleName(label)
|
||||
block.setContentsMargins(0, 0, 0, 0)
|
||||
block.setLayout(QVBoxLayout())
|
||||
for w in widget_list:
|
||||
block.layout().addWidget(w)
|
||||
block.layout().addStretch()
|
||||
item = toolbox.addItem(block, label)
|
||||
return item
|
||||
|
||||
class StartupSplash:
|
||||
|
||||
def __init__(self):
|
||||
@@ -121,11 +132,38 @@ class WndFurkaRIXS(QMainWindow, Ui_MainWindow):
|
||||
title = "Furka RIXS - Furka Resonant inelastic X-ray scattering spectrodscopy"
|
||||
self.setWindowTitle(title)
|
||||
|
||||
self._tweakers = {}
|
||||
|
||||
#self.init_settings()
|
||||
#self.setup_sliders()
|
||||
self.setup_sliders()
|
||||
self.init_graphics()
|
||||
self.init_actions()
|
||||
|
||||
def setup_sliders(self):
|
||||
cont = self._wdVert2
|
||||
self._tweak_container = QWidget()
|
||||
self._tweak_container.setLayout(QVBoxLayout())
|
||||
cont.layout().addWidget(self._tweak_container)
|
||||
layout = self._tweak_container.layout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
toolbox = QToolBox()
|
||||
layout.addWidget(toolbox)
|
||||
self.build_group_rawmotors(toolbox)
|
||||
|
||||
#self.build_group_faststage(toolbox)
|
||||
# self.build_slits_group(toolbox)
|
||||
#self.build_group_collimator(toolbox)
|
||||
#self.build_group_posttube(toolbox)
|
||||
#self.build_group_xeye(toolbox)
|
||||
#self.build_cryo_group(toolbox)
|
||||
|
||||
# monitor all axis for an axis fault
|
||||
#for key, tweaker in self.tweakers.items():
|
||||
# tweaker.event_axis_fault.connect(self.cb_axis_fault)
|
||||
|
||||
|
||||
def init_graphics(self):
|
||||
app = QApplication.instance()
|
||||
#cfg = app._cfg
|
||||
@@ -218,6 +256,73 @@ class WndFurkaRIXS(QMainWindow, Ui_MainWindow):
|
||||
self._actPreferences.triggered.connect(self.cb_modify_app_param)
|
||||
self._actAbout.triggered.connect(self.cb_about)
|
||||
|
||||
def build_group_rawmotors(self, toolbox):
|
||||
# dev | Abbr | Motion Axis | Unit |
|
||||
#-------------------------------------------------------------------------------------------------------
|
||||
# GCR.1 | MT | Mask translation | Grating Chamber |
|
||||
# GCR.2 | GTZ | Grating translation along Z-axis (Grating along beam) | Grating Chamber |
|
||||
# GCR.3 | GTY1 | Grating translation along Y-axis (Grating height) Wedge leveller 1 | Grating Chamber |
|
||||
# GCR.4 | GTY2 | Grating translation along Y-axis (Grating height) Wedge leveller 2 | Grating Chamber |
|
||||
# GCR.5 | GRX | Grating rotation around X-axis (pitch) | Grating Chamber |
|
||||
# GCR.6 | GTX | Grating translation along X-axis (Grating exchange) | Grating Chamber |
|
||||
#-------------------------------------------------------------------------------------------------------
|
||||
# DUR.1 | ABT | Aperture Blade Top | Diagnostics unit |
|
||||
# DUR.2 | ABB | Aperture Blade Bottom | Diagnostics unit |
|
||||
# DUR.3 | ABL | Aperture Blade Left | Diagnostics unit |
|
||||
# DUR.4 | ABR | Aperture Blade Right | Diagnostics unit |
|
||||
# DUR.5 | FT | Filter and Screen translation | Diagnostics unit |
|
||||
#-------------------------------------------------------------------------------------------------------
|
||||
# DCR.1 | DTZ | Detector Translation along Z-axis (R2 horizontal) | Detector Chamber |
|
||||
# DCR.2 | DTY1 | Detector Translation along Y-axis (R2 vert.) | Detector Chamber |
|
||||
# DCR.3 | DTY2 | Detector Translation along Y-axis (R2 vert.) | Detector Chamber |
|
||||
# DCR.4 | DRX | Detector Rotation around X-axis (ɣ) | Detector Chamber |
|
||||
# DCR.5 | RDC1 | RIXS Deformation Compensation (wedge leveller 1) | Spectrometer |
|
||||
# DCR.6 | RDC2 | RIXS Deformation Compensation (wedge leveller 2) | Spectrometer |
|
||||
# DCR.7 | RRY1 | RIXS Rotation around Y-axis (2θ) | Spectrometer |
|
||||
# DCR.8 | RRY2 | RIXS Rotation around Y-axis (2θ) | Spectrometer |
|
||||
|
||||
|
||||
|
||||
# pfx=QApplication.instance()._cfg.value(AppCfg.GBL_DEV_PREFIX)[1]
|
||||
pfx='SATES30-RX:MOT_'
|
||||
#SATES30-RIXS:MOT_2TRY.
|
||||
widgets=[
|
||||
self.create_tweaker(f"{pfx}MT", alias="gc_mt", label="MT", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}GTZ", alias="gc_gtz", label="GTZ", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}GTY1",alias="gc_gty1",label="GTY1",mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}GTY2",alias="gc_gty2",label="GTY2",mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}GRX", alias="gc_grx", label="GRX", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}GTX", alias="gc_gtx", label="GTX", mtype=SimMotorTweak),
|
||||
]
|
||||
add_widgets_to_toolbox(toolbox,"Grating Chamber",widget_list=widgets)
|
||||
|
||||
widgets=[
|
||||
self.create_tweaker(f"{pfx}ABT", alias="gc_mt", label="MT", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}ABB", alias="gc_gtz", label="GTZ", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}ABL",alias="gc_gty1",label="GTY1",mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}ABR",alias="gc_gty2",label="GTY2",mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}FT", alias="gc_grx", label="GRX", mtype=SimMotorTweak),
|
||||
]
|
||||
add_widgets_to_toolbox(toolbox,"Diagnostics unit",widget_list=widgets)
|
||||
|
||||
widgets=[
|
||||
self.create_tweaker(f"{pfx}DTZ", alias="gc_mt", label="MT", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}DTY1", alias="gc_gtz", label="GTZ", mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}DTY2",alias="gc_gty1",label="GTY1",mtype=SimMotorTweak),
|
||||
self.create_tweaker(f"{pfx}DRX",alias="gc_gty2",label="GTY2",mtype=SimMotorTweak),
|
||||
]
|
||||
add_widgets_to_toolbox(toolbox,"Detector Chamber",widget_list=widgets)
|
||||
|
||||
|
||||
|
||||
def create_tweaker(self, rec, alias=None, label=None, mtype=SimMotorTweak, **kwargs):
|
||||
m=mtype()
|
||||
m.connect_motor(rec, label, **kwargs)
|
||||
self._tweakers[alias] = m
|
||||
return m
|
||||
|
||||
|
||||
|
||||
def cb_really_quit(self):
|
||||
"""called when user Ctrl-Q the app"""
|
||||
if QMessageBox.question(self, "", "Are you sure you want to quit?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No,) == QMessageBox.Yes:
|
||||
|
||||
360
opticsExample.py
360
opticsExample.py
@@ -13,168 +13,306 @@ Optical system design demo
|
||||
|
||||
"""
|
||||
|
||||
#import initExample ## Add path to library (just for examples; you do not need this)
|
||||
|
||||
|
||||
#sys.path.insert(0, "/home/myname/pythonfiles")
|
||||
|
||||
from pyqtgraph.examples.optics import *
|
||||
#/usr/lib/python3/dist-packages/pyqtgraph/examples/optics/
|
||||
from pyoptic import *
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
import numpy as np
|
||||
from pyqtgraph import Point
|
||||
|
||||
app = pg.QtGui.QApplication([])
|
||||
class VLSSG(Optic): # variable line space sperical grating
|
||||
def __init__(self, **params):
|
||||
defaults = {
|
||||
'r1': 0,
|
||||
'r2': 0,
|
||||
'd': 0.01,
|
||||
}
|
||||
defaults.update(params)
|
||||
d = defaults.pop('d')
|
||||
defaults['x1'] = -d/2.
|
||||
defaults['x2'] = d/2.
|
||||
gitem = CircularSolid(brush=(100,100,100,255), **defaults)
|
||||
Optic.__init__(self, gitem, **defaults)
|
||||
|
||||
w = pg.GraphicsLayoutWidget(show=True, border=0.5)
|
||||
w.resize(1000, 900)
|
||||
w.show()
|
||||
def propagateRay(self, ray):
|
||||
"""Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays"""
|
||||
|
||||
surface = self.surfaces[0]
|
||||
p1, ai = surface.intersectRay(ray)
|
||||
if p1 is not None:
|
||||
p1 = surface.mapToItem(ray, p1)
|
||||
rd = ray['dir']
|
||||
a1 = np.arctan2(rd[1], rd[0])
|
||||
ar = a1 + np.pi - 2*ai
|
||||
print(ray['wl'])
|
||||
ar+=(ray['wl']-800)/5000
|
||||
ray.setEnd(p1)
|
||||
dp = Point(np.cos(ar), np.sin(ar))
|
||||
ray = Ray(parent=ray, dir=dp)
|
||||
else:
|
||||
ray.setEnd(None)
|
||||
return [ray]
|
||||
|
||||
|
||||
def demoRIXS2():
|
||||
app = pg.QtGui.QApplication([])
|
||||
w = pg.GraphicsLayoutWidget(show=True, border=0.5)
|
||||
w.resize(1000, 900)
|
||||
w.show()
|
||||
|
||||
### Curved mirror demo
|
||||
|
||||
vb = w.addViewBox()
|
||||
vb.setAspectLocked()
|
||||
grid =pg.GridItem(pen=(0, 255, 0), textPen=(0, 255, 0)) # green grid and labels
|
||||
vb.addItem(grid)
|
||||
vb.setRange(pg.QtCore.QRectF(-50, -30, 100, 100))
|
||||
|
||||
optics = []
|
||||
rays = []
|
||||
#m1 = Mirror(r1=-100, pos=(5,0), d=5, angle=-15)
|
||||
m1 = VLSSG(r1=-3120, r2=0, pos=(-3.459543, 10.6), d=5, angle=-68.38532441633484)
|
||||
optics.append(m1)
|
||||
|
||||
allRays = []
|
||||
#for y in np.linspace(-1, 10, 21):
|
||||
# r = Ray(start=Point(-100, y))
|
||||
# view.addItem(r)
|
||||
# allRays.append(r)
|
||||
|
||||
for wl in np.linspace(355,1040, 25):
|
||||
for y in [10,15]:
|
||||
#d=Point(1,(y-12.5)/300)
|
||||
d=Point(1,(y-12.5)/300)
|
||||
#r = Ray(start=Point(-100, y+wl/500), wl=wl, dir=d)
|
||||
r = Ray(start=Point(-100, y), wl=wl)
|
||||
vb.addItem(r)
|
||||
allRays.append(r)
|
||||
|
||||
for o in optics:
|
||||
vb.addItem(o)
|
||||
|
||||
t1 = Tracer(allRays, optics)
|
||||
|
||||
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||
QtGui.QApplication.instance().exec_()
|
||||
|
||||
|
||||
|
||||
### Curved mirror demo
|
||||
|
||||
view = w.addViewBox()
|
||||
view.setAspectLocked()
|
||||
#grid = pg.GridItem()
|
||||
#view.addItem(grid)
|
||||
view.setRange(pg.QtCore.QRectF(-50, -30, 100, 100))
|
||||
def demoRIXS1():
|
||||
app = pg.QtGui.QApplication([])
|
||||
w = pg.GraphicsLayoutWidget(show=True, border=0.5)
|
||||
w.resize(1000, 900)
|
||||
w.show()
|
||||
|
||||
optics = []
|
||||
rays = []
|
||||
m1 = Mirror(r1=-100, pos=(5,0), d=5, angle=-15)
|
||||
optics.append(m1)
|
||||
m2 = Mirror(r1=-70, pos=(-40, 30), d=6, angle=180-15)
|
||||
optics.append(m2)
|
||||
### Curved mirror demo
|
||||
|
||||
allRays = []
|
||||
for y in np.linspace(-10, 10, 21):
|
||||
r = Ray(start=Point(-100, y))
|
||||
view.addItem(r)
|
||||
allRays.append(r)
|
||||
vb = w.addViewBox()
|
||||
vb.setAspectLocked()
|
||||
grid =pg.GridItem(pen=(0, 255, 0), textPen=(0, 255, 0)) # green grid and labels
|
||||
vb.addItem(grid)
|
||||
vb.setRange(pg.QtCore.QRectF(-50, -30, 100, 100))
|
||||
|
||||
for o in optics:
|
||||
view.addItem(o)
|
||||
|
||||
t1 = Tracer(allRays, optics)
|
||||
optics = []
|
||||
rays = []
|
||||
#m1 = Mirror(r1=-100, pos=(5,0), d=5, angle=-15)
|
||||
m1 = Mirror(r1=-100, r2=0, pos=(-3.459543, 10.6), d=5, angle=-68.38532441633484)
|
||||
optics.append(m1)
|
||||
l1 = Lens(pos=(41, 52.9),r1=20, r2=20, d=10, angle=15, glass='Corning7980')
|
||||
optics.append(l1)
|
||||
|
||||
allRays = []
|
||||
#for y in np.linspace(-1, 10, 21):
|
||||
# r = Ray(start=Point(-100, y))
|
||||
# view.addItem(r)
|
||||
# allRays.append(r)
|
||||
|
||||
for wl in np.linspace(355,1040, 25):
|
||||
for y in [10,15]:
|
||||
d=Point(1,(y-12.5)/300)
|
||||
r = Ray(start=Point(-100, y+wl/500), wl=wl, dir=d)
|
||||
#r = Ray(start=Point(-100, y), wl=wl)
|
||||
vb.addItem(r)
|
||||
allRays.append(r)
|
||||
|
||||
for o in optics:
|
||||
vb.addItem(o)
|
||||
|
||||
t1 = Tracer(allRays, optics)
|
||||
|
||||
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||
QtGui.QApplication.instance().exec_()
|
||||
|
||||
|
||||
def demoOrig():
|
||||
app = pg.QtGui.QApplication([])
|
||||
|
||||
w = pg.GraphicsLayoutWidget(show=True, border=0.5)
|
||||
w.resize(1000, 900)
|
||||
w.show()
|
||||
|
||||
|
||||
|
||||
### Dispersion demo
|
||||
### Curved mirror demo
|
||||
|
||||
optics = []
|
||||
view = w.addViewBox()
|
||||
view.setAspectLocked()
|
||||
#grid = pg.GridItem()
|
||||
#view.addItem(grid)
|
||||
view.setRange(pg.QtCore.QRectF(-50, -30, 100, 100))
|
||||
|
||||
view = w.addViewBox()
|
||||
optics = []
|
||||
rays = []
|
||||
m1 = Mirror(r1=-100, pos=(5,0), d=5, angle=-15)
|
||||
optics.append(m1)
|
||||
m2 = Mirror(r1=-70, pos=(-40, 30), d=6, angle=180-15)
|
||||
optics.append(m2)
|
||||
|
||||
view.setAspectLocked()
|
||||
#grid = pg.GridItem()
|
||||
#view.addItem(grid)
|
||||
view.setRange(pg.QtCore.QRectF(-10, -50, 90, 60))
|
||||
allRays = []
|
||||
for y in np.linspace(-10, 10, 21):
|
||||
r = Ray(start=Point(-100, y))
|
||||
view.addItem(r)
|
||||
allRays.append(r)
|
||||
|
||||
optics = []
|
||||
rays = []
|
||||
l1 = Lens(r1=20, r2=20, d=10, angle=8, glass='Corning7980')
|
||||
optics.append(l1)
|
||||
for o in optics:
|
||||
view.addItem(o)
|
||||
|
||||
allRays = []
|
||||
for wl in np.linspace(355,1040, 25):
|
||||
for y in [10]:
|
||||
r = Ray(start=Point(-100, y), wl=wl)
|
||||
view.addItem(r)
|
||||
allRays.append(r)
|
||||
|
||||
for o in optics:
|
||||
view.addItem(o)
|
||||
|
||||
t2 = Tracer(allRays, optics)
|
||||
t1 = Tracer(allRays, optics)
|
||||
|
||||
|
||||
|
||||
### Scanning laser microscopy demo
|
||||
### Dispersion demo
|
||||
|
||||
w.nextRow()
|
||||
view = w.addViewBox(colspan=2)
|
||||
optics = []
|
||||
|
||||
optics = []
|
||||
view = w.addViewBox()
|
||||
|
||||
view.setAspectLocked()
|
||||
#grid = pg.GridItem()
|
||||
#view.addItem(grid)
|
||||
view.setRange(pg.QtCore.QRectF(-10, -50, 90, 60))
|
||||
|
||||
#view.setAspectLocked()
|
||||
view.setRange(QtCore.QRectF(200, -50, 500, 200))
|
||||
optics = []
|
||||
rays = []
|
||||
l1 = Lens(r1=20, r2=20, d=10, angle=8, glass='Corning7980')
|
||||
optics.append(l1)
|
||||
|
||||
allRays = []
|
||||
for wl in np.linspace(355,1040, 25):
|
||||
for y in [10]:
|
||||
r = Ray(start=Point(-100, y), wl=wl)
|
||||
view.addItem(r)
|
||||
allRays.append(r)
|
||||
|
||||
for o in optics:
|
||||
view.addItem(o)
|
||||
|
||||
t2 = Tracer(allRays, optics)
|
||||
|
||||
|
||||
|
||||
## Scan mirrors
|
||||
scanx = 250
|
||||
scany = 20
|
||||
m1 = Mirror(dia=4.2, d=0.001, pos=(scanx, 0), angle=315)
|
||||
m2 = Mirror(dia=8.4, d=0.001, pos=(scanx, scany), angle=135)
|
||||
### Scanning laser microscopy demo
|
||||
|
||||
## Scan lenses
|
||||
l3 = Lens(r1=23.0, r2=0, d=5.8, pos=(scanx+50, scany), glass='Corning7980') ## 50mm UVFS (LA4148)
|
||||
l4 = Lens(r1=0, r2=69.0, d=3.2, pos=(scanx+250, scany), glass='Corning7980') ## 150mm UVFS (LA4874)
|
||||
w.nextRow()
|
||||
view = w.addViewBox(colspan=2)
|
||||
|
||||
## Objective
|
||||
obj = Lens(r1=15, r2=15, d=10, dia=8, pos=(scanx+400, scany), glass='Corning7980')
|
||||
optics = []
|
||||
|
||||
IROptics = [m1, m2, l3, l4, obj]
|
||||
|
||||
#view.setAspectLocked()
|
||||
view.setRange(QtCore.QRectF(200, -50, 500, 200))
|
||||
|
||||
|
||||
|
||||
## Scan mirrors
|
||||
scanx = 250
|
||||
scany = 30
|
||||
m1a = Mirror(dia=4.2, d=0.001, pos=(scanx, 2*scany), angle=315)
|
||||
m2a = Mirror(dia=8.4, d=0.001, pos=(scanx, 3*scany), angle=135)
|
||||
## Scan mirrors
|
||||
scanx = 250
|
||||
scany = 20
|
||||
m1 = Mirror(dia=4.2, d=0.001, pos=(scanx, 0), angle=315)
|
||||
m2 = Mirror(dia=8.4, d=0.001, pos=(scanx, scany), angle=135)
|
||||
|
||||
## Scan lenses
|
||||
l3a = Lens(r1=46, r2=0, d=3.8, pos=(scanx+50, 3*scany), glass='Corning7980') ## 100mm UVFS (LA4380)
|
||||
l4a = Lens(r1=0, r2=46, d=3.8, pos=(scanx+250, 3*scany), glass='Corning7980') ## 100mm UVFS (LA4380)
|
||||
## Scan lenses
|
||||
l3 = Lens(r1=23.0, r2=0, d=5.8, pos=(scanx+50, scany), glass='Corning7980') ## 50mm UVFS (LA4148)
|
||||
l4 = Lens(r1=0, r2=69.0, d=3.2, pos=(scanx+250, scany), glass='Corning7980') ## 150mm UVFS (LA4874)
|
||||
|
||||
## Objective
|
||||
obja = Lens(r1=15, r2=15, d=10, dia=8, pos=(scanx+400, 3*scany), glass='Corning7980')
|
||||
## Objective
|
||||
obj = Lens(r1=15, r2=15, d=10, dia=8, pos=(scanx+400, scany), glass='Corning7980')
|
||||
|
||||
IROptics2 = [m1a, m2a, l3a, l4a, obja]
|
||||
IROptics = [m1, m2, l3, l4, obj]
|
||||
|
||||
|
||||
|
||||
for o in set(IROptics+IROptics2):
|
||||
view.addItem(o)
|
||||
|
||||
IRRays = []
|
||||
IRRays2 = []
|
||||
## Scan mirrors
|
||||
scanx = 250
|
||||
scany = 30
|
||||
m1a = Mirror(dia=4.2, d=0.001, pos=(scanx, 2*scany), angle=315)
|
||||
m2a = Mirror(dia=8.4, d=0.001, pos=(scanx, 3*scany), angle=135)
|
||||
|
||||
for dy in [-0.4, -0.15, 0, 0.15, 0.4]:
|
||||
IRRays.append(Ray(start=Point(-50, dy), dir=(1, 0), wl=780))
|
||||
IRRays2.append(Ray(start=Point(-50, dy+2*scany), dir=(1, 0), wl=780))
|
||||
|
||||
for r in set(IRRays+IRRays2):
|
||||
view.addItem(r)
|
||||
## Scan lenses
|
||||
l3a = Lens(r1=46, r2=0, d=3.8, pos=(scanx+50, 3*scany), glass='Corning7980') ## 100mm UVFS (LA4380)
|
||||
l4a = Lens(r1=0, r2=46, d=3.8, pos=(scanx+250, 3*scany), glass='Corning7980') ## 100mm UVFS (LA4380)
|
||||
|
||||
IRTracer = Tracer(IRRays, IROptics)
|
||||
IRTracer2 = Tracer(IRRays2, IROptics2)
|
||||
## Objective
|
||||
obja = Lens(r1=15, r2=15, d=10, dia=8, pos=(scanx+400, 3*scany), glass='Corning7980')
|
||||
|
||||
phase = 0.0
|
||||
def update():
|
||||
global phase
|
||||
if phase % (8*np.pi) > 4*np.pi:
|
||||
m1['angle'] = 315 + 1.5*np.sin(phase)
|
||||
m1a['angle'] = 315 + 1.5*np.sin(phase)
|
||||
else:
|
||||
m2['angle'] = 135 + 1.5*np.sin(phase)
|
||||
m2a['angle'] = 135 + 1.5*np.sin(phase)
|
||||
phase += 0.2
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
timer.start(40)
|
||||
IROptics2 = [m1a, m2a, l3a, l4a, obja]
|
||||
|
||||
|
||||
|
||||
for o in set(IROptics+IROptics2):
|
||||
view.addItem(o)
|
||||
|
||||
IRRays = []
|
||||
IRRays2 = []
|
||||
|
||||
for dy in [-0.4, -0.15, 0, 0.15, 0.4]:
|
||||
IRRays.append(Ray(start=Point(-50, dy), dir=(1, 0), wl=780))
|
||||
IRRays2.append(Ray(start=Point(-50, dy+2*scany), dir=(1, 0), wl=780))
|
||||
|
||||
for r in set(IRRays+IRRays2):
|
||||
view.addItem(r)
|
||||
|
||||
IRTracer = Tracer(IRRays, IROptics)
|
||||
IRTracer2 = Tracer(IRRays2, IROptics2)
|
||||
|
||||
global phase
|
||||
phase = 0.0
|
||||
def update():
|
||||
global phase
|
||||
if phase % (8*np.pi) > 4*np.pi:
|
||||
m1['angle'] = 315 + 1.5*np.sin(phase)
|
||||
m1a['angle'] = 315 + 1.5*np.sin(phase)
|
||||
else:
|
||||
m2['angle'] = 135 + 1.5*np.sin(phase)
|
||||
m2a['angle'] = 135 + 1.5*np.sin(phase)
|
||||
phase += 0.2
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
timer.start(40)
|
||||
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||
QtGui.QApplication.instance().exec_()
|
||||
|
||||
|
||||
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||
QtGui.QApplication.instance().exec_()
|
||||
if __name__=='__main__':
|
||||
import sys
|
||||
import argparse
|
||||
#(h, t)=os.path.split(sys.argv[0]);cmd='\n '+(t if len(h)>20 else sys.argv[0])+' '
|
||||
#exampleCmd=('', '-m0xf -v0')
|
||||
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 default=0x%(default)x", default=0)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.mode==0:
|
||||
demoOrig()
|
||||
elif args.mode==1:
|
||||
demoRIXS1()
|
||||
elif args.mode==2:
|
||||
demoRIXS2()
|
||||
else:
|
||||
demoRIXS()
|
||||
|
||||
|
||||
563
pyoptic.py
Normal file
563
pyoptic.py
Normal file
@@ -0,0 +1,563 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import csv, gzip, os
|
||||
from pyqtgraph import Point
|
||||
|
||||
class GlassDB:
|
||||
"""
|
||||
Database of dispersion coefficients for Schott glasses
|
||||
+ Corning 7980
|
||||
"""
|
||||
def __init__(self, fileName='schott_glasses.csv'):
|
||||
path = os.path.dirname(__file__)
|
||||
fh = gzip.open(os.path.join(path, 'schott_glasses.csv.gz'), 'rb')
|
||||
r = csv.reader(map(str, fh.readlines()))
|
||||
lines = [x for x in r]
|
||||
self.data = {}
|
||||
header = lines[0]
|
||||
for l in lines[1:]:
|
||||
info = {}
|
||||
for i in range(1, len(l)):
|
||||
info[header[i]] = l[i]
|
||||
self.data[l[0]] = info
|
||||
self.data['Corning7980'] = { ## Thorlabs UV fused silica--not in schott catalog.
|
||||
'B1': 0.68374049400,
|
||||
'B2': 0.42032361300,
|
||||
'B3': 0.58502748000,
|
||||
'C1': 0.00460352869,
|
||||
'C2': 0.01339688560,
|
||||
'C3': 64.49327320000,
|
||||
'TAUI25/250': 0.95, ## transmission data is fabricated, but close.
|
||||
'TAUI25/1400': 0.98,
|
||||
}
|
||||
|
||||
for k in self.data:
|
||||
self.data[k]['ior_cache'] = {}
|
||||
|
||||
|
||||
def ior(self, glass, wl):
|
||||
"""
|
||||
Return the index of refraction for *glass* at wavelength *wl*.
|
||||
|
||||
The *glass* argument must be a key in self.data.
|
||||
"""
|
||||
info = self.data[glass]
|
||||
cache = info['ior_cache']
|
||||
if wl not in cache:
|
||||
B = list(map(float, [info['B1'], info['B2'], info['B3']]))
|
||||
C = list(map(float, [info['C1'], info['C2'], info['C3']]))
|
||||
w2 = (wl/1000.)**2
|
||||
n = np.sqrt(1.0 + (B[0]*w2 / (w2-C[0])) + (B[1]*w2 / (w2-C[1])) + (B[2]*w2 / (w2-C[2])))
|
||||
cache[wl] = n
|
||||
return cache[wl]
|
||||
|
||||
def transmissionCurve(self, glass):
|
||||
data = self.data[glass]
|
||||
keys = [int(x[7:]) for x in data.keys() if 'TAUI25' in x]
|
||||
keys.sort()
|
||||
curve = np.empty((2,len(keys)))
|
||||
for i in range(len(keys)):
|
||||
curve[0][i] = keys[i]
|
||||
key = 'TAUI25/%d' % keys[i]
|
||||
val = data[key]
|
||||
if val == '':
|
||||
val = 0
|
||||
else:
|
||||
val = float(val)
|
||||
curve[1][i] = val
|
||||
return curve
|
||||
|
||||
|
||||
GLASSDB = GlassDB()
|
||||
|
||||
|
||||
def wlPen(wl):
|
||||
"""Return a pen representing the given wavelength"""
|
||||
l1 = 400
|
||||
l2 = 700
|
||||
hue = np.clip(((l2-l1) - (wl-l1)) * 0.8 / (l2-l1), 0, 0.8)
|
||||
val = 1.0
|
||||
if wl > 700:
|
||||
val = 1.0 * (((700-wl)/700.) + 1)
|
||||
elif wl < 400:
|
||||
val = wl * 1.0/400.
|
||||
#print hue, val
|
||||
color = pg.hsvColor(hue, 1.0, val)
|
||||
pen = pg.mkPen(color)
|
||||
return pen
|
||||
|
||||
|
||||
class ParamObj(object):
|
||||
# Just a helper for tracking parameters and responding to changes
|
||||
def __init__(self):
|
||||
self.__params = {}
|
||||
|
||||
def __setitem__(self, item, val):
|
||||
self.setParam(item, val)
|
||||
|
||||
def setParam(self, param, val):
|
||||
self.setParams(**{param:val})
|
||||
|
||||
def setParams(self, **params):
|
||||
"""Set parameters for this optic. This is a good function to override for subclasses."""
|
||||
self.__params.update(params)
|
||||
self.paramStateChanged()
|
||||
|
||||
def paramStateChanged(self):
|
||||
pass
|
||||
|
||||
def __getitem__(self, item):
|
||||
# bug in pyside 1.2.2 causes getitem to be called inside QGraphicsObject.parentItem:
|
||||
return self.getParam(item) # PySide bug: https://bugreports.qt.io/browse/PYSIDE-671
|
||||
|
||||
def __len__(self):
|
||||
# Workaround for PySide bug: https://bugreports.qt.io/browse/PYSIDE-671
|
||||
return 0
|
||||
|
||||
def getParam(self, param):
|
||||
return self.__params[param]
|
||||
|
||||
|
||||
class Optic(pg.GraphicsObject, ParamObj):
|
||||
|
||||
sigStateChanged = QtCore.Signal()
|
||||
|
||||
|
||||
def __init__(self, gitem, **params):
|
||||
ParamObj.__init__(self)
|
||||
pg.GraphicsObject.__init__(self) #, [0,0], [1,1])
|
||||
|
||||
self.gitem = gitem
|
||||
self.surfaces = gitem.surfaces
|
||||
gitem.setParentItem(self)
|
||||
|
||||
self.roi = pg.ROI([0,0], [1,1])
|
||||
self.roi.addRotateHandle([1, 1], [0.5, 0.5])
|
||||
self.roi.setParentItem(self)
|
||||
|
||||
defaults = {
|
||||
'pos': Point(0,0),
|
||||
'angle': 0,
|
||||
}
|
||||
defaults.update(params)
|
||||
self._ior_cache = {}
|
||||
self.roi.sigRegionChanged.connect(self.roiChanged)
|
||||
self.setParams(**defaults)
|
||||
|
||||
def updateTransform(self):
|
||||
self.resetTransform()
|
||||
self.setPos(0, 0)
|
||||
self.translate(Point(self['pos']))
|
||||
self.rotate(self['angle'])
|
||||
|
||||
def setParam(self, param, val):
|
||||
ParamObj.setParam(self, param, val)
|
||||
|
||||
def paramStateChanged(self):
|
||||
"""Some parameters of the optic have changed."""
|
||||
# Move graphics item
|
||||
self.gitem.setPos(Point(self['pos']))
|
||||
self.gitem.resetTransform()
|
||||
self.gitem.rotate(self['angle'])
|
||||
|
||||
# Move ROI to match
|
||||
try:
|
||||
self.roi.sigRegionChanged.disconnect(self.roiChanged)
|
||||
br = self.gitem.boundingRect()
|
||||
o = self.gitem.mapToParent(br.topLeft())
|
||||
self.roi.setAngle(self['angle'])
|
||||
self.roi.setPos(o)
|
||||
self.roi.setSize([br.width(), br.height()])
|
||||
finally:
|
||||
self.roi.sigRegionChanged.connect(self.roiChanged)
|
||||
|
||||
self.sigStateChanged.emit()
|
||||
|
||||
def roiChanged(self, *args):
|
||||
pos = self.roi.pos()
|
||||
# rotate gitem temporarily so we can decide where it will need to move
|
||||
self.gitem.resetTransform()
|
||||
self.gitem.rotate(self.roi.angle())
|
||||
br = self.gitem.boundingRect()
|
||||
o1 = self.gitem.mapToParent(br.topLeft())
|
||||
self.setParams(angle=self.roi.angle(), pos=pos + (self.gitem.pos() - o1))
|
||||
print(f'angle:{self.roi.angle()} pos:{self.roi.pos()}')
|
||||
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF()
|
||||
|
||||
def paint(self, p, *args):
|
||||
pass
|
||||
|
||||
def ior(self, wavelength):
|
||||
return GLASSDB.ior(self['glass'], wavelength)
|
||||
|
||||
|
||||
|
||||
class Lens(Optic):
|
||||
def __init__(self, **params):
|
||||
defaults = {
|
||||
'dia': 25.4, ## diameter of lens
|
||||
'r1': 50., ## positive means convex, use 0 for planar
|
||||
'r2': 0, ## negative means convex
|
||||
'd': 4.0,
|
||||
'glass': 'N-BK7',
|
||||
'reflect': False,
|
||||
}
|
||||
defaults.update(params)
|
||||
d = defaults.pop('d')
|
||||
defaults['x1'] = -d/2.
|
||||
defaults['x2'] = d/2.
|
||||
|
||||
gitem = CircularSolid(brush=(100, 100, 130, 100), **defaults)
|
||||
Optic.__init__(self, gitem, **defaults)
|
||||
|
||||
def propagateRay(self, ray):
|
||||
"""Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays"""
|
||||
|
||||
"""
|
||||
NOTE:: We can probably use this to compute refractions faster: (from GLSL 120 docs)
|
||||
|
||||
For the incident vector I and surface normal N, and the
|
||||
ratio of indices of refraction eta, return the refraction
|
||||
vector. The result is computed by
|
||||
k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I))
|
||||
if (k < 0.0)
|
||||
return genType(0.0)
|
||||
else
|
||||
return eta * I - (eta * dot(N, I) + sqrt(k)) * N
|
||||
The input parameters for the incident vector I and the
|
||||
surface normal N must already be normalized to get the
|
||||
desired results. eta == ratio of IORs
|
||||
|
||||
|
||||
For reflection:
|
||||
For the incident vector I and surface orientation N,
|
||||
returns the reflection direction:
|
||||
I – 2 ∗ dot(N, I) ∗ N
|
||||
N must already be normalized in order to achieve the
|
||||
desired result.
|
||||
"""
|
||||
iors = [self.ior(ray['wl']), 1.0]
|
||||
for i in [0,1]:
|
||||
surface = self.surfaces[i]
|
||||
ior = iors[i]
|
||||
p1, ai = surface.intersectRay(ray)
|
||||
if p1 is None:
|
||||
ray.setEnd(None)
|
||||
break
|
||||
p1 = surface.mapToItem(ray, p1)
|
||||
|
||||
rd = ray['dir']
|
||||
a1 = np.arctan2(rd[1], rd[0])
|
||||
ar = a1 - ai + np.arcsin((np.sin(ai) * ray['ior'] / ior))
|
||||
ray.setEnd(p1)
|
||||
dp = Point(np.cos(ar), np.sin(ar))
|
||||
ray = Ray(parent=ray, ior=ior, dir=dp)
|
||||
return [ray]
|
||||
|
||||
|
||||
class Mirror(Optic):
|
||||
def __init__(self, **params):
|
||||
defaults = {
|
||||
'r1': 0,
|
||||
'r2': 0,
|
||||
'd': 0.01,
|
||||
}
|
||||
defaults.update(params)
|
||||
d = defaults.pop('d')
|
||||
defaults['x1'] = -d/2.
|
||||
defaults['x2'] = d/2.
|
||||
gitem = CircularSolid(brush=(100,100,100,255), **defaults)
|
||||
Optic.__init__(self, gitem, **defaults)
|
||||
|
||||
def propagateRay(self, ray):
|
||||
"""Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays"""
|
||||
|
||||
surface = self.surfaces[0]
|
||||
p1, ai = surface.intersectRay(ray)
|
||||
if p1 is not None:
|
||||
p1 = surface.mapToItem(ray, p1)
|
||||
rd = ray['dir']
|
||||
a1 = np.arctan2(rd[1], rd[0])
|
||||
ar = a1 + np.pi - 2*ai
|
||||
ray.setEnd(p1)
|
||||
dp = Point(np.cos(ar), np.sin(ar))
|
||||
ray = Ray(parent=ray, dir=dp)
|
||||
else:
|
||||
ray.setEnd(None)
|
||||
return [ray]
|
||||
|
||||
|
||||
class CircularSolid(pg.GraphicsObject, ParamObj):
|
||||
"""GraphicsObject with two circular or flat surfaces."""
|
||||
def __init__(self, pen=None, brush=None, **opts):
|
||||
"""
|
||||
Arguments for each surface are:
|
||||
x1,x2 - position of center of _physical surface_
|
||||
r1,r2 - radius of curvature
|
||||
d1,d2 - diameter of optic
|
||||
"""
|
||||
defaults = dict(x1=-2, r1=100, d1=25.4, x2=2, r2=100, d2=25.4)
|
||||
defaults.update(opts)
|
||||
ParamObj.__init__(self)
|
||||
self.surfaces = [CircleSurface(defaults['r1'], defaults['d1']), CircleSurface(-defaults['r2'], defaults['d2'])]
|
||||
pg.GraphicsObject.__init__(self)
|
||||
for s in self.surfaces:
|
||||
s.setParentItem(self)
|
||||
|
||||
if pen is None:
|
||||
self.pen = pg.mkPen((220,220,255,200), width=1, cosmetic=True)
|
||||
else:
|
||||
self.pen = pg.mkPen(pen)
|
||||
|
||||
if brush is None:
|
||||
self.brush = pg.mkBrush((230, 230, 255, 30))
|
||||
else:
|
||||
self.brush = pg.mkBrush(brush)
|
||||
|
||||
self.setParams(**defaults)
|
||||
|
||||
def paramStateChanged(self):
|
||||
self.updateSurfaces()
|
||||
|
||||
def updateSurfaces(self):
|
||||
self.surfaces[0].setParams(self['r1'], self['d1'])
|
||||
self.surfaces[1].setParams(-self['r2'], self['d2'])
|
||||
self.surfaces[0].setPos(self['x1'], 0)
|
||||
self.surfaces[1].setPos(self['x2'], 0)
|
||||
|
||||
self.path = QtGui.QPainterPath()
|
||||
self.path.connectPath(self.surfaces[0].path.translated(self.surfaces[0].pos()))
|
||||
self.path.connectPath(self.surfaces[1].path.translated(self.surfaces[1].pos()).toReversed())
|
||||
self.path.closeSubpath()
|
||||
|
||||
def boundingRect(self):
|
||||
return self.path.boundingRect()
|
||||
|
||||
def shape(self):
|
||||
return self.path
|
||||
|
||||
def paint(self, p, *args):
|
||||
p.setRenderHints(p.renderHints() | p.Antialiasing)
|
||||
p.setPen(self.pen)
|
||||
p.fillPath(self.path, self.brush)
|
||||
p.drawPath(self.path)
|
||||
|
||||
|
||||
class CircleSurface(pg.GraphicsObject):
|
||||
def __init__(self, radius=None, diameter=None):
|
||||
"""center of physical surface is at 0,0
|
||||
radius is the radius of the surface. If radius is None, the surface is flat.
|
||||
diameter is of the optic's edge."""
|
||||
pg.GraphicsObject.__init__(self)
|
||||
|
||||
self.r = radius
|
||||
self.d = diameter
|
||||
self.mkPath()
|
||||
|
||||
def setParams(self, r, d):
|
||||
self.r = r
|
||||
self.d = d
|
||||
self.mkPath()
|
||||
|
||||
def mkPath(self):
|
||||
self.prepareGeometryChange()
|
||||
r = self.r
|
||||
d = self.d
|
||||
h2 = d/2.
|
||||
self.path = QtGui.QPainterPath()
|
||||
if r == 0: ## flat surface
|
||||
self.path.moveTo(0, h2)
|
||||
self.path.lineTo(0, -h2)
|
||||
else:
|
||||
## half-height of surface can't be larger than radius
|
||||
h2 = min(h2, abs(r))
|
||||
arc = QtCore.QRectF(0, -r, r*2, r*2)
|
||||
a1 = np.arcsin(h2/r) * 180. / np.pi
|
||||
a2 = -2*a1
|
||||
a1 += 180.
|
||||
self.path.arcMoveTo(arc, a1)
|
||||
self.path.arcTo(arc, a1, a2)
|
||||
self.h2 = h2
|
||||
|
||||
def boundingRect(self):
|
||||
return self.path.boundingRect()
|
||||
|
||||
def paint(self, p, *args):
|
||||
return ## usually we let the optic draw.
|
||||
|
||||
def intersectRay(self, ray):
|
||||
## return the point of intersection and the angle of incidence
|
||||
#print "intersect ray"
|
||||
h = self.h2
|
||||
r = self.r
|
||||
p, dir = ray.currentState(relativeTo=self) # position and angle of ray in local coords.
|
||||
#print " ray: ", p, dir
|
||||
p = p - Point(r, 0) ## move position so center of circle is at 0,0
|
||||
#print " adj: ", p, r
|
||||
|
||||
if r == 0:
|
||||
#print " flat"
|
||||
if dir[0] == 0:
|
||||
y = 0
|
||||
else:
|
||||
y = p[1] - p[0] * dir[1]/dir[0]
|
||||
if abs(y) > h:
|
||||
return None, None
|
||||
else:
|
||||
return (Point(0, y), np.arctan2(dir[1], dir[0]))
|
||||
else:
|
||||
#print " curve"
|
||||
## find intersection of circle and line (quadratic formula)
|
||||
dx = dir[0]
|
||||
dy = dir[1]
|
||||
dr = (dx**2 + dy**2) ** 0.5
|
||||
D = p[0] * (p[1]+dy) - (p[0]+dx) * p[1]
|
||||
idr2 = 1.0 / dr**2
|
||||
disc = r**2 * dr**2 - D**2
|
||||
if disc < 0:
|
||||
return None, None
|
||||
disc2 = disc**0.5
|
||||
if dy < 0:
|
||||
sgn = -1
|
||||
else:
|
||||
sgn = 1
|
||||
|
||||
|
||||
br = self.path.boundingRect()
|
||||
x1 = (D*dy + sgn*dx*disc2) * idr2
|
||||
y1 = (-D*dx + abs(dy)*disc2) * idr2
|
||||
if br.contains(x1+r, y1):
|
||||
pt = Point(x1, y1)
|
||||
else:
|
||||
x2 = (D*dy - sgn*dx*disc2) * idr2
|
||||
y2 = (-D*dx - abs(dy)*disc2) * idr2
|
||||
pt = Point(x2, y2)
|
||||
if not br.contains(x2+r, y2):
|
||||
return None, None
|
||||
raise Exception("No intersection!")
|
||||
|
||||
norm = np.arctan2(pt[1], pt[0])
|
||||
if r < 0:
|
||||
norm += np.pi
|
||||
#print " norm:", norm*180/3.1415
|
||||
dp = p - pt
|
||||
#print " dp:", dp
|
||||
ang = np.arctan2(dp[1], dp[0])
|
||||
#print " ang:", ang*180/3.1415
|
||||
#print " ai:", (ang-norm)*180/3.1415
|
||||
|
||||
#print " intersection:", pt
|
||||
return pt + Point(r, 0), ang-norm
|
||||
|
||||
|
||||
class Ray(pg.GraphicsObject, ParamObj):
|
||||
"""Represents a single straight segment of a ray"""
|
||||
|
||||
sigStateChanged = QtCore.Signal()
|
||||
|
||||
def __init__(self, **params):
|
||||
ParamObj.__init__(self)
|
||||
defaults = {
|
||||
'ior': 1.0,
|
||||
'wl': 500,
|
||||
'end': None,
|
||||
'dir': Point(1,0),
|
||||
}
|
||||
self.params = {}
|
||||
pg.GraphicsObject.__init__(self)
|
||||
self.children = []
|
||||
parent = params.get('parent', None)
|
||||
if parent is not None:
|
||||
defaults['start'] = parent['end']
|
||||
defaults['wl'] = parent['wl']
|
||||
self['ior'] = parent['ior']
|
||||
self['dir'] = parent['dir']
|
||||
parent.addChild(self)
|
||||
|
||||
defaults.update(params)
|
||||
defaults['dir'] = Point(defaults['dir'])
|
||||
self.setParams(**defaults)
|
||||
self.mkPath()
|
||||
|
||||
def clearChildren(self):
|
||||
for c in self.children:
|
||||
c.clearChildren()
|
||||
c.setParentItem(None)
|
||||
self.scene().removeItem(c)
|
||||
self.children = []
|
||||
|
||||
def paramStateChanged(self):
|
||||
pass
|
||||
|
||||
def addChild(self, ch):
|
||||
self.children.append(ch)
|
||||
ch.setParentItem(self)
|
||||
|
||||
def currentState(self, relativeTo=None):
|
||||
pos = self['start']
|
||||
dir = self['dir']
|
||||
if relativeTo is None:
|
||||
return pos, dir
|
||||
else:
|
||||
trans = self.itemTransform(relativeTo)[0]
|
||||
p1 = trans.map(pos)
|
||||
p2 = trans.map(pos + dir)
|
||||
return Point(p1), Point(p2-p1)
|
||||
|
||||
def setEnd(self, end):
|
||||
self['end'] = end
|
||||
self.mkPath()
|
||||
|
||||
def boundingRect(self):
|
||||
return self.path.boundingRect()
|
||||
|
||||
def paint(self, p, *args):
|
||||
#p.setPen(pg.mkPen((255,0,0, 150)))
|
||||
p.setRenderHints(p.renderHints() | p.Antialiasing)
|
||||
p.setCompositionMode(p.CompositionMode_Plus)
|
||||
p.setPen(wlPen(self['wl']))
|
||||
p.drawPath(self.path)
|
||||
|
||||
def mkPath(self):
|
||||
self.prepareGeometryChange()
|
||||
self.path = QtGui.QPainterPath()
|
||||
self.path.moveTo(self['start'])
|
||||
if self['end'] is not None:
|
||||
self.path.lineTo(self['end'])
|
||||
else:
|
||||
self.path.lineTo(self['start']+500*self['dir'])
|
||||
|
||||
|
||||
def trace(rays, optics):
|
||||
if len(optics) < 1 or len(rays) < 1:
|
||||
return
|
||||
for r in rays:
|
||||
r.clearChildren()
|
||||
o = optics[0]
|
||||
r2 = o.propagateRay(r)
|
||||
trace(r2, optics[1:])
|
||||
|
||||
|
||||
class Tracer(QtCore.QObject):
|
||||
"""
|
||||
Simple ray tracer.
|
||||
|
||||
Initialize with a list of rays and optics;
|
||||
calling trace() will cause rays to be extended by propagating them through
|
||||
each optic in sequence.
|
||||
"""
|
||||
def __init__(self, rays, optics):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.optics = optics
|
||||
self.rays = rays
|
||||
for o in self.optics:
|
||||
o.sigStateChanged.connect(self.trace)
|
||||
self.trace()
|
||||
|
||||
def trace(self):
|
||||
trace(self.rays, self.optics)
|
||||
|
||||
Reference in New Issue
Block a user