This commit is contained in:
2022-11-03 09:14:49 +01:00
parent 6eff498fba
commit b84316abd6
8 changed files with 2165 additions and 115 deletions

405
epics_widgets/MotorTweak.py Normal file
View 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
View 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>

View 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)

View 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)

View File

View File

@@ -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:

View File

@@ -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
View 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)