#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2018 Michael J. Hayford
""" Interactive 2D lens picture
The Lens Layout capability provides a 2D display of the optical model
represented by **shapes**. Shapes contain `dict` attributes to manage the
graphical rendering and editing actions associated with their parent
objects. These attributes must include:
**handles**: named graphic handles for different aspects of the parent object
**actions**: functions for **press**, **drag**, and **release** actions
.. Created on Tue Sep 18 14:23:28 2018
.. codeauthor: Michael J. Hayford
"""
import numpy as np
from rayoptics.gui.util import (GUIHandle, transform_ray_seg, bbox_from_poly,
transform_poly, inv_transform_poly)
from rayoptics.raytr.analyses import RayFan
from rayoptics.raytr.trace import (trace_boundary_rays_at_field,
boundary_ray_dict, retrieve_ray)
from rayoptics.elem import elements as ele
import rayoptics.optical.model_constants as mc
from rayoptics.util.rgb2mpl import rgb2mpl
import rayoptics.gui.appcmds as cmds
from rayoptics.util import colors
[docs]def light_or_dark(is_dark=True):
accent = colors.accent_colors(is_dark)
fb = colors.foreground_background(is_dark)
rgb = {
'profile': fb['foreground'],
'edge': fb['foreground'],
'ct': accent['blue'],
'ray': fb['foreground'],
'rayfan_fill': [254, 197, 254, 64], # magenta, 25%
'ax_ray': rgb2mpl([138, 43, 226]), # blueviolet
'pr_ray': rgb2mpl([198, 113, 113]), # sgi salmon
}
return {**rgb, **accent, **fb}
lo_rgb = {}
lo_lw = {
'line': 1,
'hilite': 2,
'guide': 1.0,
'edge': 0.5,
}
[docs]def create_optical_element(opt_model, e):
# if isinstance(e, ele.CementedElement):
# e_list = []
# for elemnt in e.ele_list:
# e_list.append(OpticalElement(opt_model, elemnt))
# else:
# e_list = (OpticalElement(opt_model, e))
# return e_list
return OpticalElement(opt_model, e)
# --- Graphics elements
[docs]class OpticalElement():
""" mediator class for rendering and editing optical elements """
def __init__(self, opt_model, e):
self.opt_model = opt_model
self.e = e
self.select_pt = None
self.move_direction = None
self.handles = {}
self.actions = self.edit_shape_actions()
self.handle_actions = self.e.handle_actions()
[docs] def update_shape(self, view):
self.e.render_handles(self.opt_model)
for key, graphics_handle in self.e.handles.items():
if isinstance(graphics_handle.polydata, tuple):
polys = []
for poly_seg in graphics_handle.polydata:
poly = np.array(poly_seg)
poly_gbl, bbox = transform_poly(graphics_handle.tfrm, poly)
polys.append(np.array(poly_gbl))
poly_gbl = tuple(polys)
else:
poly = np.array(graphics_handle.polydata)
poly_gbl, bbox = transform_poly(graphics_handle.tfrm, poly)
if graphics_handle.polytype == 'polygon':
p = view.create_polygon(poly_gbl,
fill_color=graphics_handle.color,
zorder=2.5)
elif graphics_handle.polytype == 'polyline':
hilite_kwargs = {
'color': lo_rgb['profile'],
'linewidth': lo_lw['hilite'],
'linestyle': '-'
}
priority = 2.
if key == 'ct':
priority = 3.
hilite_kwargs['color'] = lo_rgb['ct']
p = view.create_polyline(poly_gbl,
hilite=hilite_kwargs,
zorder=priority)
else:
break
self.handles[key] = GUIHandle(p, bbox)
return self.handles
[docs] def render_color(self):
return self.e.render_color
[docs] def get_label(self):
if not hasattr(self.e, 'label'):
self.e.label = 'element'
return self.e.label
[docs] def edit_shape_actions(self):
def add_event_data(self, event, handle):
gbl_pt = np.array([event.xdata, event.ydata])
lcl_pt = inv_transform_poly(self.e.handles[handle].tfrm, gbl_pt)
event.lcl_pt = lcl_pt
if self.select_pt is not None:
xdata, ydata = self.select_pt[1]
else:
xdata, ydata = 0., 0.
dxdy = event.xdata - xdata, event.ydata - ydata
event.dxdy = dxdy
def on_select_shape(fig, handle, event, info):
handle_actions = self.handle_actions[handle]
add_event_data(self, event, handle)
for key, action_obj in handle_actions.items():
action_obj.actions['press'](fig, event)
self.select_pt = ((event.x, event.y), (event.xdata, event.ydata))
# print('select pt:', self.select_pt)
# print('select pt:', event.x, event.y)
def on_edit_shape(fig, handle, event, info):
handle_actions = self.handle_actions[handle]
x, y = self.select_pt[0]
xdata, ydata = self.select_pt[1]
delta_x, delta_y = abs(x - event.x), abs(y - event.y)
delta_xdata, delta_ydata = (abs(xdata - event.xdata),
abs(ydata - event.ydata))
if self.move_direction is None:
if delta_x > delta_y:
self.move_direction = 'x'
# print('move horizontal: delta x, y:', delta_x, delta_y,
# delta_xdata, delta_ydata)
elif delta_x < delta_y:
self.move_direction = 'y'
# print('move vertical: delta x, y:', delta_x, delta_y,
# delta_xdata, delta_ydata)
else:
self.move_direction = None
# print('move same: delta x, y:', delta_x, delta_y)
add_event_data(self, event, handle)
# print('move pt:', event.xdata, event.ydata, event.lcl_pt)
if 'pt' in handle_actions:
if 'drag' in handle_actions['pt'].actions:
handle_actions['pt'].actions['drag'](fig, event,
event.lcl_pt)
elif self.move_direction in handle_actions:
if 'drag' in handle_actions[self.move_direction].actions:
if self.move_direction == 'x':
handle_actions['x'].actions['drag'](fig, event,
event.dxdy[0])
elif self.move_direction == 'y':
handle_actions['y'].actions['drag'](fig, event,
event.dxdy[1])
fig.refresh_gui(build='update')
def on_release_shape(fig, handle, event, info):
# print('release pt:', event.x, event.y)
handle_actions = self.handle_actions[handle]
add_event_data(self, event, handle)
for key, action_obj in handle_actions.items():
action_obj.actions['release'](fig, event)
self.select_pt = None
self.move_direction = None
fig.refresh_gui(build='rebuild')
actions = {}
actions['press'] = on_select_shape
actions['drag'] = on_edit_shape
actions['release'] = on_release_shape
return actions
[docs]class RayBundle():
""" class for ray bundle from a single field point """
def __init__(self, opt_model, fld, fld_label, wvl, start_offset,
ray_table_callback=None):
self.opt_model = opt_model
self.fld = fld
self.fld_label = fld_label
self.wvl = wvl
self.start_offset = start_offset
self.handles = {}
self.actions = self.edit_ray_bundle_actions()
self.handle_actions = {}
self.ray_table_callback = ray_table_callback
[docs] def get_label(self):
return self.fld_label
[docs] def render_ray(self, ray, tfrms):
poly = []
for i, r in enumerate(ray):
transform_ray_seg(poly, r, tfrms[i])
return np.array(poly)
[docs] def render_shape(self, rayset, tfrms):
poly1 = []
for i, r in enumerate(rayset['+Y'].ray):
transform_ray_seg(poly1, r, tfrms[i])
poly2 = []
for i, r in enumerate(rayset['-Y'].ray):
transform_ray_seg(poly2, r, tfrms[i])
poly2.reverse()
poly1.extend(poly2)
bbox = bbox_from_poly(poly1)
return poly1, bbox
[docs] def update_shape(self, view):
wvl = self.opt_model['optical_spec']['wvls'].central_wvl
self.wvl = wvl
rayset = trace_boundary_rays_at_field(self.opt_model,
self.fld, wvl,
use_named_tuples=True)
self.rayset = boundary_ray_dict(self.opt_model, rayset)
seq_model = self.opt_model.seq_model
tfrms = seq_model.gbl_tfrms # shift_start_of_ray_bundle(start_bundle, ray_list, rot, t)
if view.do_draw_beams:
poly, bbox = self.render_shape(self.rayset, tfrms)
p = view.create_polygon(poly, fill_color=lo_rgb['rayfan_fill'])
self.handles['shape'] = GUIHandle(p, bbox)
if view.do_draw_edge_rays:
cr = self.render_ray(self.rayset['00'].ray, tfrms)
upr = self.render_ray(self.rayset['+Y'].ray, tfrms)
lwr = self.render_ray(self.rayset['-Y'].ray, tfrms)
kwargs = {
'linewidth': lo_lw['line'],
'color': lo_rgb['ray'],
'hilite_linewidth': lo_lw['hilite'],
'hilite': lo_rgb['ray'],
}
cr_poly = view.create_polyline(cr, **kwargs)
self.handles['00'] = GUIHandle(cr_poly, bbox_from_poly(cr))
upr_poly = view.create_polyline(upr, **kwargs)
self.handles['+Y'] = GUIHandle(upr_poly, bbox_from_poly(upr))
lwr_poly = view.create_polyline(lwr, **kwargs)
self.handles['-Y'] = GUIHandle(lwr_poly, bbox_from_poly(lwr))
return self.handles
[docs] def edit_ray_bundle_actions(self):
def on_select_ray(fig, handle, event, info):
if handle != 'shape':
ray_table = self.ray_table_callback()
ray_table.root = self.rayset[handle].ray
fig.refresh_gui(build='update')
actions = {}
actions['press'] = on_select_ray
return actions
[docs]class RayFanBundle():
""" class for a RayFan from a single field point """
def __init__(self, opt_model, ray_fan, start_offset, label='ray fan'):
self.opt_model = opt_model
rayerr_filter = ray_fan.rayerr_filter
ray_fan.rayerr_filter = ('full' if rayerr_filter is None
else rayerr_filter)
self.ray_fan = ray_fan
self.start_offset = start_offset
self.label = label
self.handles = {}
self.actions = {}
self.handle_actions = {}
[docs] def get_label(self):
return self.label
[docs] def render_ray(self, ray_pkg, tfrms):
poly = []
ray, op_delta, wvl = ray_pkg
for i, r in enumerate(ray):
transform_ray_seg(poly, r, tfrms[i])
return np.array(poly)
[docs] def update_shape(self, view):
ray_fan = self.ray_fan
ray_fan.update_data()
fan = ray_fan.fan_pkg[0]
seq_model = self.opt_model.seq_model
tfrms = seq_model.gbl_tfrms
ray_list = []
for ray_item in fan:
ray_pkg = retrieve_ray(ray_item)
ray_list.append(ray_pkg)
ray_color = lo_rgb['ray'] if ray_fan.color is None else ray_fan.color
kwargs = {
'linewidth': lo_lw['line'],
'color': ray_color,
'hilite_linewidth': lo_lw['hilite'],
'hilite': lo_rgb['ray'],
}
for i, ray_pkg in enumerate(ray_list):
global_ray = self.render_ray(ray_pkg, tfrms)
ray_poly = view.create_polyline(global_ray, **kwargs)
self.handles[i] = GUIHandle(ray_poly, bbox_from_poly(global_ray))
return self.handles
[docs]class SingleRay():
""" class for a Ray from a single field point """
def __init__(self, opt_model, ray, start_offset, label='single ray'):
self.opt_model = opt_model
rayerr_filter = ray.rayerr_filter
ray.rayerr_filter = ('full' if rayerr_filter is None
else rayerr_filter)
self.ray = ray
self.start_offset = start_offset
self.label = label
self.handles = {}
self.actions = {}
self.handle_actions = {}
[docs] def get_label(self):
return self.label
[docs] def render_ray(self, ray_pkg, tfrms):
poly = []
ray, op_delta, wvl = ray_pkg
for i, r in enumerate(ray):
transform_ray_seg(poly, r, tfrms[i])
return np.array(poly)
[docs] def update_shape(self, view):
ray = self.ray
ray.update_data()
seq_model = self.opt_model['seq_model']
tfrms = seq_model.gbl_tfrms
global_ray = self.render_ray(ray.ray_pkg, tfrms)
ray_color = lo_rgb['ray'] if ray.color is None else ray.color
kwargs = {
'linewidth': lo_lw['line'],
'color': ray_color,
'hilite_linewidth': lo_lw['hilite'],
'hilite': lo_rgb['ray'],
}
ray_poly = view.create_polyline(global_ray, **kwargs)
self.handles['shape'] = GUIHandle(ray_poly, bbox_from_poly(global_ray))
return self.handles
[docs]class ParaxialRay():
""" class for paraxial ray rendering/editing """
def __init__(self, opt_model, ray, color, seq_start=1, label='paraxial'):
self.label = label
self.opt_model = opt_model
self.ray = ray
self.seq_start = seq_start
self.color = color
self.handles = {}
self.actions = self.edit_paraxial_layout_actions()
self.handle_actions = {}
self.vertex = None
[docs] def get_label(self):
return self.label
[docs] def render_ray(self, ray, tfrms):
poly = []
for i, r in enumerate(ray[self.seq_start:], self.seq_start):
rot, trns = tfrms[i]
ps = np.array([0., r[mc.ht], 0.])
p = rot.dot(ps) + trns
poly.append([p[2], p[1]])
return np.array(poly)
[docs] def update_shape(self, view):
seq_model = self.opt_model.seq_model
tfrms = seq_model.gbl_tfrms
ray_poly = self.render_ray(self.ray, tfrms)
hilite_kwargs = {
'color': self.color,
'linewidth': lo_lw['hilite'],
'linestyle': '-'
}
priority = 3
rp = view.create_polyline(ray_poly,
color=self.color,
linewidth=lo_lw['line'],
hilite=hilite_kwargs,
zorder=priority)
self.handles['shape'] = GUIHandle(rp, bbox_from_poly(ray_poly))
return self.handles
[docs] def apply_data(self, vertex, lcl_pt):
ray = self.ray
p = ray[vertex-1]
c = ray[vertex]
c_slp_new = (lcl_pt[1] - c[mc.ht])/lcl_pt[0]
pwr = (p[mc.slp] - c_slp_new)/c[mc.ht]
self.opt_model.seq_model.ifcs[vertex].optical_power = pwr
[docs] def edit_paraxial_layout_actions(self):
def add_event_data(shape, event, handle, info):
gbl_pt = np.array([event.xdata, event.ydata])
lcl_pt = inv_transform_poly(shape.tfrm, gbl_pt)
event.lcl_pt = lcl_pt
def on_select_point(fig, handle, event, info):
self.vertex = self.seq_start
if 'ind' in info:
self.vertex += info['ind'][0]
seq_model = self.opt_model.seq_model
self.tfrm = seq_model.gbl_tfrms[self.vertex]
def on_edit_point(fig, handle, event, info):
add_event_data(self, event, handle, info)
self.apply_data(self.vertex, event.lcl_pt)
fig.refresh_gui(build='update')
def on_release_point(fig, handle, event, info):
add_event_data(self, event, handle, info)
self.apply_data(self.vertex, event.lcl_pt)
fig.refresh_gui(build='rebuild')
self.vertex = None
self.tfrm = None
actions = {}
actions['press'] = on_select_point
actions['drag'] = on_edit_point
actions['release'] = on_release_point
return actions
# --- Layout manager
[docs]class LensLayout():
""" manager for live layout graphics entities """
def __init__(self, opt_model, is_dark=True, **kwargs):
from rayoptics.elem import parttree
self.opt_model = opt_model
self.ray_table = None
light_or_dark(is_dark=is_dark)
seq_model = self.opt_model['seq_model']
ele_model = self.opt_model['ele_model']
part_tree = self.opt_model['part_tree']
if len(part_tree.nodes_with_tag(tag='#element')) == 0:
parttree.elements_from_sequence(ele_model, seq_model, part_tree)
[docs] def sync_light_or_dark(self, is_dark):
global lo_rgb
lo_rgb = light_or_dark(is_dark)
[docs] def system_length(self, ele_bbox, offset_factor=0.05):
""" returns system length and ray start offset """
specsheet = self.opt_model['specsheet']
fod = self.opt_model['ar']['parax_data'].fod
ele_length = ele_bbox[1][0] - ele_bbox[0][0]
image_thi = abs(self.opt_model['seq_model'].gaps[-1].thi)
if specsheet.imager_defined():
if specsheet.conjugate_type == 'finite':
return specsheet.imager.tt, (2/3)*specsheet.imager.sp
elif specsheet.conjugate_type == 'infinite':
if fod.efl == 0:
estimated_length = ele_length
else:
# img_dist = abs(fod.img_dist)
# estimated_length = ele_length + img_dist
estimated_length = ele_length + image_thi
return estimated_length, offset_factor*estimated_length
# return specsheet.imager.sp, offset_factor*specsheet.imager.sp
elif specsheet.conjugate_type == 'afocal':
return ele_length, offset_factor*ele_length
else:
if fod.efl == 0:
estimated_length = ele_length
else:
estimated_length = ele_length + image_thi
return estimated_length, offset_factor*estimated_length
[docs] def create_element_entities(self, view):
opm = self.opt_model
pt = opm.part_tree
if opm['ss'].conjugate_type == 'infinite':
e_nodes = pt.nodes_with_tag(tag='#element#dummyifc#airgap',
not_tag='#object')
else:
e_nodes = pt.nodes_with_tag(tag='#element#dummyifc#airgap')
elements = [create_optical_element(opm, e_node.id)
for e_node in e_nodes]
return elements
[docs] def create_ray_entities(self, view, start_offset):
ray_bundles = []
fov = self.opt_model['optical_spec']['fov']
wvl = self.opt_model['seq_model'].central_wavelength()
for i, fld in enumerate(fov.fields):
fld_label = fov.index_labels[i]
rb = RayBundle(self.opt_model, fld, fld_label, wvl, start_offset,
ray_table_callback=self.get_ray_table)
ray_bundles.append(rb)
return ray_bundles
[docs] def create_ray_fan_entities(self, view, start_offset, num_rays=21):
ray_fan_bundles = []
opt_model = self.opt_model
fov = opt_model['optical_spec']['fov']
for i, fld in enumerate(fov.fields):
fld_label = fov.index_labels[i]
rayfan = RayFan(opt_model, f=fld, xyfan='y', num_rays=num_rays,
label=fld_label)
rb = RayFanBundle(opt_model, rayfan, start_offset)
ray_fan_bundles.append(rb)
return ray_fan_bundles
[docs] def create_paraxial_ray_entities(self, view):
parax_model = self.opt_model['parax_model']
ax_poly = ParaxialRay(self.opt_model, parax_model.ax,
color=lo_rgb['ax_ray'],
label='axial ray')
pr_poly = ParaxialRay(self.opt_model, parax_model.pr,
color=lo_rgb['pr_ray'],
label='chief ray')
return [ax_poly, pr_poly]
[docs] def get_ray_table(self):
if self.ray_table is None:
self.ray_table = cmds.create_ray_table_model(self.opt_model, None)
gui_parent = self.opt_model.app_manager.gui_parent
gui_parent.create_table_view(self.ray_table, "Ray Table")
return self.ray_table
[docs] def register_commands(self, *args, **kwargs):
self.apply_fct = kwargs.pop('apply_fct')
fig = kwargs.pop('figure')
actions = self.add_element_cmd_actions(**kwargs)
def do_command_action(event, target, event_key):
nonlocal fig, actions
if target is not None:
shape, handle = target.artist.shape
if handle == 'ct' and isinstance(shape.e, ele.AirGap):
try:
action = actions[event_key]
action(fig, shape, event, target.info)
except KeyError:
pass
fig.do_action = do_command_action
[docs] def add_element_cmd_actions(self, **kwargs):
idx = None
def add_event_data(tfrm, event):
gbl_pt = np.array([event.xdata, event.ydata])
lcl_pt = inv_transform_poly(tfrm, gbl_pt)
event.lcl_pt = lcl_pt
def on_select_cmd(fig, shape, event, info):
nonlocal idx
idx = shape.e.idx
tfrm = self.opt_model.seq_model.gbl_tfrms[idx]
add_event_data(tfrm, event)
self.apply_fct(self.opt_model, idx, event.lcl_pt, **kwargs)
fig.refresh_gui(build='update')
# def on_edit_cmd(fig, shape, event, info):
# add_event_data(self, event)
# self.apply_fct(idx, event.lcl_pt)
# fig.refresh_gui()
# actions['drag'] = on_edit_cmd
def on_release_cmd(fig, shape, event, info):
nonlocal idx
# add_event_data(self, event)
# self.apply_fct(idx, event.lcl_pt)
# fig.refresh_gui(build='rebuild')
idx = None
actions = {}
actions['press'] = on_select_cmd
actions['release'] = on_release_cmd
return actions
# --- Command functions
[docs]def split_gap(opt_model, idx, lcl_pt):
""" split g=gap[idx] into t_old = t_0 + t_k using t_0 = lcl_pt.x """
g = opt_model.seq_model.gaps[idx]
x, y = lcl_pt
t_k = g.thi - x
t_0 = x
return g, t_0, t_k
[docs]def add_elements(opt_model, idx, lcl_pt, create, **kwargs):
g, t_0, t_k = split_gap(opt_model, idx, lcl_pt)
g.thi = t_0
opt_model.insert_ifc_gp_ele(*create(**kwargs), idx=idx, t=t_k,
insert=True)
[docs]def add_reflector(opt_model, idx, lcl_pt, create, **kwargs):
g, t_0, t_k = split_gap(opt_model, idx, lcl_pt)
g.thi = t_0
opt_model.insert_ifc_gp_ele(*create(**kwargs), idx=idx, t=-t_k,
insert=True)
[docs]def add_thinlens(opt_model, idx, lcl_pt, **kwargs):
add_elements(opt_model, idx, lcl_pt, ele.create_thinlens, **kwargs)
[docs]def add_lens(opt_model, idx, lcl_pt, **kwargs):
g, t_0, t_k = split_gap(opt_model, idx, lcl_pt)
t_old = g.thi
if 'th' in kwargs:
t_ct = kwargs['th']
else:
t_ct = 0.05*t_old
t0_new = t_0 - t_ct/2
tk_new = t_k - t_ct/2
g.thi = t0_new
descriptor = ele.create_lens(th=t_ct)
opt_model.insert_ifc_gp_ele(*descriptor, idx=idx, t=tk_new, insert=True)
[docs]def add_mirror(opt_model, idx, lcl_pt, **kwargs):
add_reflector(opt_model, idx, lcl_pt, ele.create_mirror, **kwargs)
[docs]def add_conic(opt_model, idx, lcl_pt, **kwargs):
add_reflector(opt_model, idx, lcl_pt, ele.create_mirror, **kwargs)
[docs]def add_doublet(opt_model, idx, lcl_pt, **kwargs):
add_elements(opt_model, idx, lcl_pt, ele.create_cemented_doublet, **kwargs)
[docs]class GlassDropAction():
[docs] def dragEnterEvent(self, view, event):
def glass_target_filter(artist):
shape, handle = artist.shape
if handle == 'shape' and 'shape' in shape.handle_actions:
if 'glass' in shape.handle_actions['shape']:
return False
return True
view.figure.artist_filter = glass_target_filter
[docs] def dragMoveEvent(self, view, event):
x, y = view.mouseEventCoords(event.pos())
view.motion_notify_event(x, y, guiEvent=event)
[docs] def dragLeaveEvent(self, view, event):
view.figure.artist_filter = None
[docs] def dropEvent(self, view, event):
dropped_it = False
fig = view.figure
if fig.hilited is not None:
target = fig.hilited
shape, handle = target.artist.shape
if 'glass' in shape.handle_actions['shape']:
action = shape.handle_actions['shape']['glass']
action(fig, event)
dropped_it = True
view.figure.artist_filter = None
return dropped_it