Source code for rayoptics.elem.elements

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2018 Michael J. Hayford
""" Module for element modeling

.. Created on Sun Jan 28 16:27:01 2018

.. codeauthor: Michael J. Hayford
"""

from collections import namedtuple
import itertools
from packaging import version

from abc import abstractmethod
from typing import Protocol, ClassVar, List, Dict, Any, runtime_checkable


from math import sqrt
import numpy as np

from anytree import Node  # type: ignore

import rayoptics.util.rgbtable as rgbt
from rayoptics.oprops import thinlens
from rayoptics.elem import parttree
from rayoptics.elem.profiles import SurfaceProfile, Spherical, Conic
from rayoptics.elem.surface import Surface
from rayoptics.seq.gap import Gap
from rayoptics.seq.medium import decode_medium

from rayoptics.seq.sequential import SequentialModel
from rayoptics.seq.interface import Interface

import rayoptics.gui.appcmds as cmds
from rayoptics.gui.actions import (Action, AttrAction, SagAction, BendAction,
                                   ReplaceGlassAction)
from rayoptics.gui.util import calc_render_color_for_material

import opticalglass.glassfactory as gfact  # type: ignore
from opticalglass.modelglass import ModelGlass  # type: ignore

GraphicsHandle = namedtuple('GraphicsHandle', ['polydata', 'tfrm', 'polytype',
                                               'color'], defaults=(None,))
GraphicsHandle.polydata.__doc__ = "poly data in local coordinates"
GraphicsHandle.tfrm.__doc__ = "global transformation for polydata"
GraphicsHandle.polytype.__doc__ = "'polygon' (for filled) or 'polyline'"
GraphicsHandle.color.__doc__ = "RGBA for the polydata or None for default"


""" tuple grouping together graphics rendering data

    Attributes:
        polydata: poly data in local coordinates
        tfrm: global transformation for polydata
        polytype: 'polygon' (for filled) or 'polyline'
"""

rot_around_x = np.array([[1, 0, 0], [0, -1, 0], [0, 0, -1]])
rot_around_y = np.array([[-1, 0, 0], [0, 1, 0], [0, 0, -1]])


# --- Factory functions
[docs]def create_thinlens(power=0., indx=1.5, sd=None, **kwargs): tl = thinlens.ThinLens(power=power, ref_index=indx, max_ap=sd, **kwargs) tle = ThinElement(tl) tree = tle.tree() return [[tl, None, None, 1, +1]], [tle], tree
[docs]def create_mirror(c=0.0, r=None, cc=0.0, ec=None, power=None, profile=None, sd=None, **kwargs): '''Create a sequence and element for a mirror. Args: c: vertex curvature r: vertex radius of curvature cc: conic constant ec: = 1 + cc power: optical power of the mirror sd: semi-diameter profile: Spherical or Conic type, or a profile instance ''' delta_n = kwargs['delta_n'] if 'delta_n' in kwargs else -2 if power: cv = power/delta_n elif r: cv = 1.0/r else: cv = c if ec: k = ec - 1.0 else: k = cc if profile is Spherical: prf = Spherical(c=cv) elif profile is Conic: prf = Conic(c=cv, cc=k) elif profile is not None: prf = profile else: if k == 0.0: prf = Spherical(c=cv) else: prf = Conic(c=cv, cc=k) sd = sd if sd is not None else 1 m = Surface(profile=prf, interact_mode='reflect', max_ap=sd, delta_n=delta_n, **kwargs) ele_kwargs = {'label': kwargs['label']} if 'label' in kwargs else {} me = Mirror(m, sd=sd, **ele_kwargs) tree = me.tree() return [[m, None, None, 1, -1]], [me], tree
[docs]def lens_from_power(power=0., bending=0., th=None, sd=1., med=None, nom_wvl='d'): if med is None: med = ModelGlass(1.517, 64.2, '517642') else: med = decode_medium(med) rndx = med.rindex(nom_wvl) if th is None: th = sd/5 if power == 0: cv1 = cv2 = 0 else: abs_bending = abs(bending) if abs_bending == 1: cv1 = power/(rndx - 1) cv2 = 0 else: B = (abs_bending - 1)/(abs_bending + 1) a = (rndx - 1)*(th/rndx)*B b = 1 - B c = -power/(rndx - 1) cv1 = (-b + np.sqrt(b**2 - 4*a*c))/(2*a) cv2 = cv1*B if bending < 0: cv1, cv2 = -cv2, -cv1 return cv1, cv2, th, rndx, sd
[docs]def create_lens(power=0., bending=0., th=None, sd=1., med=None, lens=None, **kwargs): """ Create a lens element chunk of sm, em, and pt tree Args: kwargs: keyword arguments including: - idx: insertion point in the sequential model - t: the thickness following a chunk when inserting - lens: tuple of `cv1, cv2, th, glass_name_catalog, sd` where: - cv1: front curvature - cv2: rear curvature - th: lens thickness - glass_input: a str, e.g. 'N-BK7, Schott' or index (+V-number) - sd: lens semi-diameter """ if med is None: mat = ModelGlass(1.517, 64.2, '517642') else: mat = decode_medium(med) if lens is None: lens = lens_from_power(power=power, bending=bending, th=th, sd=sd, med=mat) cv1, cv2, th, rndx, sd = lens else: cv1, cv2, th, glass, sd = lens mat = decode_medium(glass) rndx = mat.rindex('d') lens = cv1, cv2, th, rndx, sd s1 = Surface(profile=Spherical(c=cv1), max_ap=sd, delta_n=(rndx - 1)) s2 = Surface(profile=Spherical(c=cv2), max_ap=sd, delta_n=(1 - rndx)) g = Gap(t=th, med=mat) le = Element(s1, s2, g, sd=sd) tree = le.tree() return [[s1, g, None, rndx, 1], [s2, None, None, 1, 1]], [le], tree
[docs]def achromat(power, Va, Vb): """Compute lens powers for a thin doublet achromat, given their V-numbers.""" power_a = (Va/(Va - Vb))*power power_b = (Vb/(Vb - Va))*power return power_a, power_b
[docs]def create_cemented_doublet(power=0., bending=0., th=None, sd=1., glasses=('N-BK7,Schott', 'N-F2,Schott'), **kwargs): from opticalglass.spectral_lines import get_wavelength # type: ignore from opticalglass import util wvls = np.array([get_wavelength(w) for w in ['d', 'F', 'C']]) gla_a = gfact.create_glass(glasses[0]) rndx_a = gla_a.calc_rindex(wvls) Va, PcDa = util.calc_glass_constants(*rndx_a) gla_b = gfact.create_glass(glasses[1]) rndx_b = gla_b.calc_rindex(wvls) Vb, PcDb = util.calc_glass_constants(*rndx_b) power_a, power_b = achromat(power, Va, Vb) if th is None: th = sd/4 t1 = 3*th/4 t2 = th/4 if power_a < 0: t1, t2 = t2, t1 lens_a = lens_from_power(power=power_a, bending=bending, th=t1, sd=sd, med=gla_a) cv1, cv2, t1, indx_a, sd = lens_a # cv1 = power_a/(rndx_a[0] - 1) # delta_cv = -cv1/2 # cv1 += delta_cv # cv2 = delta_cv # cv3 = power_b/(1 - rndx_b[0]) + delta_cv indx_b = rndx_b[0] cv3 = (power_b/(indx_b-1) - cv2)/((t2*cv2*(indx_b-1)/indx_b) - 1) s1 = Surface(profile=Spherical(c=cv1), max_ap=sd, delta_n=(rndx_a[0] - 1)) s2 = Surface(profile=Spherical(c=cv2), max_ap=sd, delta_n=(rndx_b[0] - rndx_a[0])) s3 = Surface(profile=Spherical(c=cv3), max_ap=sd, delta_n=(1 - rndx_b[0])) g1 = Gap(t=t1, med=gla_a) g2 = Gap(t=t2, med=gla_b) g_tfrm = np.identity(3), np.array([0., 0., 0.]) ifc_list = [] ifc_list.append([0, s1, g1, 1, g_tfrm]) ifc_list.append([1, s2, g2, 1, g_tfrm]) ifc_list.append([2, s3, None, 1, g_tfrm]) ce = CementedElement(ifc_list) tree = ce.tree() return [[s1, g1, None, rndx_a, 1], [s2, g2, None, rndx_b, 1], [s3, None, None, 1, 1]], [ce], tree
[docs]def create_dummy_plane(sd=1., **kwargs): s = Surface(**kwargs) se = DummyInterface(s, sd=sd) tree = se.tree() return [[s, None, None, 1, +1]], [se], tree
[docs]def create_air_gap(t=0., **kwargs): g = Gap(t=t) ag = AirGap(g, **kwargs) kwargs.pop('label', None) tree = ag.tree(**kwargs) return g, ag, tree
[docs]def create_from_file(filename, **kwargs): opm = cmds.open_model(filename, post_process_imports=False) sm = opm['seq_model'] osp = opm['optical_spec'] em = opm['ele_model'] pt = opm['part_tree'] ar = opm['analysis_results'] if len(pt.nodes_with_tag(tag='#element')) == 0: parttree.elements_from_sequence(em, sm, pt) if 'power' in kwargs: desired_power = kwargs['power'] cur_power = ar['parax_data'].fod.power # scale_factor is linear, power is 1/linear # so use reciprocal of power to compute scale_factor scale_factor = cur_power/desired_power sm.apply_scale_factor(scale_factor) # extract the system definition, minus object and image seq = [list(node) for node in sm.path(start=1, stop=-1)] seq[-1][1] = None # get the top level nodes of the input system, minus object and image part_nodes = pt.nodes_with_tag(tag='#element#airgap#assembly', not_tag='#object#image', node_list=pt.root_node.children) parts = [part_node.id for part_node in part_nodes] if (len(part_nodes) == 1 and '#assembly' in part_nodes[0].tag): asm_node = part_nodes[0] print("found root assembly node") else: # create an Assembly from the top level part list label = kwargs.get('label', None) tfrm = kwargs.get('tfrm', opm['seq_model'].gbl_tfrms[1]) asm = Assembly(parts, idx=1, label=label, tfrm=tfrm) asm_node = asm.tree(part_tree=opm['part_tree'], tag='#file') asm_node.parent = None return seq, parts, part_nodes
[docs]def create_assembly_from_seq(opt_model, idx1, idx2, **kwargs): part_list, node_list = parttree.part_list_from_seq(opt_model, idx1, idx2) label = kwargs.get('label', None) tfrm = kwargs.get('tfrm', opt_model['seq_model'].gbl_tfrms[idx1]) asm = Assembly(part_list, idx=idx1, label=label, tfrm=tfrm) asm_node = asm.tree(part_tree=opt_model['part_tree']) return asm, asm_node
[docs]def full_profile(profile, is_flipped, edge_extent, flat_id=None, hole_id=None, dir=1, steps=6): """Produce a 2d segmented approximation to the *profile*. Args: profile: optical profile to be sampled is_flipped: the flipped state of the profile edge_extent: tuple with symmetric or asymetric bounds flat_id: if not None, inside diameter of flat zone hole_id: if not None, inside diameter of centered surface hole dir: sampling direction, +1 for up, -1 for down steps: number of profile curve samples """ from rayoptics.raytr.traceerror import TraceError def flip_profile(prf): return [[-pt[0], pt[1]] for pt in prf] if len(edge_extent) == 1: sd_upr = edge_extent[0] sd_lwr = -edge_extent[0] else: sd_upr = edge_extent[1] sd_lwr = edge_extent[0] if flat_id is None: if hole_id is None: prf = profile.profile((sd_lwr, sd_upr), dir, steps) else: prf_lwr = profile.profile((sd_lwr, -hole_id), dir, steps) prf_upr = profile.profile((hole_id, sd_upr), dir, steps) prf = prf_lwr, prf_upr else: prf = [] # compute top part of flat try: sag = profile.sag(0, flat_id) except TraceError: sag = None else: if dir > 0: sd_lwr_pt = [sag, sd_lwr] sd_upr_pt = [sag, sd_upr] else: sd_lwr_pt = [sag, sd_upr] sd_upr_pt = [sag, sd_lwr] if hole_id is None: prf = [sd_lwr_pt] prf += profile.profile((flat_id,), dir, steps) if sag is not None: prf.append(sd_upr_pt) else: prf_lwr = [sd_lwr_pt] prf_lwr += profile.profile((-flat_id, -hole_id), dir, steps) prf_upr = profile.profile((hole_id, flat_id), dir, steps) if sag is not None: prf_upr.append(sd_upr_pt) prf = prf_lwr, prf_upr if is_flipped: if hole_id is None: prf = flip_profile(prf) else: prf = flip_profile(prf_lwr), flip_profile(prf_upr) return prf
[docs]def use_flat(do_flat, is_concave): if do_flat == 'always': return True elif do_flat == 'if concave' and is_concave: return True elif do_flat == 'if convex' and not is_concave: return True return False
[docs]def compute_flat(ifc, sd, under_fract=0.05): ca = ifc.surface_od() if (1.0 - ca/sd) >= under_fract: flat = ca else: flat = None return flat
[docs]def encode_obj_reference(obj, obj_attr_str, attrs): attrs[obj_attr_str+'_id'] = str(id(getattr(obj, obj_attr_str))) del attrs[obj_attr_str]
[docs]def sync_obj_reference(obj, obj_attr_str, obj_dict, alt_attr_value): if hasattr(obj, obj_attr_str+'_id'): obj_id = getattr(obj, obj_attr_str+'_id') setattr(obj, obj_attr_str, obj_dict[obj_id]) delattr(obj, obj_attr_str+'_id') else: setattr(obj, obj_attr_str, alt_attr_value)
# --- Element definitions
[docs]@runtime_checkable class Part(Protocol): """Abstract base class for all types of elements. """ label_format: ClassVar[str] label: str parent: Any is_flipped: bool = False
[docs] def flip(self): """Called by opt_model.flip when a Part is flipped. """ self.do_flip() self.is_flipped = not self.is_flipped
[docs] @abstractmethod def do_flip(self): """Subclass action when it is flipped. """ raise NotImplementedError
[docs] @abstractmethod def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): raise NotImplementedError
[docs] @abstractmethod def sync_to_seq(self, seq_model: SequentialModel): raise NotImplementedError
[docs] @abstractmethod def tree(self, **kwargs) -> Node: raise NotImplementedError
[docs] @abstractmethod def idx_list(self) -> List[int]: raise NotImplementedError
[docs] @abstractmethod def reference_idx(self) -> int: raise NotImplementedError
[docs] @abstractmethod def reference_interface(self) -> Interface: raise NotImplementedError
[docs] @abstractmethod def profile_list(self) -> List[SurfaceProfile]: raise NotImplementedError
[docs] @abstractmethod def gap_list(self) -> List[Gap]: raise NotImplementedError
[docs] @abstractmethod def update_size(self) -> None: raise NotImplementedError
[docs] @abstractmethod def render_shape(self) -> List[GraphicsHandle]: '''return a polyline that is representative of the cemented element. ''' raise NotImplementedError
[docs] @abstractmethod def render_handles(self, opt_model) -> Dict[str, GraphicsHandle]: raise NotImplementedError
[docs] @abstractmethod def handle_actions(self) -> Dict[str, Any]: raise NotImplementedError
[docs]def do_flip_with_part_list(part_list: List[Part], flip_pt_tfrm) -> None: """Flip a list of parts around a flip_pt. """ r_asm, flip_pt = flip_pt_tfrm r_asm_new = np.matmul(r_asm, rot_around_y) for p in part_list: r, t = p.tfrm # get part into flip_pt_tfrm coordinate system r_part = np.matmul(r_asm.T, r) t_part = r_asm.T.dot(t - flip_pt) # apply the 180 degree rotation to the part r_part_new = np.matmul(r_asm_new, r_part) t_part_new = flip_pt + r_asm_new.dot(t_part) # set the new transform and flip the is_flipped bit p.tfrm = r_part_new, t_part_new p.is_flipped = not p.is_flipped
[docs]class Element(Part): """Lens element domain model. Manage rendering and selection/editing. An Element consists of 2 Surfaces, 1 Gap, and edge_extent information. Attributes: s1: first/origin :class:`~rayoptics.seq.interface.Interface` s2: second/last :class:`~rayoptics.seq.interface.Interface` gap: element thickness and material :class:`~rayoptics.seq.gap.Gap` tfrm: global transform to element origin, (Rot3, trans3) medium_name: the material filling the gap flat1, flat2: semi-diameter of flat or None. Setting to None will result in re-evaluation of flat ID do_flat1, do_flat2: 'if concave', 'always', 'never', 'if convex' handles: dict of graphical entities actions: dict of actions associated with the graphical handles """ clut = rgbt.RGBTable(filename='red_blue64.csv', data_range=[10.0, 100.]) label_format = 'E{}' serial_number = 0 def __init__(self, s1, s2, g, tfrm=None, idx=0, idx2=1, sd=1., label=None): if label is None: Element.serial_number += 1 self.label = Element.label_format.format(Element.serial_number) else: self.label = label if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.s1 = s1 self.profile1 = s1.profile self.s1_indx = idx self.s2 = s2 self.s2_indx = idx2 self.profile2 = s2.profile self.gap = g self.medium_name = self.gap.medium.name() self._sd = sd self.hole_sd = None self.flat1 = None self.flat2 = None self.do_flat1 = 'if concave' # alternatives are 'never', 'always', self.do_flat2 = 'if concave' # or 'if convex' self.handles = {} self.actions = {} @property def sd(self): """Semi-diameter """ return self._sd @sd.setter def sd(self, semidiam): self._sd = semidiam self.edge_extent = (-semidiam, semidiam) def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['s1'] del attrs['s2'] del attrs['gap'] del attrs['handles'] del attrs['actions'] encode_obj_reference(self, 'profile1', attrs) encode_obj_reference(self, 'profile2', attrs) if hasattr(self, 'profile_polys'): del attrs['profile_polys'] return attrs def __str__(self): fmt = 'Element: {!r}, {!r}, t={:.4f}, sd={:.4f}, glass: {}' return fmt.format(self.s1.profile, self.s2.profile, self.gap.thi, self.sd, self.gap.medium.name())
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): # when restoring, we want to use the stored indices to look up the # new object instances self.parent = ele_model if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.s1_indx] self.s1 = surfs[self.s1_indx] sync_obj_reference(self, 'profile1', profile_dict, self.s1.profile) if self.is_flipped: self.gap = gaps[self.s2_indx] else: self.gap = gaps[self.s1_indx] self.s2 = surfs[self.s2_indx] sync_obj_reference(self, 'profile2', profile_dict, self.s2.profile) if not hasattr(self, 'medium_name'): self.medium_name = self.gap.medium.name() if not hasattr(self, 'do_flat1'): self.do_flat1 = 'if concave' if not hasattr(self, 'do_flat2'): self.do_flat2 = 'if concave' if not hasattr(self, 'hole_sd'): self.hole_sd = None self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): # when updating, we want to use the stored object instances to get the # current indices into the interface list (e.g. to handle insertion and # deletion of interfaces) self.s1_indx = seq_model.ifcs.index(self.s1) self.s2_indx = seq_model.ifcs.index(self.s2) self.profile1 = self.s1.profile self.profile2 = self.s2.profile self.medium_name = self.gap.medium.name()
[docs] def tree(self, **kwargs): """Build tree linking sequence to element model. """ default_tag = '#element#lens' tag = default_tag + kwargs.get('tag', '') zdir = kwargs.get('z_dir', 1) # Interface branch 1 e = Node(self.label, id=self, tag=tag) p1 = Node('p1', id=self.profile1, tag='#profile', parent=e) Node(f'i{self.s1_indx}', id=self.s1, tag='#ifc', parent=p1) # Gap branch t = Node('t', id=self.gap, tag='#thic', parent=e) Node(f'g{self.s1_indx}', id=(self.gap, zdir), tag='#gap', parent=t) # Interface branch 2 p2 = Node('p2', id=self.profile2, tag='#profile', parent=e) Node(f'i{self.s2_indx}', id=self.s2, tag='#ifc', parent=p2) return e
[docs] def idx_list(self): seq_model = self.parent.opt_model['seq_model'] self.s1_indx = seq_model.ifcs.index(self.s1) self.s2_indx = seq_model.ifcs.index(self.s2) return [self.s1_indx, self.s2_indx]
[docs] def reference_idx(self): return self.s1_indx
[docs] def reference_interface(self): return self.s1
[docs] def profile_list(self): return [self.profile1, self.profile2]
[docs] def gap_list(self): return [self.gap]
[docs] def get_bending(self): cv1 = self.s1.profile_cv cv2 = self.s2.profile_cv delta_cv = cv1 - cv2 bending = 0. if delta_cv != 0.0: bending = (cv1 + cv2)/delta_cv return bending
[docs] def set_bending(self, bending): cv1 = self.s1.profile_cv cv2 = self.s2.profile_cv delta_cv = cv1 - cv2 cv2_new = 0.5*(bending - 1.)*delta_cv cv1_new = bending*delta_cv - cv2_new self.s1.profile_cv = cv1_new self.s2.profile_cv = cv2_new
[docs] def do_flip(self): r, t = self.tfrm thi = self.gap.thi if self.is_flipped: r_new = np.matmul(rot_around_y, r).T t_new = t - r_new.dot(np.array([0, 0, thi])) else: t_new = t + r.dot(np.array([0, 0, thi])) r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t_new
[docs] def update_size(self): extents = np.union1d(self.s1.get_y_aperture_extent(), self.s2.get_y_aperture_extent()) self.edge_extent = (extents[0], extents[-1]) self.sd = max(self.s1.surface_od(), self.s2.surface_od()) return self.sd
[docs] def extent(self): if hasattr(self, 'edge_extent'): return self.edge_extent else: return (-self.sd, self.sd)
[docs] def render_shape(self): is_concave_s1 = self.s1.profile_cv < 0.0 is_concave_s2 = self.s2.profile_cv > 0.0 self.profile_polys = [] if use_flat(self.do_flat1, is_concave_s1): if self.flat1 is None: flat1 = self.flat1 = compute_flat(self.s1, self.sd) else: flat1 = self.flat1 else: flat1 = None poly1 = full_profile(self.profile1, self.is_flipped, self.extent(), flat1, hole_id=self.hole_sd) self.profile_polys.append(poly1) if use_flat(self.do_flat2, is_concave_s2): if self.flat2 is None: flat2 = self.flat2 = compute_flat(self.s2, self.sd) else: flat2 = self.flat2 else: flat2 = None poly2 = full_profile(self.profile2, self.is_flipped, self.extent(), flat2, hole_id=self.hole_sd, dir=-1) self.profile_polys.append(poly2) if self.hole_sd is None: for p in poly2: p[0] += self.gap.thi poly1 += poly2 poly1.append(poly1[0]) poly = poly1 else: poly = [] for p1, p2 in zip(poly1, poly2): for p in p2: p[0] += self.gap.thi p1 += p2 p1.append(p1[0]) poly.append(p1) poly = tuple(poly) return poly
[docs] def render_handles(self, opt_model): self.handles = {} thi = self.gap.thi shape = self.render_shape() color = calc_render_color_for_material(self.gap.medium) self.handles['shape'] = GraphicsHandle(shape, self.tfrm, 'polygon', color) extent = self.extent() poly_s1 = self.profile_polys[0] gh1 = GraphicsHandle(poly_s1, self.tfrm, 'polyline') self.handles['s1_profile'] = gh1 poly_s2 = self.profile_polys[1] gh2 = GraphicsHandle(poly_s2, self.tfrm, 'polyline') self.handles['s2_profile'] = gh2 if self.hole_sd is None: poly_sd_upr = [] poly_sd_upr.append([poly_s1[-1][0], extent[1]]) poly_sd_upr.append([poly_s2[0][0], extent[1]]) self.handles['sd_upr'] = GraphicsHandle(poly_sd_upr, self.tfrm, 'polyline') poly_sd_lwr = [] poly_sd_lwr.append([poly_s2[-1][0], extent[0]]) poly_sd_lwr.append([poly_s1[0][0], extent[0]]) self.handles['sd_lwr'] = GraphicsHandle(poly_sd_lwr, self.tfrm, 'polyline') else: poly_s1_lwr, poly_s1_upr = poly_s1 poly_s2_lwr, poly_s2_upr = poly_s2 poly_sd_upr = [] poly_sd_upr.append([poly_s1_upr[-1][0], extent[1]]) poly_sd_upr.append([poly_s2_upr[0][0], extent[1]]) self.handles['sd_upr'] = GraphicsHandle(poly_sd_upr, self.tfrm, 'polyline') poly_sd_lwr = [] poly_sd_lwr.append([poly_s2_lwr[-1][0], extent[0]]) poly_sd_lwr.append([poly_s1_lwr[0][0], extent[0]]) self.handles['sd_lwr'] = GraphicsHandle(poly_sd_lwr, self.tfrm, 'polyline') poly_ct = [] poly_ct.append([0., 0.]) poly_ct.append([thi, 0.]) self.handles['ct'] = GraphicsHandle(poly_ct, self.tfrm, 'polyline') return self.handles
[docs] def handle_actions(self): self.actions = {} shape_actions = {} shape_actions['pt'] = BendAction(self) shape_actions['y'] = AttrAction(self, 'sd') shape_actions['glass'] = ReplaceGlassAction(self.gap) self.actions['shape'] = shape_actions s1_prof_actions = {} s1_prof_actions['pt'] = SagAction(self.s1) self.actions['s1_profile'] = s1_prof_actions s2_prof_actions = {} s2_prof_actions['pt'] = SagAction(self.s2) self.actions['s2_profile'] = s2_prof_actions sd_upr_action = {} sd_upr_action['y'] = AttrAction(self, 'sd') self.actions['sd_upr'] = sd_upr_action sd_lwr_action = {} sd_lwr_action['y'] = AttrAction(self, 'sd') self.actions['sd_lwr'] = sd_lwr_action ct_action = {} ct_action['x'] = AttrAction(self.gap, 'thi') self.actions['ct'] = ct_action return self.actions
[docs]class Mirror(Part): label_format = 'M{}' serial_number = 0 def __init__(self, ifc, tfrm=None, idx=0, sd=1., thi=None, z_dir=1.0, label=None): if label is None: Mirror.serial_number += 1 self.label = Mirror.label_format.format(Mirror.serial_number) else: self.label = label self.render_color = (158, 158, 158, 64) if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.s = ifc self.s_indx = idx self.profile = ifc.profile self.z_dir = z_dir self.sd = sd self.hole_sd = None self.flat = None self.do_flat = 'if concave' self.thi = thi self.medium_name = 'Mirror' self.handles = {} self.actions = {}
[docs] def get_thi(self): thi = self.thi if self.thi is None: thi = 0.05*self.sd return thi
def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['s'] del attrs['handles'] del attrs['actions'] encode_obj_reference(self, 'profile', attrs) if hasattr(self, 'profile_polys'): del attrs['profile_polys'] return attrs def __str__(self): thi = self.get_thi() fmt = 'Mirror: {!r}, t={:.4f}, sd={:.4f}' return fmt.format(self.profile, thi, self.sd)
[docs] def listobj_str(self): o_str = f"part: {type(self).__name__}, " o_str += self.profile.listobj_str() o_str += f"t={self.get_thi():.4f}, sd={self.sd:.4f}\n" return o_str
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.s_indx] self.s = surfs[self.s_indx] sync_obj_reference(self, 'profile', profile_dict, self.s.profile) if not hasattr(self, 'medium_name'): self.medium_name = 'Mirror' if not hasattr(self, 'hole_sd'): self.hole_sd = None self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): self.s_indx = seq_model.ifcs.index(self.s) self.profile = self.s.profile
[docs] def tree(self, **kwargs): default_tag = '#element#mirror' tag = default_tag + kwargs.get('tag', '') # Interface branch m = Node('M', id=self, tag=tag) p = Node('p', id=self.profile, tag='#profile', parent=m) Node(f'i{self.s_indx}', id=self.s, tag='#ifc', parent=p) # Gap branch = None return m
[docs] def reference_interface(self): return self.s
[docs] def reference_idx(self): return self.s_indx
[docs] def idx_list(self): seq_model = self.parent.opt_model['seq_model'] self.s_indx = seq_model.ifcs.index(self.s) return [self.s_indx]
[docs] def profile_list(self): return [self.profile]
[docs] def gap_list(self): return []
[docs] def do_flip(self): r, t = self.tfrm if self.is_flipped: r_new = np.matmul(rot_around_y, r).T else: r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t
[docs] def update_size(self): self.edge_extent = self.s.get_y_aperture_extent() self.sd = self.s.surface_od() return self.sd
[docs] def extent(self): if hasattr(self, 'edge_extent'): return self.edge_extent else: self.edge_extent = self.s.get_y_aperture_extent() return self.edge_extent
[docs] def substrate_offset(self): thi = self.get_thi() # We want to extend the mirror substrate along the same direction # of the incoming ray. The mirror's z_dir is following reflection so # flip the sign to get the preceding direction. offset = -self.z_dir*thi return offset
[docs] def render_shape(self): is_concave_s1 = self.s.profile_cv < 0.0 is_concave_s2 = self.s.profile_cv > 0.0 self.profile_polys = [] computed_flat = compute_flat(self.s, self.sd) if use_flat(self.do_flat, is_concave_s1): if self.flat is None: flat = computed_flat else: flat = self.flat else: flat = None poly1 = full_profile(self.profile, self.is_flipped, self.extent(), flat, hole_id=self.hole_sd) self.profile_polys.append(poly1) if use_flat(self.do_flat, is_concave_s2): if self.flat is None: flat = computed_flat else: flat = self.flat else: flat = None poly2 = full_profile(self.profile, self.is_flipped, self.extent(), flat, hole_id=self.hole_sd, dir=-1) self.profile_polys.append(poly2) offset = self.substrate_offset() if self.hole_sd is None: for p in poly2: p[0] += offset poly1 += poly2 poly1.append(poly1[0]) poly = poly1 else: poly = [] for p1, p2 in zip(poly1, poly2): for p in p2: p[0] += offset p1 += p2 p1.append(p1[0]) poly.append(p1) poly = tuple(poly) return poly
[docs] def render_handles(self, opt_model): self.handles = {} self.handles['shape'] = GraphicsHandle(self.render_shape(), self.tfrm, 'polygon', self.render_color) extent = self.extent() poly_s1 = self.profile_polys[0] gh1 = GraphicsHandle(poly_s1, self.tfrm, 'polyline') self.handles['s_profile'] = gh1 poly_s2 = self.profile_polys[1] if self.hole_sd is None: poly_sd_upr = [] poly_sd_upr.append([poly_s1[-1][0], extent[1]]) poly_sd_upr.append([poly_s2[0][0], extent[1]]) self.handles['sd_upr'] = GraphicsHandle(poly_sd_upr, self.tfrm, 'polyline') poly_sd_lwr = [] poly_sd_lwr.append([poly_s2[-1][0], extent[0]]) poly_sd_lwr.append([poly_s1[0][0], extent[0]]) self.handles['sd_lwr'] = GraphicsHandle(poly_sd_lwr, self.tfrm, 'polyline') else: poly_s1_lwr, poly_s1_upr = poly_s1 poly_s2_lwr, poly_s2_upr = poly_s2 poly_sd_upr = [] poly_sd_upr.append([poly_s1_upr[-1][0], extent[1]]) poly_sd_upr.append([poly_s2_upr[0][0], extent[1]]) self.handles['sd_upr'] = GraphicsHandle(poly_sd_upr, self.tfrm, 'polyline') poly_sd_lwr = [] poly_sd_lwr.append([poly_s2_lwr[-1][0], extent[0]]) poly_sd_lwr.append([poly_s1_lwr[0][0], extent[0]]) self.handles['sd_lwr'] = GraphicsHandle(poly_sd_lwr, self.tfrm, 'polyline') return self.handles
[docs] def handle_actions(self): self.actions = {} shape_actions = {} shape_actions['pt'] = SagAction(self.s) self.actions['shape'] = shape_actions s_prof_actions = {} s_prof_actions['pt'] = SagAction(self.s) self.actions['s_profile'] = s_prof_actions sd_upr_action = {} sd_upr_action['y'] = AttrAction(self, 'edge_extent[1]') self.actions['sd_upr'] = sd_upr_action sd_lwr_action = {} sd_lwr_action['y'] = AttrAction(self, 'edge_extent[0]') self.actions['sd_lwr'] = sd_lwr_action return self.actions
[docs]class CementedElement(Part): """Cemented element domain model. Manage rendering and selection/editing. A CementedElement consists of 3 or more Surfaces, 2 or more Gaps, and edge_extent information. Attributes: idxs: list of seq_model interface indices (depends on is_flipped) ifcs: list of :class:`~rayoptics.seq.interface.Interface` gaps: list of thickness and material :class:`~rayoptics.seq.gap.Gap` tfrm: global transform to element origin, (Rot3, trans3) medium_name: the material filling the gap flats: semi-diameter of flat if ifc is concave, or None handles: dict of graphical entities actions: dict of actions associated with the graphical handles """ clut = rgbt.RGBTable(filename='red_blue64.csv', data_range=[10.0, 100.]) label_format = 'CE{}' serial_number = 0 def __init__(self, ifc_list, label=None): if label is None: CementedElement.serial_number += 1 self.label = CementedElement.label_format.format( CementedElement.serial_number) else: self.label = label g_tfrm = ifc_list[0][4] if g_tfrm is not None: self.tfrm = g_tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.idxs = [] self.ifcs = [] self.profiles = [] self.gaps = [] self.medium_name = '' for interface in ifc_list: i, ifc, g, z_dir, g_tfrm = interface self.idxs.append(i) self.ifcs.append(ifc) self.profiles.append(ifc.profile) if g is not None: self.gaps.append(g) if self.medium_name != '': self.medium_name += ', ' self.medium_name += g.medium.name() if len(self.gaps) == len(self.ifcs): self.gaps.pop() self.medium_name = self.medium_name.rpartition(',')[0] self._sd = self.update_size() self.flats = [None]*len(self.ifcs) self.do_flat_0 = 'if concave' # alternatives are 'never', 'always', self.do_flat_k = 'if concave' # or 'if convex' self.handles = {} self.actions = {} @property def sd(self): """Semi-diameter """ return self._sd @sd.setter def sd(self, semidiam): self._sd = semidiam self.edge_extent = (-semidiam, semidiam) def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['ifcs'] del attrs['gaps'] del attrs['flats'] del attrs['handles'] del attrs['actions'] attrs['profile_ids'] = [str(id(p)) for p in self.profiles] del attrs['profiles'] if hasattr(self, 'profile_polys'): del attrs['profile_polys'] return attrs def __str__(self): fmt = 'CementedElement: {}' return fmt.format(self.idxs)
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): # when restoring, we want to use the stored indices to look up the # new object instances self.parent = ele_model if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.idxs[0]] self.ifcs = [surfs[i] for i in self.idxs] if hasattr(self, 'profile_ids'): self.profiles = [] for p_id in self.profile_ids: self.profiles.append(profile_dict[p_id]) delattr(self, 'profile_ids') else: self.profiles = [ifc.profile for ifc in self.ifcs] if self.is_flipped: self.gaps = [gaps[i] for i in self.idxs[1:]] else: self.gaps = [gaps[i] for i in self.idxs[:-1]] self.flats = [None]*len(self.ifcs) if not hasattr(self, 'do_flat_0'): self.do_flat_0 = 'if concave' if not hasattr(self, 'do_flat_k'): self.do_flat_k = 'if concave' if not hasattr(self, 'medium_name'): self.medium_name = self.gap.medium.name() self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): # when updating, we want to use the stored object instances to get the # current indices into the interface list (e.g. to handle insertion and # deletion of interfaces) self.idxs = [seq_model.ifcs.index(ifc) for ifc in self.ifcs] self.profiles = [ifc.profile for ifc in self.ifcs]
[docs] def tree(self, **kwargs): default_tag = '#element#cemented' tag = default_tag + kwargs.get('tag', '') zdir = kwargs.get('z_dir', 1) ce = Node(self.label, id=self, tag=tag) for i, sg in enumerate(itertools.zip_longest(self.ifcs, self.gaps)): i1 = i + 1 ifc, gap = sg pid = f'p{i1}' p = Node(pid, id=self.profiles[i], tag='#profile', parent=ce) Node(f'i{self.idxs[i]}', id=ifc, tag='#ifc', parent=p) # Gap branch if gap is not None: t = Node(f't{i1}', id=gap, tag='#thic', parent=ce) Node(f'g{self.idxs[i]}', id=(gap, zdir), tag='#gap', parent=t) return ce
[docs] def idx_list(self): seq_model = self.parent.opt_model['seq_model'] self.idxs = [seq_model.ifcs.index(ifc) for ifc in self.ifcs] return self.idxs
[docs] def reference_idx(self): return self.idxs[0]
[docs] def reference_interface(self): return self.ifcs[0]
[docs] def profile_list(self): return self.profiles
[docs] def gap_list(self): return self.gaps
[docs] def do_flip(self): r, t = self.tfrm thi = 0 for g in self.gaps: thi += g.thi if self.is_flipped: r_new = np.matmul(rot_around_y, r).T t_new = t - r_new.dot(np.array([0, 0, thi])) else: t_new = t + r.dot(np.array([0, 0, thi])) r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t_new
[docs] def update_size(self): extents = np.union1d(self.ifcs[0].get_y_aperture_extent(), self.ifcs[-1].get_y_aperture_extent()) self.edge_extent = (extents[0], extents[-1]) self.sd = max([ifc.surface_od() for ifc in self.ifcs]) return self.sd
[docs] def compute_inner_flat(self, idx, sd): ''' compute flats, if needed, for the inner cemented surfaces. Args: idx: index of inner surface in profile list sd: the semi-diameter of the cemented element This function is needed to handle the cases where one of the outer surfaces has a flat and the inner surface would intersect this flat. All inner cemented surfaces are assumed to be spherical. See model US007277232_Example04P.roa ''' def sphere_sag_to_zone(sag, c): if c == 0.: return sd else: R = 1/c try: zone = sqrt(2*sag*R - sag**2) except ValueError: zone = R return zone p = self.profiles[idx] flat_0 = self.flats[0] sag0 = self.profiles[0].sag(0., flat_0) if flat_0 else 0.0 flat_k = self.flats[-1] sagk = self.profiles[-1].sag(0., flat_k) if flat_k else 0.0 thi_b4 = thi_aftr = 0. for i in range(idx): thi_b4 += self.gaps[i].thi for i in range(idx, len(self.gaps)): thi_aftr += self.gaps[i].thi flat_i = None if p.cv < 0.0: if self.flats[0] is not None: flat_i = sphere_sag_to_zone(sag0 + thi_b4, p.cv) elif p.cv > 0.0: if self.flats[-1] is not None: flat_i = sphere_sag_to_zone(sagk + thi_aftr, p.cv) if flat_i is not None and flat_i > sd: flat_i = sd return flat_i
[docs] def extent(self): if hasattr(self, 'edge_extent'): return self.edge_extent else: return (-self.sd, self.sd)
[docs] def render_shape(self): '''return a polyline that is representative of the cemented element. ''' # examine all profiles for possible (or required) flats. # only consider first profile for a flat if it is concave is_concave_0 = self.profiles[0].cv < 0.0 if use_flat(self.do_flat_0, is_concave_0): self.flats[0] = compute_flat(self.ifcs[0], self.sd) else: self.flats[0] = None # only consider last profile for a flat if it is concave is_concave_k = self.profiles[-1].cv > 0.0 if use_flat(self.do_flat_k, is_concave_k): self.flats[-1] = compute_flat(self.ifcs[-1], self.sd) else: self.flats[-1] = None # compute flats for inner profiles that intersect outer flats for i, p in enumerate(self.profiles[1:-1], start=1): self.flats[i] = self.compute_inner_flat(i, self.sd) # generate the profile polylines self.profile_polys = [] sense = 1 for profile, flat in zip(self.profiles, self.flats): poly = full_profile(profile, self.is_flipped, self.extent(), flat, dir=sense) self.profile_polys.append(poly) sense = -sense # offset the profiles wrt the element origin thi = 0 for i, poly_profile in enumerate(self.profile_polys[1:]): thi += self.gaps[i].thi for p in poly_profile: p[0] += thi # just return outline poly_shape = [] poly_shape += self.profile_polys[0] if sense == -1: poly_shape += self.profile_polys[-1][-1::-1] else: poly_shape += self.profile_polys[-1] poly_shape.append(poly_shape[0]) return poly_shape
[docs] def render_handles(self, opt_model): self.handles = {} shape = self.render_shape() self.handles['shape'] = GraphicsHandle(shape, self.tfrm, 'polyline') for i, gap in enumerate(self.gaps): poly = [] poly += self.profile_polys[i] poly += self.profile_polys[i+1] poly.append(self.profile_polys[i][0]) color = calc_render_color_for_material(gap.medium) self.handles['shape'+str(i+1)] = GraphicsHandle(poly, self.tfrm, 'polygon', color) return self.handles
[docs] def handle_actions(self): self.actions = {} return self.actions
[docs]class ThinElement(Part): label_format = 'TL{}' serial_number = 0 def __init__(self, ifc, tfrm=None, idx=0, sd=None, label=None): if label is None: ThinElement.serial_number += 1 self.label = ThinElement.label_format.format( ThinElement.serial_number) else: self.label = label self.render_color = (192, 192, 192) if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.intrfc = ifc self.intrfc_indx = idx self.medium_name = 'Thin Element' if sd is not None: self.sd = sd else: self.sd = ifc.max_aperture self.handles = {} self.actions = {} def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['intrfc'] del attrs['handles'] del attrs['actions'] return attrs def __str__(self): return str(self.intrfc)
[docs] def tree(self, **kwargs): default_tag = '#element#thinlens' tag = default_tag + kwargs.get('tag', '') tle = Node('TL', id=self, tag=tag) Node('tl', id=self.intrfc, tag='#ifc', parent=tle) return tle
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.intrfc_indx] self.intrfc = surfs[self.intrfc_indx] if not hasattr(self, 'medium_name'): self.medium_name = 'Thin Element' self.handles = {} self.actions = {} ro_version = ele_model.opt_model.ro_version if version.parse(ro_version) < version.parse("0.7.0a"): ThinElement.serial_number += 1 self.label = ThinElement.label_format.format(ThinElement.serial_number)
[docs] def sync_to_seq(self, seq_model): self.intrfc_indx = seq_model.ifcs.index(self.intrfc)
[docs] def reference_interface(self): return self.intrfc
[docs] def reference_idx(self): return self.intrfc_indx
[docs] def profile_list(self): return []
[docs] def idx_list(self): seq_model = self.parent.opt_model['seq_model'] self.intrfc_indx = seq_model.ifcs.index(self.intrfc) return [self.intrfc_indx]
[docs] def gap_list(self): return []
[docs] def do_flip(self): r, t = self.tfrm if self.is_flipped: r_new = np.matmul(rot_around_y, r).T else: r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t
[docs] def update_size(self): self.sd = self.intrfc.surface_od() return self.sd
[docs] def render_shape(self): poly = self.intrfc.full_profile((-self.sd, self.sd)) return poly
[docs] def render_handles(self, opt_model): self.handles = {} shape = self.render_shape() self.handles['shape'] = GraphicsHandle(shape, self.tfrm, 'polygon', self.render_color) return self.handles
[docs] def handle_actions(self): self.actions = {} return self.actions
[docs]class DummyInterface(Part): label_format = 'D{}' serial_number = 0 def __init__(self, ifc, idx=0, sd=None, tfrm=None, label=None): if label is None: DummyInterface.serial_number += 1 self.label = DummyInterface.label_format.format( DummyInterface.serial_number) else: self.label = label self.render_color = (192, 192, 192) if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.ref_ifc = ifc self.idx = idx self.profile = ifc.profile self.medium_name = 'Interface' if sd is not None: self.sd = sd else: self.sd = ifc.max_aperture self.handles = {} self.actions = {} def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['ref_ifc'] del attrs['handles'] del attrs['actions'] encode_obj_reference(self, 'profile', attrs) return attrs def __str__(self): return str(self.ref_ifc)
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.idx] self.ref_ifc = surfs[self.idx] sync_obj_reference(self, 'profile', profile_dict, self.ref_ifc.profile) if not hasattr(self, 'medium_name'): self.medium_name = 'Interface' self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): self.idx = seq_model.ifcs.index(self.ref_ifc) self.profile = self.ref_ifc.profile
[docs] def tree(self, **kwargs): default_tag = '#dummyifc' tag = default_tag + kwargs.get('tag', '') di = Node('DI', id=self, tag=tag) p = Node('p', id=self.profile, tag='#profile', parent=di) Node(f'i{self.idx}', id=self.ref_ifc, tag='#ifc', parent=p) return di
[docs] def reference_interface(self): return self.ref_ifc
[docs] def reference_idx(self): return self.idx
[docs] def interface_list(self): return [self.ref_ifc]
[docs] def profile_list(self): return [self.profile]
[docs] def idx_list(self): seq_model = self.parent.opt_model['seq_model'] self.idx = seq_model.ifcs.index(self.ref_ifc) return [self.idx]
[docs] def gap_list(self): return []
[docs] def do_flip(self): r, t = self.tfrm if self.is_flipped: r_new = np.matmul(rot_around_y, r).T else: r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t
[docs] def update_size(self): self.sd = self.ref_ifc.surface_od() return self.sd
[docs] def render_shape(self): poly = full_profile(self.profile, self.is_flipped, (-self.sd, self.sd)) return poly
[docs] def render_handles(self, opt_model): self.handles = {} self.handles['shape'] = GraphicsHandle(self.render_shape(), self.tfrm, 'polyline') return self.handles
[docs] def handle_actions(self): self.actions = {} def get_adj_spaces(): seq_model = self.parent.opt_model.seq_model if self.idx > 0: before = seq_model.gaps[self.idx-1].thi else: before = None if self.idx < seq_model.get_num_surfaces() - 1: after = seq_model.gaps[self.idx].thi else: after = None return (before, after) def set_adj_spaces(cur_value, change): seq_model = self.parent.opt_model.seq_model if cur_value[0] is not None: seq_model.gaps[self.idx-1].thi = cur_value[0] + change if cur_value[1] is not None: seq_model.gaps[self.idx].thi = cur_value[1] - change slide_action = {} slide_action['x'] = Action(get_adj_spaces, set_adj_spaces) self.actions['shape'] = slide_action return self.actions
[docs]class AirGap(Part): label_format = 'AG{}' serial_number = 0 def __init__(self, g, idx=0, tfrm=None, label=None, z_dir=1, **kwargs): if label is None: AirGap.serial_number += 1 self.label = AirGap.label_format.format(AirGap.serial_number) else: self.label = label if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.render_color = (237, 243, 254, 64) # light blue self.gap = g self.z_dir = z_dir self.medium_name = self.gap.medium.name() self.idx = idx self.handles = {} self.actions = {} def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['gap'] del attrs['handles'] del attrs['actions'] return attrs def __str__(self): return str(self.gap)
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model self.gap = gaps[self.idx] if not hasattr(self, 'tfrm'): self.tfrm = tfrms[self.idx] if not hasattr(self, 'render_color'): self.render_color = (237, 243, 254, 64) # light blue if not hasattr(self, 'medium_name'): self.medium_name = self.gap.medium.name() self.handles = {} self.actions = {} ro_version = ele_model.opt_model.ro_version if version.parse(ro_version) < version.parse("0.7.0a"): AirGap.serial_number += 1 self.label = AirGap.label_format.format(AirGap.serial_number)
[docs] def sync_to_seq(self, seq_model): self.idx = seq_model.gaps.index(self.gap) self.z_dir = seq_model.z_dir[self.idx]
[docs] def tree(self, **kwargs): default_tag = '#airgap' tag = default_tag + kwargs.get('tag', '') ag = Node(self.label, id=self, tag=tag) t = Node('t', id=self.gap, tag='#thic', parent=ag) zdir = kwargs.get('z_dir', self.z_dir) Node(f'g{self.idx}', id=(self.gap, zdir), tag='#gap', parent=t) return ag
[docs] def reference_interface(self): return None
[docs] def reference_idx(self): return self.idx
[docs] def profile_list(self): return []
[docs] def idx_list(self): return []
[docs] def gap_list(self): return [self.gap]
[docs] def do_flip(self): r, t = self.tfrm thi = self.gap.thi if self.is_flipped: r_new = np.matmul(rot_around_y, r).T t_new = t - r_new.dot(np.array([0, 0, thi])) else: t_new = t + r.dot(np.array([0, 0, thi])) r_new = np.matmul(r, rot_around_y) self.tfrm = r_new, t_new
[docs] def update_size(self): pass
[docs] def render_shape(self): pass
[docs] def render_handles(self, opt_model): self.handles = {} poly_ct = [] poly_ct.append([0., 0.]) poly_ct.append([self.gap.thi, 0.]) # Modify the tfrm to account for any decenters following # the reference ifc. tfrm = self.tfrm decenter = opt_model.seq_model.ifcs[self.idx].decenter if decenter is not None: r_global, t_global = tfrm r_after_ifc, t_after_ifc = decenter.tform_after_surf() t = r_global.dot(t_after_ifc) + t_global r = r_global if r_after_ifc is None else r_global.dot(r_after_ifc) tfrm = r, t self.handles['ct'] = GraphicsHandle(poly_ct, tfrm, 'polyline') return self.handles
[docs] def handle_actions(self): self.actions = {} ct_action = {} ct_action['x'] = AttrAction(self.gap, 'thi') self.actions['ct'] = ct_action return self.actions
[docs]class Assembly(Part): label_format = 'ASM{}' serial_number = 0 def __init__(self, part_list, idx=0, tfrm=None, label=None): if label is None: Assembly.serial_number += 1 self.label = Assembly.label_format.format(Assembly.serial_number) else: self.label = label if tfrm is not None: self.tfrm = tfrm else: self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self.parts = part_list self.idx = idx self.handles = {} self.actions = {} def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['parts'] del attrs['handles'] del attrs['actions'] part_ids = [str(id(p)) for p in self.parts] attrs['part_ids'] = part_ids return attrs def __str__(self): part_labels = [p.label for p in self.parts] return f"{self.label}: {part_labels}"
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model self.parts = [parts_dict[pid] for pid in self.part_ids] delattr(self, 'part_ids') self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): self.tfrm = seq_model.gbl_tfrms[self.reference_idx()]
[docs] def tree(self, **kwargs): if 'part_tree' in kwargs: part_tree = kwargs.get('part_tree') else: part_tree = self.parent.opt_model['part_tree'] default_tag = '#group#assembly' tag = default_tag + kwargs.get('tag', '') asm = Node('ASM', id=self, tag=tag) child_nodes = [part_tree.node(p) for p in self.parts] asm.children = child_nodes return asm
[docs] def idx_list(self): idxs = [] for p in self.parts: idxs += p.idx_list() return idxs
[docs] def reference_idx(self): idxs = self.idx_list() self.idx = idxs[0] return self.idx
[docs] def reference_interface(self): seq_model = self.parent.opt_model['seq_model'] ref_idx = self.reference_idx() return seq_model.ifcs[ref_idx]
[docs] def profile_list(self): profiles = [] for p in self.parts: profiles += p.profile_list() return profiles
[docs] def gap_list(self): gaps = [] for p in self.parts: gaps += p.gap_list() return gaps
[docs] def do_flip(self): sm = self.parent.opt_model['seq_model'] idxs = self.idx_list() idx1, idx2 = idxs[0], idxs[-1] flip_pt = 0.5*(sm.gbl_tfrms[idx2][1] + sm.gbl_tfrms[idx1][1]) flip_pt_tfrm = sm.gbl_tfrms[idx1][0], flip_pt do_flip_with_part_list(self.parts, flip_pt_tfrm)
[docs] def update_size(self): pass
[docs] def render_shape(self): pass
[docs] def render_handles(self, opt_model): self.handles = {} return self.handles
[docs] def handle_actions(self): self.actions = {} return self.actions
# --- Element model
[docs]class ElementModel: """Maintain the element based representation of the optical model Attributes: opt_model: the :class:`~optical.opticalmodel.OpticalModel` elements: list of element type things """ def __init__(self, opt_model, **kwargs): self.opt_model = opt_model self.elements: List[Part] = []
[docs] def reset(self): self.__init__(self.opt_model)
def __json_encode__(self): attrs = dict(vars(self)) del attrs['opt_model'] del attrs['elements'] return attrs
[docs] def sync_to_restore(self, opt_model): self.opt_model = opt_model profile_dict = {} if hasattr(opt_model, 'profile_dict'): profile_dict = opt_model.profile_dict parts_dict = {} if not hasattr(self, 'elements'): if hasattr(opt_model, 'parts_dict'): self.elements = [e for e in opt_model.parts_dict.values()] parts_dict = opt_model.parts_dict seq_model = opt_model.seq_model surfs = seq_model.ifcs gaps = seq_model.gaps tfrms = seq_model.compute_global_coords(1) self.reset_serial_numbers() for i, e in enumerate(self.elements, start=1): e.sync_to_restore(self, surfs, gaps, tfrms, profile_dict, parts_dict) if not hasattr(e, 'label'): e.label = e.label_format.format(i) self.sequence_elements()
# self.relabel_airgaps()
[docs] def reset_serial_numbers(self): Element.serial_number = 0 Mirror.serial_number = 0 CementedElement.serial_number = 0 ThinElement.serial_number = 0 DummyInterface.serial_number = 0 AirGap.serial_number = 0 Assembly.serial_number = 0
[docs] def airgaps_from_sequence(self, seq_model, tfrms): """ add airgaps and dummy interfaces to an older version model """ for e in self.elements: if isinstance(e, AirGap): return # found an AirGap, model probably OK num_elements = 0 seq_model = self.opt_model.seq_model for i, g in enumerate(seq_model.gaps): if g.medium.name().lower() == 'air': if i > 0: s = seq_model.ifcs[i] tfrm = tfrms[i] num_elements = self.process_airgap( seq_model, i, g, s, tfrm, num_elements, add_ele=False)
[docs] def add_dummy_interface_at_image(self, seq_model, tfrms): if len(self.elements) and self.elements[-1].label == 'Image': return s = seq_model.ifcs[-1] idx = seq_model.get_num_surfaces() - 1 di = DummyInterface(s, sd=s.surface_od(), tfrm=tfrms[-1], idx=idx, label='Image') self.opt_model.part_tree.add_element_to_tree(di, tag='#image') self.add_element(di)
[docs] def update_model(self, **kwargs): # dynamically build element list from part_tree part_tree = self.opt_model['part_tree'] part_tag = '#element#airgap#dummyifc#assembly' nodes = part_tree.nodes_with_tag(tag=part_tag) elements = [n.id for n in nodes] # hook or unhook elements from ele_model cur_set = set(self.elements) new_set = set(elements) added_ele = list(new_set.difference(cur_set)) for e in added_ele: e.parent = self removed_ele = list(cur_set.difference(new_set)) for e in removed_ele: e.parent = None self.elements = elements # if the number of elements have changed, update the # links to the seq_model indices. issue 127 if len(added_ele) > 0 or len(removed_ele) > 0: seq_model = self.opt_model['seq_model'] for e in elements: e.sync_to_seq(seq_model) # use the seq_model to sort the element_model list self.sequence_elements() # unless updated by the ele_model, sync it to the seq_model. src_model = kwargs.get('src_model', None) if src_model is not self: self.sync_to_seq(self.opt_model['seq_model'])
[docs] def sync_to_seq(self, seq_model): """ Update element positions and ref_idx using the sequential model. """ tfrms = seq_model.compute_global_coords(1) # update the elements for e in self.elements: e.update_size() e.sync_to_seq(seq_model) r, t = tfrms[e.reference_idx()] r_new = np.matmul(rot_around_y, r).T if e.is_flipped else r e.tfrm = r_new, t
[docs] def sequence_elements(self): """ Sort elements in order of reference interfaces in seq_model """ seq_model = self.opt_model.seq_model # sort by element reference interface sequential index self.elements.sort(key=lambda e: e.reference_idx()) # Make sure z_dir matches the sequential model. Used to get # the correct substrate offset. if hasattr(seq_model, 'z_dir'): for e in self.elements: if hasattr(e, 'z_dir'): e.z_dir = seq_model.z_dir[e.reference_idx()]
[docs] def relabel_airgaps(self): for i, e in enumerate(self.elements): if isinstance(e, AirGap): eb = self.elements[i-1].label ea = self.elements[i+1].label e.label = AirGap.label_format.format(eb + '-' + ea)
[docs] def add_element(self, e: Part): e.parent = self self.elements.append(e)
[docs] def remove_element(self, e: Part): e.parent = None self.elements.remove(e)
[docs] def remove_node(self, e_node): part_tree = self.opt_model.part_tree nodes = part_tree.nodes_with_tag(tag='#element#airgap#dummyifc', root=e_node) eles = [n.id for n in nodes] for e in eles: self.remove_element(e)
[docs] def get_num_elements(self): return len(self.elements)
[docs] def list_model(self, tag: str = '#element#assembly#dummyifc'): nodes = self.opt_model['part_tree'].nodes_with_tag(tag=tag) for i, node in enumerate(nodes): ele = node.id print("%d: %s (%s): %s" % (i, ele.label, type(ele).__name__, ele))
[docs] def list_elements(self): for i, ele in enumerate(self.elements): print("%d: %s (%s): %s" % (i, ele.label, type(ele).__name__, ele))
[docs] def element_type(self, i): return type(self.elements[i]).__name__