Source code for rayoptics.seq.medium

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2017 Michael J. Hayford
""" Module building on :mod:`opticalglass` for ray-optics material support

.. Created on Fri Sep 15 17:06:17 2017

.. codeauthor: Michael J. Hayford
"""
import json
import difflib
import logging

import deprecation
import rayoptics

from scipy.interpolate import interp1d

from rayoptics.util.misc_math import isanumber

from opticalglass import glass as cat_glass
from opticalglass import glassfactory as gfact
from opticalglass.glassfactory import create_glass
from opticalglass import glasserror
from opticalglass import util
from opticalglass.spectral_lines import get_wavelength
from opticalglass.buchdahl import Buchdahl2

from opticalglass import opticalmedium as om
from opticalglass import modelglass as mg
from opticalglass import rindexinfo as rii

logger = logging.getLogger(__name__)


[docs]def glass_encode(n: float, v: float) -> str: return f'{int(1000*round((n - 1), 3)):3d}.{int(round(10*v, 3)):3d}'
[docs]def glass_decode(gc: float) -> float: return round(1.0 + (int(gc)/1000), 3), round(100.0*(gc - int(gc)), 3)
[docs]def decode_medium(*inputs, **kwargs): """ Input utility for parsing various forms of glass input. The **inputs** can have several forms: - **refractive_index, v-number**: float -> :class:`opticalglass.modelglass.ModelGlass` - **refractive_index** only: float -> :class:`opticalglass.opticalmedium.ConstantIndex` - **glass_name, catalog_name** as 1 or 2 strings - an instance with a `rindex` attribute - **air**: str -> :class:`opticalglass.opticalmedium.Air` - blank -> defaults to :class:`opticalglass.opticalmedium.Air` """ if 'cat_list' in kwargs: cat = kwargs['cat_list'] else: cat = None mat = None logger.debug(f"num inputs = {len(inputs)}, inputs[0] = {inputs[0]}, " f"{type(inputs[0])}") if isanumber(inputs[0]): # assume all args are numeric if len(inputs) <= 1: if inputs[0] == 1.0: mat = om.Air() else: mat = om.ConstantIndex(inputs[0], f"n:{inputs[0]:.3f}") else: if inputs[0] == 1.0: mat = om.Air() elif inputs[1] == '': mat = om.ConstantIndex(inputs[0], f"n:{inputs[0]:.3f}") else: mat = mg.ModelGlass(inputs[0], inputs[1], '') elif isinstance(inputs[0], str): # string args num_str_args = 0 for tkn in inputs: if isinstance(tkn, str) and len(tkn) > 0: num_str_args += 1 if num_str_args == 2: name, cat = inputs[0].strip(), inputs[1].strip() elif num_str_args == 1: if inputs[0].upper() == 'AIR': mat = om.Air() elif cat is not None: name = inputs[0] else: name, cat = inputs[0].split(',') name, cat = name.strip(), cat.strip() elif num_str_args == 0: mat = om.Air() if mat is None: try: mat = gfact.create_glass(name, cat) except glasserror.GlassNotFoundError as gerr: logger.info('%s glass data type %s not found', gerr.catalog, gerr.name) logger.info('Replacing material with air.') mat = om.Air() # glass instance args. if they respond to `rindex`, they're in elif hasattr(inputs[0], 'rindex'): mat = inputs[0] if mat: logger.info(f"mat = {mat.name()}, {mat.catalog_name()}, {type(mat)}") return mat
# --- glass finder base class
[docs]class GlassHandlerBase(): """Base class for glass matching capability. This class is used to match catalog glasses to input glass names. It is implemented as a class for ease of use by file importers. If the glass can be matched up with an existing :mod:`opticalglass` catalog, the glass is instantiated and entered into the model. If the glass cannot be found, a search for a .smx file of the same name as the model file is made. If found, it is a JSON file with a dict that provides an eval() string to create an instance to replace the missing glass name. If this file isn't found, it is created and contains a JSON template of a dict that has the missing glass names as keys; the values are the number of times the glass occurs in the file. These values should be replaced with the desired eval() string to create a replacement glass. Subclasses, e.g. used for different importers, should implement a single method that can be called during the import process to return a glass instance given an input string. """ def __init__(self, filename): self.glass_catalogs = [] self.glasses_not_found = util.Counter() self.track_contents = util.Counter() self.filename = None if filename: self.filename = filename.with_suffix('.smx') self.glasses_not_found = self.load_replacements(self.filename) self.no_replacements = not self.glasses_not_found
[docs] def load_replacements(self, filename): glasses_not_found = util.Counter() if filename.exists(): with filename.open('r') as file: glasses_not_found = json.load(file) return glasses_not_found
[docs] def save_replacements(self): """If unfound glasses, write smx template file. """ if self.track_contents['glass not found'] > 0: if self.glasses_not_found and self.filename: fname = self.filename.name.rsplit('.', 1)[0] fname += '_tmpl.smx' self.filename = self.filename.with_name(fname) with self.filename.open('w') as file: json.dump(self.glasses_not_found, file)
[docs] def find_glass(self, name, catalog, always=True): """ find `name` glass or a substitute or, if always is True, n=1.5 """ try: if catalog is None or len(catalog) == 0: catalog = gfact._cat_names medium = create_glass(name, catalog) except glasserror.GlassNotFoundError: pass else: self.track_contents['glass found'] += 1 return medium if self.no_replacements: medium = self.find_substitute_glass(name) if medium is not None: self.track_contents['glass substituted'] += 1 return medium medium = self.handle_glass_not_found(name) if medium is None and always is True: self.track_contents['glass not found'] += 1 medium = om.ConstantIndex(1.5, 'not '+name) return medium
[docs] def find_6_digit_code(self, name): """ process `name` as a 6 digit glass code""" if isanumber(name): if len(name) == 6: # process as a 6 digit code, no decimal point nd = 1 + float(name[:3])/1000 vd = float(name[3:])/10 medium = mg.ModelGlass(nd, vd, name) self.track_contents['6 digit code'] += 1 return medium else: return None
[docs] def find_substitute_glass(self, name): """Try to find a similar glass to ``name``.""" # create a list of catalogs # the original lookup didn't find anything so # look in all of our catalogs cat_names = [gc.upper() for gc in self.glass_catalogs] for gc in gfact._cat_names_uc: if gc not in cat_names: cat_names.append(gc) # create a glass list for the given catalogs glist = [] for glass_cat in cat_names: try: glass_cat = gfact.get_glass_catalog(glass_cat) except glasserror.GlassCatalogNotFoundError: pass else: glist += glass_cat.glass_list # Add legacy glasses glist += cat_glass.Robb1983Catalog().glass_list # decode the input name gn_decode = cat_glass.decode_glass_name(name) # build an uppercase version for comparisons gn_decode_uc = gn_decode[0][0].upper() + gn_decode[0][1] subs_glasses = [] for g in glist: gn_decode, gn, gc = g if gn_decode_uc == gn_decode[0][0].upper() + gn_decode[0][1]: subs_glasses.append(g) if len(subs_glasses): possibilities = [gn for gn_decode, gn, gc in subs_glasses] matches = difflib.get_close_matches(name, possibilities) if len(matches) > 0: gn = matches[0] gc = next((g[2] for g in subs_glasses if g[1] == gn), None) else: gn_decode, gn, gc = subs_glasses[0] medium = create_glass(gn, gc) self.glasses_not_found[name] = gn, gc return medium else: return None
[docs] def handle_glass_not_found(self, name): """Record missing glasses or create new replacement glass instances.""" if self.no_replacements: # track the number of times self.glasses_not_found[name] += 1 # each missing glass is used return None else: # create a new instance of the replacement glass if name in self.glasses_not_found: val = self.glasses_not_found[name] if len(val) == 2: # call create_glass with name and catalog gn, gc = val return create_glass(gn, gc) else: # eval code to create a new glass instance mat = eval(self.glasses_not_found[name]) if hasattr(mat, 'convert_to_OG'): mat = mat.convert_to_OG() return mat else: return None
# --- material definitions class Medium: """ Constant refractive index medium. """ @deprecation.deprecated(deprecated_in="0.8.4", removed_in="0.9.0", current_version=rayoptics.__version__, details="Use opticalglass.opticalmedium." "ConstantIndex instead") def __init__(self, nd, lbl, cat=''): self.label = lbl self.n = nd self._catalog_name = cat def __repr__(self): return ('Medium(' + str(self.n) + ', ' + f"'{self.label}'" + ', cat=' + f"'{self._catalog_name}'" + ')') def convert_to_OG(self): """ returns an equivalent material from the :mod:`opticalglass` package. """ return om.ConstantIndex(self.n, self.label, cat=self._catalog_name) def name(self): return self.label def catalog_name(self): return self._catalog_name def rindex(self, wv_nm): """ returns the interpolated refractive index at wv_nm Args: wv_nm: the wavelength in nm for the refractive index query Returns: float: the refractive index at wv_nm """ return self.n class Air(Medium): """ Optical definition for air (low fidelity definition) """ @deprecation.deprecated(deprecated_in="0.8.4", removed_in="0.9.0", current_version=rayoptics.__version__, details="Use opticalglass.opticalmedium." "Air instead") def __init__(self): self.label = 'air' self.n = 1.0 def __repr__(self): return 'Air()' def convert_to_OG(self): """ returns an equivalent material from the :mod:`opticalglass` package. """ return om.Air() def name(self): return self.label def catalog_name(self): return '' class Glass(Medium): """ Optical medium defined by a glass code, i.e. index - V number pair """ @deprecation.deprecated(deprecated_in="0.8.4", removed_in="0.9.0", current_version=rayoptics.__version__, details="Use opticalglass.modelglass." "ModelGlass instead") def __init__(self, nd=1.5168, vd=64.17, mat='', cat=''): self.label = mat self._catalog_name = cat self.n = nd self.v = vd self.bdhl_model = Buchdahl2(self.n, self.v) def __str__(self): return 'Glass ' + self.label + ': ' + glass_encode(self.n, self.v) def __repr__(self): return ('Glass(nd=' + str(self.n) + ', vd=' + str(self.v) + ', mat=' + f"'{self.label}'" + ', cat=' + f"'{self._catalog_name}'" + ')') def convert_to_OG(self): """ returns an equivalent material from the :mod:`opticalglass` package. """ return mg.ModelGlass(self.n, self.v, self.label, cat=self._catalog_name) def sync_to_restore(self): if not hasattr(self, 'bdhl_model'): self.bdhl_model = Buchdahl2(self.n, self.v) def glass_code(self): return str(1000*round((self.n - 1), 3) + round(self.v/100, 3)) def name(self): if self.label == '': return glass_encode(self.n, self.v) else: return self.label def rindex(self, wv_nm): return self.bdhl_model.rindex(wv_nm) def update(self, nd, vd): self.n = nd self.v = vd self.bdhl_model.update_model(nd, vd) class InterpolatedGlass(): """ Optical medium defined by a list of wavelength/index pairs Attributes: label: required string identifier for the material wvls: list of wavelenghts in nm, used as x axis rndx: list of refractive indices corresponding to the values in wvls rindex_interp: the interpolation function """ @deprecation.deprecated(deprecated_in="0.8.4", removed_in="0.9.0", current_version=rayoptics.__version__, details="Use opticalglass.opticalmedium." "InterpolatedMedium instead") def __init__(self, label, pairs=None, rndx=None, wvls=None, cat=''): self.label = label self._catalog = cat if pairs is not None: self.wvls = [] self.rndx = [] for w, n in pairs: self.wvls.append(w) self.rndx.append(n) else: self.wvls = wvls self.rndx = rndx self.update() def __repr__(self): return ('InterpolatedGlass(' + f"'{self.label}'" + ', cat=' + f"'{self._catalog}'" + ', wvls=' + repr(self.wvls) + ', rndx=' + repr(self.rndx) + ')') def convert_to_OG(self): """ returns an equivalent material from the :mod:`opticalglass` package. """ return om.InterpolatedMedium(self.label, rndx=self.rndx, wvls=self.wvls, cat=self._catalog) def __json_encode__(self): attrs = dict(vars(self)) del attrs['rindex_interp'] return attrs def sync_to_restore(self): """ rebuild interpolating function """ self.update() def update(self): self.rindex_interp = interp1d(self.wvls, self.rndx, kind='cubic', assume_sorted=False) def glass_code(self): nd = self.rindex('d') nF = self.rindex('F') nC = self.rindex('C') vd = (nd - 1)/(nF - nC) return str(glass_encode(nd, vd)) def name(self): if self.label == '': return self.glass_code() else: return self.label def catalog_name(self): """ returns the glass catalog name """ return self._catalog def rindex(self, wv_nm): """ returns the interpolated refractive index at wv_nm Args: wvl: either the wavelength in nm or a string with a spectral line identifier. for the refractive index query Returns: float: the refractive index at wv_nm Raises: KeyError: if ``wvl`` is not in the spectra dictionary """ return float(self.rindex_interp(get_wavelength(wv_nm)))