Skip to content

Commit 498e7e9

Browse files
jakerogers-1pre-commit-ci[bot]cclauss
authored
Add Algorithm for Fresnel Diffraction (#11580)
* Create fresnel_diffract.py Code for wave diffraction in the Fresnel regime was missing from the algorithms list and has been added. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Altered Styling for Ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Adjusted styling for ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Included Tests Tests covering dimensionality, error checking, conservation of energy, and zero propagation distance, have all been included. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated styling for ruff * Updated Tests * updating DIRECTORY.md * Fix typos and improve code readability * Change isclose check to return boolean directly --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: cclauss <cclauss@users.noreply.github.com>
1 parent 4a5f9b6 commit 498e7e9

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@
10171017
* [Escape Velocity](physics/escape_velocity.py)
10181018
* [Faraday Lenz Law](physics/faraday_lenz_law.py)
10191019
* [First Law Of Thermodynamics](physics/first_law_of_thermodynamics.py)
1020+
* [Fresnel Diffract](physics/fresnel_diffract.py)
10201021
* [Grahams Law](physics/grahams_law.py)
10211022
* [Hamiltonian](physics/hamiltonian.py)
10221023
* [Hookes Law](physics/hookes_law.py)

physics/fresnel_diffract.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""
2+
Title: Fresnel Diffraction for Coherent and Monochromatic
3+
Wave Fields
4+
5+
Fresnel Diffraction describes the behavior of a wave field as it
6+
moves through free space or interacts with an object under the
7+
small-angle approximation. It is particularly useful for near
8+
field diffraction.
9+
10+
The following algorithm is an adaptation of the 'transfer function'
11+
based approach contained in the reference. It is critically
12+
sampled when:
13+
pixel_size = wavelength * prop_dist / side_length
14+
15+
Or equivalently:
16+
pixel_size = sqrt(wavelength * prop_dist / pixel_num)
17+
18+
Under and oversampling occur when the left-hand side is less
19+
than or greater than the right-hand side, respectively.
20+
21+
This code is adapted and modified from:
22+
Computational Fourier Optics: A MATLAB Tutorial by David Voelz
23+
"""
24+
25+
from math import pi
26+
27+
import numpy as np
28+
from scipy.fft import fft, fft2, fftshift, ifft, ifft2, ifftshift
29+
30+
31+
def fresnel_diffract(
32+
wavefunc_0: np.ndarray, pixel_size: float, wavelength: float, prop_dist: float
33+
) -> np.ndarray:
34+
"""
35+
Fresnel Diffraction of 1D or 2D Wave Fields.
36+
37+
This function calculates the Fresnel diffraction of a
38+
given wave field, suitable for near-field diffraction. The
39+
wave field is assumed to be coherent and monochromatic.
40+
41+
Args:
42+
wavefunc0 (np.ndarray): The initial wave field at the unpropagated plane.
43+
pixel_size (float): The physical size of a pixel (or data point) at the
44+
pixel_size (float): The physical size of a pixel (or data point) at the
45+
unpropagated plane.
46+
wavelength (float): The wavelength of the wave field.
47+
prop_dist (float): The desired propagation distance.
48+
49+
Raises:
50+
ValueError: If the input wave field is not 1D or 2D.
51+
52+
Returns:
53+
np.ndarray: The wave field at the propagated plane.
54+
55+
Examples:
56+
>>> import numpy as np
57+
>>> res = fresnel_diffract(np.ones(64), 1, 1, 1)
58+
>>> res.shape
59+
(64,)
60+
>>> import numpy as np
61+
>>> res = fresnel_diffract(np.ones((64, 64)), 1, 1, 1)
62+
>>> res.shape
63+
(64, 64)
64+
>>> import numpy as np
65+
>>> res = fresnel_diffract(np.ones((4, 4, 4)), 1, 1, 1)
66+
Traceback (most recent call last):
67+
...
68+
ValueError: Expected a 1D or 2D wavefield, but got (4, 4, 4)
69+
70+
# Test that conservation of energy is obeyed
71+
>>> import numpy as np
72+
>>> wf0 = np.ones(64)
73+
>>> wfz = fresnel_diffract(wf0, 1, 1, 1)
74+
>>> bool(np.isclose(np.sum(abs(wf0)**2), np.sum(abs(wfz)**2)))
75+
True
76+
>>> import numpy as np
77+
>>> wf0 = np.ones((64, 64))
78+
>>> wfz = fresnel_diffract(wf0, 1, 1, 1)
79+
>>> bool(np.isclose(np.sum(abs(wf0)**2), np.sum(abs(wfz)**2)))
80+
True
81+
82+
# Test that propagation distance of 0 returns the contact image
83+
>>> import numpy as np
84+
>>> x = np.linspace(-32, 32, 1)
85+
>>> wf0 = np.where(abs(x)<=8, 1, 0)
86+
>>> wfz = fresnel_diffract(wf0, 1, 1, 0)
87+
>>> np.allclose(wf0, wfz)
88+
True
89+
"""
90+
91+
if len(wavefunc_0.shape) == 1:
92+
return _fresnel_diffract_1d(wavefunc_0, pixel_size, wavelength, prop_dist)
93+
elif len(wavefunc_0.shape) == 2:
94+
return _fresnel_diffract_2d(wavefunc_0, pixel_size, wavelength, prop_dist)
95+
else:
96+
error_message = f"Expected a 1D or 2D wavefield, but got {wavefunc_0.shape}"
97+
raise ValueError(error_message)
98+
99+
100+
def _fresnel_diffract_2d(
101+
wavefunc_0: np.ndarray, pixel_size: float, wavelength: float, prop_dist: float
102+
) -> np.ndarray:
103+
"""
104+
Fresnel Diffraction of 2D Wave Fields.
105+
This private function is called by 'fresnel_diffract' to handle the
106+
fresnel diffraction of 2D wave fields specifically.
107+
Args:
108+
wavefunc_0 (np.ndarray): The initial 2D wave field at the unpropagated plane.
109+
pixel_size (float): The physical size of a pixel (or data point) at the
110+
unpropagated plane.
111+
wavelength (float): The wavelength of the wave field.
112+
prop_dist (float): The desired propagation distance.
113+
114+
Returns:
115+
np.ndarray: The 2D wave field at the propagated plane.
116+
117+
118+
Examples:
119+
>>> import numpy as np
120+
>>> res = _fresnel_diffract_2d(np.ones((64, 64)), 1, 1, 1)
121+
>>> res.shape
122+
(64, 64)
123+
>>> import numpy as np
124+
>>> wf0 = np.ones((64, 64))
125+
>>> wfz = _fresnel_diffract_2d(wf0, 1, 1, 1)
126+
>>> bool(np.isclose(np.sum(abs(wf0)**2), np.sum(abs(wfz)**2)))
127+
True
128+
129+
# Test that propagation distance of 0 returns the contact image
130+
>>> import numpy as np
131+
>>> x = np.linspace(-32, 32, 1)
132+
>>> X1, X2 = np.meshgrid(x, x)
133+
>>> wf0 = np.where(abs(X1)<=8, 1, 0) * np.where(abs(X2)<=8, 1, 0)
134+
>>> wfz = _fresnel_diffract_2d(wf0, 1, 1, 0)
135+
>>> np.allclose(wf0, wfz)
136+
True
137+
"""
138+
pixel_num, _ = wavefunc_0.shape
139+
side_length = pixel_num * pixel_size
140+
141+
# Coordinates in Fourier space are proportionate to 1 / pixel_size
142+
f_x = np.arange(-1 / (2 * pixel_size), 1 / (2 * pixel_size), 1 / side_length)
143+
144+
f_x2d, f_y2d = np.meshgrid(f_x, f_x)
145+
146+
# Transfer function which models diffraction
147+
transferf = np.exp(-1j * np.pi * wavelength * prop_dist * (f_x2d**2 + f_y2d**2))
148+
transferf = fftshift(transferf)
149+
150+
# Fourier space wave function at the unpropagated plane
151+
f_wavefunc_0 = fft2(fftshift(wavefunc_0))
152+
# Wave function at the propagated, or 'z' plane
153+
wavefuncz = ifftshift(ifft2(transferf * f_wavefunc_0))
154+
155+
return wavefuncz
156+
157+
158+
def _fresnel_diffract_1d(
159+
wavefunc_0: np.ndarray, pixel_size: float, wavelength: float, prop_dist: float
160+
) -> np.ndarray:
161+
"""
162+
Fresnel Diffraction of 1D Wave Fields.
163+
This private function is called by 'fresnel_diffract' to handle the
164+
fresnel diffraction of 1D wave fields specifically.
165+
Args:
166+
wavefunc0 (np.ndarray): The initial 1D wave field at the unpropagated plane.
167+
pixel_size (float): The physical size of a pixel (or data point) at the
168+
unpropagated plane.
169+
wavelength (float): The wavelength of the wave field.
170+
prop_dist (float): The desired propagation distance.
171+
172+
Returns:
173+
np.ndarray: The 1D wave field at the propagated plane.
174+
175+
176+
Examples:
177+
>>> import numpy as np
178+
>>> res = _fresnel_diffract_1d(np.ones(64), 1, 1, 1)
179+
>>> res.shape
180+
(64,)
181+
182+
# Conservation of energy
183+
>>> import numpy as np
184+
>>> wf0 = np.ones(64)
185+
>>> wfz = _fresnel_diffract_1d(wf0, 1, 1, 1)
186+
>>> bool(np.isclose(np.sum(abs(wf0)**2), np.sum(abs(wfz)**2)))
187+
True
188+
189+
# Test that propagation distance of 0 returns the contact image
190+
>>> import numpy as np
191+
>>> x = np.linspace(-32, 32, 1)
192+
>>> wf0 = np.where(abs(x)<=8, 1, 0)
193+
>>> wfz = _fresnel_diffract_1d(wf0, 1, 1, 0)
194+
>>> np.allclose(wf0, wfz)
195+
True
196+
"""
197+
pixel_num = len(wavefunc_0)
198+
side_length = pixel_num * pixel_size
199+
fx = np.arange(-1 / (2 * pixel_size), 1 / (2 * pixel_size), 1 / side_length)
200+
transferf = np.exp(-1j * pi * wavelength * prop_dist * (fx**2))
201+
transferf = fftshift(transferf)
202+
203+
f_wavefunc_0 = fft(fftshift(wavefunc_0))
204+
205+
wavefunc_z = ifftshift(ifft(transferf * f_wavefunc_0))
206+
207+
return wavefunc_z

0 commit comments

Comments
 (0)