Files
fourierOptics/fourierOptics/optics.py
T

867 lines
26 KiB
Python

import numpy as np
from scipy.optimize import fsolve
from scipy import ndimage
from .data import glassCatalog
from .misc import *
from pdb import set_trace
from scipy.interpolate import interp2d
from copy import deepcopy
class LensObject:
def __init__(self, CA, name=None):
if name:
assert type(name)==str, 'No string for a name.'
self.name = name
else:
self.name = str(self)
self.CA = CA
self.type = None
self.f = None
self.sag = None
self.wavefrontError = False
self.mask = None
def add_wavefront_error(self, z):
assert len(z.shape)==2, 'Wavefront data is not 2D. Cannot continue'
self.sag = z
self.wavefrontError = True
def add_mask(self, mask):
self.mask = mask
class ParaxialLens(LensObject):
def __init__(self, f, CA, name=None):
super().__init__(CA, name)
self.f = f
self.type = 'PARAXIAL'
def find_focal_length(self):
return self.f
def phase_delay(self, x, y, l, **kwargs):
_k = 2 * np.pi / l
_t_l = np.exp(1j * _k / (2 * self.f) * (x**2 + y**2))
if self.wavefrontError:
_t_l *= np.exp(-1j * _k * self.sag / l)
if self.mask is not None:
_t_l *= self.mask
# Masking out the parts that fall outside the lens clear aperture
# dist_from_center = np.sqrt(x**2 + y**2)
# _lensMask = (dist_from_center <= (self.CA/2)).astype(int)
# _t_l *= _lensMask
return _t_l
class SphericalLens(LensObject):
'''
The SphericalLens object describes any spherical lens and
finds the phase delay for all light traveling through it.
The refractive index, of course, depends on the wavelength.
'''
def __init__(self, glass, thickness, R1, R2, CA, name=None):
super().__init__(CA, name)
assert glassCatalog.catalog.get(glass), 'Glass type not in catalog'
self.type = 'SPHERICAL'
self.thickness = thickness # mm
self.R1 = R1 # mm
self.R2 = R2 # mm
self.glass = glass
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
self.referenceWavelength = 1.35e-3 # mm
self.f = self.find_focal_length()
def set_first_surface_sag(self, x,y):
'''
Defines the shape of the first surface and bound the corresponding sag function to
the lens object.
Input:
x,y - Coordinates of the sag.
'''
# A spherical lens surface requires only the radius of curvature
_first_surface_sag = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_first_surface_sag *= _lensMask
self.first_surface_sag = _first_surface_sag
return self.first_surface_sag
def set_second_surface_sag(self, x,y):
'''
Defines the shape of the second surface and bound the corresponding sag function to
the lens object.
Input:
x,y - Coordinates of the sag.
'''
# A spherical lens surface requires only the radius of curvature
_second_surface_sag = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_second_surface_sag *= _lensMask
self.second_surface_sag = _second_surface_sag
return self.second_surface_sag
def get_first_surface_sag(self):
return self.first_surface_sag
def get_second_surface_sag(self):
return self.second_surface_sag
def find_refractive_index(self, l):
# l in mm needs to be converted to l in micron
l *= 1e3
_a = self.dispersionCoefficients
_n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1)
return _n
def find_focal_length(self):
_n = self.find_refractive_index(self.referenceWavelength)
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
return 1/_f_recip
def phase_delay(self, x, y, l, paraxial=False):
'''
This function calculates the phase delay introduced by the lens.
Input:
x, y - Coordinates of the lens
l - Wavelength in millimeters
paraxial - [bool] Apply a paraxial approximation?
'''
_k = 2 * np.pi / l
_n = self.find_refractive_index(l)
if not paraxial:
# For all non-paraxial lenses:
# Retrieve the sag of the lens surfaces for coordinates x,y
_A1 = self.set_first_surface_sag(x,y)
_A2 = self.set_second_surface_sag(x,y)
_dxy = self.thickness - _A1 + _A2
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy)
elif paraxial:
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2))
if self.wavefrontError:
_t_l *= np.exp(-1j * _k * (_n - 1) * self.sag)
if self.mask is not None:
_t_l *= self.mask
# Masking out the parts that fall outside the lens clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_t_l *= _lensMask
return _t_l
class EvenAsphereLens(LensObject):
'''
The EvenAsphereLens object describes an even aspherical lens and
finds the phase delay for all light traveling through it.
The refractive index, of course, depends on the wavelength.
'''
def __init__(self, glass, thickness, R1, R2, k1, k2, a1, a2, CA, name=None):
super().__init__(CA, name)
assert glassCatalog.catalog.get(glass), 'Glass type not in catalog'
self.type = 'EVEN ASPHERE'
self.thickness = thickness # mm
self.R1 = R1 # mm
self.R2 = R2 # mm
self.conicConstantS1 = k1
self.conicConstantS2 = k2
self.asphereCoefficientS1 = a1
self.asphereCoefficientS2 = a2
self.glass = glass
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
self.referenceWavelength = 1.35 # micron
self.f = self.find_focal_length()
@staticmethod
def calculate_surface_sag(x, y, R, k, a):
'''
Defines the sag shape of the lens
Follows the ZEMAX definition on Even Aspheres
Input parameters:
x, y - x,y coordinates of the lens in mm
R - Radius of curvature of the surface in mm
k - Conic constant of the surface
a - Coefficients of the polynomial expansion of the lens sag.
'''
_r = np.sqrt(x**2 + y**2)
_c = 1 / R
_expansion = np.sum(np.asarray([a[i] * _r**( 2*(i+1) ) for i in range(len(a))]))
_z = _c * _r**2 / ( 1 + np.sqrt( 1 - (1 + k) * _c**2 * _r**2) ) + _expansion
return _z
def set_first_surface_sag(self, x,y):
'''
Defines the shape of the first surface and bound the corresponding sag function to
the lens object.
Input:
x,y - Coordinates of the sag.
'''
# A spherical lens surface requires only the radius of curvature
_first_surface_sag = self.calculate_surface_sag(x, y, self.R1, self.conicConstantS1, self.asphereCoefficientS1)
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_first_surface_sag *= _lensMask
self.first_surface_sag = _first_surface_sag
return self.first_surface_sag
def set_second_surface_sag(self, x,y):
'''
Defines the shape of the second surface and bound the corresponding sag function to
the lens object.
Input:
x,y - Coordinates of the sag.
'''
# A spherical lens surface requires only the radius of curvature
_second_surface_sag = self.calculate_surface_sag(x, y, self.R2, self.conicConstantS2, self.asphereCoefficientS2)
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_second_surface_sag *= _lensMask
self.second_surface_sag = _second_surface_sag
return self.second_surface_sag
def get_first_surface_sag(self):
return self.first_surface_sag
def get_second_surface_sag(self):
return self.second_surface_sag
def find_refractive_index(self, l):
# l in mm needs to be converted to l in micron
l *= 1e3
_a = self.dispersionCoefficients
_n = np.sqrt( (_a[0]* l**2)/(l**2 - _a[1]) + (_a[2] * l**2) / (l**2 - _a[3]) + (_a[4] * l**2) / (l**2 - _a[5]) + 1)
return _n
def find_focal_length(self):
'''
Calculates the focal length, where we assume that the
asphere component is small, so that the focal length is
approximately the same as for the spherical lens with
radii of curvature of R1 and R2.
'''
_n = self.find_refractive_index(self.referenceWavelength)
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
return 1/_f_recip
def phase_delay(self, x, y, l, paraxial=False):
'''
This function calculates the phase delay introduced by the lens.
Input:
x, y - Coordinates of the lens
l - Wavelength in millimeters
paraxial - [bool] Apply a paraxial approximation?
'''
_k = 2 * np.pi / l
_n = self.find_refractive_index(l)
if not paraxial:
# For all non-paraxial lenses:
# Retrieve the sag of the lens surfaces for coordinates x,y
_A1 = self.set_first_surface_sag(x,y)
_A2 = self.set_second_surface_sag(x,y)
_dxy = self.thickness - _A1 + _A2
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy)
elif paraxial:
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2))
if self.wavefrontError:
_t_l *= np.exp(-1j * _k * (_n - 1) * self.sag)
if self.mask is not None:
_t_l *= self.mask
# Masking out the parts that fall outside the lens clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_lensMask = (dist_from_center <= (self.CA/2)).astype(int)
_t_l *= _lensMask
return _t_l
class MirrorObject:
'''
Describes any mirror. No wavelength dependence exist.
Coatings are not considered.
'''
def __init__(self, dx, dy, name=None):
self.dx = dx
self.dy = dy
if name:
assert type(name)==str, 'No string for a name.'
self.name = name
else:
self.name = str(self)
self.type = None
self.sag = None
self.wavefrontError = False
self.mask = None
def add_wavefront_error(self, z):
assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue'
self.sag = z
self.wavefrontError = True
def add_mask(self, mask):
self.mask = mask
class ParabolicMirror(MirrorObject):
def __init__(self, f, CA, dx=None, dy=None, name=None):
MirrorObject.__init__(self, dx=dx, dy=dy, name=name)
self.f = f
self.CA = CA
self.type = 'PARABOLIC MIRROR'
def phase_delay(self, x, y, l, **kwargs):
_k = 2 * np.pi / l
_dz = (x**2 + y**2) / (4 * self.f)
if self.wavefrontError:
_sagPhase = self.sag
_t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) )
else:
_t_m = np.exp(1j * _k * 2 * _dz)
if self.mask is not None:
_t_m *= self.mask
# Masking out the parts that fall outside the mirror clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_mirrorMask = (dist_from_center <= (self.CA/2)).astype(int)
_t_m *= _mirrorMask
return _t_m
class SphericalMirror(MirrorObject):
def __init__(self, R, CA, dx=None, dy=None, name=None):
MirrorObject.__init__(self, dx=dx, dy=dy, name=name)
self.R = R
self.f = R/2
self.CA = CA
self.type = 'SPHERICAL MIRROR'
def phase_delay(self, x, y, l, **kwargs):
_k = 2 * np.pi / l
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
if self.wavefrontError:
_sagPhase = self.sag
_t_m = np.exp(1j * _k * 2 * (_dz + _sagPhase) )
else:
_t_m = np.exp(1j * _k * 2 * _dz)
if self.mask is not None:
_t_m *= self.mask
# Masking out the parts that fall outside the mirror clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_mirrorMask = (dist_from_center <= (self.CA/2)).astype(int)
_t_m *= _mirrorMask
return _t_m
class FreeformMirror(MirrorObject):
def __init__(self, x, y, z, dx=None, dy=None, name=None):
MirrorObject.__init__(self, dx=dx, dy=dy, name=name)
self.x = x
self.y = y
self.z = z
self.type = 'USER DEFINED MIRROR'
def phase_delay(self, l, **kwargs):
_k = 2 * np.pi / l
if self.wavefrontError:
_t_m = np.exp(1j * _k * 2 * ( self.z + self.sag) )
else:
_t_m = np.exp(1j * _k * 2 * self.z)
if self.mask is not None:
_t_m *= self.mask
return _t_m
class OpticalSurface:
def __init__(self, mask=None, name=None):
self.type = 'Dummy surface'
if mask:
self.x, self.y, self.mask = mask
self.type = 'Mask'
if name:
assert type(name)==str, 'No string for a name.'
self.name = name
self.mask = None
self.wavefrontError = False
def add_mask(self, mask):
self.mask = mask
def phase_delay(self, x, y, l, ip_type='phase', kind='cubic', **kwargs):
'''
Calculate the phase delay introduced by the WFE.
The tricky part is that we want the phase delay only
in the small calculated area of the full surface.
ip_type - 'phase' or 'surface'
Defines what method to use to obtain the interpolated surface
height map. 'surface' does a normal interpolation on the height
map. This does not conserve the WFE rms value. Then, 'phase'
interpolates on the phase information in the PSD algorithm. This
does conserve the WFE rms within the window.
If 'phase', then the WavefrontError object has to be supplied,
containing the generated wavefront error.
'''
_t_l = np.ones(np.shape(x)).astype('complex128')
_k = 2 * np.pi / l
if self.wavefrontError:
if ip_type == 'surface':
ximg, yimg = self.sag_coordinates
wfeMap = interp2d(ximg[0,:], yimg[:,0], self.sag, kind=kind, **kwargs)
wavefrontErrorMap = wfeMap(x[0,:].real, y[:,0].real)
_t_l *= np.exp(1j * _k * wavefrontErrorMap )
elif ip_type == 'phase':
try:
xy = deepcopy((x,y))
xy_img = deepcopy(self.sag_coordinates)
wavefrontErrorMap = self.wfe.generate_height_map_window(
xy, xy_img, ip_type='phase', kind=kind)
_t_l *= np.exp(1j * _k * wavefrontErrorMap )
except AttributeError:
print('No WavefrontError object detected. Continuing without!')
if self.mask is not None:
_t_l *= self.mask
return _t_l
def add_wavefront_error(self, z, x, y, wfe=None, **kwargs):
assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue'
self.sag = z
# self.sag = interp2d(x, y, z, kind='cubic', **kwargs)
self.sag_coordinates = (x.real,y.real)
self.wavefrontError = True
if wfe is not None:
self.wfe = wfe
class ADCObject:
def __init__(self, wedge=(1.6554, 6.28927, -1.48306)):
self.p1 = Prism('S-LAH71', 15, wedge[0], wedge[1], CA=125, name='MCD-ADC-P1')
self.p2 = Prism('S-FPL51', 15, wedge[1], wedge[2], CA=125, name='MCD-ADC-P2')
self.p3 = Prism('S-FPL51', 15, wedge[2], wedge[1], CA=125, name='MCD-ADC-P3')
self.p4 = Prism('S-LAH71', 15, wedge[1], wedge[0], CA=125, name='MCD-ADC-P4')
self.theta = None
self.set_orientation(90)
def set_orientation(self, theta):
self.p1.set_prism_orientation(theta)
self.p2.set_prism_orientation(theta)
self.p3.set_prism_orientation(180-1*theta)
self.p4.set_prism_orientation(180-1*theta)
self.theta = theta
def set_field_rotation(self, FR):
self.p1.set_prism_orientation(self.theta-FR)
self.p2.set_prism_orientation(self.theta-FR)
self.p3.set_prism_orientation(180-1*self.theta-FR)
self.p4.set_prism_orientation(180-1*self.theta-FR)
def get_phase_delay(self, x, y, l, theta=None, paraxial=False, prism=None):
if theta is not None:
self.set_orientation(theta)
else:
theta = self.theta
if not prism:
_t_p1 = self.p1.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial)
_t_p2 = self.p2.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial)
_t_p3 = self.p3.phase_delay(x, y, l)#, theta=180-theta, paraxial=paraxial)
_t_p4 = self.p4.phase_delay(x, y, l)#, theta=180-theta, paraxial=paraxial)
return _t_p1, _t_p2, _t_p3, _t_p4
elif prism == 1:
_t_p1 = self.p1.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial)
return _t_p1
elif prism == 2:
_t_p2 = self.p2.phase_delay(x, y, l)#, theta=theta, paraxial=paraxial)
return _t_p2
elif prism == 3:
_t_p3 = self.p3.phase_delay(x, y, l)#, theta=180-1*theta, paraxial=paraxial)
return _t_p3
elif prism == 4:
_t_p4 = self.p4.phase_delay(x, y, l)#, theta=180-1*theta, paraxial=paraxial)
return _t_p4
else:
print('This prism ({}) does not exist.'.format(prism))
def add_to_optical_train(self, opticalTrainObject, d, paraxial=True):
'''
Adds the whole ADC object to an optical train object.
Because the ADC consists of multiple prisms, where each
prism has two surfaces, this is a bit more involved than
just doing opticalTrainObjec.add_object(ADC, dist=d)
Input:
opticalTrainObject - optical train object, see class OpticalTrain
d - distance in mm to next object after the ADC
paraxial - [bool] if true, each ADC only acts as a phase
transformation. No diffraction will happen
inside the prism.
'''
if not paraxial:
print('This feature has not been implemented yet. Please use paraxial mode.')
pass
if paraxial:
opticalTrainObject.add_object(self.p1, dist=d)
opticalTrainObject.add_object(self.p2, dist=1)
opticalTrainObject.add_object(self.p3, dist=8)
opticalTrainObject.add_object(self.p4, dist=1)
def calculate_dispersion(self, l1, l2, hx=0, hy=0, unit='millimeter'):
'''
Calculates the dispersion for the ADC (using geometric optics analysis)
at its current position, for a normalized on sky field angle (hx, hy).
Default field position is in the center of the FoV.
Input:
l1, l2 - Wavelength range of interest (in millimeter)
hx, hy - Field of view of the instrument (normalized between -1 and 1)
Output:
dispX, dispY - The dispersion in sky angles in milli arc seconds.
This particular ADC model is only valid for an ADC located in the pupil of the instrument.
Change the pupil ratio (Diameter_instrument_pupil / Diameter_Entrance_Pupil) accordingly
'''
if unit == 'millimeter':
l1 *= 1e3
l2 *= 1e3
elif unit == 'micron':
pass
_prismList = [self.p1, self.p2, self.p3, self.p4]
_d1x_list, _d1y_list = [], []
# _pupilRatio = 0.07943/38.542
_pupilRatio = 0.0801/38.542
hx = hx * 7.5e-3
hy = hy * 7.5e-3
ix = hx * 1/_pupilRatio
iy = hy * 1/_pupilRatio
for l in [l1,l2]:
_deltaListX = np.zeros(len(_prismList))
_deltaListY = np.zeros(len(_prismList))
for p in range(4):
_n_g = _prismList[p].find_refractive_index(l, unit='micron')
_b1x = _prismList[p].tilt1x
_b1y = _prismList[p].tilt1y
_b2x = _prismList[p].tilt2x
_b2y = _prismList[p].tilt2y
if p==0:
_t_iz_n_x = ix # Zero before first prism
_t_iz_n_y = iy # Zero before first prism
# X direction
_t_i1_n_x = _t_iz_n_x #+ _b1x
_d1x = self.calculate_deviation(_t_i1_n_x, _b1x, _b2x, _n_g)
_t_tz_n_x = _t_iz_n_x - _d1x
_t_iz_n_x = _t_tz_n_x # Set output to new input (for the next prism)
# Y direction
_t_i1_n_y = _t_iz_n_y #+ _b1y
_d1y = self.calculate_deviation(_t_i1_n_y, _b1y, _b2y, _n_g)
_t_tz_n_y = _t_iz_n_y - _d1y
_t_iz_n_y = _t_tz_n_y # Set output to new input (for the next prism)
# Adding the deviations to the list with delta terms
_deltaListX[p] = _d1x
_deltaListY[p] = _d1y
_d1x_list.append(_deltaListX.sum())
_d1y_list.append(_deltaListY.sum())
self.dispX = ( _d1x_list[1] - _d1x_list[0] ) * 3600 * 1000 * _pupilRatio
self.dispY = ( _d1y_list[1] - _d1y_list[0] ) * 3600 * 1000 * _pupilRatio
return self.dispX, self.dispY
@staticmethod
def calculate_deviation(t_i1, b1, b2, n):
'''
Returns deviation of the ray after the prism with apex angle alpha
only now using Snell's law only. Using the formula from Hagen & Tkazcyk (2011)
Units: degrees
'''
t_i1 = np.deg2rad(t_i1)
b1 = np.deg2rad(b1)
b2 = np.deg2rad(b2)
alpha = b2 - b1
delta = t_i1 - b2 - np.arcsin( n * np.sin( np.arcsin(1/n * np.sin(t_i1 - b1) ) - alpha ) )
return np.degrees(delta)
def find_optimum_angle(self, inputDispersion, l1, l2, x0=50):
'''
Simple minimizer to find the (local) minimum for a best position of the ADC.
'''
_solver = fsolve(self.optimizer, x0=x0, args=(self, inputDispersion, l1, l2))
return _solver[0]
@staticmethod
def optimizer(theta, adcObject, inputDispersion, l1, l2, FR=None):
'''
Minimization function to find the optimum rotation angle
Input:
theta - Angle in degrees relative to initial position
adcObject - the ADC class
inputDispersion - The amount of dispersion to correct for (in mas)
l1, l2 - The wavelength range to correct for (micrometer)
FR - Field rotation, not used currently.
'''
if FR:
FRdeg = np.deg2rad(FR)
inputDispersionX = np.cos(FRdeg) * inputDispersion
inputDispersionY = np.sin(FRdeg) * inputDispersion
adcObject.set_orientation(float(theta))
ADCdisp = adcObject.calculate_dispersion(l1,l2)
return np.sqrt((ADCdisp[1] - inputDispersionY)**2 + (ADCdisp[0] - inputDispersionX)**2)
else:
adcObject.set_orientation(float(theta))
ADCdisp = adcObject.calculate_dispersion(l1,l2)
ADCdisp = ADCdisp[1]
return ADCdisp - inputDispersion
class Prism:
def __init__(self, glass, thickness, tilt1, tilt2, CA, name=None):
if name:
assert type(name)==str, 'No string for a name.'
self.name = name
else:
self.name = str(self)
self.type = 'PRISM'
self.glass = glass
self.thickness = thickness
self.tilt1x = tilt1
self.tilt1y = 0
self.tilt2x = tilt2
self.tilt2y = 0
self.CA = CA
self.sag_combined = None
self.sag1 = None
self.sag2 = None
self.wavefrontError = False
self.thetaPrism = 0
self.mask = None
# Remember original tilts for reset:
self.tilt1x_OG = tilt1
self.tilt1y_OG = 0
self.tilt2x_OG = tilt2
self.tilt2y_OG = 0
def find_refractive_index(self, l, unit='millimeter'):
if unit == 'millimeter':
l *= 1e3
elif unit == 'micron':
pass
if self.glass == 'S-FPL51':
a = [1.170105050E+00, 4.757107830E-02, 7.638324450E-01]
b = [6.162039240E-03, 2.633728760E-02, 1.418826420E+02]
elif self.glass == 'S-LAH71':
a = [1.982800310E+00, 3.167584500E-01, 2.444726460E+00]
b = [1.189874560E-02, 5.271560010E-02, 2.132206970E+02]
return self.calculate_refractive_index(l, a, b, unit='micron')
def calculate_refractive_index(self, l, a, b, unit='millimeter'):
# l in mm needs to be converted to l in micron
if unit == 'millimeter':
l *= 1e3
elif unit == 'micron':
pass
n = np.sqrt( (a[0]* l**2)/(l**2 - b[0]) + (a[1] * l**2) / (l**2 - b[1]) + (a[2] * l**2) / (l**2 - b[2]) + 1)
return n
def set_prism_orientation(self, theta):
self.tilt1x, self.tilt1y = self.tilt_rotation(self.tilt1x_OG, self.tilt1y_OG, theta)
self.tilt2x, self.tilt2y = self.tilt_rotation(self.tilt2x_OG, self.tilt2y_OG, theta)
if self.wavefrontError:
if self.sag_combined is not None:
self.sag_combined = ndimage.rotate(self.sag_combined, theta, reshape=False)
elif self.sag1 is not None:
self.sag1 = ndimage.rotate(self.sag1, theta, reshape=False)
elif self.sag2 is not None:
self.sag2 = ndimage.rotate(self.sag2, theta, reshape=False)
self.thetaPrism = theta
def tilt_rotation(self, tx,ty, theta):
theta = np.deg2rad(theta-90)
tx_new = tx * np.cos(theta) - ty * np.sin(theta)
ty_new = tx * np.sin(theta) + ty * np.cos(theta)
return tx_new, ty_new
def get_surface_sag(self, x, y):
self.first_surface_sag = y * np.tan(np.deg2rad(self.tilt1x)) + x * np.tan(np.deg2rad(self.tilt1y))
self.second_surface_sag = y * np.tan(np.deg2rad(self.tilt2x)) + x * np.tan(np.deg2rad(self.tilt2y))
# Masking out the parts that fall outside the prism clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_prismMask = (dist_from_center <= (self.CA/2)).astype(int)
self.first_surface_sag *= _prismMask
self.second_surface_sag *= _prismMask
return self.first_surface_sag, -1*self.second_surface_sag
def add_wavefront_error(self, z, surface=0):
assert len(z.shape)==2, 'Wavefront data does not have the proper shape. Cannot continue'
assert surface in [0,1,2], 'Surface not understood. Use 0 for transmitted wavefront error, \
1 for surface 1 or 2 for surface 2.'
if surface == 0:
self.sag_combined = z
elif surface == 1:
self.sag1 = z
elif surface == 2:
self.sag2 = z
self.wavefrontError = True
def add_mask(self, mask):
self.mask = mask
def phase_delay(self, x, y, l, theta=None, paraxial=True):
'''
This function calculates the phase delay introduced by the lens.
Input:
x, y - Coordinates of the lens
l - Wavelength in millimeters
paraxial - [bool] Apply a paraxial approximation? In the
case of a prism, this means that the prism acts
only as a phase transformation. Diffraction
inside the prism will not take place.
'''
if theta is None:
theta = self.thetaPrism
_k = 2 * np.pi / l
_n = self.find_refractive_index(l)
# Retrieve the sag of the lens surfaces for coordinates x,y
_A1, _A2 = self.get_surface_sag(x,y)
# Masking out the parts that fall outside the lens clear aperture
dist_from_center = np.sqrt(x**2 + y**2)
_prismMask = (dist_from_center <= (self.CA/2)).astype(int)
if paraxial:
# Calculates the phase transformation of this prism.
# Diffraction inside the prism will not take place.
_dxy = self.thickness + _A1 + _A2
_t_p = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy)
if self.wavefrontError:
if self.sag_combined is not None:
_t_p *= np.exp(-1j * _k * (_n - 1) * self.sag_combined)
elif self.sag1 is not None:
_t_p *= np.exp(-1j * _k * (_n - 1) * self.sag1)
elif self.sag2 is not None:
_t_p *= np.exp(-1j * _k * (_n - 1) * self.sag2)
if self.mask is not None:
_t_p *= self.mask
# _t_p *= _prismMask
return _t_p
if not paraxial:
# Return two different phase transformations
# Option to add surface form error (or wavefront error)
# to each interface.
_t_p_s1 = np.exp(-1j * _k * (_n - 1) * _A1)
_t_p_s2 = np.exp(-1j * _k * (_n - 1) * _A2)
if self.wavefrontError:
if self.sag1 is not None:
_t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag1)
if self.sag2 is not None:
_t_p_s2 *= np.exp(-1j * _k * (_n - 1) * self.sag2)
if self.sag_combined is not None and self.sag1 is None and self.sag2 is None:
_t_p_s1 *= np.exp(-1j * _k * (_n - 1) * self.sag_combined)
# _t_p_s1 *= _prismMask
# _t_p_s2 *= _prismMask
return _t_p_s1, _t_p_s2