mirror of
https://github.com/yosata/fourierOptics.git
synced 2026-10-30 05:01:04 +00:00
867 lines
26 KiB
Python
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
|
|
|
|
|