'Angular spectrum method using python

I am trying to numerically propagate a given (electric) field using the angular spectrum method. For this I am following "Principles and Applications of Fourier Optics" (Robert K. Tyson) Chapter 3, Page 2

enter image description here

I tried to recreate the maths using the following code

import numpy as np
import imageio

U = imageio.imread("ap.png")[:,:, 0] # load circular aperture
_lambda = 800e-9

def propagate2(self,z):
    A = np.fft.fft2(U, norm="ortho") # 2D FFT 
    alpha = np.fft.fftfreq(U.shape[0])*_lambda # direction cosine in x direction
    beta = np.fft.fftfreq(U.shape[1])*_lambda # direction cosine in x direction
    gamma = np.zeros([alpha.shape[0], beta.shape[0]])
    k = 2*np.pi/_lambda # wavevector

    for i,j in itertools.product(range(alpha.shape[0]), range(beta.shape[0])): # determine phase value for each (i,j)
        if alpha[i]**2+beta[j]**2 < 1:
            gamma[i,j] = np.sqrt(1-alpha[i]**2-beta[j]**2)
        else:
            gamma[i,j] = 1j*np.sqrt(np.abs(1-alpha[i]**2-beta[j]**2))
    phi = np.exp(1j*k*z*gamma)
    field = np.fft.ifft2(A*phi, norm="ortho") # 2D IFFT
    return field

This code should produce the usual double slit diffraction pattern, however (as to be seen below) won't produce and diffraction at all.

Code output

I am fairly certain that there is some problem with my alpha and beta values, however I can't seem to find it. Any help is highly appreciated.

ap.png:

ap.png



Solution 1:[1]

I made a complete implementation of the Angular Spectrum Method using Python: https://github.com/rafael-fuente/Diffraction-Simulations--Angular-Spectrum-Method

To use it for your example, you need to clone the repository and put your double slit image in the path ./apertures/double_slit.png

Then, from the repository folder run the following script:

from diffractsim import MonochromaticField, mm, nm, cm, ApertureFromImage


F = MonochromaticField(
    wavelength=632.8 * nm, extent_x=5.6 * mm, extent_y=5.6 * mm, Nx=1024, Ny=1024
)

F.add(ApertureFromImage(
   "./apertures/double_slit.jpg", image_size=(4.0* mm, 4.0 * mm), simulation = F))
rgb = F.compute_colors_at(20*cm)
F.plot_colors(rgb, xlim=[-4.5, 4.5], ylim=[-4.5, 4.5])

This is the result of the script of the near field (20 cm) for different wavelengths: Diffraction of a double-slit for different wavelengths

As we can see in the plots, the higher the light's wavelength, the wider the length of the interference fringes.

You can also compute the diffraction pattern with a broad spectrum, like white light, with the PolychromaticField class:

from diffractsim import PolychromaticField, cf, mm, cm, ApertureFromImage


F = PolychromaticField(
    spectrum=2 * cf.illuminant_d65, extent_x=5.6 * mm, extent_y=5.6 * mm, Nx=1024, Ny=1024
)

F.add(ApertureFromImage(
    "./apertures/double_slit.png", image_size=(4.0* mm, 4.0 * mm), simulation = F))

rgb = F.compute_colors_at(20*cm)
F.plot_colors(rgb, xlim=[-4.5, 4.5], ylim=[-4.5, 4.5])

which results in:

white light double slit angular spectrum method

The problem with your code is that you are not using the Fast Fourier Transform correctly.

The core of my implementation (propagation of the Angular Spectrum) is in the MonochromaticField propagate method, in monochromatic_simulator.py:

def propagate(self, z):
    self.z += z

    # compute angular spectrum
    fft_c = fft2(self.E)
    c = fftshift(fft_c)

    kx = 2*bd.pi*bd.fft.fftshift(bd.fft.fftfreq(simulation.Nx, d = simulation.dx))
    ky = 2*bd.pi*bd.fft.fftshift(bd.fft.fftfreq(simulation.Ny, d = simulation.dy))
    kx, ky = np.meshgrid(kx, ky)
    kz = np.sqrt((2 * np.pi / self.?) ** 2 - kx ** 2 - ky ** 2)

    # propagate the angular spectrum a distance z
    E = ifft2(ifftshift(c * np.exp(1j * kz * z)))

    # compute Field Intensity
    self.I = np.real(E * np.conjugate(E)) 

You can see that I used the method fftshift after performing the FFT to match the definition of the Fourier Transform.

Also, if you want to discard the evanescent fields as you seem you want to do, it's a much better approach to use numpy.where instead of looping every pixel, since Python loops are much slower:

    # propagate the angular spectrum a distance z
    mask = (2*np.pi/self.?)**2 - kx**2 - ky**2 > 0
    A = np.where(mask, c*np.exp(1j*kz * z),  0)
    E = ifft2(ifftshift(A))

This code should replace the last lines in the compute_colors_at method. Hope that this helps!

Solution 2:[2]

It can be tricky to get this right. Here it is:

u = ... # this is your 2D complex field that you wish to propagate
z = ... # this is the distance by which you wish to propagate

dx, dy = 1, 1 # or whatever

wavelen = 1 # or whatever
wavenum = 2 * np.pi / wavelen
wavenum_sq = wavenum * wavenum

kx = np.fft.fftfreq(u.shape[0], dx / (2 * np.pi))
ky = np.fft.fftfreq(u.shape[1], dy / (2 * np.pi))

# this is just for broadcasting, the "indexing" argument prevents NumPy
# from doing a transpose (default meshgrid behaviour)
kx, ky = np.meshgrid(kx, ky, indexing = 'ij', sparse = True)

kz_sq = kx * kx + ky * ky

# we zero out anything for which this is not true, see many textbooks on
# optics for an explanation
mask = wavenum * wavenum > kz_sq

g = np.zeros((len(kx), len(ky)), dtype = np.complex_)
g[mask] = np.exp(1j * np.sqrt(wavenum_sq - kz_sq[mask]) * z)

res = np.fft.ifft2(g * np.fft.fft2(u)) # this is the result

You probably want to pad u to prevent wrap around. In that case, simply compute g with the shape doubled then slice the result.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2