mirror of
https://github.com/yosata/fourierOptics.git
synced 2026-10-29 20:51:10 +00:00
Initial commit
This commit is contained in:
1 parent
91aeef5a90
commit
f4cc304953
23 files changed
+8948
-27
No files matched your search
+3
-27
@@ -1,30 +1,6 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
.DS_Store
|
||||
__pychache__/
|
||||
.ipynb_checkpoints/
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
from .fourier import *
|
||||
from .gaussian import *
|
||||
from .opticalTrain import *
|
||||
from .optics import *
|
||||
from .profiles import *
|
||||
from .psfFitter import *
|
||||
from .wavefrontErrors import *
|
||||
from .misc import *
|
||||
Executable
BIN
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env Python3
|
||||
|
||||
# Pyton dictionary with some glass types
|
||||
|
||||
catalog = {
|
||||
|
||||
'N-BK7' : [1.039612120E+00, 6.000698670E-03, 2.317923440E-01, 2.001791440E-02, 1.010469450E+00, 1.035606530E+02 ],
|
||||
'SiO2' : [0.6961663, 0.0684043, 0.4079426, 0.1162414, 0.8974794, 9.896161 ],
|
||||
'F2' : [1.345333590E+00, 9.977438710E-03, 2.090731760E-01, 4.704507670E-02, 9.373571620E-01, 1.118867640E+02 ],
|
||||
'S-FPL51' : [1.170105050E+00, 6.162039240E-03, 4.757107830E-02, 2.633728760E-02, 7.638324450E-01, 1.418826420E+02 ],
|
||||
'S-LAH71' : [1.982800310E+00, 1.189874560E-02, 3.167584500E-01, 5.271560010E-02, 2.444726460E+00, 2.132206970E+02 ],
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def plane_wave_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates=None):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(xin))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(xin))
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2) # Factor to scale the intensity after the FFTs
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=_dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=_dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phase_exponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
# _y, _x = np.mgrid[-_n / 2 * _dpix : _n / 2 * _dpix : 1j*_n, -_m / 2 * _dpix : _m / 2 * _dpix : 1j * _m]
|
||||
_y, _x = yin, xin
|
||||
|
||||
if outputCoordinates is not None:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*_dpix, _n/2*_dpix, _n)
|
||||
_y = np.linspace(-_m/2*_dpix, _m/2*_dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputCoordinates[0],outputCoordinates[1],_n)
|
||||
_yi = np.linspace(outputCoordinates[2],outputCoordinates[3],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputCoordinates[0]:outputCoordinates[1]:1j*_n, outputCoordinates[2]:outputCoordinates[3]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x.real), np.max(_x.real), np.min(_y.real), np.max(_x.real)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x.real, _y.real])
|
||||
|
||||
|
||||
def fresnel_propagation(self, A, xin, yin, dz=None, oversample=None, show=False, save=False,
|
||||
scaleInputArray=False, outputCoordinates=None, apply_interpolation=True):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beam_radius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
elif outputCoordinates is not None:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = xin[0]
|
||||
_yip = yin[:,0]
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
if outputCoordinates and apply_interpolation:
|
||||
_f_ip_real = interpolate.interp2d(_x.real, _y.real, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x.real, _y.real, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x.max()/_xip.max())*(_y.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
elif outputCoordinates and not apply_interpolation:
|
||||
_arraySelection = self.select_subsection(_x, _y, outputCoordinates)
|
||||
_xip = _x.real[_arraySelection[0]:_arraySelection[1]+1]
|
||||
_yip = _y.real[_arraySelection[2]:_arraySelection[3]+1]
|
||||
_E_out_ip = _E_out[_arraySelection[0]:_arraySelection[1]+1,_arraySelection[2]:_arraySelection[3]+1]
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
else:
|
||||
_E_out_ip = _E_out
|
||||
_xip, _yip = _x, _y
|
||||
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x.real, _y.real])
|
||||
|
||||
|
||||
def freq_array(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phase_exponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussian_beam_fit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussian_beam_fit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
def optical_transfer_function(self, PSF, x, y, dpix=None):
|
||||
'''
|
||||
Calculates the optical transfer function of the system response
|
||||
Input is the image on the detector, preferably of a PSF
|
||||
The frequencies are given over the whole provided range, but are
|
||||
typically limited to 1 / (lambda * F/#).
|
||||
'''
|
||||
if dpix is None:
|
||||
dpix = np.mean(np.diff(x.real))
|
||||
if dpix < 1e-99:
|
||||
dpix = np.mean(np.diff((x.real).T))
|
||||
print('dpix is {}'.format(dpix))
|
||||
# FFT input
|
||||
_A_in = pyfftw.empty_aligned((PSF.shape), dtype='complex128')
|
||||
_A_in[:] = PSF
|
||||
|
||||
# Output of the FFT
|
||||
_A_out = pyfftw.empty_aligned(PSF.shape, dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_A_in), _A_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_A_out = np.fft.fftshift(_A_out)
|
||||
|
||||
# Normalise the output
|
||||
_OTF = _A_out / np.max(_A_out)
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(x)[0], d=dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(y)[0], d=dpix))
|
||||
return _OTF, _fx, _fy
|
||||
|
||||
|
||||
def modulation_transfer_function(self, *args):
|
||||
'''
|
||||
Calculates the modulation transfer function of the system response
|
||||
Input is the image on the detector, preferably of a PSF
|
||||
The frequencies are given over the whole provided range, but are
|
||||
typically limited to 1 / (lambda * F/#).
|
||||
'''
|
||||
_OTF, _fx, _fy = self.optical_transfer_function(*args)
|
||||
return np.abs(_OTF), _fx, _fy
|
||||
|
||||
|
||||
def encircled_energy_function(self, PSF, x, y, x0=0, y0=0, dpix=None, originalPSF=None):
|
||||
'''
|
||||
Function that returns the encircled energy as a function of distance
|
||||
from the centroid position, defined by x0 and y0.
|
||||
|
||||
Parameters:
|
||||
PSF - Intensity map of the PSF
|
||||
x,y - x and y coordinates corresponding to the PSF
|
||||
x0,y0 - Optional; location of the centroid. If not given,
|
||||
x0=0 and y0=0 are assumed.
|
||||
dpix - Optional; pixel scale, default=1
|
||||
|
||||
Returns:
|
||||
_EEF - Array with encircled energy values
|
||||
_r - Array with radius values
|
||||
|
||||
'''
|
||||
if dpix is None:
|
||||
dpix = np.mean(np.diff(x))
|
||||
if dpix < 1e-99:
|
||||
dpix = np.mean(np.diff(x.T))
|
||||
|
||||
if originalPSF is None:
|
||||
_total_energy = np.sum(PSF)
|
||||
else:
|
||||
_total_energy = np.sum(originalPSF)
|
||||
_r = np.sqrt((x.real-x0)**2 + (y.real-y0)**2)
|
||||
_r_ls = np.linspace(0,_r.max(),len(_r))
|
||||
_mask = [ _r <= i for i in _r_ls]
|
||||
_EEF = np.asarray([np.sum(PSF[m]) for m in _mask])/_total_energy
|
||||
_r = _r_ls * dpix
|
||||
return _EEF, _r
|
||||
|
||||
@staticmethod
|
||||
def find_nearest(array, value):
|
||||
array = np.asarray(array)
|
||||
diff = (np.abs(array - value)).argmin()
|
||||
idx = np.unravel_index(diff, array.shape)
|
||||
return idx
|
||||
|
||||
def select_subsection(self, x, y , outputCoordinates):
|
||||
select_1 = np.max(self.find_nearest(x, outputCoordinates[0]))
|
||||
select_2 = np.max(self.find_nearest(x, outputCoordinates[1]))
|
||||
select_3 = np.max(self.find_nearest(y, outputCoordinates[2]))
|
||||
select_4 = np.max(self.find_nearest(y, outputCoordinates[3]))
|
||||
return [select_1, select_2, select_3, select_4]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,113 @@
|
||||
import numpy as np
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
|
||||
def fundamental_mode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beam_radius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phase_shift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
|
||||
def beam_radius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
|
||||
def phase_shift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
|
||||
def relative_power(self, r, z):
|
||||
return np.exp(-2 * (r / self.beam_radius(z))**2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GaussianBeamPropagation(GaussianBeam):
|
||||
def __init__(self, gaussianBeamObject):
|
||||
self.inputBeam = gaussianBeamObject
|
||||
self.l = self.inputBeam.l
|
||||
self.z_c = self.inputBeam.z_c
|
||||
self.theta_0 = self.inputBeam.theta_0
|
||||
self.w0 = self.inputBeam.w0
|
||||
self.fwhm = self.inputBeam.fwhm
|
||||
self.beam_width = self.w0
|
||||
|
||||
|
||||
def propagate_with_a_lens(self, ABCD, d_out, n, m, d_in=0, rmax=None):
|
||||
'''
|
||||
Propagates the gaussian beam through a lens with the ray transfer matrix ABCD.
|
||||
This follows the equations of Ch. 3.3 of Goldsmith's Quasi-optical Systems
|
||||
|
||||
Parameters:
|
||||
ABCD - Ray transfer matrix, use ABCD_paraxial_lens to calculate
|
||||
this matrix for a paraxial lens, with focal length f.
|
||||
d_out - Distance after the lens over which the beam is propagated.
|
||||
d_in - Distance from the beam waist (w0) to the lens. Default is 0.
|
||||
rmax - Maximum radius for which to calculate the beam profile.
|
||||
If None, it will be set to three times the beam width at
|
||||
the output distance.
|
||||
|
||||
Output:
|
||||
The beam information. Can be plotted using imshow()
|
||||
The beam width at the given point can be retrieved by using self.beam_width
|
||||
|
||||
'''
|
||||
_A,_B,_C,_D = ABCD
|
||||
_focalDistance = - ( (_A * d_in + _B) * (_C * d_in + _D) + _A * _C * self.z_c**2 ) / ( (_C * d_in + _D)**2 + _C**2 * self.z_c**2 )
|
||||
_z = d_out - _focalDistance
|
||||
|
||||
if d_out > _focalDistance:
|
||||
d_l = d_out - _focalDistance
|
||||
d_out = _focalDistance
|
||||
_w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2)
|
||||
_z_c_new = np.pi * _w0_out**2 / self.l
|
||||
_wl_out = _w0_out * np.sqrt(1 + (d_l/_z_c_new)**2)
|
||||
|
||||
# _q_out = ((_A + _C * d_out )*1j*self.z_c + (_A + _C * d_out)*d_in + (_B + _D*d_out)) / (_C * 1j * self.z_c + _C * d_in + _D)
|
||||
_w0_out = self.w0 / np.sqrt((_C * d_in + _D)**2 + _C**2 * self.z_c**2)
|
||||
|
||||
self.z_c = np.pi * _w0_out**2 / self.l
|
||||
self.w0 = _w0_out
|
||||
self.theta_0 = self.l / (np.pi * _w0_out)
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
self.beam_width = self.beam_radius(_z)
|
||||
|
||||
if rmax is None:
|
||||
rmax = 3 * self.beam_width
|
||||
|
||||
_y, _x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
_r = np.sqrt(_x**2 + _y**2)
|
||||
if _z != 0:
|
||||
_w = self.beam_radius(_z)
|
||||
_R = self.curvature(_z)
|
||||
_phi0 = self.phase_shift(_z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
|
||||
def ABCD_paraxial_lens(self, f):
|
||||
return [1, 0, -1/f, 1]
|
||||
@@ -0,0 +1,95 @@
|
||||
import numpy as np
|
||||
import pickle
|
||||
|
||||
'''
|
||||
Miscellaneous functions that do not belong specifically to any of the
|
||||
other files.
|
||||
|
||||
Author: Joost van den Born
|
||||
Contact: born@astron.nl
|
||||
'''
|
||||
|
||||
|
||||
def perform_zero_padding(A, xs, ys):
|
||||
'''
|
||||
Add zero padding around the array
|
||||
|
||||
Input:
|
||||
A - 2D-Input array
|
||||
xs,ys - The new shape after padding.
|
||||
|
||||
Output:
|
||||
- Array with the new padded zeros
|
||||
|
||||
'''
|
||||
assert A.shape[0] <= xs and A.shape[1] <= ys, 'New size is smaller than array size.'
|
||||
x_margin = int((xs - A.shape[0])/2)
|
||||
y_margin = int((ys - A.shape[1])/2)
|
||||
return np.pad(A, (x_margin, y_margin))
|
||||
|
||||
def find_nearest(array, value):
|
||||
'''
|
||||
For a value it finds the nearest element in an array
|
||||
Returns the index of that element in the original
|
||||
array.
|
||||
'''
|
||||
array = np.asarray(array)
|
||||
diff = (np.abs(array - value)).argmin()
|
||||
idx = np.unravel_index(diff, array.shape)
|
||||
return idx
|
||||
|
||||
def find_maximum(array):
|
||||
'''
|
||||
Finds the index of the maximum of the given array
|
||||
'''
|
||||
idx = np.unravel_index(array.argmax(), array.shape)
|
||||
return idx
|
||||
|
||||
def select_subsection(x, y , outputCoordinates):
|
||||
'''
|
||||
Returns the indices of a subsection of an x,y meshgrid
|
||||
'''
|
||||
select_1 = find_nearest(x, outputCoordinates[0])[1]
|
||||
select_2 = find_nearest(x, outputCoordinates[1])[1]
|
||||
select_3 = find_nearest(y, outputCoordinates[2])[0]
|
||||
select_4 = find_nearest(y, outputCoordinates[3])[0]
|
||||
return [select_1, select_2, select_3, select_4]
|
||||
|
||||
def perform_extrapolation(x, y, n, m):
|
||||
'''
|
||||
Performs linear extrapolation of a meshgrid to a new
|
||||
shape n,m
|
||||
|
||||
Returns the new extrapolated x,y meshgrid
|
||||
'''
|
||||
|
||||
# Determine properties of x,y meshgrid
|
||||
xshape = x.shape[0]
|
||||
yshape = y.shape[0]
|
||||
dpix = (np.abs(np.min(x)) + np.abs(np.max(x))) / (xshape-1)
|
||||
|
||||
# Extrapolate from, and include, the old x,y meshgrid
|
||||
xnew = np.linspace(-(n-1)/2*dpix, (n-1)/2*dpix, n)
|
||||
ynew = np.linspace(-(m-1)/2*dpix, (m-1)/2*dpix, m)
|
||||
xnew, ynew = np.meshgrid(xnew, ynew)
|
||||
return xnew, ynew
|
||||
|
||||
|
||||
def save_variable(filename, variable):
|
||||
'''
|
||||
Dumps the given variable to a binary pickle file
|
||||
for later use.
|
||||
'''
|
||||
with open(filename, 'wb') as f:
|
||||
pickle.dump(variable, f, protocol=-1 )
|
||||
|
||||
|
||||
def load_variable(filename):
|
||||
'''
|
||||
Loads a binary pickle file for use.
|
||||
'''
|
||||
with open(filename, 'rb') as f:
|
||||
variable = pickle.load(f)
|
||||
return variable
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import numpy as np
|
||||
from tabulate import tabulate
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
|
||||
def print_lens_data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def fresnel_number(self, a, z, l):
|
||||
return a**2 / (z * l)
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_fou = FourierOptics(l=l)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
oC = [x.min(), x.max(), y.min(), y.max()]
|
||||
show = False
|
||||
_wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[0], oversample=None, show=show, save=False, outputCoordinates=oC)
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
|
||||
for _lens in self.opticalTrain[:-1]: # Except for image plane
|
||||
|
||||
title = f"{self.opticalTrain[:-1].index(_lens)+1:02d}"
|
||||
print (' Calculating lens {0} of {1}'.format(title, f"{len(self.opticalTrain[:-1]):02d}"), end="\r")
|
||||
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i+1]
|
||||
_wf *= _lens.phase_delay(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number(_lens.CA, _dz, l)
|
||||
_F = 1
|
||||
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
if _lens == self.opticalTrain[-2]:
|
||||
oC = [-2, 2, -2, 2]
|
||||
show=True
|
||||
_wf, x, y = _fou.fresnel_propagation(_wf, x, y, dz=self.sequentialDistance[_i+1], oversample=None, show=show, save=False, outputCoordinates=oC)
|
||||
elif _F >= 5:
|
||||
print('Using Plane Wave!')
|
||||
# Angular spectrum (p2p) propagation
|
||||
# _wf, x, y = self.plane_wave_propagation()
|
||||
|
||||
pass
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
@@ -0,0 +1,773 @@
|
||||
import numpy as np
|
||||
from scipy.optimize import fsolve
|
||||
from .data import glassCatalog
|
||||
|
||||
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
|
||||
|
||||
def add_wavefront_error(self, z):
|
||||
assert len(z.shape)==2, 'Wavefront data is not 2D. Cannot continue'
|
||||
self.sag = z
|
||||
self.wavefrontError = True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
def phase_delay(self, x, y, l, **kwargs):
|
||||
return np.ones(np.shape(x))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
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, theta):
|
||||
# self.set_prism_orientation(theta)
|
||||
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 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, theta)
|
||||
|
||||
# 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)
|
||||
|
||||
# _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
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# from .__init__ import __data_dir__
|
||||
import numpy as np
|
||||
import os
|
||||
from astropy.io import fits
|
||||
from scipy import interpolate
|
||||
|
||||
|
||||
|
||||
class Profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def top_hat(self, r, n, m, dpix, oversample=1, dx=0, dy=0):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
|
||||
def obscured_top_hat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spider_top_hat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
def gaussian(self, r, n, m, dpix, oversample=1, dx=0, dy=0):
|
||||
'''
|
||||
Creates a grid of n x m points, following a gaussian function
|
||||
where the radius r is the FWHM of the gaussian shape.
|
||||
'''
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt((i-dx)**2 + (j-dy)**2)
|
||||
return np.exp(-((j-dx)**2 + (i-dy)**2) / r**2)
|
||||
|
||||
|
||||
def ELT_pupil(self, n, m, dpix):
|
||||
# Find data directory
|
||||
__pkg_dir__ = os.path.dirname(os.path.realpath(__file__))
|
||||
__data_dir__ = os.path.join(__pkg_dir__, 'data')
|
||||
_fits_image = __data_dir__ + '/ELT_M1_pupil_profile.fits'
|
||||
_ELT_pupil = fits.open(_fits_image)[0].data
|
||||
_ELT_inner_diam = 38542 # all glass diameter of M1 in mm
|
||||
_dpix_fits = _ELT_inner_diam / 623 # pixels on the diameter filled with 'glass'
|
||||
_resx, _resy = np.shape(_ELT_pupil)
|
||||
_xf = np.linspace(-_resx/2*_dpix_fits, _resx/2*_dpix_fits, _resx)
|
||||
_yf = np.linspace(-_resy/2*_dpix_fits, _resy/2*_dpix_fits, _resy)
|
||||
_f_pupil = interpolate.interp2d(_xf, _yf, _ELT_pupil, kind='linear')
|
||||
_x = np.linspace(-n/2*dpix, n/2*dpix, n)
|
||||
_y = np.linspace(-m/2*dpix, m/2*dpix, m)
|
||||
_ELT_pupil = _f_pupil(_x, _y)
|
||||
return _ELT_pupil
|
||||
@@ -0,0 +1,255 @@
|
||||
import numpy as np
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
import types
|
||||
import scipy.optimize as opt
|
||||
from scipy.special import j1
|
||||
|
||||
|
||||
class PSFFitter:
|
||||
'''
|
||||
Wrapper around scipy.optimize.curve_fit(), so that point spread functions can be fitted
|
||||
using some common profiles (e.g. Gaussian, Moffat and Airy pattern profiles).
|
||||
'''
|
||||
def __init__(self, x, y, psfdata):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.psfdata = psfdata
|
||||
|
||||
|
||||
def gaussian_PSF(self, xy, amplitude, x0, y0, sigma_x, sigma_y, theta, offset):
|
||||
'''
|
||||
Elliptical 2D gaussian profile
|
||||
|
||||
Input:
|
||||
xy - Combined x,y coordinates. x and y should be a meshgrid
|
||||
amplitude - amplitude of the profile, i.e. maximum of the profile
|
||||
x0, y0 - shift in x and y of the central peak
|
||||
sigma_x - width of the profile in x
|
||||
sigma_y - width of the profile in y
|
||||
theta - Rotation of the elliptical gaussian profile
|
||||
offset - noise floor or sky background level
|
||||
'''
|
||||
_x,_y = xy
|
||||
_x0 = float(x0)
|
||||
_y0 = float(y0)
|
||||
_a = (np.cos(theta)**2)/(2*sigma_x**2) + (np.sin(theta)**2)/(2*sigma_y**2)
|
||||
_b = -(np.sin(2*theta))/(4*sigma_x**2) + (np.sin(2*theta))/(4*sigma_y**2)
|
||||
_c = (np.sin(theta)**2)/(2*sigma_x**2) + (np.cos(theta)**2)/(2*sigma_y**2)
|
||||
_g = offset + amplitude*np.exp( - (_a*((_x-_x0)**2) + 2*_b*(_x-_x0)*(_y-_y0) + _c*((_y-_y0)**2)))
|
||||
return _g.ravel()
|
||||
|
||||
|
||||
def fit_gaussian_PSF(self, initialGuess=None):
|
||||
'''
|
||||
Fit a gaussian PSF profile, a Moffat PSF profile and an Airy pattern
|
||||
An OptimizeWarning is given when the best fit is reached, for some reason;
|
||||
see also:
|
||||
https://stackoverflow.com/questions/41725377/why-isnt-curve-fit-able-to-estimate-the-covariance-of-the-parameter-if-the-pa
|
||||
|
||||
Input:
|
||||
initialGuess - Tuple, containing (amplitude, x0, y0, sigma_x, sigma_y, theta, offset)
|
||||
'''
|
||||
_popt, _pcov = opt.curve_fit(self.gaussian_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess)
|
||||
return self.gaussian_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov
|
||||
|
||||
|
||||
def moffat_PSF(self, xy, amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta):
|
||||
'''
|
||||
Elliptical moffat distribution, often used to fit a PSF without diffraction rings
|
||||
Equation taken from http://www.aspylib.com/doc/aspylib_fitting.html
|
||||
As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned
|
||||
here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html
|
||||
|
||||
Input:
|
||||
xy - Combined x,y coordinates. x and y should be a meshgrid
|
||||
amplitude - amplitude of the profile, i.e. maximum of the profile
|
||||
x0, y0 - shift in x and y of the central peak
|
||||
fwhm1, fwhm2 - width of the profile in perpendicular directions
|
||||
theta - Rotation of the elliptical moffat profile
|
||||
offset - noise floor or sky background level
|
||||
beta - shape parameter of the moffat profile
|
||||
'''
|
||||
_x,_y = xy
|
||||
_x0 = float(x0)
|
||||
_y0 = float(y0)
|
||||
_alpha_1 = fwhm1 / (2 * np.sqrt(2**(1/beta) - 1))
|
||||
_alpha_2 = fwhm2 / (2 * np.sqrt(2**(1/beta) - 1))
|
||||
_a = (np.cos(theta)/_alpha_1)**2 + (np.sin(theta)/_alpha_2)**2
|
||||
_b = (np.sin(theta)/_alpha_1)**2 + (np.cos(theta)/_alpha_2)**2
|
||||
_c = np.sin(theta) * np.cos(theta) * (1/(_alpha_1**2) - 1/(_alpha_2**2))
|
||||
_m = offset + amplitude/((1 + _a*(_x-_x0)**2 + _b*(_y-_y0)**2 + 2*_c*(_x-_x0)*(_y-_y0))**beta)
|
||||
return _m.ravel()
|
||||
|
||||
|
||||
def fit_moffat_PSF(self, initialGuess=None):
|
||||
'''
|
||||
Fit a Moffat PSF profile
|
||||
As beta goes to infinity, the moffat profile approaches a gaussian; as mentioned
|
||||
here https://www.gnu.org/software/gnuastro/manual/html_node/PSF.html
|
||||
This might raise a RuntimeWarning.
|
||||
|
||||
Input:
|
||||
initialGuess - Tuple, containing (amplitude, x0, y0, fwhm1, fwhm2, theta, offset, beta)
|
||||
'''
|
||||
_popt, _pcov = opt.curve_fit(self.moffat_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess)
|
||||
return self.moffat_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov
|
||||
|
||||
|
||||
def airy_PSF(self, xy, amplitude, x0, y0, offset, radius):
|
||||
'''
|
||||
2D Airy pattern with offset included
|
||||
|
||||
Input:
|
||||
xy - Combined x,y coordinates. x and y should be a meshgrid
|
||||
amplitude - amplitude of the profile, i.e. maximum of the profile
|
||||
x0, y0 - shift in x and y of the central peak
|
||||
offset - noise floor or sky background level
|
||||
'''
|
||||
_x,_y = xy
|
||||
_x0 = float(x0)
|
||||
_y0 = float(y0)
|
||||
_r = np.sqrt((_x-_x0)**2 + (_y-_y0)**2)
|
||||
_Rz = 1.2196698912665045
|
||||
_inp = (np.pi * _r) / (radius/_Rz)
|
||||
_J1 = j1(_inp)
|
||||
_fraction = np.nan_to_num(2*_J1 / _inp, nan=1.0)
|
||||
_a = offset + amplitude * (_fraction)**2
|
||||
return _a.ravel()
|
||||
|
||||
|
||||
def fit_airy_PSF(self, initialGuess=None):
|
||||
'''
|
||||
Repackaging of scipy's curve_fit to fit an Airy pattern to the data.
|
||||
No arguments are necessary, but an initial guess of the parameters
|
||||
is recommended.
|
||||
|
||||
Input:
|
||||
initialGuess - Tuple, containing (amplitude, x0, y0, offset, radius)
|
||||
'''
|
||||
_popt, _pcov = opt.curve_fit(self.airy_PSF, (self.x,self.y), self.psfdata.flatten(), p0=initialGuess)
|
||||
return self.airy_PSF((self.x,self.y), *_popt).reshape(self.x.shape), _popt, _pcov
|
||||
|
||||
|
||||
def reference_PSF(self, p, xy, perturbedPSF):
|
||||
'''
|
||||
Residual function used for the mimizer functions in
|
||||
self.fit_custom_field(), see function below.
|
||||
'''
|
||||
_x0, _y0 = p
|
||||
_x, _y = xy
|
||||
_a = self.f_Eref_real(_x - _x0, _y - _y0) + 1j * self.f_Eref_imag(_x - _x0, _y - _y0)
|
||||
_coupling = self.get_coupling(_a, perturbedPSF)
|
||||
return 1 - np.abs(_coupling)**2
|
||||
|
||||
|
||||
def fit_custom_field(self, Eref, Eper=None, method='local', niter_success=10, bounds=None, return_coupling=False):
|
||||
'''
|
||||
Fit any custom PSF to the data (self.psfdata). It requires an input
|
||||
of the custom PSF in the same shape as the data. Then a global
|
||||
optimization algorithm (scipy.optimization.differential_evolution)
|
||||
is performed to find the translation parameters; x0 and y0.
|
||||
|
||||
Note:
|
||||
The current implementation is not able to find other parameters,
|
||||
such as amplitude, amplitude offset or radius. It assumes that
|
||||
the custom PSF and data are quite similar in shape.
|
||||
Also note that what is here called the PSF is actually the complex
|
||||
field distribution of the PSF, i.e. not the intensity, but the
|
||||
amplitude and phase information of the PSF.
|
||||
|
||||
Input:
|
||||
Eref - Custom PSF, including phase information
|
||||
Eper - optional, perturbed PSF. By default self.psfdata is taken.
|
||||
method - Optimization strategy to use. Choose between
|
||||
'local'
|
||||
'global'
|
||||
'local' first applies a local search and is very sensitive
|
||||
to the initial guess.
|
||||
'global' searches a global minimum. This is less sensitive
|
||||
to the initial guess, but requires much more computing
|
||||
time.
|
||||
niter_succes- From scipy.optimize.basinhopping:
|
||||
Stop the run if the global minimum candidate remains the
|
||||
same for this number of iterations.
|
||||
bounds - Bounds for the local minimizer
|
||||
|
||||
Returns:
|
||||
_best_match - Best matched PSF amplitude and phase data.
|
||||
_popt - Optimum parameters, i.e. translation of the centroid.
|
||||
_pvar - Variances of the parameters.
|
||||
_coupling - Optional; coupling factor
|
||||
|
||||
'''
|
||||
|
||||
assert Eref.shape == self.psfdata.shape, 'The shape of the new (Eper) and reference PSF (Eref) do not match.'
|
||||
# Set x and y coordinates for interpolation. No 2D meshgrid is necessary.
|
||||
_x = self.x[0]
|
||||
_y = self.y[:,0]
|
||||
|
||||
# Check for custom psf data and custom bounds
|
||||
if Eper is None:
|
||||
Eper = self.psfdata
|
||||
print('taken self.psfdata')
|
||||
|
||||
# Create interpolation function to match amplitude and phase to x and y coordinates.
|
||||
self.f_Eref_real = interpolate.interp2d( _x, _y, Eref.real, kind='cubic')
|
||||
self.f_Eref_imag = interpolate.interp2d( _x, _y, Eref.imag, kind='cubic')
|
||||
|
||||
# Determining a good initial guess
|
||||
# Reasonable to assume >0.9 of the maximum amplitude is near the centroid position.
|
||||
_max_amplitude = np.max(np.abs(Eref))
|
||||
_index_i, _index_j = np.where(np.abs(Eref) > (0.9 * _max_amplitude) )
|
||||
_x0_guess = np.mean(self.x[_index_i, _index_j])
|
||||
_y0_guess = np.mean(self.y[_index_i, _index_j])
|
||||
_p0 = [_x0_guess, _y0_guess]
|
||||
|
||||
if method == 'global':
|
||||
# Arguments for basinhopping function (i.e. global search function)
|
||||
_min_kwargs = {"args":((_x,_y), Eper), "jac":False}
|
||||
_popt = opt.basinhopping(self.reference_PSF, _p0, minimizer_kwargs=_min_kwargs, niter_success=niter_success)
|
||||
_p0 = _popt.x # Set optimal parameters as new first guess.
|
||||
method = 'local' # Switch to local minimzer to also calculate the uncertainties
|
||||
|
||||
if method == 'local':
|
||||
# Mimizer function using L-BFGS-B method.
|
||||
if bounds is None:
|
||||
bounds = [(_x.min(), _x.max()),(_y.min(), _y.max())]
|
||||
_res = opt.minimize(self.reference_PSF, _p0, bounds=bounds, args=((_x,_y), Eper) )
|
||||
_popt = _res.x
|
||||
|
||||
# Getting the uncertainty in the minimum
|
||||
_ftol = 2.220446049250313e-09 # ftol assuming L-BFGS-B method in the minimizer.
|
||||
_hess_inv_i = np.diag(_res.hess_inv.todense())
|
||||
_pvar = max(1, abs(_res.fun)) * _ftol * _hess_inv_i
|
||||
|
||||
# Centroid coordinates
|
||||
_x0_opt, _y0_opt = _popt
|
||||
_best_match = self.f_Eref_real(_x - _x0_opt, _y - _y0_opt) + 1j * self.f_Eref_imag(_x - _x0_opt, _y - _y0_opt)
|
||||
|
||||
if return_coupling:
|
||||
_coupling = self.get_coupling(_best_match, Eper)
|
||||
return _best_match, _popt, _pvar, _coupling
|
||||
else:
|
||||
return _best_match, _popt, _pvar
|
||||
|
||||
|
||||
def get_coupling(self, Eref, Eper):
|
||||
'''
|
||||
Calculates the coupling integral as given by eq. 6.5 in
|
||||
Quasi-optical systems by Goldsmith.
|
||||
|
||||
This complex value is a measure of the match between a reference
|
||||
field and a perturbed field. If the complex coupling factor is c,
|
||||
then |c|**2 is the degradation of the perturbed field relative to
|
||||
the reference field, not unlike the Strehl ratio.
|
||||
|
||||
Parameters:
|
||||
Eref - Array; Reference field
|
||||
Eper - Array; Perturbed field
|
||||
|
||||
Returns:
|
||||
_c - Complex float; Coupling factor
|
||||
'''
|
||||
_c = np.sum(Eref * np.conj(Eper))/ np.sqrt(np.sum(np.abs(Eref)**2)*np.sum(np.abs(Eper)**2))
|
||||
return _c
|
||||
@@ -0,0 +1,506 @@
|
||||
import numpy as np
|
||||
import pyfftw
|
||||
import types
|
||||
from math import ceil
|
||||
from scipy.special import factorial, j1
|
||||
|
||||
|
||||
class WavefrontError:
|
||||
'''
|
||||
This class contains methods to describe wavefront errors;
|
||||
either by means of zernike polynomials (description and fits)
|
||||
or by means of Power Spectral Density analysis.
|
||||
'''
|
||||
def __init__(self, n, m):
|
||||
self.n = n # Resolution in x-direction
|
||||
self.m = m # Resolution in y-direction
|
||||
self.resolution = (n,m)
|
||||
|
||||
|
||||
def calculate_rnm(self,n,m,rho):
|
||||
'''
|
||||
@author: Tibor Agocs
|
||||
Return an array with the zernike Rnm polynomial calculated at rho points.
|
||||
|
||||
|
||||
**ARGUMENTS:**
|
||||
|
||||
=== ==========================================
|
||||
n n order of the Zernike polynomial
|
||||
m m order of the Zernike polynomial
|
||||
rho Matrix containing the radial coordinates.
|
||||
=== ==========================================
|
||||
|
||||
.. note:: For rho>1 the returned value is 0
|
||||
|
||||
.. note:: Values for rho<0 are silently returned as rho=0
|
||||
|
||||
'''
|
||||
|
||||
if (n-m)%2!=0:
|
||||
raise Exception('n-m must be even')
|
||||
if abs(m)>n:
|
||||
raise Exception('The following must be true |m|<=n')
|
||||
|
||||
if(n==0 and m==0):
|
||||
data=np.ones(rho.shape)
|
||||
return data
|
||||
_mask = rho > 1
|
||||
rho[_mask] = 0
|
||||
_Rnm = np.zeros(rho.shape)
|
||||
_S = (n-np.abs(m))/2
|
||||
for s in range (0,int(_S+1)):
|
||||
_CR =(-1)**s*factorial(n-s)/ \
|
||||
(factorial(s)*factorial(-s+(n+np.abs(m))/2)* \
|
||||
factorial(-s+(n-np.abs(m))/2))
|
||||
_p = _CR * ( rho**(n-2*s) )
|
||||
_Rnm = _Rnm + _p
|
||||
_Rnm[_mask] = 0
|
||||
return _Rnm
|
||||
|
||||
|
||||
def zernike_polynomial_polar(self,n,m,rho,theta):
|
||||
'''
|
||||
@author: Tibor Agocs
|
||||
Returns the an array with the Zernike polynomial evaluated in the rho and
|
||||
theta.
|
||||
|
||||
**ARGUMENTS:**
|
||||
===== ==========================================
|
||||
n n order of the Zernike polynomial
|
||||
m m order of the Zernike polynomial
|
||||
rho Matrix containing the radial coordinates.
|
||||
theta Matrix containing the angular coordinates
|
||||
===== ==========================================
|
||||
|
||||
.. note:: For rho>1 the returned value is 0
|
||||
|
||||
.. note:: Values for rho<0 are silently returned as rho=0
|
||||
|
||||
**RETURNS**
|
||||
===== ===========================================
|
||||
_Zmn Array with values of the Zernike polynomial
|
||||
===== ===========================================
|
||||
'''
|
||||
|
||||
_Rnm = self.calculate_rnm(n,m,rho)
|
||||
|
||||
# Normalization constant
|
||||
_delta_m0 = bool(m==0)
|
||||
_NC = np.sqrt(2*(n+1)/(1+_delta_m0))
|
||||
|
||||
if m >= 0:
|
||||
_Zmn = _NC * _Rnm * np.cos(m*theta)
|
||||
elif m < 0:
|
||||
_Zmn = _NC * _Rnm * np.sin(m*theta)
|
||||
return _Zmn
|
||||
|
||||
|
||||
def zernike_polar(self, coefficient, rho, theta, noll=False, rescale='auto'):
|
||||
'''
|
||||
Returns the full surface created from a list of coefficients,
|
||||
using the Zernike Polynomials.
|
||||
|
||||
rho must be between 0 and 1, if not it will be rescaled,
|
||||
see also zernikePolynomialPolar()
|
||||
|
||||
**ARGUMENTS:**
|
||||
============ ==========================================
|
||||
coefficient n order of the Zernike polynomial
|
||||
rho Matrix containing the radial coordinates.
|
||||
theta Matrix containing the angular coordinates
|
||||
noll Boolean to indicate to use noll indices
|
||||
============ ==========================================
|
||||
|
||||
**RETURNS**
|
||||
**RETURNS**
|
||||
===== ===========================================
|
||||
_zP Array with values of the Zernike polynomial
|
||||
===== ===========================================
|
||||
'''
|
||||
if rescale == 'auto':
|
||||
if np.max(rho) > np.sqrt(2):
|
||||
print('Rescaling!')
|
||||
rho = np.sqrt(2) * rho / np.max(rho)
|
||||
|
||||
_termList = []
|
||||
for i in range(len(coefficient)):
|
||||
_C = coefficient[i]
|
||||
if noll:
|
||||
_jTerm = self.convert_noll_to_standard(i+1)
|
||||
_n,_m = self.i_to_nm(i)
|
||||
_termList.append( _C * self.zernike_polynomial_polar(_n,_m,rho,theta) )
|
||||
_zP = sum(_termList)
|
||||
return _zP
|
||||
|
||||
|
||||
def zernike_cartesian(self, coefficient, x, y, noll=False, addToData=False, **kwargs):
|
||||
'''
|
||||
Converts the cartesian grid x,y into polar
|
||||
coordinates and returns the data, such that
|
||||
it can be plotted with e.g. plt.imshow()
|
||||
|
||||
**ARGUMENTS:**
|
||||
|
||||
============ ==========================================
|
||||
coefficient n order of the Zernike polynomial
|
||||
rho Matrix containing the radial coordinates.
|
||||
theta Matrix containing the angular coordinates
|
||||
noll Boolean to indicate to use noll indices
|
||||
============ ==========================================
|
||||
|
||||
**RETURNS**
|
||||
===== ===========================================
|
||||
_zP Array with values of the Zernike polynomial
|
||||
===== ===========================================
|
||||
'''
|
||||
_x0 = (np.max(x) - np.min(x)) / 2 + np.min(x)
|
||||
_y0 = (np.max(y) - np.min(y)) / 2 + np.min(y)
|
||||
_rho = np.sqrt(( x - _x0 )**2 + ( y - _y0 )**2 )
|
||||
_theta = np.arctan2( ( y - _y0 ) , ( x - _x0 ) )
|
||||
_zP = self.zernike_polar(coefficient, _rho, _theta, noll, **kwargs)
|
||||
if not addToData:
|
||||
self.data = _zP
|
||||
else:
|
||||
self.data += _zP
|
||||
self.rms = np.sqrt(np.mean(np.asarray(self.data)**2))
|
||||
self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data))
|
||||
return _zP
|
||||
|
||||
|
||||
def i_to_nm(self, i):
|
||||
'''
|
||||
Converts a single index to n and m for the Zernike Polynomials
|
||||
'''
|
||||
n = ceil((-3+np.sqrt(9+8*i))/2)
|
||||
m = 2 * i - n*(n+2)
|
||||
return int(n),int(m)
|
||||
|
||||
|
||||
def convert_noll_to_standard(self, j):
|
||||
'''
|
||||
|
||||
'''
|
||||
conversionList = [1, 3, 2, 5, 4, 6, 9, 7, 8, 10, 15, 13, 11, 12, 14, 21, 19, 17, 16, 18, 20, 27, 25, 23, 22, 24, 26, 28, 35, 33, 31, 29, 30, 32, 34, 36, 45, 43, 41, 39, 37, 38, 40, 42, 44, 55, 53, 51, 49, 47, 46, 48, 50, 52, 54, 65, 63, 61, 59, 57, 56, 58, 60, 62, 64, 66, 77, 75, 73, 71, 69, 67, 68, 70, 72, 74, 76, 78, 91, 89, 87, 85, 83, 81, 79, 80, 82, 84, 86, 88, 90, 105, 103, 101, 99, 97, 95, 93, 92, 94, 96, 98, 100, 102, 104, 119, 117, 115, 113, 111, 109, 107, 106, 108, 110, 112, 114, 116, 118, 120]
|
||||
i = conversionList.index(j)
|
||||
return i
|
||||
|
||||
|
||||
def fit_zernike_coefficients(self, data, x=None, y=None, modes=21):
|
||||
'''
|
||||
Fits the zernike coefficients to a data set, using
|
||||
that the zernike modes can be treated independently.
|
||||
The modes are set following standard index notation.
|
||||
We solve by an analytical least squares fit
|
||||
Z * A = F
|
||||
Z.T * Z * A = Z.T * F
|
||||
A = (Z.T * Z)^-1 * Z.T * F
|
||||
|
||||
**ARGUMENTS:**
|
||||
|
||||
===== ==========================================
|
||||
data array; the data that needs fitting
|
||||
x array; the x-coordinates of the data
|
||||
y array; the y-coordinates of the data
|
||||
modes integer; number of modes to be fitted
|
||||
===== ==========================================
|
||||
|
||||
**RETURNS**
|
||||
===== ===========================================
|
||||
_A Array with values of the Zernike polynomial
|
||||
===== ===========================================
|
||||
'''
|
||||
if x is None and y is None:
|
||||
y,x = np.mgrid[-1:1:self.n*1j, -1:1:self.m*1j]
|
||||
_F = data.flatten()[:,np.newaxis]
|
||||
_x = x.flatten()
|
||||
_y = y.flatten()
|
||||
_x0 = (np.max(_x) - np.min(_x)) / 2 + np.min(_x)
|
||||
_y0 = (np.max(_y) - np.min(_y)) / 2 + np.min(_y)
|
||||
_rho = np.sqrt( ( _x - _x0 )**2 + ( _y - _y0 )**2 )
|
||||
_theta = np.arctan2( ( _y - _y0 ), ( _x - _x0 ) )
|
||||
|
||||
_Zern = []
|
||||
for i in range(modes):
|
||||
_n, _m = self.i_to_nm(i)
|
||||
_Znm = self.zernike_polynomial_polar(_n,_m,_rho,_theta)
|
||||
_Zern.append(_Znm)
|
||||
_Znm = None
|
||||
_Zern = np.asarray(_Zern).T
|
||||
_Zern_transpose = _Zern.T
|
||||
_pseudo_inv = np.linalg.pinv(np.dot(_Zern_transpose, _Zern))
|
||||
_int1 = np.dot(_pseudo_inv, _Zern_transpose)
|
||||
_A = np.dot(_int1,_F)
|
||||
self.zernikeCoefficients = _A[:,0]
|
||||
return _A[:,0]
|
||||
|
||||
|
||||
def scale_zernike_coefficients(self, c, rms):
|
||||
# Scaling the coefficients to some total value
|
||||
_rms_coeffic = np.sqrt(np.sum( np.asarray(c)**2))
|
||||
_scale_factor = rms / _rms_coeffic
|
||||
_c = (_scale_factor * np.asarray(c)).tolist()
|
||||
# _rms_coeffic = np.sqrt(np.sum( np.asarray(_c)**2))
|
||||
self.zernikeCoefficients = _c
|
||||
return _c
|
||||
|
||||
|
||||
def power_spectral_density(self, s0, p, rho_c, dx, dy, A):
|
||||
# Lorentzian equation used as PSD function
|
||||
m = self.m
|
||||
n = self.n
|
||||
UmList = [(M - ( m + 1) / 2) / (m * dx) for M in range(m)]
|
||||
VnList = [(N - ( n + 1) / 2) / (n * dy) for N in range(n)]
|
||||
VnList, UmList = np.meshgrid(UmList, VnList)
|
||||
|
||||
_rho_mn_list = np.sqrt(UmList**2 + VnList**2)
|
||||
_h0list = 1 / ( 1 + np.power( ( _rho_mn_list / rho_c ), p) )
|
||||
_h0_norm = np.sum(_h0list)
|
||||
_rho_mn_list = np.asarray(_rho_mn_list).reshape(m,n)
|
||||
|
||||
_PSD = (s0**2 * A) / (_h0_norm * (1 + np.power((_rho_mn_list/rho_c), p)))
|
||||
self.PSD2D = _PSD
|
||||
|
||||
return _PSD, _rho_mn_list
|
||||
|
||||
|
||||
def phase_map(self, phase):
|
||||
# Uniform distribution of the phase
|
||||
_phase_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_phase_in[:] = phase
|
||||
_phase_out = pyfftw.empty_aligned((self.n, self.m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _phase_in, _phase_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_phaseAngle = np.angle(_phase_out)
|
||||
|
||||
return _phaseAngle
|
||||
|
||||
|
||||
def generate_height_map(self, s0, p, rho_c, addToData=False):
|
||||
'''
|
||||
This function generates a surface height distribution based
|
||||
on a Lorentzian PSD profile. Following the paper by E. Sidick (2009).
|
||||
|
||||
2020-01-20: Removed "mean=1e-4" from function def. --> Did this do anything?
|
||||
'''
|
||||
|
||||
# Calculation of the necessary constants
|
||||
_dx = 1 / self.m
|
||||
_dy = 1 / self.n
|
||||
_A = (self.m * _dx) * (self.n * _dy)
|
||||
_phase = np.random.uniform(0,1,(self.m,self.m))
|
||||
|
||||
# Calculating the PSD and phase
|
||||
_PSD, _rho_mn_list = self.power_spectral_density(s0, p, rho_c, _dx, _dy, _A)
|
||||
_phiMap = self.phase_map(_phase)
|
||||
|
||||
# Shifting the coordinates for the FFT
|
||||
_PSD = np.fft.ifftshift(_PSD)
|
||||
|
||||
# Doing the fourier transform
|
||||
_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_in[:] = np.exp( 1j * _phiMap) * np.sqrt(_A * np.abs(_PSD))
|
||||
_out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# The final height distribution
|
||||
_hMap = 1 / ( _dx * _dy ) * _out
|
||||
if not addToData:
|
||||
self.data = _hMap.real
|
||||
else:
|
||||
self.data += _hMap.real
|
||||
|
||||
self.rms = np.sqrt(np.mean(np.asarray(self.data)**2))
|
||||
self.ptv = np.abs(np.max(self.data)) + np.abs(np.min(self.data))
|
||||
return self.data
|
||||
|
||||
|
||||
def scale_rms(self, factor, data=None, filteredDataFlag=False):
|
||||
# Scale the surface data such that the RMS gets multiplied by factor
|
||||
_factor = factor ** 2
|
||||
if data is None:
|
||||
if filteredDataFlag:
|
||||
self.scaledPSD2D = self.filteredPSD2D * _factor
|
||||
_data = self.filteredData
|
||||
else:
|
||||
self.calculate_2D_PSD()
|
||||
self.scaledPSD2D = self.PSD2D * _factor
|
||||
_data = self.data
|
||||
elif data is not None:
|
||||
self.PSD2D = self.calculate_2D_PSD(data=data)
|
||||
self.scaledPSD2D = self.PSD2D * _factor
|
||||
_data = data
|
||||
|
||||
# Fourier transform of the surface profile
|
||||
_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_in[:] = _data
|
||||
_out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_phase = np.angle(_out)
|
||||
|
||||
_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_in[:] = np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.scaledPSD2D))
|
||||
_out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
self.scaledData = _out.real
|
||||
self.scaledrms = np.sqrt(np.mean(np.asarray(self.scaledData)**2))
|
||||
self.scaledptv = np.abs(np.max(self.scaledData)) + np.abs(np.min(self.scaledData))
|
||||
|
||||
return _out.real
|
||||
|
||||
|
||||
def calculate_2D_PSD(self, data=None):
|
||||
''' Returns the 2D PSD map '''
|
||||
if data is None:
|
||||
_in = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
_in[:] = self.data
|
||||
_out = pyfftw.empty_aligned((self.n,self.m), dtype='complex128')
|
||||
elif data is not None:
|
||||
_n, _m = np.asarray(data).shape
|
||||
_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_in[:] = np.asarray(data)
|
||||
_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _in, _out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
if data is None:
|
||||
self.D1 = np.fft.fftshift(_out)
|
||||
self.PSD2D = np.abs(self.D1)**2
|
||||
return self.PSD2D
|
||||
elif data is not None:
|
||||
_D1 = np.fft.fftshift(_out)
|
||||
_PSD2D = np.abs(_D1)**2
|
||||
return _PSD2D
|
||||
|
||||
|
||||
def calculate_1D_PSD(self, data=None, filteredDataFlag=False, rmax=None):
|
||||
''' Returns the azimuthally averaged PSD map '''
|
||||
if data is None:
|
||||
try:
|
||||
if not filteredDataFlag:
|
||||
self.PSD1D = self.azimuthal_average(rmax)
|
||||
else:
|
||||
self.filteredPSD1D = self.azimuthal_average(filteredDataFlag=filteredDataFlag)
|
||||
return self.filteredPSD1D
|
||||
except AttributeError:
|
||||
self.calculate_2D_PSD(data)
|
||||
self.PSD1D = self.azimuthal_average(rmax)
|
||||
return self.PSD1D
|
||||
elif data is not None:
|
||||
_PSD2D = self.calculate_2D_PSD(data=data)
|
||||
_PSD1D = self.azimuthal_average(PSD=_PSD2D, rmax=rmax)
|
||||
return _PSD1D
|
||||
|
||||
|
||||
def set_lowpass_filter(self, r, dpix=1):
|
||||
''' Returns a map where all frequencies higher than r have been filtered out '''
|
||||
self.calculate_2D_PSD()
|
||||
# Masking out the parts that fall outside the lens clear aperture
|
||||
_x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j]
|
||||
_dist_from_center = np.sqrt(_x**2 + _y**2) * dpix
|
||||
_mask = (_dist_from_center <= (r)).astype(int)
|
||||
_maskedPSD2D = self.PSD2D.copy()
|
||||
_maskedPSD2D *= _mask
|
||||
_phase = np.angle(np.fft.fft2(self.data))
|
||||
self.filteredPSD2D = _maskedPSD2D
|
||||
self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real
|
||||
return self.filteredData
|
||||
|
||||
|
||||
def set_highpass_filter(self, r, dpix=1):
|
||||
'''Returns a map where all spatial frequencies lower than r have been filtered out'''
|
||||
self.calculate_2D_PSD()
|
||||
# Masking out the parts that fall outside the lens clear aperture
|
||||
# x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2]
|
||||
_x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j]
|
||||
_dist_from_center = np.sqrt(_x**2 + _y**2) * dpix
|
||||
_mask = (_dist_from_center <= (r)).astype(int)
|
||||
_maskedPSD2D = self.PSD2D.copy()
|
||||
_maskedPSD2D *= (1-_mask)
|
||||
_phase = np.angle(np.fft.fft2(self.data))
|
||||
self.filteredPSD2D = _maskedPSD2D
|
||||
self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real
|
||||
return self.filteredData
|
||||
|
||||
|
||||
def set_bandpass_filter(self, rmin, rmax, dpix=1):
|
||||
'''Returns a map where all spatial frequencies lower than r2 and higher than r1 have been filtered out'''
|
||||
assert rmax > rmin, 'rmax < rmin, not physical!'
|
||||
self.calculate_2D_PSD()
|
||||
# Masking out the parts that fall outside the lens clear aperture
|
||||
# x,y = np.ogrid[-self.resolution[0]/2:self.resolution[0]/2, -self.resolution[1]/2:self.resolution[1]/2]
|
||||
_x, _y = np.mgrid[-self.n/2*dpix : self.n/2*dpix : self.n*1j, -self.m/2*dpix : self.m/2*dpix : self.m*1j]
|
||||
_dist_from_center = np.sqrt(_x**2 + _y**2) * dpix
|
||||
_mask_1 = (_dist_from_center <= (rmax)).astype(int)
|
||||
_mask_2 = (_dist_from_center >= (rmin)).astype(int)
|
||||
_maskedPSD2D = self.PSD2D.copy()
|
||||
_maskedPSD2D *= (_mask_1 & _mask_2)
|
||||
_phase = np.angle(np.fft.fft2(self.data))
|
||||
self.filteredPSD2D = _maskedPSD2D
|
||||
self.filteredData = np.fft.ifft2(np.exp(1j*_phase) * np.sqrt(np.fft.fftshift(self.filteredPSD2D))).real
|
||||
return self.filteredData
|
||||
|
||||
|
||||
def azimuthal_average(self, PSD=None, center=None, filteredDataFlag=False, rmax=None):
|
||||
"""
|
||||
Calculate the azimuthally averaged radial profile.
|
||||
|
||||
image - The 2D image
|
||||
center - The [x,y] pixel coordinates used as the center. The default is
|
||||
None, which then uses the center of the image (including
|
||||
fracitonal pixels).
|
||||
|
||||
"""
|
||||
|
||||
# Calculate the indices from the image
|
||||
_y, _x = np.indices(self.resolution)
|
||||
if not center:
|
||||
center = np.array([(_x.max()-_x.min())/2.0, (_y.max()-_y.min())/2.0])
|
||||
|
||||
_r = np.hypot(_x - center[0], _y - center[1])
|
||||
|
||||
# Get sorted radii
|
||||
_ind = np.argsort(_r.flat)
|
||||
_r_sorted = _r.flat[_ind]
|
||||
|
||||
if not filteredDataFlag and PSD is None:
|
||||
_PSD = self.PSD2D
|
||||
|
||||
elif PSD is None:
|
||||
_PSD = self.filteredPSD2D
|
||||
|
||||
elif PSD is not None:
|
||||
_PSD = PSD
|
||||
|
||||
if rmax == True:
|
||||
_PSD[ _r > _x.max()/2 ] = np.nan
|
||||
elif rmax:
|
||||
_PSD[ _r > rmax ] = np.nan
|
||||
|
||||
_i_sorted = _PSD.flat[_ind]
|
||||
|
||||
# Get the integer part of the radii (bin size = 1)
|
||||
_r_int = _r_sorted.astype(int)
|
||||
|
||||
# Find all pixels that fall within each radial bin.
|
||||
_deltar = _r_int[1:] - _r_int[:-1] # Assumes all radii represented
|
||||
_rind = np.where(_deltar)[0] # location of changed radius
|
||||
_nr = _rind[1:] - _rind[:-1] # number of radius bin
|
||||
|
||||
# Cumulative sum to figure out sums for each radius bin
|
||||
_csim = np.cumsum(_i_sorted, dtype=float)
|
||||
_tbin = _csim[_rind[1:]] - _csim[_rind[:-1]]
|
||||
_radial_prof = _tbin / _nr
|
||||
|
||||
return _radial_prof
|
||||
|
||||
def generate_sine_pattern(self, ampx, ampy, perx, pery):
|
||||
_x, _y = np.mgrid[-1:1:self.n*1j, -1:1:self.m*1j]
|
||||
_z = ampx * np.sin(2*np.pi*_x*perx/2) + ampy * np.sin(2 * np.pi * _y * pery/2)
|
||||
return _z
|
||||
|
||||
def generate_tilt(self, x, y, tx, ty):
|
||||
_z = y * np.tan(np.deg2rad(tx)) + x * np.tan(np.deg2rad(ty))
|
||||
return _z
|
||||
@@ -0,0 +1,735 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
import time
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
|
||||
return 1/_f_recip
|
||||
|
||||
def phaseDelayFunction(self, x, y, l, paraxial=False):
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
if paraxial:
|
||||
_dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2)
|
||||
_k = 2 * np.pi / 30e-3
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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 phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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 def. mirror'
|
||||
|
||||
def phaseDelayFunction(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(-1j * _k * 2 * self.z)
|
||||
return _t_m
|
||||
|
||||
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i]
|
||||
_wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number()
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
_wf, x, y = self.fresnel_propagation()
|
||||
elif _F >= 5:
|
||||
# Angular spectrum (p2p) propagation
|
||||
_wf, x, y = self.angular_spectrum_propagation()
|
||||
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
# Start time counter
|
||||
start = time.time()
|
||||
|
||||
# Pick a wavelength
|
||||
l = 30e-3
|
||||
|
||||
# Initiate Fourier Optics
|
||||
fou = FourierOptics(l=l)
|
||||
|
||||
# Pick sampling parameters
|
||||
dpix = 1/50*l
|
||||
n, m = 2001, 2001
|
||||
print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix))
|
||||
|
||||
# Initiate the profiles class and pick a profile (tophat, obscured tophat or spider)
|
||||
prof = profiles()
|
||||
# A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1)
|
||||
A = prof.topHat(4*l, n, m, dpix=dpix, oversample=1)
|
||||
|
||||
# Alternatively, create a gaussian profile
|
||||
# gauss = GaussianBeam(0.3, l*1000)
|
||||
# A = gauss.fundamentalMode(0,n,m,np.max(x))
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
# The input distribution gets a curved wavefront after the lens
|
||||
U_in = A
|
||||
|
||||
|
||||
Fresnel = False
|
||||
P2P = not Fresnel
|
||||
|
||||
oC = [x.min(), x.max(), y.min(), y.max()]
|
||||
# Performing the propagation using Fresnel approximation to the focal plane.
|
||||
if Fresnel:
|
||||
savename = 'fourierOptics_01_diffracting_aperture_fresnel.pdf'
|
||||
Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=4*l, oversample=None, show=True, save=savename, outputCoordinates=oC)
|
||||
if P2P:
|
||||
savename = 'fourierOptics_01_diffracting_aperture_p2p.pdf'
|
||||
Exyz,_,_ = fou.planeWavePropagationFFTW(A, dz=4*l, dpix=dpix, oversample=None, show=True, save=savename)
|
||||
|
||||
# Plot a cross section of the new field (useful to better see the diffraction rings)
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.semilogy(np.linspace(np.min(x), np.max(x),n), np.abs(Exyz[1000,:]**2), 'r-')
|
||||
ax.set_xlabel('x (mm)')
|
||||
ax.set_ylabel('Intensity')
|
||||
ax.tick_params(axis ='both', direction='in', which='both')
|
||||
if Fresnel:
|
||||
plt.savefig('fourierOptics_01_diffracting_aperture_fresnel_slice.pdf', bbox_inches='tight', transparent=True)
|
||||
if P2P:
|
||||
plt.savefig('fourierOptics_01_diffracting_aperture_p2p_slice.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves.
|
||||
if True:
|
||||
Ics = []
|
||||
Ics_x = []
|
||||
Isum = []
|
||||
xmax = []
|
||||
zList = np.linspace(1,10.1,18).tolist()
|
||||
xin, yin = x, y
|
||||
|
||||
for i in zList:
|
||||
title = f"{zList.index(i)+1:03d}"
|
||||
print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r")
|
||||
|
||||
# Propagate the wavefront
|
||||
if Fresnel:
|
||||
Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=None, show=False, save=False)#, scaleInputArray=(n,m))
|
||||
if P2P:
|
||||
Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i*l, oversample=None, show=False, save=False, outputCoordinates=oC)
|
||||
|
||||
# Process the results by taking a slice of the data
|
||||
Isum.append(np.sum(np.abs(Exyz)**2))
|
||||
Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2))
|
||||
Ics_x.append(x.real[0])
|
||||
y_ext = [y.real[0,0],y.real[-1,0]]
|
||||
|
||||
# Reset the data
|
||||
x = None
|
||||
y = None
|
||||
|
||||
# Plotting all slices
|
||||
fig = plt.figure(figsize=(8,4))
|
||||
ax = fig.add_subplot(211)
|
||||
ax.tick_params(axis ='both', direction='in')
|
||||
|
||||
# Plotting the square root of the amplitude for better contrast
|
||||
im = ax.imshow(np.asarray(Ics).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]])
|
||||
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax.set_ylim([-0.2, 0.2])
|
||||
|
||||
# Plotting the intensity relative to the input
|
||||
ax2 = fig.add_subplot(212)
|
||||
ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-')
|
||||
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax2.set_xlabel('Propagation distance ($\\times \lambda$)')
|
||||
ax2.tick_params(axis ='both', direction='in')
|
||||
ax2.set_ylabel('Intensity')
|
||||
ax2.set_xlim([np.min(zList), np.max(zList)])
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# Saving the figure? Then show.
|
||||
plt.tight_layout()
|
||||
# plt.savefig('fresnel_cross.pdf', bbox_inches='tight')
|
||||
# plt.savefig('fresnel_cross.png', bbox_inches='tight')
|
||||
if P2P:
|
||||
plt.savefig('fourierOptics_01_diffracting_aperture_p2p_cross.pdf', bbox_inches='tight', transparent=True)
|
||||
if Fresnel:
|
||||
plt.savefig('fourierOptics_01_diffracting_aperture_fresnel_cross.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# Closing down the script and stopping the timer.
|
||||
end = time.time()
|
||||
print('The program took {:.3f} seconds!'.format((end-start)))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,729 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
import time
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
|
||||
return 1/_f_recip
|
||||
|
||||
def phaseDelayFunction(self, x, y, l, paraxial=False):
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
if paraxial:
|
||||
_dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2)
|
||||
_k = 2 * np.pi / 30e-3
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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 phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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 def. mirror'
|
||||
|
||||
def phaseDelayFunction(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(-1j * _k * 2 * self.z)
|
||||
return _t_m
|
||||
|
||||
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i]
|
||||
_wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number()
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
_wf, x, y = self.fresnel_propagation()
|
||||
elif _F >= 5:
|
||||
# Angular spectrum (p2p) propagation
|
||||
_wf, x, y = self.angular_spectrum_propagation()
|
||||
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
# Start time counter
|
||||
start = time.time()
|
||||
|
||||
# Pick a wavelength
|
||||
l = 30e-3
|
||||
|
||||
# Initiate Fourier Optics
|
||||
fou = FourierOptics(l=l)
|
||||
|
||||
# Pick sampling parameters
|
||||
dpix = 1/50
|
||||
n, m = 2001, 2001
|
||||
print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix))
|
||||
|
||||
# Initiate the profiles class and pick a profile (tophat, obscured tophat or spider)
|
||||
prof = profiles()
|
||||
# A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1)
|
||||
A = prof.topHat(2.5, n, m, dpix=dpix, oversample=1)
|
||||
|
||||
# Alternatively, create a gaussian profile
|
||||
# gauss = GaussianBeam(0.3, l*1000)
|
||||
# A = gauss.fundamentalMode(0,n,m,np.max(x))
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
# Start an optical train object. Add lenses and propagate. Currently not in use.
|
||||
# LDE = OpticalTrain()
|
||||
|
||||
# Create a lens with a focal length ~100 mm, with a clear aperture radius of 4 mm and thickness of 1 mm.
|
||||
L1 = lensObject('N-BK7', 50, 1e99, 1, 4, name='L1')
|
||||
print('Focal length of lens {} is {:.1f} mm.'.format(L1.name, L1.f))
|
||||
|
||||
# Calculate the phase delay introduced by the lens
|
||||
t_l = L1.phaseDelayFunction(x,y,1.35e-6, paraxial=False)
|
||||
|
||||
# The input distribution gets a curved wavefront after the lens
|
||||
U_in = A * t_l
|
||||
|
||||
|
||||
# Performing the propagation using Fresnel approximation to the focal plane.
|
||||
oC = [-5, 5, -5, 5]
|
||||
savename = 'simple_lens.pdf'
|
||||
Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=L1.f, oversample=None, show=True, save=savename, outputCoordinates=oC)
|
||||
|
||||
# Plot a cross section of the new field (useful to better see the diffraction rings)
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[1000,:]**2), 'r-')
|
||||
ax.set_xlabel('x (mm)')
|
||||
ax.set_ylabel('Intensity')
|
||||
ax.tick_params(axis ='both', direction='in', which='both')
|
||||
plt.savefig('simple_lens_slice.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves.
|
||||
if True:
|
||||
Ics = []
|
||||
Ics_x = []
|
||||
Isum = []
|
||||
xmax = []
|
||||
zList = np.linspace(20,220,20).tolist()
|
||||
xin, yin = x, y
|
||||
|
||||
for i in zList:
|
||||
title = f"{zList.index(i)+1:03d}"
|
||||
print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r")
|
||||
|
||||
# Propagate the wavefront
|
||||
# Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m))
|
||||
Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC)
|
||||
|
||||
# Process the results by taking a slice of the data
|
||||
Isum.append(np.sum(np.abs(Exyz)**2))
|
||||
Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2))
|
||||
Ics_x.append(x.real[0])
|
||||
y_ext = [y.real[0,0],y.real[-1,0]]
|
||||
|
||||
# Reset the data
|
||||
x = None
|
||||
y = None
|
||||
|
||||
# Plotting all slices
|
||||
fig = plt.figure(figsize=(8,4))
|
||||
ax = fig.add_subplot(211)
|
||||
ax.tick_params(axis ='both', direction='in')
|
||||
|
||||
# Plotting the square root of the amplitude for better contrast
|
||||
im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]])
|
||||
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
|
||||
# Plotting the intensity relative to the input
|
||||
ax2 = fig.add_subplot(212)
|
||||
ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-')
|
||||
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax2.set_xlabel('Propagation distance (mm)')
|
||||
ax2.tick_params(axis ='both', direction='in')
|
||||
ax2.set_ylabel('Amplitude')
|
||||
ax2.set_xlim([np.min(zList), np.max(zList)])
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# Saving the figure? Then show.
|
||||
plt.tight_layout()
|
||||
# plt.savefig('fresnel_cross.pdf', bbox_inches='tight')
|
||||
# plt.savefig('fresnel_cross.png', bbox_inches='tight')
|
||||
plt.savefig('simple_lens_cross.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# Closing down the script and stopping the timer.
|
||||
end = time.time()
|
||||
print('The program took {:.3f} seconds!'.format((end-start)))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,771 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
import time
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
# _im = _ax.imshow(np.sqrt(np.abs(_E_out_ip)), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
# _fig.colorbar(_im, ax=_ax, label='$\\sqrt{Amplitude}$')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
|
||||
return 1/_f_recip
|
||||
|
||||
def phaseDelayFunction(self, x, y, l, paraxial=False):
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
if paraxial:
|
||||
_dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2)
|
||||
_k = 2 * np.pi / 30e-3
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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 phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phaseDelayFunction(self, x, y, l):
|
||||
# _k = 2 * np.pi / l
|
||||
# _dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
# _t_m = np.exp(1j * _k * 2 * _dz)
|
||||
# return _t_m
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
|
||||
# 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 def. mirror'
|
||||
|
||||
def phaseDelayFunction(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(1j * _k * 2 * self.z)
|
||||
return _t_m
|
||||
|
||||
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i]
|
||||
_wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number()
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
_wf, x, y = self.fresnel_propagation()
|
||||
elif _F >= 5:
|
||||
# Angular spectrum (p2p) propagation
|
||||
_wf, x, y = self.angular_spectrum_propagation()
|
||||
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
# Start time counter
|
||||
start = time.time()
|
||||
|
||||
# Pick a wavelength
|
||||
l = 30e-3
|
||||
|
||||
# Initiate Fourier Optics
|
||||
fou = FourierOptics(l=l)
|
||||
|
||||
# Pick sampling parameters
|
||||
dpix = 8
|
||||
n, m = 4001, 4001
|
||||
print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix))
|
||||
|
||||
# Initiate the profiles class and pick a profile (tophat, obscured tophat or spider)
|
||||
prof = profiles()
|
||||
A = prof.topHat(4000, n, m, dpix=dpix, oversample=1)
|
||||
# A = prof.spiderTopHat(4000, 1000, n, m, dpix=dpix, oversample=1)
|
||||
# A = prof.obscuredTopHat(4000, 1000, n, m, dpix=dpix, oversample=1)
|
||||
|
||||
# Alternatively, create a gaussian profile
|
||||
# gauss = GaussianBeam(0.3, l*1000)
|
||||
# A = gauss.fundamentalMode(0,n,m,np.max(x))
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
# Start an optical train object. Add lenses and propagate. Currently not in use.
|
||||
# LDE = OpticalTrain()
|
||||
|
||||
# Create a mirror with a focal length 64 m, with a clear aperture radius of 4 m.
|
||||
M1 = parabolicMirror(dx=0, dy=0, name='M1', CA=8000, f=64000)
|
||||
print('Focal length of mirror {} is {:.1f} mm.'.format(M1.name, M1.f))
|
||||
|
||||
# Calculate the phase delay introduced by the mirror
|
||||
t_l = M1.phaseDelayFunction(x,y,l)
|
||||
|
||||
# The input distribution gets a curved wavefront after the mirror
|
||||
U_in = A * t_l
|
||||
|
||||
plt.imshow(np.abs(U_in))
|
||||
plt.colorbar()
|
||||
plt.savefig('telescope_mask.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show(block=False)
|
||||
|
||||
# Performing the propagation using Fresnel approximation to the focal plane.
|
||||
# oC = [-0.001, 0.001, -0.001, 0.001]
|
||||
oC = [-2, 2, -2, 2]
|
||||
# savename = 'fourierOptics_02_telescope_fresnel.pdf'
|
||||
Exyz,xout,yout = fou.FresnelPropagationFFTW(U_in, x, y, dz=M1.f, oversample=None, show=True, save=False, outputCoordinates=oC)
|
||||
|
||||
# Plot a cross section of the new field (useful to better see the diffraction rings)
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[int((n-1)/2),:]**2), 'r-')
|
||||
ax.set_xlabel('x (mm)')
|
||||
ax.set_ylabel('Intensity')
|
||||
ax.tick_params(axis ='both', direction='in', which='both')
|
||||
# plt.savefig('fourierOptics_02_telescope_slice.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# Combined plot
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(12,3))
|
||||
_ax = _fig.add_subplot(131)
|
||||
_im = _ax.imshow(np.abs(Exyz), cmap='hot', extent=oC)
|
||||
_ax.set_xlim([oC[0], oC[1]])
|
||||
_ax.set_ylim([oC[2], oC[3]])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(132)
|
||||
_im2 = _ax2.imshow(np.angle(Exyz), cmap='hot', extent=oC)
|
||||
_ax2.set_xlim([oC[0], oC[1]])
|
||||
_ax2.set_ylim([oC[2], oC[3]])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
_ax3 = _fig.add_subplot(133)
|
||||
_ax3.grid(True, alpha=0.3)
|
||||
_ax3.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[int((n-1)/2),:]**2), 'r-')
|
||||
_ax3.set_xlabel('x (mm)')
|
||||
_ax3.set_ylabel('Intensity')
|
||||
_ax3.tick_params(axis ='both', direction='in', which='both')
|
||||
plt.tight_layout()
|
||||
plt.savefig('fourierOptics_02_telescope_complete.pdf', bbox_inches='tight', transparent=True)
|
||||
|
||||
plt.show()
|
||||
|
||||
# The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves.
|
||||
if False:
|
||||
Ics = []
|
||||
Ics_x = []
|
||||
Isum = []
|
||||
xmax = []
|
||||
zList = np.linspace(20,220,20).tolist()
|
||||
xin, yin = x, y
|
||||
|
||||
for i in zList:
|
||||
title = f"{zList.index(i)+1:03d}"
|
||||
print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r")
|
||||
|
||||
# Propagate the wavefront
|
||||
# Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m))
|
||||
Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC)
|
||||
|
||||
# Process the results by taking a slice of the data
|
||||
Isum.append(np.sum(np.abs(Exyz)**2))
|
||||
Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2))
|
||||
Ics_x.append(x.real[0])
|
||||
y_ext = [y.real[0,0],y.real[-1,0]]
|
||||
|
||||
# Reset the data
|
||||
x = None
|
||||
y = None
|
||||
|
||||
# Plotting all slices
|
||||
fig = plt.figure(figsize=(8,4))
|
||||
ax = fig.add_subplot(211)
|
||||
ax.tick_params(axis ='both', direction='in')
|
||||
|
||||
# Plotting the square root of the amplitude for better contrast
|
||||
im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]])
|
||||
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
|
||||
# Plotting the intensity relative to the input
|
||||
ax2 = fig.add_subplot(212)
|
||||
ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-')
|
||||
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax2.set_xlabel('Propagation distance (mm)')
|
||||
ax2.tick_params(axis ='both', direction='in')
|
||||
ax2.set_ylabel('Intensity')
|
||||
ax2.set_xlim([np.min(zList), np.max(zList)])
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# Saving the figure? Then show.
|
||||
plt.tight_layout()
|
||||
# plt.savefig('fresnel_cross.pdf', bbox_inches='tight')
|
||||
# plt.savefig('fresnel_cross.png', bbox_inches='tight')
|
||||
plt.show()
|
||||
|
||||
|
||||
# Closing down the script and stopping the timer.
|
||||
end = time.time()
|
||||
print('The program took {:.3f} seconds!'.format((end-start)))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
|
||||
return 1/_f_recip
|
||||
|
||||
def phaseDelayFunction(self, x, y, l, paraxial=False):
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
if paraxial:
|
||||
_dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2)
|
||||
_k = 2 * np.pi / 30e-3
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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 phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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 def. mirror'
|
||||
|
||||
def phaseDelayFunction(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(-1j * _k * 2 * self.z)
|
||||
return _t_m
|
||||
|
||||
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i]
|
||||
_wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number()
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
_wf, x, y = self.fresnel_propagation()
|
||||
elif _F >= 5:
|
||||
# Angular spectrum (p2p) propagation
|
||||
_wf, x, y = self.angular_spectrum_propagation()
|
||||
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
def FGB_1D(w0,l,z,x):
|
||||
k = 2 * np.pi / l
|
||||
z = z + 1e-16
|
||||
w = w0 * np.sqrt( 1 + ( (l*z) / (np.pi*w0**2) )**2)
|
||||
R = z + 1/z * ( np.pi * w0**2 / l)**2
|
||||
phi = np.arctan2( (l * z) , (np.pi * w0**2) )
|
||||
|
||||
norm = (2./ np.pi)**(.25) * (1. / w0**2)**(.5)
|
||||
beam = np.exp( -x**2/w**2 - (1j * k * z) - ((1j*np.pi*x**2)/(l*R)) + 1j*phi/2)
|
||||
|
||||
return beam * norm
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
l = 30e-3
|
||||
# l = 1e-6
|
||||
fou = FourierOptics(l=l)
|
||||
dpix = l/6
|
||||
n = int(500 / dpix * l)
|
||||
m = int(500 / dpix * l)
|
||||
n, m = 3001, 3001
|
||||
print (n,m)
|
||||
prof = profiles()
|
||||
# A = prof.topHat(4*l, 2001, 2001, dpix=dpix, oversample=1)
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
w0_init = 4*l
|
||||
z = 400*l
|
||||
wg = w0_init * np.sqrt( 1 + ( (l*z) / (np.pi*w0_init**2) )**2)
|
||||
# rmax_init = 4 * w0_init
|
||||
# rmax_init = 1000/8*l
|
||||
|
||||
xGauss = np.linspace(-(n/2)*dpix, (n/2)*dpix, n)
|
||||
rmax_init = np.max(xGauss)
|
||||
# A = GaussianBeam(w0_init, l).fundamentalMode(z, n, m, rmax_init)
|
||||
A0 = FGB_1D(w0_init, l, 0, xGauss)
|
||||
A0 /= np.sqrt(np.sum(np.abs(A0)**2))
|
||||
A = FGB_1D(wg, l, z, xGauss)
|
||||
|
||||
A /= np.sqrt(np.sum(np.abs(A)**2))
|
||||
|
||||
A2D = GaussianBeam(w0_init, l).fundamentalMode(z, n, m, rmax_init)
|
||||
A2D /= np.sqrt(np.sum(np.abs(A2D[int(n/2)+1,:])**2))
|
||||
|
||||
A2D0 = GaussianBeam(w0_init, l).fundamentalMode(0, n, m, rmax_init)
|
||||
|
||||
Ap2p,xp2p,_ = fou.planeWavePropagationFFTW(A2D0, dz=z, dpix=dpix, show=False, save=False)
|
||||
Ap2p /= np.sqrt(np.sum(np.abs(Ap2p[int(n/2),:])**2))
|
||||
|
||||
oC = [xGauss.min(), xGauss.max(), xGauss.min(), xGauss.max()]
|
||||
|
||||
# savename= 'FourierOptics_03_verification_fresnel.pdf'
|
||||
savename=None
|
||||
Afren,xfren,_ = fou.FresnelPropagationFFTW(A2D0, x,y, dz=z, show=True, save=savename, outputCoordinates=oC)
|
||||
Afren /= np.sqrt(np.sum(np.abs(Afren[int(n/2),:])**2))
|
||||
# Afren = np.conj(Afren)
|
||||
|
||||
norm_fac = np.max(np.abs(A2D)[int(n/2),:])
|
||||
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
# ax.plot(np.abs(A0), label='z=0, analytical')
|
||||
# ax.plot(np.abs(A)/norm_fac, label='z=%i, analytical' %(int(z/l)))
|
||||
ax.plot(np.abs(A2D)[int(n/2),:]/norm_fac, 'r--', label='z=%i$\lambda$, analytical' %(int(z/l)))
|
||||
ax.plot(np.abs(Ap2p)[int(n/2),:]/norm_fac, label='z=%i$\lambda$, plane to plane' %(int(z/l)))
|
||||
ax.plot(np.abs(Afren)[int(n/2),:]/norm_fac, label='z=%i$\lambda$, Fresnel' %(int(z/l)))
|
||||
ax.grid(alpha=0.3)
|
||||
ax.set_xlabel('Location [pixel no.]')
|
||||
ax.set_ylabel('Normalized amplitude')
|
||||
ax.legend()
|
||||
plt.savefig('FourierOptics_03_verification_amplitude.pdf', bbox_inches='tight', transparent=True)
|
||||
|
||||
fig2 = plt.figure(figsize=(12,3))
|
||||
# ax1 = fig2.add_subplot(151)
|
||||
# ax1.plot(np.angle(A0), label='z=0, analytical')
|
||||
# ax1.set_title('z=0, analytical')
|
||||
# ax2 = fig2.add_subplot(131)
|
||||
# ax2.plot(np.angle(A), label='z=%i, analytical' %(int(z/l)))
|
||||
# ax2.set_title('z=%i, analytical' %(int(z/l)))
|
||||
ax3 = fig2.add_subplot(131)
|
||||
ax3.plot(np.angle(A2D)[int(n/2),:], label='z=%i$\lambda$, analytical' %(int(z/l)))
|
||||
ax3.set_title('z=%i$\lambda$, Gaussian beam' %(int(z/l)))
|
||||
ax3.set_ylabel('Phase')
|
||||
ax3.set_xlabel('Location [pixel no.]')
|
||||
ax3.grid(True, alpha=0.3)
|
||||
ax4 = fig2.add_subplot(132)
|
||||
ax4.plot(np.angle(Ap2p)[int(n/2),:], label='z=%i$\lambda$, plane to plane' %(int(z/l)))
|
||||
ax4.set_title('z=%i$\lambda$, Plane to plane' %(int(z/l)))
|
||||
ax4.set_ylabel('Phase')
|
||||
ax4.set_xlabel('Location [pixel no.]')
|
||||
ax4.grid(True, alpha=0.3)
|
||||
ax5 = fig2.add_subplot(133)
|
||||
ax5.plot(np.angle(Afren)[int(n/2),:], label='z=%i$\lambda$, Fresnel' %(int(z/l)))
|
||||
ax5.set_title('z=%i$\lambda$, Fresnel' %(int(z/l)))
|
||||
ax5.set_ylabel('Phase')
|
||||
ax5.set_xlabel('Location [pixel no.]')
|
||||
ax5.grid(True, alpha=0.3)
|
||||
# ax1.grid(alpha=0.3)
|
||||
# ax6 = fig2.add_subplot(166)
|
||||
# ax6.legend(ax=ax6)
|
||||
plt.savefig('FourierOptics_03_verification_phase.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
# set_trace()
|
||||
# Afren,_,_ = fou.FresnelPropagationFFTW(A2D, dz=400*l, dpix=dpix, oversample=1, show=True, save=False, outputCoordinates=oC)
|
||||
|
||||
gauss = GaussianBeam(w0_init, l)
|
||||
# A = prof.topHat(4*l, 2001, 2001, dpix=dpix, oversample=1)
|
||||
A = gauss.fundamentalMode(0,n,m,rmax_init)
|
||||
if False:
|
||||
|
||||
Ics = []
|
||||
Ics_x = []
|
||||
Isum = []
|
||||
lList = np.linspace(1,201,10).tolist()
|
||||
for i in lList:
|
||||
title = f"{lList.index(i):03d}"
|
||||
print (' Calculating {0}/{1}'.format(title, f"{len(lList):03d}"), end="\r")
|
||||
# Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, show=False, save=False)#, scaleInputArray=(n,m))
|
||||
Exyz, x, y = fou.FresnelPropagationFFTW(A, dz=i*l, dpix=dpix, show=False, save=False, outputCoordinates=oC)
|
||||
# Exyz = gauss.fundamentalMode(i*l, n, m, rmax_init)
|
||||
# y, x = np.mgrid[-rmax_init:rmax_init:n*1j,-rmax_init:rmax_init:n*1j]
|
||||
Isum.append(np.sum(np.abs(Exyz)**2))
|
||||
Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2))
|
||||
Ics_x.append(x.real[0])
|
||||
y_ext = [y.real[0,0],y.real[-1,0]]
|
||||
print(y_ext)
|
||||
Exyz = None
|
||||
x = None
|
||||
y = None
|
||||
|
||||
fig = plt.figure(figsize=(8,4))
|
||||
ax = fig.add_subplot(211)
|
||||
ax.tick_params(axis ='both', direction='in')
|
||||
im = ax.imshow(np.asarray(Ics).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[lList[0],lList[-1],y_ext[0],y_ext[1]])
|
||||
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
# ax.set_ylim([-2.5e-4,2.5e-4])
|
||||
ax2 = fig.add_subplot(212)
|
||||
ax2.plot(lList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-')
|
||||
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax2.set_xlabel('Propagation distance [$\lambda$]')
|
||||
ax2.tick_params(axis ='both', direction='in')
|
||||
ax2.set_ylabel('Intensity')
|
||||
ax2.set_xlim([np.min(lList), np.max(lList)])
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# ax3 = fig.add_subplot(133)
|
||||
# ax3.set_ylabel('Integrated intensity')
|
||||
# ax3.set_xlabel('Propagation distance [$\lambda$]')
|
||||
# ax3.grid(True, alpha=0.3)
|
||||
# ax3.tick_params(axis ='both', direction='in')
|
||||
# ax3.plot(Isum)
|
||||
plt.tight_layout()
|
||||
# plt.savefig('fresnel_cross.pdf', bbox_inches='tight')
|
||||
# plt.savefig('fresnel_cross.png', bbox_inches='tight')
|
||||
plt.show()
|
||||
|
||||
# E_xyz,_,_ = fou.planeWavePropagationFFTW(A, dz=4*l, dpix=dpix, oversample=1, show=True, save=False, outputLimits=(-0.4*1e-3,0.4*1e-3))#, scaleInputArray=(n,m))
|
||||
# Exyz,_,_ = fou.FresnelPropagationFFTW(A, dz=4*l, dpix=dpix, oversample=1, show=True, save=False)#, scaleInputArray=(n,m))
|
||||
end = time.time()
|
||||
print((end-start)*1000)
|
||||
# set_trace()
|
||||
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
import time
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x.real, _y.real, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x.real, _y.real, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
self.type = 'Thin paraxial lens'
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(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):
|
||||
if self.type == 'Thin paraxial lens':
|
||||
assert self.type=='Thin paraxial lens' and paraxial, 'Can not calculate non-paraxial phase delay on paraxial lens!'
|
||||
|
||||
_k = 2 * np.pi / l
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
if paraxial:
|
||||
_t_l = np.exp(-1j * _k * self.thickness) * np.exp(1j * _k / (2 * self.f) * (x**2 + y**2))
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phase_delay(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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 def. mirror'
|
||||
|
||||
def phase_delay(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(-1j * _k * 2 * self.z)
|
||||
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
|
||||
|
||||
pass
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
def fresnel_number(self, a, z, l):
|
||||
return a**2 / (z * l)
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_fou = FourierOptics(l=l)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
oC = [x.min(), x.max(), y.min(), y.max()]
|
||||
show = False
|
||||
_wf, x, y = _fou.FresnelPropagationFFTW(_wf, x, y, dz=self.sequentialDistance[0], oversample=None, show=show, save=False, outputCoordinates=oC)
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
|
||||
for _lens in self.opticalTrain[:-1]: # Except for image plane
|
||||
|
||||
title = f"{self.opticalTrain[:-1].index(_lens)+1:02d}"
|
||||
print (' Calculating lens {0} of {1}'.format(title, f"{len(self.opticalTrain[:-1]):02d}"), end="\r")
|
||||
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i+1]
|
||||
_wf *= _lens.phase_delay(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number(_lens.CA, _dz, l)
|
||||
_F = 1
|
||||
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
if _lens == self.opticalTrain[-2]:
|
||||
oC = [-2, 2, -2, 2]
|
||||
show=True
|
||||
_wf, x, y = _fou.FresnelPropagationFFTW(_wf, x, y, dz=self.sequentialDistance[_i+1], oversample=None, show=show, save=False, outputCoordinates=oC)
|
||||
elif _F >= 5:
|
||||
print('Using Plane Wave!')
|
||||
# Angular spectrum (p2p) propagation
|
||||
# _wf, x, y = self.plane_wave_propagation()
|
||||
|
||||
pass
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
# Start time counter
|
||||
start = time.time()
|
||||
|
||||
# Pick a wavelength
|
||||
l = 30e-3
|
||||
|
||||
# Initiate Fourier Optics
|
||||
fou = FourierOptics(l=l)
|
||||
|
||||
# Pick sampling parameters
|
||||
dpix = 1/20
|
||||
n, m = 4001, 4001
|
||||
print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix))
|
||||
|
||||
# Initiate the profiles class and pick a profile (tophat, obscured tophat or spider)
|
||||
A = profiles().topHat(25, n, m, dpix=dpix, oversample=1)
|
||||
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
# Start an optical train object. Add lenses and propagate. Currently not in use.
|
||||
LDE = OpticalTrain()
|
||||
# Create a lens with D=50 mm, f=200 mm
|
||||
L1 = lensObject('N-BK7', None, None, 0, 25, f=200, name='L1')
|
||||
L2 = lensObject('N-BK7', None, None, 0, 12.5, f=-100, name='L2')
|
||||
L3 = lensObject('N-BK7', None, None, 0, 25, f=200, name='L3')
|
||||
IMG = opticalSurface(name='Image plane')
|
||||
|
||||
L0 = lensObject('N-BK7', None, None, 0, 25, f=400, name='Input intiater lens')
|
||||
t_l = L0.phase_delay(x,y,l, paraxial=True)
|
||||
U_in = A * t_l
|
||||
set_trace()
|
||||
# U_in = Fresnel propagation to L0.f
|
||||
|
||||
oC = [x.min(), x.max(), y.min(), y.max()]
|
||||
Exyz, x1, y1 = fou.FresnelPropagationFFTW(U_in, x, y, dz=L0.f, oversample=None, show=False, save=False, outputCoordinates=oC)
|
||||
U_in = Exyz
|
||||
|
||||
|
||||
LDE.add_object(L1, 400)
|
||||
LDE.add_object(L2, 200)
|
||||
LDE.add_object(L3, 200)
|
||||
LDE.add_object(IMG, 400)
|
||||
|
||||
LDE.print_lens_Data()
|
||||
LDE.propagate_system(U_in, x, y, l)
|
||||
# fig = plt.figure()
|
||||
# ax1 = fig.add_subplot(121)
|
||||
# ax2 = fig.add_subplot(122)
|
||||
# im1 = ax1.imshow(np.abs(wf), cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()])
|
||||
# im2 = ax2.imshow(np.angle(wf), cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()])
|
||||
# plt.colorbar(im1, ax=ax1)
|
||||
# plt.colorbar(im2, ax=ax2)
|
||||
# plt.show()
|
||||
set_trace()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Closing down the script and stopping the timer.
|
||||
end = time.time()
|
||||
print('The program took {:.3f} seconds!'.format((end-start)))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,729 @@
|
||||
#!/usr/bin/env Python3
|
||||
import numpy as np
|
||||
from pdb import set_trace
|
||||
import matplotlib.pyplot as plt
|
||||
from cmath import sqrt as csqrt
|
||||
from scipy import interpolate
|
||||
from scipy.optimize import leastsq
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
from matplotlib.colors import LogNorm
|
||||
import pyfftw
|
||||
import glassCatalog
|
||||
from tabulate import tabulate
|
||||
import time
|
||||
|
||||
|
||||
class FourierOptics:
|
||||
def __init__(self, l):
|
||||
self.l = l # Wavelength
|
||||
|
||||
|
||||
def planeWavePropagationFFTW(self, A, dpix=None, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputLimits=False):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='cubic')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
if oversample:
|
||||
_Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
_Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
|
||||
# A at z=0 is then
|
||||
_E_xy0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_E_xy0[:] = np.fft.ifftshift(A) # use ifftshift to shift origin to start array
|
||||
# _IntensityNormFactor = (np.abs(_E_xy0)**2).sum() # Factor to scale the intensity after the FFTs
|
||||
_IntensityNormFactor = np.sum(np.abs(_E_xy0)**2)
|
||||
_Az0 = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( _E_xy0, _Az0, axes=(0,1), direction='FFTW_BACKWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
|
||||
# E-field at z=dz is the fourier transform of A(z=0)*e^(-ikz)
|
||||
_fx_l = np.fft.fftfreq(_n, d=dpix)
|
||||
_fy_l = np.fft.fftfreq(_m, d=dpix)
|
||||
_fx, _fy = np.meshgrid(_fx_l, _fy_l)
|
||||
_kz = self.phaseFactorExponent(_fx, _fy, self.l) # origin at start of array
|
||||
_phaseFactor = np.exp(-1j * _kz * dz) # origin at start of array
|
||||
|
||||
# Calculating the spatial frequencies in k-space
|
||||
_kx = np.fft.fftshift( _fx * 2 * np.pi )
|
||||
_ky = np.fft.fftshift( _fy * 2 * np.pi )
|
||||
|
||||
_E_xyz = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_A_in[:] = _Az0 * _phaseFactor
|
||||
|
||||
_FFTW_Obj2 = pyfftw.FFTW( _A_in, _E_xyz, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj2()
|
||||
_E_xyz = np.fft.fftshift(_E_xyz) # use fftshift to move the origin from edge to center
|
||||
_E_xyz /= np.sqrt(_IntensityNormFactor) # Normalization back to initial energy.
|
||||
|
||||
# Coordinates of E_xy0
|
||||
_y, _x = np.mgrid[-_n / 2 * dpix : _n / 2 * dpix : 1j*_n, -_m / 2 * dpix : _m / 2 * dpix : 1j * _m]
|
||||
if outputLimits:
|
||||
# Decide only use for plotting or can be used for output as well?
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_x = np.linspace(-_n/2*dpix, _n/2*dpix, _n)
|
||||
_y = np.linspace(-_m/2*dpix, _m/2*dpix, _m)
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_xyz.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_xyz.imag, kind='linear')
|
||||
_xi = np.linspace(outputLimits[0],outputLimits[1],_n)
|
||||
_yi = np.linspace(outputLimits[0],outputLimits[1],_m)
|
||||
_E_out_ip_real = _f_ip_real(_xi, _yi)
|
||||
_E_out_ip_imag = _f_ip_imag(_xi, _yi)
|
||||
_E_xyz = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_y, _x = np.mgrid[outputLimits[0]:outputLimits[1]:1j*_n, outputLimits[0]:outputLimits[1]:1j * _m]
|
||||
|
||||
|
||||
_E_xyz_extent = [np.min(_x), np.max(_x), np.min(_y), np.max(_x)]
|
||||
|
||||
|
||||
|
||||
if show or save:
|
||||
fig = plt.figure(figsize=(8,3))
|
||||
ax = fig.add_subplot(121)
|
||||
im = ax.imshow(np.abs(_E_xyz), cmap='hot', extent=_E_xyz_extent)#, vmax=1, vmin=0)
|
||||
ax.set_xlabel('x [mm]')
|
||||
ax.set_ylabel('y [mm]')
|
||||
fig.colorbar(im, ax=ax, label='Normalized intensity')
|
||||
ax2 = fig.add_subplot(122)
|
||||
im2 = ax2.imshow(np.angle(_E_xyz), cmap='hot', extent=_E_xyz_extent)
|
||||
ax2.set_xlabel('x [mm]')
|
||||
ax2.set_ylabel('y [mm]')
|
||||
fig.colorbar(im2, ax=ax2)
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight')
|
||||
if show:
|
||||
plt.show()
|
||||
elif not show:
|
||||
plt.close()
|
||||
|
||||
return np.array([_E_xyz, _x, _y])
|
||||
|
||||
|
||||
def FresnelPropagationFFTW(self, A, xin, yin, dz=None, oversample=None, show=False, save=False, scaleInputArray=False, outputCoordinates='auto'):
|
||||
'''
|
||||
Propagation through the Fresnel approximation, which is valid in many different situations.
|
||||
A separate code for the Fraunhofer propagation is not striclty necessary, as the exponent
|
||||
converges to 1 for large z values in any case.
|
||||
|
||||
Input parameters:
|
||||
A - [array] (Complex) Input field
|
||||
dpix - [float] pixel scale at which the calculations are done
|
||||
dz - [float] propagation distance
|
||||
oversample - [bool] Apply padding on the outside of A, increasing the size of A
|
||||
by the given factor.
|
||||
show - [bool] Show the resulting amplitude and phase map after propagation
|
||||
save - [bool] Save the figure
|
||||
scaleInputArray - [tuple] Rescale the resolution of the input array using interpolation.
|
||||
outputCoordinates - [list] list the corners of the desired output coordinate plane.
|
||||
Order: [xmin, xmax, ymin, ymax]
|
||||
If 'auto': the output coordinates will be determined
|
||||
automatically.
|
||||
|
||||
Output:
|
||||
E_out - [array] (Complex) Output field distribution
|
||||
x, y - [array] Output coordinates
|
||||
'''
|
||||
assert np.shape(xin) == np.shape(A), 'Input coordinates (xin) do not match shape of input field.'
|
||||
assert np.shape(yin) == np.shape(A), 'Input coordinates (yin) do not match shape of input field.'
|
||||
A = np.conj(A)
|
||||
|
||||
if scaleInputArray:
|
||||
A = np.asarray(A)
|
||||
_n, _m = A.shape
|
||||
_nA, _mA = np.linspace(-1, 1, _n), np.linspace(-1, 1, _m)
|
||||
A_ip_real = interpolate.interp2d(_nA, _mA, A.real, kind='linear')
|
||||
A_ip_imag = interpolate.interp2d(_nA, _mA, A.imag, kind='linear')
|
||||
|
||||
_a, _b = scaleInputArray
|
||||
_nx, _my = np.linspace(-1, 1, _a), np.linspace(-1, 1, _b)
|
||||
A_out_ip_real = A_ip_real(_nx, _my)
|
||||
A_out_ip_imag = A_ip_imag(_nx, _my)
|
||||
A_out_ip = A_out_ip_real + A_out_ip_imag*1j
|
||||
A = np.rint(A_out_ip_real)
|
||||
|
||||
# if oversample:
|
||||
# _Aos = np.zeros((np.shape(A)[0]*oversample, np.shape(A)[1]*oversample))
|
||||
# _Aos[int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2), int(len(_Aos)/2-len(A)/2): int(len(_Aos)/2+len(A)/2)] = A
|
||||
# A = _Aos
|
||||
|
||||
_n, _m = np.shape(A)
|
||||
_k = 2 * np.pi / self.l
|
||||
|
||||
# Define (xi, eta) plane at z=0 in meters, with nxm points. We must use the pixel scale.
|
||||
_xi, _eta = xin, yin
|
||||
|
||||
# Calculate the pixel size from the xi, eta plane
|
||||
_dpix = np.mean(np.diff(_xi))
|
||||
if _dpix < 1e-99:
|
||||
_dpix = np.mean(np.diff(_xi.T))
|
||||
|
||||
# Gaussian test: use 8*w0 radius for FFT:
|
||||
if outputCoordinates == 'auto':
|
||||
_w0_fit, _ = leastsq(self.minimize, self.l, args=(0, _xi, _eta, A))
|
||||
_gauss = GaussianBeam(_w0_fit, self.l)
|
||||
_halfwidth = 8 * _gauss.beamRadius(dz)[0]
|
||||
_xip = np.linspace(-_halfwidth, _halfwidth, _n) # Desired x,y - plane
|
||||
_yip = np.linspace(-_halfwidth, _halfwidth, _m) # Desired x,y - plane
|
||||
else:
|
||||
_xip = np.linspace(outputCoordinates[0], outputCoordinates[1], _n) # Desired x,y - plane
|
||||
_yip = np.linspace(outputCoordinates[2], outputCoordinates[3], _m) # Desired x,y - plane
|
||||
|
||||
|
||||
# The quadratic phase exponential of the input wavefront
|
||||
_phaseComponent = np.exp(1j * _k / (2 * dz) * (_xi**2 + _eta**2))
|
||||
|
||||
# Complete input
|
||||
_U_in = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_U_in[:] = A * _phaseComponent
|
||||
|
||||
# Output of the FFT evaluated at frequency fx = x / (lambda * z)
|
||||
_U_out = pyfftw.empty_aligned((_n,_m), dtype='complex128')
|
||||
_FFTW_Obj = pyfftw.FFTW( np.fft.ifftshift(_U_in), _U_out, axes=(0,1), direction='FFTW_FORWARD', threads=6, flags=('FFTW_ESTIMATE',))
|
||||
_FFTW_Obj()
|
||||
_U_out = np.fft.fftshift(_U_out)
|
||||
|
||||
# Evaluate frequencies:
|
||||
_fx = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[0], d=_dpix))
|
||||
_fy = np.fft.fftshift(np.fft.fftfreq(np.shape(_xi)[1], d=_dpix))
|
||||
_x = _fx * self.l * dz
|
||||
_y = _fy * self.l * dz
|
||||
_x2, _y2 = np.meshgrid(_x, _y)
|
||||
|
||||
|
||||
# Final output and frequencies
|
||||
_pre_amble = np.exp(1j * _k * dz) / (1j * self.l * dz) * np.exp(1j * _k / (2 * dz) * (_x2**2 + _y2**2))
|
||||
_E_out = _pre_amble * _U_out
|
||||
|
||||
# Normalization of the output values
|
||||
_IntensityNormFactor = np.sum(np.abs(A)**2)
|
||||
_E_out /= (np.abs(np.exp(1j*_k*dz)/(1j*self.l*dz)) * np.sqrt(_IntensityNormFactor) * np.sqrt(_n*_m))
|
||||
|
||||
# Performing interpolation to get to the required final plane, done for real and imaginary part.
|
||||
_f_ip_real = interpolate.interp2d(_x, _y, _E_out.real, kind='linear')
|
||||
_f_ip_imag = interpolate.interp2d(_x, _y, _E_out.imag, kind='linear')
|
||||
_E_out_ip_real = _f_ip_real(_xip, _yip)
|
||||
_E_out_ip_imag = _f_ip_imag(_xip, _yip)
|
||||
_E_out_ip = _E_out_ip_real + _E_out_ip_imag*1j
|
||||
_E_out_ip /= np.sqrt(((_x2.max()/_xip.max())*(_y2.max()/_yip.max())))
|
||||
_E_out_ip = np.conj(_E_out_ip)
|
||||
|
||||
if show:
|
||||
# Plotting the results
|
||||
_fig = plt.figure(figsize=(8,3))
|
||||
_ax = _fig.add_subplot(121)
|
||||
_im = _ax.imshow(np.abs(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax.set_xlabel('x [mm]')
|
||||
_ax.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im, ax=_ax, label='Amplitude')
|
||||
_ax2 = _fig.add_subplot(122)
|
||||
_im2 = _ax2.imshow(np.angle(_E_out_ip), cmap='hot', extent=[_xip.min(), _xip.max(), _yip.min(), _yip.max()])
|
||||
_ax2.set_xlim([_xip.min(), _xip.max()])
|
||||
_ax2.set_ylim([_yip.min(), _yip.max()])
|
||||
_ax2.set_xlabel('x [mm]')
|
||||
_ax2.set_ylabel('y [mm]')
|
||||
_fig.colorbar(_im2, ax=_ax2, label='Phase')
|
||||
plt.tight_layout()
|
||||
if save:
|
||||
plt.savefig(save, bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
_x, _y = np.meshgrid(_xip, _yip)
|
||||
return np.array([_E_out_ip, _x, _y])
|
||||
|
||||
|
||||
def freqArray(self, A, dpix):
|
||||
n, m = np.shape(A)
|
||||
|
||||
fy, fx = np.mgrid[0:m, 0:n]
|
||||
|
||||
fx = fx.flatten()
|
||||
fy = fy.flatten()
|
||||
|
||||
for i in range(len(fx)):
|
||||
fx[i] = (fx[i] - n*bool(fx[i]>(n-1)/2) ) / (n*dpix)
|
||||
fy[i] = (fy[i] - m*bool(fy[i]>(m-1)/2) ) / (m*dpix)
|
||||
|
||||
fx = fx.reshape(n,m)
|
||||
fy = fy.reshape(n,m)
|
||||
|
||||
return fx, fy
|
||||
|
||||
|
||||
def phaseFactorExponent(self, fx, fy, l):
|
||||
k = 2*np.pi / l
|
||||
kx = np.asarray(2*np.pi * fx)
|
||||
ky = np.asarray(2*np.pi * fy)
|
||||
K_squared = (kx**2 + ky**2)
|
||||
kA = np.ones(K_squared.shape) * k
|
||||
K_1 = np.sqrt((kA**2-K_squared).astype('complex128'))
|
||||
kz = K_1.real + -1j*K_1.imag
|
||||
return kz
|
||||
|
||||
|
||||
def minimize(self, a, dz, x, y, og_data):
|
||||
'''
|
||||
Residual function used for fitting the gaussian beam to the input distribution.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
og_data - [array] input distribution to be fitted.
|
||||
'''
|
||||
return np.abs((self.gaussianBeamFit(a,dz,x,y) - og_data).flatten()).real
|
||||
|
||||
|
||||
def gaussianBeamFit(self, a, dz, x, y):
|
||||
'''
|
||||
Function used to fit the Gaussian beam to an input plane. Very similar
|
||||
to the GaussianBeam class below, but modified to allow a best fit.
|
||||
Input:
|
||||
a - [float] Guess for w0
|
||||
dz - [float] Propagation distance, should be known
|
||||
x,y - [array] meshgrid of the x and y coordinates.
|
||||
'''
|
||||
_w0 = a
|
||||
_z = dz
|
||||
_r = np.sqrt(x**2 + y**2)
|
||||
if _z != 0:
|
||||
_z_c = np.pi * _w0**2 / self.l
|
||||
_w = _w0 * np.sqrt(1 + (_z/_z_c)**2)
|
||||
_R = _z + _z_c **2 / _z
|
||||
_phi0 = np.arctan(_z/_z_c)
|
||||
_k = 2 * np.pi / self.l
|
||||
_results = np.sqrt(2 / (np.pi*_w**2)) * np.exp(- _r**2 / _w**2 - 1j * _k * _z - 1j * np.pi * _r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif _z == 0:
|
||||
_results = np.sqrt(2 / (np.pi*_w0**2)) * np.exp( - _r**2 / _w0**2 )
|
||||
return _results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class profiles:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def topHat(self, r, n, m, dpix, oversample=1):
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r)).astype(int)
|
||||
return _Anew
|
||||
|
||||
def obscuredTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
return _Anew.astype(int)
|
||||
|
||||
|
||||
def spiderTopHat(self, r1, r2, n, m, dpix, oversample=1):
|
||||
'''
|
||||
Creates a grid of n x m points, following a top hat function
|
||||
where all points inside a radius r is 1 and 0 otherwise.
|
||||
'''
|
||||
assert r1 > r2, "r2 < r1, change input"
|
||||
j,i = np.mgrid[-n/2*dpix : n/2*dpix : 1j*n*oversample, -m/2*dpix : m/2*dpix : 1j*m*oversample]
|
||||
n *= oversample
|
||||
m *= oversample
|
||||
dist_from_center = np.sqrt(i**2 + j**2)
|
||||
_Anew = (dist_from_center <= (r1)) & (dist_from_center >= (r2))
|
||||
_Aspider = (np.abs(i) < (0.05*r1)) | (np.abs(j) < (0.05*r1))
|
||||
_Anew = _Anew & ~_Aspider
|
||||
return _Anew
|
||||
|
||||
|
||||
|
||||
class GaussianBeam:
|
||||
def __init__(self, w0, l):
|
||||
# Confocal distance
|
||||
self.z_c = np.pi * w0**2 / l
|
||||
|
||||
# Far-field divergence angle
|
||||
self.theta_0 = l / (np.pi * w0)
|
||||
self.l = l
|
||||
self.w0 = w0
|
||||
self.fwhm = 1.18 * self.theta_0
|
||||
|
||||
def fundamentalMode(self, z, n, m, rmax):
|
||||
y, x = np.mgrid[-rmax:rmax:n*1j,-rmax:rmax:n*1j]
|
||||
r = np.sqrt(x**2 + y**2)
|
||||
if z != 0:
|
||||
_w = self.beamRadius(z)
|
||||
_R = self.curvature(z)
|
||||
_phi0 = self.phaseShift(z)
|
||||
_k = 2 * np.pi / self.l
|
||||
return np.sqrt(2 / (np.pi*_w**2)) * np.exp(- r**2 / _w**2 - 1j * _k * z - 1j * np.pi * r**2 / (self.l * _R) + 1j*_phi0)
|
||||
elif z == 0:
|
||||
return np.sqrt(2 / (np.pi*self.w0**2)) * np.exp( - r**2 / self.w0**2 )
|
||||
|
||||
def curvature(self, z):
|
||||
return z + self.z_c **2 / z
|
||||
|
||||
def beamRadius(self, z):
|
||||
return self.w0 * np.sqrt(1 + (z/self.z_c)**2)
|
||||
|
||||
def phaseShift(self, z):
|
||||
return np.arctan(z/self.z_c)
|
||||
|
||||
def relativePower(self, r, z):
|
||||
return np.exp(-2 * (r / self.beamRadius(z))**2)
|
||||
|
||||
|
||||
|
||||
class lensObject:
|
||||
'''
|
||||
The lens object describes any spherical lens and find the phase delay
|
||||
for all light traveling through it.
|
||||
The refractive index, of course, depends on the wavelength.
|
||||
'''
|
||||
def __init__(self, glass, R1, R2, thickness, CA, f=None, name=None):
|
||||
self.glass = glass
|
||||
assert glassCatalog.catalog.get(self.glass), 'Glass type not in catalog'
|
||||
self.R1 = R1 # mm
|
||||
self.R2 = R2 # mm
|
||||
self.thickness = thickness # mm
|
||||
self.CA = CA # mm
|
||||
self.dispersionCoefficients = glassCatalog.catalog.get(self.glass)
|
||||
self.referenceWavelength = 1.35 # micron
|
||||
self.type = 'Spherical lens'
|
||||
if not f:
|
||||
self.f = self.findFocalLength()
|
||||
else:
|
||||
self.f = f
|
||||
if name:
|
||||
assert type(name)==str, 'No string for a name.'
|
||||
self.name = name
|
||||
else:
|
||||
self.name = str(self)
|
||||
|
||||
def findRefractiveIndex(self, _l):
|
||||
_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 findFocalLength(self):
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_f_recip = (_n - 1) * (1 / self.R1 - 1 / self.R2)
|
||||
return 1/_f_recip
|
||||
|
||||
def phaseDelayFunction(self, x, y, l, paraxial=False):
|
||||
if not paraxial:
|
||||
_A1 = self.R1 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R1**2 ) )
|
||||
_A2 = self.R2 * (1 - np.sqrt(1 - (x**2 + y**2) / self.R2**2 ) )
|
||||
_dxy = self.thickness - _A1 + _A2
|
||||
if paraxial:
|
||||
_dxy = self.thickness - (x**2 + y**2)/2 * (1/self.R1 - 1/self.R2)
|
||||
_k = 2 * np.pi / 30e-3
|
||||
_n = self.findRefractiveIndex(self.referenceWavelength)
|
||||
_t_l = np.exp(-1j * _k * self.thickness)* np.exp(-1j * _k * (_n - 1) * _dxy)
|
||||
|
||||
# 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)).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)
|
||||
|
||||
|
||||
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 phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = (x**2 + y**2) / (4 * self.f)
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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.CA = CA
|
||||
self.type = 'Spherical mirror'
|
||||
|
||||
def phaseDelayFunction(self, x, y, l):
|
||||
_k = 2 * np.pi / l
|
||||
_dz = self.R * (1 - np.sqrt(1 - (x**2 + y**2)/self.R**2) )
|
||||
_t_m = np.exp(-1j * _k * 2 * _dz)
|
||||
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 def. mirror'
|
||||
|
||||
def phaseDelayFunction(self, l):
|
||||
_k = 2 * np.pi / l
|
||||
_t_m = np.exp(-1j * _k * 2 * self.z)
|
||||
return _t_m
|
||||
|
||||
|
||||
|
||||
|
||||
class OpticalTrain:
|
||||
'''
|
||||
The main class where an optical system is build up sequentially.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.opticalTrain = []
|
||||
self.sequentialDistance = []
|
||||
|
||||
def add_object(self, obj, dist):
|
||||
'''
|
||||
Adding a lens or mirror object to the opticalTrain
|
||||
obj The lens or mirror object
|
||||
dist The distance from the the previous object.
|
||||
'''
|
||||
assert type(dist) == float or type(dist) == int, 'Distance is not a float or integer.'
|
||||
self.opticalTrain.append(obj)
|
||||
self.sequentialDistance.append(float(dist))
|
||||
|
||||
def print_lens_Data(self):
|
||||
'''
|
||||
Print the data of the current opticalTrain.
|
||||
'''
|
||||
_tableData = []
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_distances = self.sequentialDistance
|
||||
_distances.append('-')
|
||||
_tableData.append([_i, _lens.name, _lens.type, _distances[_i+1] ])
|
||||
print(tabulate(_tableData, headers=['#', 'Name', 'Type', 'Distance to next surface']))
|
||||
|
||||
|
||||
def propagate_system(self, U_in, x, y, l, output=None, paraxial=True):
|
||||
# We need to have an input distribution
|
||||
# We need to determine what propagation method is required
|
||||
# Call the objects in the self.opticalTrain sequentially
|
||||
# Call the subsequent phaseDelay
|
||||
if output == 'complete':
|
||||
_wf_list = []
|
||||
_x_list = []
|
||||
_y_list = []
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
_wf = U_in
|
||||
|
||||
for _lens in self.opticalTrain:
|
||||
_i = self.opticalTrain.index(_lens)
|
||||
_dz = self.sequentialDistance[_i]
|
||||
_wf = _lens.phaseDelayFunction(x, y, l, paraxial=paraxial)
|
||||
|
||||
# Perform Fresnel number test:
|
||||
_F = self.fresnel_number()
|
||||
|
||||
if _F < 5:
|
||||
# Fresnel propagation
|
||||
_wf, x, y = self.fresnel_propagation()
|
||||
elif _F >= 5:
|
||||
# Angular spectrum (p2p) propagation
|
||||
_wf, x, y = self.angular_spectrum_propagation()
|
||||
|
||||
if output == 'complete':
|
||||
_wf_list.append(_wf)
|
||||
_x_list.append(x)
|
||||
_y_list.append(y)
|
||||
|
||||
if output == 'Complete':
|
||||
return np.array([_wf_list, _x_list, _y_list])
|
||||
|
||||
else:
|
||||
return np.array([_wf, x, y])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plt.rc('font', family='serif')
|
||||
plt.rc('axes', labelsize='large')
|
||||
|
||||
# Start time counter
|
||||
start = time.time()
|
||||
|
||||
# Pick a wavelength
|
||||
l = 30e-3
|
||||
|
||||
# Initiate Fourier Optics
|
||||
fou = FourierOptics(l=l)
|
||||
|
||||
# Pick sampling parameters
|
||||
dpix = 1/50
|
||||
n, m = 2001, 2001
|
||||
print ('Your resolution is {}x{} with a pixel size of {}'.format(n,m,dpix))
|
||||
|
||||
# Initiate the profiles class and pick a profile (tophat, obscured tophat or spider)
|
||||
prof = profiles()
|
||||
# A = prof.spiderTopHat(250, 100, 2001, 2001, dpix=1, oversample=1)
|
||||
A = prof.topHat(2.5, n, m, dpix=dpix, oversample=1)
|
||||
|
||||
# Alternatively, create a gaussian profile
|
||||
# gauss = GaussianBeam(0.3, l*1000)
|
||||
# A = gauss.fundamentalMode(0,n,m,np.max(x))
|
||||
|
||||
# Create an input grid
|
||||
x, y = np.meshgrid(np.linspace(-n/2*dpix, n/2*dpix, n), np.linspace(-m/2*dpix, m/2*dpix, m))
|
||||
|
||||
# Start an optical train object. Add lenses and propagate. Currently not in use.
|
||||
# LDE = OpticalTrain()
|
||||
|
||||
# Create a lens with a focal length ~100 mm, with a clear aperture radius of 4 mm and thickness of 1 mm.
|
||||
L1 = lensObject('N-BK7', 50, 1e99, 1, 4, name='L1')
|
||||
print('Focal length of lens {} is {:.1f} mm.'.format(L1.name, L1.f))
|
||||
|
||||
# Calculate the phase delay introduced by the lens
|
||||
t_l = L1.phaseDelayFunction(x,y,1.35e-6, paraxial=False)
|
||||
|
||||
# The input distribution gets a curved wavefront after the lens
|
||||
U_in = A * t_l
|
||||
|
||||
|
||||
# Performing the propagation using Fresnel approximation to the focal plane.
|
||||
oC = [-5, 5, -5, 5]
|
||||
savename = 'simple_lens.pdf'
|
||||
Exyz,_,_ = fou.FresnelPropagationFFTW(U_in, x, y, dz=L1.f, oversample=None, show=True, save=savename, outputCoordinates=oC)
|
||||
|
||||
# Plot a cross section of the new field (useful to better see the diffraction rings)
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.semilogy(np.linspace(np.min(oC), np.max(oC),n), np.abs(Exyz[1000,:]**2), 'r-')
|
||||
ax.set_xlabel('x (mm)')
|
||||
ax.set_ylabel('Intensity')
|
||||
ax.tick_params(axis ='both', direction='in', which='both')
|
||||
plt.savefig('simple_lens_slice.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# The following produces a cross-section of the propagation for various z, so that you can see how the beam evolves.
|
||||
if True:
|
||||
Ics = []
|
||||
Ics_x = []
|
||||
Isum = []
|
||||
xmax = []
|
||||
zList = np.linspace(20,220,20).tolist()
|
||||
xin, yin = x, y
|
||||
|
||||
for i in zList:
|
||||
title = f"{zList.index(i)+1:03d}"
|
||||
print (' Calculating {0}/{1}'.format(title, f"{len(zList):03d}"), end="\r")
|
||||
|
||||
# Propagate the wavefront
|
||||
# Exyz, x, y = fou.planeWavePropagationFFTW(A, dz=i*l, dpix=dpix, oversample=1, show=False, save=False)#, scaleInputArray=(n,m))
|
||||
Exyz, x, y = fou.FresnelPropagationFFTW(U_in, xin, yin, dz=i, oversample=None, show=False, save=False, outputCoordinates=oC)
|
||||
|
||||
# Process the results by taking a slice of the data
|
||||
Isum.append(np.sum(np.abs(Exyz)**2))
|
||||
Ics.append((np.abs(Exyz[int(Exyz.shape[0]/2)])**2))
|
||||
Ics_x.append(x.real[0])
|
||||
y_ext = [y.real[0,0],y.real[-1,0]]
|
||||
|
||||
# Reset the data
|
||||
x = None
|
||||
y = None
|
||||
|
||||
# Plotting all slices
|
||||
fig = plt.figure(figsize=(8,4))
|
||||
ax = fig.add_subplot(211)
|
||||
ax.tick_params(axis ='both', direction='in')
|
||||
|
||||
# Plotting the square root of the amplitude for better contrast
|
||||
im = ax.imshow(np.sqrt(np.asarray(Ics)).T, cmap='hot', aspect='auto', interpolation='bicubic', extent=[zList[0],zList[-1],y_ext[0],y_ext[1]])
|
||||
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
|
||||
# Plotting the intensity relative to the input
|
||||
ax2 = fig.add_subplot(212)
|
||||
ax2.plot(zList, np.asarray(Ics)[:,int(np.asarray(Ics).shape[1]/2)], 'k-')
|
||||
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1E'))
|
||||
ax2.set_xlabel('Propagation distance (mm)')
|
||||
ax2.tick_params(axis ='both', direction='in')
|
||||
ax2.set_ylabel('Amplitude')
|
||||
ax2.set_xlim([np.min(zList), np.max(zList)])
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# Saving the figure? Then show.
|
||||
plt.tight_layout()
|
||||
# plt.savefig('fresnel_cross.pdf', bbox_inches='tight')
|
||||
# plt.savefig('fresnel_cross.png', bbox_inches='tight')
|
||||
plt.savefig('simple_lens_cross.pdf', bbox_inches='tight', transparent=True)
|
||||
plt.show()
|
||||
|
||||
|
||||
# Closing down the script and stopping the timer.
|
||||
end = time.time()
|
||||
print('The program took {:.3f} seconds!'.format((end-start)))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env Python3
|
||||
|
||||
# Pyton dictionary with some glass types
|
||||
|
||||
catalog = {
|
||||
|
||||
'N-BK7' : [1.039612120E+00, 6.000698670E-03, 2.317923440E-01, 2.001791440E-02, 1.010469450E+00, 1.035606530E+02 ],
|
||||
'SiO2' : [0.6961663, 0.0684043, 0.4079426, 0.1162414, 0.8974794, 9.896161 ],
|
||||
'F2' : [1.345333590E+00, 9.977438710E-03, 2.090731760E-01, 4.704507670E-02, 9.373571620E-01, 1.118867640E+02 ],
|
||||
'S-FPL51' : [1.170105050E+00, 6.162039240E-03, 4.757107830E-02, 2.633728760E-02, 7.638324450E-01, 1.418826420E+02 ],
|
||||
'S-LAH71' : [1.982800310E+00, 1.189874560E-02, 3.167584500E-01, 5.271560010E-02, 2.444726460E+00, 2.132206970E+02 ],
|
||||
}
|
||||
Reference in new issue
Block a user