Source code for ultrafast.core

"""Ultrafast core module

This module contains the core functionality required for performing ultrafast
optics calculations. It currently focusses on providing classes for describing
materials commonly employed in ultrafast optics.

This file is part of ultrafast. ultrafast is free software: you can
redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, either version 3 of the
License, or any later version. ultrafast is distributed in the hope that it
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details. You should have received a copy of the GNU
General Public License along with ultrafast. If not, see
<http://www.gnu.org/licenses/>.

Copyright © 2016 Marcelo J P Alcocer
"""

# 	Package Structure
# 	==================
#
# 	Aim of package is to provide tools for performing ultrafast optical
# 	calculations. These typically involve several different parameters, e.g.
# 	material properties, geometrical constructions, etc.
#
# 	Whilst eventually wish the package to encompass many of these parameters,
# 	perhaps best to start by only handling dispersive materials. These form the
# 	basis of many calculations (e.g. compression, phase-matching, etc) and are
# 	often the most annoying to work with analytically. As such, numerical tools
# 	could be of real use.
#
# 	Can keep the rest of the current classes as examples to show how to use the
# 	materials in more complex constructions (e.g. optical elements, optical
# 	constructions). Upon polishing, these can be brought into sub-packages.
#
# 	Desired functionality:
#
# 		>>> import ultrafast
# 		>>> a = ultrafast.RIIDMaterial()
# 			<ultrafast.RIIDMaterial class>
#
# 	...and eventually, something like:
#
# 		>>> import ultrafast.elements
# 		>>> a = ultrafast.elements.Prism()
# 			<ultrafast.elements.Prism class>
#
# 	Proposed package structure:
#
# 		ultrafast/							# SCM directory
# 			README.rst
# 			LICENSE.txt
# 			DEPENDENCIES.txt
# 			setup.py						# Setup file (for PyPi)
# 			ultrafast/						# Top level package (actual import)
# 				__init__.py
# 				core.py						# Core module (Material, Error, etc)
# 				...							# ...more modules...
# 				elements.py					# Optical elements (prisms, lenses, etc)
# 			tests/							# Unit tests (repo only, not imported)
# 				tests_1.py
# 				tests_2.py
# 				...
# 			docs/							# Dox (repo only, not imported)
# 				index.rst
# 			examples/						# Examples (not imported)
# 				compressor.py				# Prism compressor example
# 				phasematching.py			# Phase matching exmaple
# 				...							# ... more useful examples...
#

# Imports
from scipy.constants import pi, speed_of_light
from math import atan, sqrt, pow
import yaml
import urllib.request
from urllib.parse import urlparse


[docs]class Material: """Material class""" # ENHANCEMENT: Add Fresnel coefficients # # Magnitudes and phases of both S and P pols. Perhaps as complex values? # # ENHANCEMENT: Add dielectric constants (requires k too) name = None """Material name Name of the material. This can be used for reference, e.g. in case the instance name is not related to the material """ _range_ = None """Frequency range Property attribute. See setter and getter methods for further details. """ references = None """Reference string Reference(s) for material properties, e.g. journal articles """ comments = None """Comments""" _n = None """Dispersion function Property attribute. See setter and getter methods for further details. """ def __init__( self, n, range_, name=None, references=None, comments=None ): """Material class init :param n: Dispersion function :type n: callable :param range_: Valid range for dispersion function (low,high) :type range_: tuple :param name: Material name :type name: string :param references: Reference(s) for material properties :type references: string :param comments: Comments :type comments: string Base class describing a (dispersive) material utilized in ultrafast optics. The dispersion function *n* is a callable which takes one argument, the angular frequency in :math:`rad/fs`, and returns the refractive index at this angular frequency. The frequency range *range_* is a tuple of length 2 describing the lower and upper angular frequencies for which *n* is valid. """ self.n = n self.range_ = range_ self.name = name self.references = references self.comments = comments def _assert_frequency(self, omega): """Frequency assertion :param omega: Angular frequency in :math:`rad / fs` :type omega: numeric Asserts the angular frequency *omega* is within the valid range of the dispersion function as defined by *range_* """ if(not (self.range_[0] <= omega <= self.range_[1])): raise RangeError( omega, self.range_, "Angular frequency out of material range" ) ''' def _assert_incidence_angle(self, phi): range_ = (radians(-90), radians(90)) if(not (range_[0] < phi < range_[1])): raise RangeError( phi, range_, "Incidence angle out of range" ) pass ''' @property def range_(self): """Frequency range Range of angular frequencies (:math:`rad/fs`) over which dispersion function :attr:`n` is valid. A numeric tuple of the form (low,high). """ return(self._range_) @range_.setter def range_(self, value): '''Frequency range setter method - Asserts range length (tuple of length 2) - Asserts low-high ordering ''' # Assert length if(len(value) is not 2): raise PropertySetError( "range_", "Frequency range is not a 2-tuple" ) # Assert ordering if(value[0] > value[1]): value = value[::-1] # Set frequency range self._range_ = value @property def n(self): """Dispersion function A callable which takes one argument, the angular frequency in :math:`rad/fs`, and returns the refractive index at this angular frequency. """ return(self._n) @n.setter def n(self, value): """Dispersion function setter method - Assert callable - Prepends frequency assertion of first argument to function """ # Assert callable if(not callable(value)): raise PropertySetError( "n", "Refractive index function is not callable" ) # Set refractive index function def n(*args): self._assert_frequency(args[0]) return(value(*args)) # Set refractive index function self._n = n
[docs] def wavevector(self, omega): """Effective wavevector :param omega: Angular frequency in :math:`rad / fs` :type omega: float Returns the effective wavevector (:math:`\\omega n / c`) at the angular frequency *omega* """ # Assert frequency self._assert_frequency(omega) # Return wavevector return(omega * self.n(omega) / c)
[docs] def brewster(self, omega, inc_mat=None): """Brewster angle :param omega: Angular frequency in :math:`rad / fs` :type omega: float :param inc_mat: Incident material :type inc_mat: :class:`ultrafast.core.Material` Returns the Brewster angle for light rays incident from the material *inc_mat*. If None, *inc_mat* is assumed to be :attr:`air`. """ # Assert frequency self._assert_frequency(omega) # Define default external material if(inc_mat is None): inc_mat = air # Return brewster angle return(atan(self.n(omega) / inc_mat.n(omega)))
[docs]class RIIDMaterial(Material): """RefractiveIndex.info material class""" type_ = None """Dispersion data type String describing the dispersion data type as defined in the RefractiveIndex.info database, e.g.: ``formula 1``, ``tabulated k``, etc """ def __init__(self, db): """RIIDMaterial class init :param db: Database entry :type db: string Class describing a dispersive material catalogued in the `RefractiveIndex.info <http://www.refractiveindex.info>`_ database. An instance is built from the RefractiveIndex.info database entry *db*. Entries are stored as YAML files. As such, *db* may be a path to a local YAML file, or a URL to a remote YAML file accessible via HTTP. """ # YAML keys keys = { "entry": { "data": "DATA", "ref": "REFERENCES", "com": "COMMENTS" }, "data": { "type": "type", "range": "range", "coeff": "coefficients" } } # Fetch remote YAML database entry if(urlparse(db).scheme != ""): file = urllib.request.urlopen(db) # Fetch local YAML database entry else: file = open(db, newline="\r\n") entry = yaml.safe_load(file) file.close() # Extract dispersion function and range n = None range_ = None if((keys["entry"]["data"] in entry)): for datum in entry[keys["entry"]["data"]]: self.type_ = datum[keys["data"]["type"]] # Analytical dispersion if(self.type_.startswith("formula")): # Parse range range_ = [ frequency(float(x)) for x in reversed(datum[keys["data"]["range"]].split()) ] # Parse coefficients coefficients = [ float(x) for x in datum[keys["data"]["coeff"]].split() ] # Define dispersion function frequency wrapper def frequency_wrap(fun_of_lambda): def fun_of_omega(omega): # self._assert_frequency(omega) return(fun_of_lambda(wavelength(omega))) return(fun_of_omega) # Construct frequency wrapped dispersion function formula = int(self.type_.split()[1]) if formula == 1: @frequency_wrap def n(lambda_): """Formula 1 - Sellmeier (preferred)""" n2 = 1 + coefficients[0] for x in [ coefficients[i:i + 2] for i in range(1, len(coefficients), 2) ]: n2 += ( (x[0] * pow(lambda_, 2)) / (pow(lambda_, 2) - pow(x[1], 2)) ) return(sqrt(n2)) elif formula == 2: @frequency_wrap def n(lambda_): """Formula 2 - Sellmeier-2""" n2 = 1 + coefficients[0] for x in [ coefficients[i:i + 2] for i in range(1, len(coefficients), 2) ]: n2 += ( (x[0] * pow(lambda_, 2)) / (pow(lambda_, 2) - x[1]) ) return(sqrt(n2)) elif formula == 3: @frequency_wrap def n(lambda_): """Formula 3 - Polynomial""" n2 = coefficients[0] for x in [ coefficients[i:i + 2] for i in range(1, len(coefficients), 2) ]: n2 += x[0] * pow(lambda_, x[1]) return(sqrt(n2)) elif formula == 4: @frequency_wrap def n(lambda_): """Formula 4 - RefractiveIndex.info""" n2 = coefficients[0] for x in [ coefficients[i:i + 4] for i in [1, 5] ]: n2 += ( (x[0] * pow(lambda_, x[1])) / (pow(lambda_, 2) - pow(x[2], x[3])) ) for x in [ coefficients[i:i + 2] for i in range(9, len(coefficients), 2) ]: n2 += x[0] * pow(lambda_, x[1]) return(sqrt(n2)) elif formula == 5: @frequency_wrap def n(lambda_): """Formula 5 - Cauchy""" n = coefficients[0] for x in [ coefficients[i:i + 2] for i in range(1, len(coefficients), 2) ]: n += x[0] * pow(lambda_, x[1]) return(n) elif formula == 6: @frequency_wrap def n(lambda_): """Formula 6 - Gases""" n = 1 + coefficients[0] for x in [ coefficients[i:i + 2] for i in range(1, len(coefficients), 2) ]: n += ( x[0] / (x[1] - pow(lambda_, -2)) ) return(n) elif formula == 7: @frequency_wrap def n(lambda_): """Formula 7 - Herzberger""" n = coefficients[0] n += coefficients[1] / (pow(lambda_, 2) - 0.028) n += coefficients[2] * pow(pow(lambda_, 2) - 0.028, -2) for i, x in enumerate(coefficients[3:], 1): n += x * pow(lambda_, 2 * i) return(n) elif formula == 8: @frequency_wrap def n(lambda_): """Formula 8 - Retro""" alpha = ( coefficients[0] + ( (coefficients[1] * pow(lambda_, 2)) / (pow(lambda_, 2) - coefficients[2]) ) + (coefficients[3] * pow(lambda_, 2)) ) n2 = -(((2 * alpha) + 1) / (alpha - 1)) return(sqrt(n2)) elif formula == 9: @frequency_wrap def n(lambda_): """Formula 9 - Exotic""" n2 = coefficients[0] n2 += ( coefficients[1] / (pow(lambda_, 2) - coefficients[2]) ) n2 += ( (coefficients[3] * (lambda_ - coefficients[4])) / (pow(lambda_ - coefficients[4], 2) + coefficients[5]) ) return(sqrt(n2)) else: raise RangeError( formula, (1, 9), "RIID dispersion formula out of range" ) # Break out of datum loop once dispersion function found break # Tabulated dispersion elif(self.type_.startswith("tabulated n")): # ENHANCEMENT: RIIDMaterial: Tabulated data handling # - Range given by table extrema # - Interpolate print("Tabulated dispersion function") # Break out of datum loop once dispersion function found break # No dispersion function found (neither formula nor tabulated) if(n is None): raise UltrafastError("No dispersion data found in RIID entry") # Extract references references = None if(keys["entry"]["ref"] in entry): references = entry[keys["entry"]["ref"]] # Extract comments comments = None if(keys["entry"]["com"] in entry): comments = entry[keys["entry"]["com"]] # Call Material constructor Material.__init__( self, n, range_, name=db, references=references, comments=comments )
[docs]class UltrafastError(Exception): """Ultrafast module base exception class Ultrafast module base exception class """ pass
[docs]class RangeError(UltrafastError): """Out of range exception """ def __init__(self, value, valid, message): """Out of range exception :param value: Invalid value :type value: float, int :param valid: Valid range :type valid: tuple :param message: Error message :type message: string Raised when the numeric value *value* is found to be out of the valid range defined by *valid*. *valid* is a tuple of length 2 in the form of (low,high). """ self.value = value self.valid = valid self.message = message def __str__(self): return( "{}. Value: {}. Valid range: {}".format( self.message, self.value, self.valid ) )
[docs]class PropertySetError(UltrafastError): """Property set exception""" def __init__(self, property_, message): """Property set exception :param property_: Property attribute :type property_: string :param message: Error message :type message: string Raised when a attempting to set the property attribute *property_* to an invalid value. """ self.property_ = property_ self.message = message def __str__(self): return( "{}. Property: {}.".format( self.message, self.property_ ) )
def _converter(value): """Wavelength <-> angular frequency conversion :param value: Wavelength in :math:`\\mu m` or angular frequency in :math:`rad / fs` :type value: float General method for conversion between wavelength and angular frequency. """ return(c * 2 * pi / value)
[docs]def frequency(lambda_): """Wavelength to angular frequency conversion :param lambda_: Wavelength in :math:`\\mu m` :type lambda_: float Wavelength to angular frequency conversion. Returns the corresponding angular frequency in :math:`rad / fs` """ return(_converter(lambda_))
[docs]def wavelength(omega): """Angular frequency to wavelength conversion :param omega: Angular frequency in :math:`rad / fs` :type omega: float Angular frequency to wavelength conversion. Returns the corresponding wavelength in :math:`\\mu m` """ return(_converter(omega))
c = speed_of_light * (1e-9) """Speed of light Speed of light in :math:`\\mu m / fs`. Defined for convenience """ air = RIIDMaterial( "http://refractiveindex.info/database/other/mixed%20gases/air/Ciddor.yml" ) """Air :class:`ultrafast.core.RIIDMaterial` describing air (Ciddor 1996). Defined for convenience """