Files
fourierOptics/test cases (standalone)/fourierOptics_025_single_lens_system.py
2020-05-28 09:41:08 +02:00

730 lines
24 KiB
Python

#!/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)))