Source code for rayoptics.raytr.opticalspec

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2018 Michael J. Hayford
""" Container class for optical usage information

.. Created on Thu Jan 25 11:01:04 2018

.. codeauthor: Michael J. Hayford
"""

import math
import numpy as np

from rayoptics.parax.firstorder import compute_first_order, list_parax_trace
from rayoptics.raytr.trace import aim_chief_ray
from rayoptics.optical import model_enums
import rayoptics.optical.model_constants as mc
from opticalglass.spectral_lines import get_wavelength
import rayoptics.util.colour_system as cs
from rayoptics.util.misc_math import transpose
import rayoptics.gui.util as gui_util
from rayoptics.util import colors
srgb = cs.cs_srgb


[docs]class OpticalSpecs: """ Container class for optical usage information Contains optical usage information to specify the aperture, field of view, spectrum and focal position. These can be accessed via the mapping interface: - self['wvls']: instance of :class:`~.WvlSpec` - self['pupil']: instance of :class:`~.PupilSpec` - self['fov']: instance of :class:`~.FieldSpec` - self['focus']: instance of :class:`~.FocusRange` It also maintains a repository of paraxial data. Attributes: do_aiming: if True, iterate chief rays to stop center, else entrance pupil """ do_aiming_default = True def __init__(self, opt_model, specsheet=None, **kwargs): self.opt_model = opt_model self._submodels = {} self['wvls'] = WvlSpec(**kwargs) self['pupil'] = PupilSpec(self) self['fov'] = FieldSpec(self) self['focus'] = FocusRange(0.0) self.do_aiming = OpticalSpecs.do_aiming_default if specsheet: self.set_from_specsheet(specsheet) def __getitem__(self, key): """ Provide mapping interface to submodels. """ return self._submodels[key] def __setitem__(self, key, value): """ Provide mapping interface to submodels. """ self._submodels[key] = value def __json_encode__(self): attrs = dict(vars(self)) del attrs['opt_model'] del attrs['_submodels'] del attrs['do_aiming'] attrs['spectral_region'] = self['wvls'] attrs['pupil'] = self['pupil'] attrs['field_of_view'] = self['fov'] attrs['defocus'] = self['focus'] return attrs def __json_decode__(self, **attrs): submodels = {} submodels['wvls'] = attrs['spectral_region'] submodels['pupil'] = attrs['pupil'] submodels['fov'] = attrs['field_of_view'] submodels['focus'] = (attrs['defocus'] if 'defocus' in attrs else FocusRange(0.0)) self._submodels = submodels
[docs] def listobj_str(self): o_str = self["pupil"].listobj_str() o_str += self["fov"].listobj_str() o_str += self["wvls"].listobj_str() o_str += self["focus"].listobj_str() return o_str
@property def spectral_region(self): return self._submodels['wvls'] @spectral_region.setter def spectral_region(self, sr): self._submodels['wvls'] = sr @property def pupil(self): return self._submodels['pupil'] @pupil.setter def pupil(self, pup): self._submodels['pupil'] = pup @property def field_of_view(self): return self._submodels['fov'] @field_of_view.setter def field_of_view(self, fov): self._submodels['fov'] = fov @property def defocus(self): return self._submodels['focus'] @defocus.setter def defocus(self, foc): self._submodels['focus'] = foc
[docs] def set_from_list(self, dl): self.spectral_region = dl[0] self.pupil = dl[1] self.field_of_view = dl[2]
[docs] def set_from_specsheet(self, ss): self.spectral_region.set_from_specsheet(ss) self.pupil.set_from_specsheet(ss) self.field_of_view.set_from_specsheet(ss) self.defocus.set_from_specsheet(ss)
[docs] def sync_to_restore(self, opt_model): self.opt_model = opt_model if not hasattr(self, 'do_aiming'): self.do_aiming = OpticalSpecs.do_aiming_default self['wvls'].sync_to_restore(self) self['pupil'].sync_to_restore(self) self['fov'].sync_to_restore(self)
[docs] def update_model(self, **kwargs): self.spectral_region.update_model(**kwargs) self.pupil.update_model(**kwargs) self.field_of_view.update_model(**kwargs)
[docs] def update_optical_properties(self, **kwargs): if self.opt_model.seq_model.get_num_surfaces() > 2: stop = self.opt_model.seq_model.stop_surface wvl = self.spectral_region.central_wvl self.opt_model['analysis_results']['parax_data'] = \ compute_first_order(self.opt_model, stop, wvl) if self.do_aiming: for i, fld in enumerate(self.field_of_view.fields): aim_pt = aim_chief_ray(self.opt_model, fld, wvl) fld.aim_pt = aim_pt
[docs] def lookup_fld_wvl_focus(self, fi, wl=None, fr=0.0): """ returns field, wavelength and defocus data Args: fi (int): index into the field_of_view list of Fields wl (int): index into the spectral_region list of wavelengths fr (float): focus range parameter, -1.0 to 1.0 Returns: (**fld**, **wvl**, **foc**) - **fld** - :class:`Field` instance for field_of_view[fi] - **wvl** - wavelength in nm - **foc** - focus shift from image interface """ if wl is None: wvl = self.spectral_region.central_wvl else: wvl = self.spectral_region.wavelengths[wl] fld = self.field_of_view.fields[fi] foc = self.defocus.get_focus(fr) return fld, wvl, foc
[docs] def obj_coords(self, fld): return self.field_of_view.obj_coords(fld)
[docs] def list_first_order_data(self): self.opt_model['parax_model'].first_order_data()
[docs] def list_parax_trace(self, **kwargs): list_parax_trace(self.opt_model, **kwargs)
[docs]class WvlSpec: """ Class defining a spectral region A spectral region is a list of wavelengths (in nm) and corresponding weights. The central wavelength of the spectral region is central_wvl. The index into the wavelength list for central_wvl is reference_wvl. """ def __init__(self, wlwts=[('d', 1.)], ref_wl=0, do_init=True, **kwargs): if do_init: self.set_from_list(wlwts) else: self.wavelengths = [] self.spectral_wts = [] self.reference_wvl = ref_wl self.coating_wvl = 550.0
[docs] def listobj_str(self): wvls = self.wavelengths ref_wvl = self.reference_wvl o_str = f"central wavelength={wvls[ref_wvl]} nm\n" o_str += "wavelength (weight) =" for i, wlwt in enumerate(zip(wvls, self.spectral_wts)): wl, wt = wlwt comma = "," if i > 0 else "" ref_mark = "*" if i == ref_wvl else "" o_str += comma + f"{wl:10.4f} ({wt:5.3f})" + ref_mark o_str += "\n" return o_str
@property def central_wvl(self): return self.wavelengths[self.reference_wvl] @central_wvl.setter def central_wvl(self, wvl): self.wavelengths[self.reference_wvl] = wvl
[docs] def set_from_list(self, wlwts): self.wavelengths = [] self.spectral_wts = [] for wlwt in wlwts: self.wavelengths.append(get_wavelength(wlwt[0])) self.spectral_wts.append(wlwt[1]) self.calc_colors()
[docs] def sync_to_restore(self, optical_spec): self.calc_colors()
[docs] def set_from_specsheet(self, ss): pass
[docs] def update_model(self, **kwargs): self.calc_colors()
[docs] def add(self, wl, wt): self.wavelengths.append(get_wavelength(wl)) self.spectral_wts.append(wt) self.sort_spectrum()
[docs] def sort_spectrum(self): spectrum = [[wl, wt] for wl, wt in zip(self.wavelengths, self.spectral_wts)] spectrum.sort(key=lambda w: w[0]) spectrumT = transpose(spectrum) self.wavelengths = spectrumT[0] self.spectral_wts = spectrumT[1]
[docs] def calc_colors(self): accent = colors.accent_colors() self.render_colors = [] num_wvls = len(self.wavelengths) if num_wvls == 1: self.render_colors.append(accent['green']) elif num_wvls > 1: step = 1 if self.wavelengths[0] < self.wavelengths[-1] else -1 if num_wvls == 2: c = ['blue', 'red'] elif num_wvls == 3: c = ['blue', 'green', 'red'] elif num_wvls == 4: c = ['blue', 'green', 'yellow', 'red'] elif num_wvls == 5: c = ['violet', 'cyan', 'green', 'yellow', 'red'] elif num_wvls == 6: c = ['violet', 'cyan', 'green', 'yellow', 'red', 'magenta'] else: c = ['violet', 'blue', 'cyan', 'green', 'yellow', 'red', 'magenta'] self.render_colors = [accent[clr] for clr in c[::step]]
# else: # for w in self.wavelengths: # print("calc_colors", w) # rgb = srgb.wvl_to_rgb(w) # print("rgb", rgb) # self.render_colors.append(rgb)
[docs]class PupilSpec: """ Aperture specification Attributes: key: 'aperture', 'object'|'image', 'pupil'|'NA'|'f/#' value: size of the pupil pupil_rays: list of relative pupil coordinates for pupil limiting rays ray_labels: list of string labels for pupil_rays """ default_pupil_rays = [[0., 0.], [1., 0.], [-1., 0.], [0., 1.], [0., -1.]] default_ray_labels = ['00', '+X', '-X', '+Y', '-Y'] def __init__(self, parent, key=('object', 'pupil'), value=1.0): self.optical_spec = parent self.key = 'aperture', key[0], key[1] self.value = value self.pupil_rays = PupilSpec.default_pupil_rays self.ray_labels = PupilSpec.default_ray_labels def __json_encode__(self): attrs = dict(vars(self)) del attrs['optical_spec'] return attrs
[docs] def listobj_str(self): key = self.key o_str = f"{key[0]}: {key[1]} {key[2]}; value={self.value}\n" return o_str
[docs] def sync_to_restore(self, optical_spec): self.optical_spec = optical_spec
[docs] def set_from_specsheet(self, ss): self.key, self.value = ss.get_etendue_inputs('aperture')
[docs] def get_input_for_specsheet(self): return self.key, self.value
[docs] def update_model(self, **kwargs): if not hasattr(self, 'pupil_rays'): self.pupil_rays = PupilSpec.default_pupil_rays self.ray_labels = PupilSpec.default_ray_labels
[docs] def mutate_pupil_type(self, ape_key): aperture, obj_img_key, value_key = ape_key if self.optical_spec is not None: opm = self.optical_spec.opt_model if opm['ar']['parax_data'] is not None: fod = opm['ar']['parax_data'].fod if obj_img_key == 'object': if value_key == 'pupil': self.value = 2*fod.enp_radius elif value_key == 'NA': self.value = fod.obj_na elif obj_img_key == 'image': if value_key == 'f/#': self.value = fod.fno elif value_key == 'NA': self.value = fod.img_na self.key = ape_key
[docs]class FieldSpec: """ Field of view specification Attributes: key: 'field', 'object'|'image', 'height'|'angle' value: maximum field, per the key fields: list of Field instances is_relative: if True, `fields` are relative to max field """ def __init__(self, parent, key=('object', 'angle'), value=0., flds=[0.], is_relative=False, do_init=True, **kwargs): self.optical_spec = parent self.key = 'field', key[0], key[1] self.value = value self.is_relative = is_relative if do_init: self.set_from_list(flds) else: self.fields = [] def __json_encode__(self): attrs = dict(vars(self)) del attrs['optical_spec'] return attrs
[docs] def listobj_str(self): key = self.key o_str = f"{key[0]}: {key[1]} {key[2]}; value={self.value}\n" for i, fld in enumerate(self.fields): o_str += fld.listobj_str() return o_str
[docs] def sync_to_restore(self, optical_spec): if not hasattr(self, 'is_relative'): self.is_relative = False if not hasattr(self, 'value'): self.value, _ = self.max_field() self.optical_spec = optical_spec
def __str__(self): return "key={}, max field={}".format(self.key, self.max_field()[0])
[docs] def set_from_list(self, flds): self.fields = [Field() for f in range(len(flds))] for i, f in enumerate(self.fields): f.y = flds[i] self.value, _ = self.max_field()
[docs] def set_from_specsheet(self, ss): key, value = ss.get_etendue_inputs('field') if value != 0 and len(self.fields) == 1: # just one field, add a second one for max value self.is_relative = True self.fields.append(Field(x=0, y=1)) if not self.is_relative: fld_scale = 1 if self.value == 0 else value/self.value for i, f in enumerate(self.fields): f.x *= fld_scale f.y *= fld_scale self.key, self.value = key, value
[docs] def get_input_for_specsheet(self): return self.key, self.value
[docs] def update_model(self, **kwargs): for f in self.fields: f.update() # recalculate max_field and relabel fields. # relabeling really assumes the fields are radial, specifically, # y axis only if self.is_relative: field_norm = 1 else: field_norm = 1 if self.value == 0 else 1.0/self.value self.index_labels = [] for f in self.fields: if f.x != 0.0: fldx = '{:5.2f}x'.format(field_norm*f.x) else: fldx = '' if f.y != 0.0: fldy = '{:5.2f}y'.format(field_norm*f.y) else: fldy = '' self.index_labels.append(fldx + fldy) self.index_labels[0] = 'axis' if len(self.index_labels) > 1: self.index_labels[-1] = 'edge' return self
[docs] def mutate_field_type(self, fld_key): osp = self.optical_spec parax_data = osp.opt_model['ar']['parax_data'] fod = parax_data.fod field, obj_img_key, value_key = fld_key if obj_img_key == 'object': if value_key == 'height': self.value = parax_data.pr_ray[0][mc.ht] elif value_key == 'angle': self.value = fod.obj_ang elif obj_img_key == 'image': if value_key == 'height': self.value = fod.img_ht self.key = fld_key
[docs] def obj_coords(self, fld): fld_coord = np.array([fld.x, fld.y, 0.0]) if self.is_relative: fld_coord *= self.value field, obj_img_key, value_key = self.key fod = self.optical_spec.opt_model['ar']['parax_data'].fod if obj_img_key == 'object': if value_key == 'angle': dir_tan = np.tan(np.deg2rad(fld_coord)) obj_pt = -dir_tan*(fod.obj_dist+fod.enp_dist) elif value_key == 'height': obj_pt = fld_coord elif obj_img_key == 'image': if value_key == 'height': img_pt = fld_coord obj_pt = fod.red*img_pt return obj_pt
[docs] def max_field(self): """ calculates the maximum field of view Returns: magnitude of maximum field, maximum Field instance """ max_fld = None max_fld_sqrd = -1.0 for i, f in enumerate(self.fields): fld_sqrd = f.x*f.x + f.y*f.y if fld_sqrd > max_fld_sqrd: max_fld_sqrd = fld_sqrd max_fld = i max_fld_value = math.sqrt(max_fld_sqrd) if self.is_relative: max_fld_value *= self.value return max_fld_value, max_fld
[docs]class Field: """ a single field point, largely a data container Attributes: x: x field component y: y field component vux: +x vignetting factor vuy: +y vignetting factor vlx: -x vignetting factor vly: -y vignetting factor wt: field weight aim_pt: x, y chief ray coords on the paraxial entrance pupil plane chief_ray: ray package for the ray from the field point throught the center of the aperture stop, traced in the central wavelength ref_sphere: a tuple containing (image_pt, ref_dir, ref_sphere_radius) """ def __init__(self, x=0., y=0., wt=1.): self.x = x self.y = y self.vux = 0.0 self.vuy = 0.0 self.vlx = 0.0 self.vly = 0.0 self.wt = wt self.aim_pt = None self.chief_ray = None self.ref_sphere = None def __json_encode__(self): attrs = dict(vars(self)) items = ['chief_ray', 'ref_sphere', 'pupil_rays'] for item in items: if item in attrs: del attrs[item] return attrs def __str__(self): return "{}, {}".format(self.x, self.y) def __repr__(self): return "Field(x={}, y={}, wt={})".format(self.x, self.y, self.wt)
[docs] def listobj_str(self): if self.x != 0. and self.y != 0.: o_str = (f"x={self.x}, y={self.y}" f" vlx={self.vlx:6.3f} vux={self.vux:6.3f}" f" vly={self.vly:6.3f} vuy={self.vuy:6.3f}\n") elif self.x == 0. and self.y != 0.: o_str = (f"y={self.y}" f" vly={self.vly:6.3f} vuy={self.vuy:6.3f}" f" vlx={self.vlx:6.3f} vux={self.vux:6.3f}\n") elif self.x != 0. and self.y == 0.: o_str = (f"x={self.x}" f" vlx={self.vlx:6.3f} vux={self.vux:6.3f}" f" vly={self.vly:6.3f} vuy={self.vuy:6.3f}\n") else: o_str = (f"x,y={self.y}" f" vlx={self.vlx:6.3f} vux={self.vux:6.3f}" f" vly={self.vly:6.3f} vuy={self.vuy:6.3f}\n") return o_str
[docs] def update(self): self.chief_ray = None self.ref_sphere = None
[docs] def vignetting_bbox(self, pupil_spec: PupilSpec, oversize=1.02): """ returns a bbox of the vignetted pupil ray extents. """ poly = [] for pup_ray in pupil_spec.pupil_rays: vig_pup_ray = self.apply_vignetting(pup_ray) poly.append(vig_pup_ray) vig_bbox = oversize*gui_util.bbox_from_poly(poly) return vig_bbox
[docs] def apply_vignetting(self, pupil): vig_pupil = pupil[:] if pupil[0] < 0.0: if self.vlx != 0.0: vig_pupil[0] *= (1.0 - self.vlx) else: if self.vux != 0.0: vig_pupil[0] *= (1.0 - self.vux) if pupil[1] < 0.0: if self.vly != 0.0: vig_pupil[1] *= (1.0 - self.vly) else: if self.vuy != 0.0: vig_pupil[1] *= (1.0 - self.vuy) return vig_pupil
[docs]class FocusRange: """ Focus range specification Attributes: focus_shift: focus shift (z displacement) from nominal image interface defocus_range: +/- half the total focal range, from the focus_shift position """ def __init__(self, focus_shift=0.0, defocus_range=0.0): self.focus_shift = focus_shift self.defocus_range = defocus_range def __repr__(self): return ("FocusRange(focus_shift={}, defocus_range={})" .format(self.focus_shift, self.defocus_range))
[docs] def listobj_str(self): o_str = f"focus shift={self.focus_shift}" o_str += (f", defocus range={self.defocus_range}\n" if self.defocus_range != 0. else "\n") return o_str
[docs] def set_from_specsheet(self, ss): pass
[docs] def update(self): pass
[docs] def get_focus(self, fr=0.0): """ return focus position for input focus range parameter Args: fr (float): focus range parameter, -1.0 to 1.0 Returns: focus position for input focus range parameter """ return self.focus_shift + fr*self.defocus_range