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
from copy import deepcopy
import itertools
from itertools import zip_longest
from packaging import version

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

from math import sqrt
import numpy as np

from anytree import Node  # type: ignore

import rayoptics.optical.model_constants as mc

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.elem import transform as trns
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

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

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(ifc=tl) tree = tle.tree() if 'prx' in kwargs: pm, node, type_sel = prx = kwargs['prx'] dgm_pkg = [pm.get_pt(node)], [[1.0, 'transmit']] dgm = prx, dgm_pkg else: dgm = None descriptor = [[tl, None, None, 1, +1]], [tle], tree, dgm return descriptor
[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(ifc=m, sd=sd, **ele_kwargs) tree = me.tree() if 'prx' in kwargs: pm, node, type_sel = prx = kwargs['prx'] dgm_pkg = [pm.get_pt(node)], [[-1, 'reflect']] dgm = prx, dgm_pkg else: dgm = None return [[m, None, None, 1, -1]], [me], tree, dgm
[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
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(sg_def=(s1, s2, g), sd=sd) tree = le.tree() return [[s1, g, None, rndx, 1], [s2, None, None, 1, 1]], [le], tree
[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, pt tree, and |ybar| entry 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 """ descriptor = _create_lens(power, bending, th, sd, med, lens, **kwargs) descriptor += (None,) return descriptor
[docs] def create_lens_from_dgm(prx=None, **kwargs): """ Use diagram points to create a lens. Adds a |ybar| component to the descriptor tuple. dgm = prx, dgm_pkg prx = parax_model, node_idx, type_sel dgm_pkg = node_list, sys_data sys_data = list([rndx, 'transmit'|'reflect']) """ pm, node, type_sel = prx dgm_pkg, lens_from_dgm = pm.lens_from_dgm(node, **kwargs) descriptor = _create_lens(lens=lens_from_dgm, **kwargs) descriptor += ((prx, dgm_pkg),) return descriptor
[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=ifc_list) tree = ce.tree() return [[s1, g1, None, rndx_a[0], 1], [s2, g2, None, rndx_b[0], 1], [s3, None, None, 1, 1]], [ce], tree, None
[docs] def create_dummy_plane(sd=1., **kwargs): s = Surface(interact_mode='dummy', max_ap=sd, **kwargs) se = DummyInterface(ifc=s, sd=sd) tree = se.tree() if 'prx' in kwargs: pm, node, type_sel = prx = kwargs['prx'] dgm_pkg = [pm.get_pt(node)], [[1, 'transmit']] dgm = prx, dgm_pkg else: dgm = None descriptor = [[s, None, None, 1, +1]], [se], tree, dgm return descriptor
[docs] def create_air_gap(t=0., **kwargs): g = Gap(t=t) ag = AirGap(g=g, **kwargs) kwargs.pop('label', None) tree = ag.tree(**kwargs) return g, ag, tree, None
[docs] def create_from_file(filename, create_asm: bool=True, **kwargs): """ Import an optical model into the current optical model. Args: filename: filename or url create_asm: if True, create an assembly for the imported model label: used for assembly, if created Returns: model descriptor: seq, parts, nodes, dgm """ import rayoptics.gui.appcmds as cmds opm_file = cmds.open_model(filename, post_process_imports=False) sm_file = opm_file['seq_model'] osp_file = opm_file['optical_spec'] pm_file = opm_file['parax_model'] em_file = opm_file['ele_model'] pt_file = opm_file['part_tree'] ar_file = opm_file['analysis_results'] if len(pt_file.nodes_with_tag(tag='#element')) == 0: parttree.sequence_to_elements(sm_file, em_file, pt_file) if 'power' in kwargs: desired_power = kwargs['power'] cur_power = ar_file['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 opm_file.apply_scale_factor(scale_factor) # extract the system definition, minus object and image seq = [list(node) for node in sm_file.path(start=1, stop=-1)] seq[-1][1] = None if 'prx' in kwargs: dgm = pm_file.match_pupil_and_conj(kwargs['prx']) else: dgm = None # get the top level nodes of the input system, minus object and image part_nodes = pt_file.nodes_with_tag(tag='#element#airgap#assembly', not_tag='#object#image', node_list=pt_file.root_node.children) parts = [part_node.id for part_node in part_nodes] if create_asm: 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_file['seq_model'].gbl_tfrms[1]) asm = Assembly(parts, idx=1, label=label, tfrm=tfrm) asm_node = asm.tree(part_tree=opm_file['part_tree'], tag='#file') parts.append(asm) asm_node.parent = None nodes = asm_node else: nodes = part_nodes return seq, parts, nodes, dgm
[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 render_lens_shape(s1, profile1, s2, profile2, thi, z_dir, extent, sd, is_flipped: bool, hole_sd=None, apply_tfrm=True, flat1_pkg=None, flat2_pkg=None): profile_polys = [] flat = None if flat1_pkg is not None: do_flat1, flat1, is_concave_s1 = flat1_pkg if use_flat(do_flat1, is_concave_s1): if flat1 is None: flat = flat1 = compute_flat(s1, sd) else: flat = flat1 poly1 = full_profile(profile1, is_flipped, extent, flat, hole_id=hole_sd) profile_polys.append(poly1) flat = None if flat2_pkg is not None: do_flat2, flat2, is_concave_s2 = flat2_pkg if use_flat(do_flat2, is_concave_s2): if flat2 is None: flat = flat2 = compute_flat(s2, sd) else: flat = flat2 poly2 = full_profile(profile2, is_flipped, extent, flat, hole_id=hole_sd, dir=-1) if apply_tfrm: # get the full transform between lens surfaces r_new, t_new = trns.forward_transform(s1, thi, s2) else: r_new, t_new = np.identity(3), np.array([0., 0., thi]) # apply transformation to poly2 profile poly2_tfrmd = [] for polyline in poly2: poly = np.array(polyline) poly_tfrmd = transform_poly((r_new, t_new), poly) poly2_tfrmd.append(poly_tfrmd.tolist()) profile_polys.append(poly2_tfrmd) # assemble the tuple for handling holes, 1 or 2 polylines if hole_sd is None: poly = [] poly += poly1[0] orig_pt = deepcopy(poly1[0][0]) poly += poly2_tfrmd[0] poly.append(orig_pt) poly = poly, else: poly_list = [] for p1, p2 in zip(poly1, poly2_tfrmd): poly = [] poly += p1 orig_pt = deepcopy(p1[0]) poly += p2 poly.append(orig_pt) poly_list.append(poly) poly = tuple(poly_list) return poly, profile_polys
[docs] def render_surf_shape(srf, profile, extent, sd, is_flipped, hole_sd=None, flat_pkg=None): flat = None if flat_pkg is not None: do_flat1, flat1, is_concave_srf = flat_pkg if use_flat(do_flat1, is_concave_srf): if flat1 is None: flat = flat1 = compute_flat(srf, sd) else: flat = flat1 poly_list = full_profile(profile, is_flipped, extent, flat, hole_id=hole_sd) return poly_list
[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*. In the case of a hole, 2 polyline segments are returned. Otherwise, a single polyline is returned (as a tuple of len=1) 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 Returns: a tuple of 2d coord lists. a tuple is returned even in the case of a single coord list """ 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: sd_lwr_pt = [sag, sd_lwr] sd_upr_pt = [sag, sd_upr] if hole_id is None: if dir > 0: prf_full = [sd_lwr_pt] if sag is not None else [] prf_full += profile.profile((flat_id,), dir, steps) if sag is not None: prf_full.append(sd_upr_pt) else: prf_full = [sd_upr_pt] if sag is not None else [] prf_full += profile.profile((flat_id,), dir, steps) if sag is not None: prf_full.append(sd_lwr_pt) prf = prf_full, else: if dir > 0: prf_lwr = [sd_lwr_pt] if sag is not None else [] 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) else: prf_upr = [sd_upr_pt] if sag is not None else [] prf_upr += profile.profile((hole_id, flat_id), dir, steps) prf_lwr = profile.profile((-flat_id, -hole_id), dir, steps) if sag is not None: prf_lwr.append(sd_lwr_pt) prf = prf_lwr, prf_upr if is_flipped: if hole_id is None: prf = flip_profile(prf[0]), else: prf_lwr, prf_upr = prf prf = flip_profile(prf_lwr), flip_profile(prf_upr) return prf
[docs] def is_concave(cv:float, cur_idx:int, other_idx:int, z_dir:int) -> bool: """ returns whether a surface is concave or not. Args: cv: the curvature of the surface profile cur_idx: The seq_model index of the surface other_idx: The seq_model index of the surface at the other end of the part z_dir: z-dir for the element Returns: True if the surface is concave, False otherwise. """ return z_dir*cv*(1 if cur_idx < other_idx else -1) < 0
[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.005): 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 ele_token: str
[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 sync_to_ele_def(self, seq_model, ele_def): """ Update idx_list and gap_list according to ele_def. ele_def: (ele_type, idx_list, gap_list) """ raise NotImplementedError
[docs] @abstractmethod def seq(self, **kwargs) -> SeqPath: 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 Part. """ 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 default_ele_token = 'lens' def __init__(self, sg_def=None, ele_def_pkg=None, 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.z_dir = 1 if sg_def is not None: s1, s2, g = sg_def 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.ele_token = Element.default_ele_token elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) 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 listobj_str(self): ele_type = self.ele_token, type(self).__module__, type(self).__name__ idx_list = tuple(i for i in self.idx_list()) gap_list = tuple(idx_list[i] for i, g in enumerate(self.gap_list())) o_str = f"{ele_type[0]}: {ele_type[2]}\n" o_str += f"idx={idx_list}, gaps={gap_list} conic cnst={self.cc}\n" o_str += f"coefficients: {self.coefs}\n" return o_str fmt = f"Element: {self.s1.profile!r}, {self.s2.profile!r}, t={self.gap.thi:.4f}, sd={self.sd:.4f}, glass: {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, 'ele_token'): self.ele_token = Element.default_ele_token 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 if not hasattr(self, 'z_dir'): self.z_dir = 1 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.z_dir = seq_model.z_dir[self.s1_indx] self.medium_name = self.gap.medium.name()
[docs] def sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token self.s1_indx = idx_list[0] self.s2_indx = idx_list[1] self.s1 = seq_model.ifcs[self.s1_indx] self.s2 = seq_model.ifcs[self.s2_indx] self.profile1 = self.s1.profile self.profile2 = self.s2.profile self.gap = seq_model.gaps[gap_list[0]] self.z_dir = seq_model.z_dir[self.s1_indx] self.medium_name = self.gap.medium.name()
[docs] def seq(self, **kwargs) -> SeqPath: rndx = self.gap.medium.rindex('d') return [[self.s1, self.gap, None, rndx, 1], [self.s2, None, None, 1, 1]]
[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): if hasattr(self, 'parent') and self.parent is not None: seq_model = self.parent.opt_model['seq_model'] try: self.s1_indx = seq_model.ifcs.index(self.s1) except ValueError: self.s1_indx = str(self.s1_indx) try: self.s2_indx = seq_model.ifcs.index(self.s2) except ValueError: self.s2_indx = str(self.s2_indx) return [self.s1_indx, self.s2_indx] else: print(f"idx_list: {self.label}") return []
[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_power(self, nom_wvl='d'): cv1 = self.s1.profile_cv cv2 = self.s2.profile_cv rndx = self.gap.medium.rindex(nom_wvl) th = self.gap.thi power = (rndx - 1)*(cv1 - cv2 + th*cv1*cv2*(rndx - 1)/rndx) return power
[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): power = self.get_power() lens = lens_from_power(power=power, bending=bending, th=self.gap.thi, med=self.gap.medium) cv1_new, cv2_new, _ = lens 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 - np.matmul(r_new, np.array([0, 0, thi])) else: t_new = t + np.matmul(r, 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): s1 = self.s1 s2 = self.s2 is_cc_1 = is_concave(s1.profile_cv, self.s1_indx, self.s2_indx, self.z_dir) is_cc_2 = is_concave(s2.profile_cv, self.s2_indx, self.s1_indx, self.z_dir) flat1_pkg = self.do_flat1, self.flat1, is_cc_1 flat2_pkg = self.do_flat2, self.flat2, is_cc_2 poly, self.profile_polys = render_lens_shape( s1, s1.profile, s2, s2.profile, self.gap.thi, self.z_dir, self.extent(), self.sd, self.is_flipped, hole_sd=self.hole_sd, flat1_pkg=flat1_pkg, flat2_pkg=flat2_pkg) 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) for i, poly in enumerate(shape): self.handles['shape'+str(i+1)] = GraphicsHandle( poly, self.tfrm, 'polygon', color ) extent = self.extent() for i, poly_segs in enumerate(self.profile_polys, start=1): for poly_seg in poly_segs: gh = GraphicsHandle(poly_seg, self.tfrm, 'polyline') self.handles[f's{i}_profile'] = gh poly_s1 = self.profile_polys[0] poly_s2 = self.profile_polys[1] if self.hole_sd is None: poly_s1 = poly_s1[0] poly_s2 = poly_s2[0] 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 SurfaceInterface(Part): label_format = 'S{}' serial_number = 0 default_ele_token = 'surface' def __init__(self, ifc=None, ele_def_pkg=None, tfrm=None, idx=0, sd=1., z_dir=1, label=None): if label is None: SurfaceInterface.serial_number += 1 self.label = SurfaceInterface.label_format.format( SurfaceInterface.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.])) if ifc is not None: self.s = ifc self.s_indx = idx self.profile = ifc.profile self.ele_token = SurfaceInterface.default_ele_token elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) self.z_dir = z_dir self.sd = sd self.hole_sd = None self.flat = None self.do_flat = 'if concave' self.medium_name = 'Surface' self.handles = {} self.actions = {} 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): return f"Surface: {self.profile!r}, sd={self.sd:.4f}"
[docs] def listobj_str(self): o_str = f"part: {type(self).__name__}, " o_str += self.profile.listobj_str() o_str += f"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 = 'Surface' if not hasattr(self, 'ele_token'): self.ele_token = SurfaceInterface.default_ele_token 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 sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token self.s_indx = idx_list[0] self.s = seq_model.ifcs[idx_list[0]] self.profile = self.s.profile
[docs] def seq(self, **kwargs) -> SeqPath: return [[self.s, None, None, 1, 1]]
[docs] def tree(self, **kwargs): default_label_prefix = kwargs.get('default_label_prefix', 'S') default_tag = kwargs.get('default_tag', '#element#surface') tag = default_tag + kwargs.get('tag', '') # Interface branch m = Node(default_label_prefix, 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 render_shape(self): is_concave_s = is_concave(self.s.profile_cv, self.s_indx, self.s_indx, self.z_dir) self.profile_polys = [] computed_flat = compute_flat(self.s, self.sd) if use_flat(self.do_flat, is_concave_s): if self.flat is None: flat = computed_flat else: flat = self.flat else: flat = None poly, = full_profile(self.profile, self.is_flipped, self.extent(), flat, hole_id=self.hole_sd) self.profile_polys.append(poly) return poly
[docs] def render_handles(self, opt_model): self.handles = {} render_color = (158, 158, 158, 64) shape = self.render_shape() self.handles['shape'] = GraphicsHandle( shape, self.tfrm, 'polyline', render_color ) poly_s1 = self.profile_polys[0] if self.hole_sd is None: pt_sd_upr = deepcopy(poly_s1[-1]) self.handles['sd_upr'] = GraphicsHandle(pt_sd_upr, self.tfrm, 'vertex') pt_sd_lwr = deepcopy(poly_s1[0]) self.handles['sd_lwr'] = GraphicsHandle(pt_sd_lwr, self.tfrm, 'vertex') else: poly_s1_lwr, poly_s1_upr = poly_s1 pt_sd_upr = deepcopy(poly_s1_upr[-1]) self.handles['sd_upr'] = GraphicsHandle(pt_sd_upr, self.tfrm, 'vertex') pt_sd_lwr = deepcopy(poly_s1_lwr[0]) self.handles['sd_lwr'] = GraphicsHandle(pt_sd_lwr, self.tfrm, 'vertex') 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 Mirror(SurfaceInterface): label_format = 'M{}' serial_number = 0 default_ele_token = 'mirror' def __init__(self, ifc=None, ele_def_pkg=None, thi=None, label=None, **kwargs): if label is None: Mirror.serial_number += 1 label = Mirror.label_format.format(Mirror.serial_number) if ifc is not None: super().__init__(ifc=ifc, label=label, **kwargs) self.ele_token = Mirror.default_ele_token elif ele_def_pkg is not None: super().__init__(ele_def_pkg=ele_def_pkg, label=label, **kwargs) self.render_color = (158, 158, 158, 64) self.thi = thi self.medium_name = 'Mirror' def __str__(self): thi = self.get_thi() fmt = f"Mirror: {self.profile!r}, t={thi:.4f}, sd={self.sd:.4f}" return fmt
[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): if not hasattr(self, 'medium_name'): self.medium_name = 'Mirror' if not hasattr(self, 'ele_token'): self.ele_token = Mirror.default_ele_token super().sync_to_restore(ele_model, surfs, gaps, tfrms, profile_dict, parts_dict)
[docs] def get_thi(self) -> float: thi = self.thi if thi is None: thi = 0.05*self.sd return thi
[docs] def seq(self, **kwargs) -> SeqPath: return [[self.s, None, None, -1, -1]] # type: ignore
[docs] def tree(self, **kwargs): kwargs['default_label_prefix'] = 'M' kwargs['default_tag'] = '#element#mirror' return super().tree(**kwargs)
[docs] def substrate_offset(self) -> float: 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): s = self.s thi = self.substrate_offset() is_cc = is_concave(s.profile_cv, self.s_indx, self.s_indx, self.z_dir) flat_pkg = self.do_flat, self.flat, is_cc poly, self.profile_polys = render_lens_shape( s, s.profile, s, s.profile, thi, self.z_dir, self.extent(), self.sd, self.is_flipped, apply_tfrm=False, hole_sd=self.hole_sd, flat1_pkg=flat_pkg, flat2_pkg=flat_pkg) return poly
[docs] def render_handles(self, opt_model): self.handles = {} shape = self.render_shape() for i, poly in enumerate(shape): self.handles['shape'+str(i+1)] = GraphicsHandle( poly, self.tfrm, 'polygon', self.render_color ) extent = self.extent() poly_s1 = self.profile_polys[0] for poly_seg in poly_s1: gh1 = GraphicsHandle(poly_seg, self.tfrm, 'polyline') self.handles['s_profile'] = gh1 poly_s2 = self.profile_polys[1] if self.hole_sd is None: poly_s1 = poly_s1[0] poly_s2 = poly_s2[0] 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] 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 default_ele_token = 'cemented' def __init__(self, ifc_list=None, ele_def_pkg=None, label=None): if label is None: CementedElement.serial_number += 1 self.label = CementedElement.label_format.format( CementedElement.serial_number) else: self.label = label if ifc_list is not None: 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 = [] 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 len(self.gaps) == len(self.ifcs): self.gaps.pop() self.medium_name = self._construct_medium_name() self.ele_token = CementedElement.default_ele_token elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) self.tfrm = (np.identity(3), np.array([0., 0., 0.])) self._sd = self.update_size() self.hole_sd = None 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.do_render_shape = True # if true, render elements, otherwise surfaces 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) def _construct_medium_name(self): medium_name = '' for g in self.gaps: if medium_name != '': medium_name += ', ' medium_name += g.medium.name() return medium_name
[docs] def listobj_str(self): ele_type = self.ele_token, type(self).__module__, type(self).__name__ idx_list = tuple(i for i in self.idx_list()) gap_list = tuple(idx_list[i] for i, g in enumerate(self.gap_list())) o_str = f"{ele_type[0]}: {ele_type[2]}\n" o_str += f"idx={idx_list}, gaps={gap_list}\n" return o_str
#fmt = f"Element: {self.s1.profile!r}, {self.s2.profile!r}, t={self.gap.thi:.4f}, sd={self.sd:.4f}, glass: {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.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, 'hole_sd'): self.hole_sd = None if not hasattr(self, 'medium_name'): self.medium_name = self._construct_medium_name() if not hasattr(self, 'ele_token'): self.ele_token = CementedElement.default_ele_token if not hasattr(self, 'do_render_shape'): self.do_render_shape = True if not hasattr(self, 'z_dir'): self.z_dir = 1 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] self.z_dir = seq_model.z_dir[self.idxs[0]]
[docs] def sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token if self.ele_token == 'cemented': self.idxs = [idx for idx in idx_list] self.ifcs = [seq_model.ifcs[idx] for idx in idx_list] self.profiles = [ifc.profile for ifc in self.ifcs] self.gaps = [seq_model.gaps[i] for i in gap_list] self.medium_name = self._construct_medium_name() elif self.ele_token == 'mangin': num_gaps = (idx_list[-1] - idx_list[0])>>1 self.idxs = [idx for idx in idx_list] self.ifcs = [seq_model.ifcs[idx] for idx in idx_list] self.profiles = [ifc.profile for ifc in self.ifcs[:num_gaps+1]] self.gaps = [seq_model.gaps[i] for i in gap_list] self.medium_name = self._construct_medium_name() self.z_dir = seq_model.z_dir[self.idxs[0]]
[docs] def seq(self, **kwargs) -> SeqPath: ifcs = self.ifcs gaps = self.gaps seq_seg_list = [] if self.ele_token == 'cemented': for i, sg in enumerate(zip_longest(ifcs, gaps)): s, gap = sg rndx = gap.medium.rindex('d') if gap is not None else 1 seq_seg_list.append([s, gap, None, rndx, 1]) elif self.ele_token == 'mangin': for i, gap in enumerate(gaps): rndx = gap.medium.rindex('d') if gap is not None else 1 seq_seg_list.append([ifcs[i], gap, None, rndx, 1]) seq_seg_list.append([ifcs[i+1], gap, None, -rndx, -1]) for i, sg in enumerate(zip_longest(ifcs[len(ifcs)-2::-1], gaps[len(gaps)-2::-1])): s, gap = sg rndx = gap.medium.rindex('d') if gap is not None else -1 seq_seg_list.append([s, gap, None, rndx, -1]) return seq_seg_list
[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) if self.ele_token == 'cemented': for i, sg in enumerate(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) elif self.ele_token == 'mangin': idx1 = self.idxs[0] idxk = self.idxs[-1] len_ifcs = len(self.ifcs) len_gaps = len(self.gaps) num_gaps = (idxk-idx1)>>1 for i in range(num_gaps): i1 = i + 1 ifc = self.ifcs[i] gap = self.gaps[i] pid = f'p{i1}' p = Node(pid, id=self.profiles[i], tag='#profile', parent=ce) Node(f'i{self.idxs[i]}', id=self.ifcs[i], tag='#ifc', parent=p) i2 = len_ifcs - i1 Node(f'i{self.idxs[i2]}', id=self.ifcs[i2], tag='#ifc', parent=p) # Gap branch t = Node(f't{i1}', id=self.gaps[i], tag='#thic', parent=ce) Node(f'g{self.idxs[i]}', id=(self.gaps[i], zdir), tag='#gap', parent=t) i2 = len_gaps - i1 Node(f'g{self.idxs[i2]}', id=(self.gaps[i2], -zdir), tag='#gap', parent=t) i += 1 i1 += 1 p = Node(f'p{i1}', id=self.profiles[i], tag='#profile', parent=ce) Node(f'i{self.idxs[i]}', id=self.ifcs[i], tag='#ifc', parent=p) return ce
[docs] def idx_list(self): if self.parent is not None: # if seq_model is accessible, refresh idxs seq_model = self.parent.opt_model['seq_model'] self.idxs = [seq_model.ifcs.index(ifc) for ifc in self.ifcs if ifc in seq_model.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, k): ''' 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 k: final, k-th, profile index 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 See also cv_fisheye.roa ''' def sphere_sag_to_zone(sag, c): if c == 0.: return sd else: R = abs(1/c) try: zone = sqrt(2*sag*R - sag**2) except ValueError: zone = R return zone p = self.profiles[idx] R = 1.0e12 if p.cv == 0 else abs(1/p.cv) ifc = self.ifcs[idx] ca = ifc.surface_od() flat_0 = self.flats[0] sag0 = self.profiles[0].sag(0., flat_0) if flat_0 else 0.0 flat_k = self.flats[k] sagk = self.profiles[k].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 is_concave(p.cv, self.idxs[idx], self.idxs[0], self.z_dir): # if there's a first flat, check for intersection if self.flats[0] is not None: flat_i = sphere_sag_to_zone(sag0 + thi_b4, p.cv) # check if radius is smaller than semi-diameter, add flat if needed elif R < sd: flat_i = ca if ca < R else R elif is_concave(p.cv, self.idxs[idx], self.idxs[k], self.z_dir): # if there's a last flat, check for intersection if self.flats[k] is not None: flat_i = sphere_sag_to_zone(sagk + thi_aftr, p.cv) # check if radius is smaller than semi-diameter, add flat if needed if R < sd: flat_i = ca if ca < R else R 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 tuple of polylines of the lenses of the cemented element. Flats on the outer surfaces of the cemented assembly are checked for intersections with the internal surfaces. The first outer surface is the zeroth interface; the other outer surface is interface `k`. In the case of the mangin assembly, the k-th surface is in the middle of the interface list. ''' ifcs = self.ifcs idxs = self.idxs profiles = self.profiles gaps = self.gaps z_dir = self.z_dir polygon_list = [] len_gaps = len(gaps) if self.ele_token == 'mangin': num_gaps = len_gaps>>1 k = num_gaps else: k = -1 # examine all profiles for possible (or required) flats. # only consider first profile for a flat if it is concave is_cc_0 = is_concave(profiles[0].cv, idxs[0], idxs[k], z_dir) if use_flat(self.do_flat_0, is_cc_0): self.flats[0] = compute_flat(ifcs[0], self.sd) else: self.flats[0] = None # for the mangin case, the first and last surfaces are the # same. Sync the flat definitions. if self.ele_token == 'mangin': self.flats[-1] = self.flats[0] # only consider last profile for a flat if it is concave # note that "last profile" for a mangin mirror is # the middle profile in the list. is_cc_k = is_concave(profiles[k].cv, idxs[k], idxs[0], z_dir) if use_flat(self.do_flat_k, is_cc_k): self.flats[k] = compute_flat(ifcs[k], self.sd) else: self.flats[k] = None flat1_pkg = 'always', self.flats[0], is_cc_0 for i, gap in enumerate(gaps): # compute flats for inner profiles that intersect outer flats if i+1<len(gaps) and i+1 != k: self.flats[i+1] = self.compute_inner_flat(i+1, self.sd, k) is_cc_1 = is_concave(profiles[i].cv, idxs[i], idxs[i+1], z_dir) is_cc_2 = is_concave(profiles[i+1].cv, idxs[i+1], idxs[i], z_dir) if self.flats[i+1] is not None: flat2_pkg = 'always', self.flats[i+1], is_cc_2 else: flat2_pkg = None poly_list, profile_polys = render_lens_shape( ifcs[i], profiles[i], ifcs[i+1], profiles[i+1], gap.thi, self.z_dir, self.extent(), self.sd, self.is_flipped, hole_sd=self.hole_sd, flat1_pkg=flat1_pkg, flat2_pkg=flat2_pkg ) if i>0: r, t = trns.forward_transform(ifcs[i-1], zdist, ifcs[i]) t_new = np.matmul(r_prev, t) + t_prev r_new = np.matmul(r_prev, r) else: r_new, t_new = np.identity(3), np.array([0., 0., 0.]) poly_tfrmd_list = [] for polyline in poly_list: poly = np.array(polyline) poly_tfrmd = transform_poly((r_new, t_new), poly) poly_tfrmd_list.append(poly_tfrmd.tolist()) polygon_list.append(tuple(poly_tfrmd_list)) zdist = gap.thi flat1_pkg = flat2_pkg r_prev, t_prev = r_new, t_new return tuple(polygon_list)
[docs] def render_as_surfs(self): '''return a tuple of polylines of the surfaces of the cemented element. ''' ifcs = self.ifcs idxs = self.idxs profiles = self.profiles gaps = self.gaps z_dir = self.z_dir len_gaps = len(gaps) if self.ele_token == 'mangin': num_gaps = len_gaps>>1 k = num_gaps else: k = -1 polygon_list = [] # examine all profiles for possible (or required) flats. # only consider first profile for a flat if it is concave is_concave_0 = is_concave(profiles[0].cv, idxs[0], idxs[k], z_dir) 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 = is_concave(profiles[k].cv, idxs[k], idxs[0], z_dir) 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 for i, ifc in enumerate(ifcs): # compute flats for inner profiles that intersect outer flats self.flats[i] = self.compute_inner_flat(i, self.sd, k) is_concave_i = is_concave(profiles[i].cv, idxs[i], idxs[i], z_dir) if self.flats[i] is not None: flat_pkg = 'always', self.flats[i], is_concave_i else: flat_pkg = None poly_list = render_surf_shape(ifc, profiles[i], self.extent(), self.sd, self.is_flipped, hole_sd=self.hole_sd, flat_pkg=flat_pkg) if i>0: r, t = trns.forward_transform(b4_ifc, zdist, ifc) t_new = np.matmul(r_prev, t) + t_prev r_new = np.matmul(r_prev, r) else: r_new, t_new = np.identity(3), np.array([0., 0., 0.]) poly_tfrmd_list = [] for polyline in poly_list: poly = np.array(polyline) poly_tfrmd = transform_poly((r_new, t_new), poly) poly_tfrmd_list.append(poly_tfrmd.tolist()) polygon_list.append(tuple(poly_tfrmd_list)) if i<len(gaps): zdist = gaps[i].thi b4_ifc = ifc r_prev, t_prev = r_new, t_new return tuple(polygon_list)
[docs] def render_handles(self, opt_model): self.handles = {} if self.do_render_shape: shape = self.render_shape() else: shape = self.render_as_surfs() self.handles['shape'] = GraphicsHandle(shape, self.tfrm, 'polyline') if self.do_render_shape: for i, gap in enumerate(self.gaps): polygon_list = shape[i] color = calc_render_color_for_material(gap.medium) for j, poly in enumerate(polygon_list): self.handles['shape'+f"{i+1}"+f"{j+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 default_ele_token = 'thin_lens' def __init__(self, ifc=None, ele_def_pkg=None, 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 if ifc is not None: self.intrfc = ifc self.intrfc_indx = idx self.medium_name = 'Thin Element' self.ele_token = ThinElement.default_ele_token elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) 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.])) if sd is not None: self.sd = sd else: self.sd = self.intrfc.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 seq(self, **kwargs) -> SeqPath: tl_ifc = self.intrfc return [[tl_ifc, None, None, tl_ifc.ref_index, 1]]
[docs] def tree(self, **kwargs): default_tag = '#element#thinlens' tag = default_tag + kwargs.get('tag', '') tle = Node('TL', id=self, tag=tag) Node(f'i{self.intrfc_indx}', id=self.intrfc, tag='#ifc#tl', 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' if not hasattr(self, 'ele_token'): self.ele_token = ThinElement.default_ele_token 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 sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token self.intrfc_indx = idx_list[0] self.intrfc = seq_model.ifcs[idx_list[0]]
[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, 'polyline', 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 default_ele_token = 'dummy' def __init__(self, ifc=None, ele_def_pkg=None, 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.ele_token = DummyInterface.default_ele_token if ifc is not None: self.ref_ifc = ifc self.idx = idx self.profile = ifc.profile elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) self.medium_name = 'Interface' if sd is not None: self.sd = sd else: self.sd = self.ref_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' if not hasattr(self, 'ele_token'): if self.label == 'Object': self.ele_token = 'object' elif self.label == 'Image': self.ele_token = 'image' else: self.ele_token = DummyInterface.default_ele_token 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 sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token if ele_token == 'object': self.label = 'Object' elif ele_token == 'image': self.label = 'Image' self.idx = idx_list[0] self.ref_ifc = seq_model.ifcs[self.idx] self.profile = self.ref_ifc.profile
[docs] def seq(self, **kwargs) -> SeqPath: return [[self.ref_ifc, None, None, 1, 1]]
[docs] def tree(self, **kwargs): default_tag = '#dummyifc' if self.ele_token == 'object': default_tag += '#object' elif self.ele_token == 'image': default_tag += '#image' 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#di', 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 = {} shape = self.render_shape() self.handles['shape'] = GraphicsHandle(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 Space(Part): label_format = 'SP{}' serial_number = 0 default_ele_token = 'space' def __init__(self, label=None, g=None, ele_def_pkg=None, idx=0, tfrm=None, z_dir=1, **kwargs): if label is None: Space.serial_number += 1 self.label = Space.label_format.format(Space.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.])) if g is not None: self.gaps = [g] self.idxs = [idx] self.z_dir = 1 self.s1 = None self.s2 = None self.medium_name = g.medium.name() self.ele_token = Space.default_ele_token elif ele_def_pkg is not None: seq_model, ele_def = ele_def_pkg self.sync_to_ele_def(seq_model, ele_def) # self.render_color = (237, 243, 254, 64) # light blue self.render_color = (0, 243, 0, 64) # light blue self.z_dir = z_dir self.handles = {} self.actions = {} def __json_encode__(self): attrs = dict(vars(self)) del attrs['parent'] del attrs['s1'] del attrs['s2'] del attrs['gaps'] del attrs['handles'] del attrs['actions'] return attrs def __str__(self): return str(self.gaps[0])
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): self.parent = ele_model if hasattr(self, 'idx'): idx = self.idx self.idxs = [idx] self.gaps = [gaps[idx]] self.s1 = surfs[idx] self.s2 = surfs[idx+1] delattr(self, 'idx') elif hasattr(self, 'idxs'): self.gaps = [gaps[i] for i in self.idxs] self.s1 = surfs[self.idxs[0]] self.s2 = surfs[self.idxs[-1]+1] 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.gaps[0].medium.name() if not hasattr(self, 'ele_token'): self.ele_token = Space.default_ele_token self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): self.idxs = [seq_model.gaps.index(g) for g in self.gaps] idx_0 = self.idxs[0] idx_k = self.idxs[-1] self.s1 = seq_model.ifcs[idx_0] self.s2 = seq_model.ifcs[idx_k+1] self.z_dir = seq_model.z_dir[idx_0]
[docs] def sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token self.idxs = [idx for idx in gap_list] idx_0 = self.idxs[0] idx_k = self.idxs[-1] self.z_dir = seq_model.z_dir[idx_0] self.s1 = seq_model.ifcs[idx_0] self.s2 = seq_model.ifcs[idx_k+1] self.gaps = [seq_model.gaps[i] for i in gap_list] self.medium_name = self.gaps[0].medium.name()
[docs] def seq(self, **kwargs) -> SeqPath: rndx = self.gaps[0].medium.rindex('d') return [[None, self.gaps[0], None, rndx, 1]]
[docs] def tree(self, **kwargs): default_label_prefix = kwargs.get('default_label_prefix', 'SP') default_tag = kwargs.get('default_tag', '#space') if hasattr(self, 'parent'): seq_model = self.parent.opt_model['seq_model'] if self.idxs[0] == 0: default_tag += '#object' elif self.idxs[0] == len(seq_model.gaps)-1: default_tag += '#image' tag = default_tag + kwargs.get('tag', '') sp = Node(default_label_prefix, id=self, tag=tag) for i, g in enumerate(self.gaps): i1 = i + 1 t = Node(f't{i1}', id=g, tag='#thic', parent=sp) Node(f'g{self.idxs[i]}', id=(g, self.z_dir), tag='#gap', parent=t) return sp
[docs] def reference_interface(self): return None
[docs] def reference_idx(self): return self.idxs[0]
[docs] def profile_list(self): return []
[docs] def idx_list(self): return self.idxs
[docs] def gap_list(self): return self.gaps
[docs] def do_flip(self): r, t = self.tfrm thi = self.cumulative_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): if self.s1 is not None and self.s2 is not None: 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 else: return None
[docs] def extent(self): if hasattr(self, 'edge_extent'): return self.edge_extent else: return (-self.sd, self.sd)
[docs] def cumulative_thi(self): thi = 0. for g in self.gaps: thi += g.thi return thi
[docs] def render_shape(self): s1 = self.s1 s2 = self.s2 thi = self.cumulative_thi() poly_pkg, profile_polys = render_lens_shape( s1, s1.profile, s2, s2.profile, thi, self.z_dir, self.extent(), self.sd, self.is_flipped ) poly, = poly_pkg return poly
[docs] def render_handles(self, opt_model): self.handles = {} shape = self.render_shape() color = calc_render_color_for_material(self.gaps[0].medium) self.handles['shape'] = GraphicsHandle(shape, self.tfrm, 'polygon', color) poly_ct = [] poly_ct.append([0., 0.]) poly_ct.append([self.cumulative_thi(), 0.]) # Modify the tfrm to account for any decenters following # the reference ifc. decenter = opt_model.seq_model.ifcs[self.idxs[0]].decenter tfrm = self.apply_decenter_to_tfrm(decenter) self.handles['ct'] = GraphicsHandle(poly_ct, tfrm, 'polyline') return self.handles
[docs] def apply_decenter_to_tfrm(self, decenter): """ Modify the tfrm using any decenters following the reference ifc.""" tfrm = self.tfrm if decenter is not None: r_global, t_global = tfrm r_after_ifc, t_after_ifc = decenter.tform_after_surf() r = r_global if r_after_ifc is None else r_global.dot(r_after_ifc) t = r.dot(t_after_ifc) + t_global tfrm = r, t return tfrm
[docs] def handle_actions(self): self.actions = {} ct_action = {} ct_action['x'] = AttrAction(self.gaps[0], 'thi') self.actions['ct'] = ct_action return self.actions
[docs] class AirGap(Space): label_format = 'AG{}' serial_number = 0 default_ele_token = 'air' def __init__(self, g=None, ele_def_pkg=None, label=None, **kwargs): if label is None: AirGap.serial_number += 1 label = AirGap.label_format.format(AirGap.serial_number) self.ele_token = AirGap.default_ele_token if g is not None: super().__init__(g=g, label=label, **kwargs) elif ele_def_pkg is not None: super().__init__(ele_def_pkg=ele_def_pkg, label=label, **kwargs)
[docs] def sync_to_restore(self, ele_model, surfs, gaps, tfrms, profile_dict, parts_dict): if not hasattr(self, 'ele_token'): self.ele_token = AirGap.default_ele_token super().sync_to_restore(ele_model, surfs, gaps, tfrms, profile_dict, parts_dict)
[docs] def seq(self, **kwargs) -> SeqPath: return [[None, self.gaps[0], None, 1, 1]]
[docs] def tree(self, **kwargs): kwargs['default_label_prefix'] = 'AG' kwargs['default_tag'] = '#space#airgap' return super().tree(**kwargs)
[docs] def render_handles(self, opt_model): self.handles = {} poly_ct = [] poly_ct.append([0., 0.]) poly_ct.append([self.cumulative_thi(), 0.]) # Modify the tfrm to account for any decenters following # the reference ifc. decenter = opt_model.seq_model.ifcs[self.idxs[0]].decenter tfrm = self.apply_decenter_to_tfrm(decenter) self.handles['ct'] = GraphicsHandle(poly_ct, tfrm, 'polyline') return self.handles
[docs] class Assembly(Part): label_format = 'ASM{}' serial_number = 0 default_ele_token = 'asm' 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 = [p for p in part_list] self.idx = idx self.ele_token = Assembly.default_ele_token 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') if not hasattr(self, 'ele_token'): self.ele_token = Assembly.default_ele_token self.handles = {} self.actions = {}
[docs] def sync_to_seq(self, seq_model): self.tfrm = seq_model.gbl_tfrms[self.reference_idx()]
[docs] def sync_to_ele_def(self, seq_model, ele_def): ele_type, idx_list, gap_list = ele_def ele_token, ele_module, ele_class = ele_type self.ele_token = ele_token part_tree = self.parent.opt_model['part_tree'] ref_idx = idx_list[0] if len(idx_list)>0 else gap_list[0] for p in self.parts: if ref_idx == p.reference_idx(): p_node = part_tree.node(p) self.medium_name = self._construct_medium_name()
[docs] def seq(self, **kwargs) -> SeqPath: asm_seq = [] for p in self.parts: asm_seq.extend(p.seq()) return asm_seq
[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(self.label, 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() if len(idxs) > 0: self.idx = idxs[0] else: self.idx = 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)) attrs['serial_numbers'] = self.save_serial_numbers() 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 = [] for e in opt_model.parts_dict.values(): self.add_element(e) 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) if hasattr(self, 'serial_numbers'): self.restore_serial_numbers(self.serial_numbers) delattr(self, 'serial_numbers') else: 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()
[docs] def reset_serial_numbers(self): Element.serial_number = 0 Mirror.serial_number = 0 CementedElement.serial_number = 0 ThinElement.serial_number = 0 SurfaceInterface.serial_number = 0 DummyInterface.serial_number = 0 Space.serial_number = 0 AirGap.serial_number = 0 Assembly.serial_number = 0
[docs] def save_serial_numbers(self)->dict: serial_numbers = { 'Element': Element.serial_number, 'Mirror': Mirror.serial_number, 'CementedElement': CementedElement.serial_number, 'ThinElement': ThinElement.serial_number, 'SurfaceInterface': SurfaceInterface.serial_number, 'DummyInterface': DummyInterface.serial_number, 'Space': Space.serial_number, 'AirGap': AirGap.serial_number, 'Assembly': Assembly.serial_number, } return serial_numbers
[docs] def restore_serial_numbers(self, serial_numbers): Element.serial_number = serial_numbers.get('Element', 0) Mirror.serial_number = serial_numbers.get('Mirror', 0) CementedElement.serial_number = \ serial_numbers.get('CementedElement', 0) ThinElement.serial_number = serial_numbers.get('ThinElement', 0) SurfaceInterface.serial_number = \ serial_numbers.get('SurfaceInterface', 0) DummyInterface.serial_number = serial_numbers.get('DummyInterface', 0) Space.serial_number = serial_numbers.get('Space', 0) AirGap.serial_number = serial_numbers.get('AirGap', 0) Assembly.serial_number = serial_numbers.get('Assembly', 0)
[docs] def update_model(self, **kwargs): """ dynamically build element list from part_tree. """ opm = self.opt_model info = parttree.sequence_to_elements(opm['sm'], opm['em'], opm['pt']) # 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(opm['sm'])
[docs] def apply_scale_factor(self, scale_factor): """ Apply scale factor by resyncing with the sequential model. """ seq_model = self.opt_model['seq_model'] self.sync_to_seq(seq_model)
[docs] def sync_to_seq(self, seq_model): """ Update element positions and ref_idx using the sequential model. """ gbl_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 = gbl_tfrms[e.reference_idx()] r_new = np.matmul(r, rot_around_y) 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()+0.5 if isinstance(e, Space) else e.reference_idx()) # Make sure z_dir matches the sequential model. Used to get # the correct substrate offset. for e in self.elements: if hasattr(e, 'z_dir'): e.z_dir = seq_model.z_dir[e.reference_idx()]
[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__
[docs] def build_ele_sg_lists(self): part_tag = '#element#space#airgap#dummyifc' nodes = self.opt_model['part_tree'].nodes_with_tag(tag=part_tag) eles = [n.id for n in nodes] ele_list = [] ele_dict = {} seq_model = self.opt_model['seq_model'] for e in eles: ele_type, idx_list, gap_list = build_ele_def(e, seq_model) ele_list.append((ele_type, idx_list, gap_list)) ele_dict[(ele_type, idx_list, gap_list)] = e return ele_list, ele_dict
[docs] def list_ele_sg(self, part_tree, seq_model): ele_list, ele_dict = self.build_ele_sg_lists() for elem in ele_list: ele_type, idx_list, gap_list = elem e = ele_dict[elem] print(f"{e.label}: {ele_type[0]} {idx_list} {gap_list}")
[docs] def build_ele_def(e, seq_model): """Package defining element info, including effect of flipping. """ def guarded_gap_idx(g): try: return seq_model.gaps.index(g) except ValueError: return str(id(g)) ele_type = e.ele_token, type(e).__module__, type(e).__name__ if e.ele_token == 'air' or e.ele_token == 'space': idx_list = () else: idx_list = tuple(i for i in e.idx_list()) gap_list = tuple(guarded_gap_idx(g) for g in e.gap_list()) if e.is_flipped: # reverse the list contents to match sequential order idx_list = tuple(idx for idx in idx_list[::-1]) gap_list = tuple(g for g in gap_list[::-1]) return ele_type, idx_list, gap_list