Skip to content

Commit 4ec633d

Browse files
committed
Add shifted AS
1 parent f5a46ad commit 4ec633d

File tree

5 files changed

+189
-32
lines changed

5 files changed

+189
-32
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Otherwise, just look into the [examples folder](examples/).
3030
* [x] Angular Spectrum Method of Plane Waves (AS)
3131
* [x] Fraunhofer Diffraction
3232
* [x] [Scalable Angular Spectrum propagation](https://opg.optica.org/optica/viewmedia.cfm?uri=optica-10-11-1407&html=true)
33+
* [x] Shifted Angular Spectrum propagation
3334
* [ ] Fresnel Propagation with Scaling Behaviour (no priority yet, PR are welcome for that. In principle very similar to the other methods.)
3435
* [x] CUDA support
3536
* [x] Differentiable (mainly based on Zygote.jl and ChainRulesCore.jl)

docs/src/functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ They create efficient functions which avoid recalculating some parts.
44
AngularSpectrum
55
Fraunhofer
66
ScalableAngularSpectrum
7+
ShiftedAngularSpectrum
78
```
89

910
# Propagation

examples/shifted_angular_spectrum.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ end
2727
md"# Define field with a ramp"
2828

2929
# ╔═╡ 2f6871e8-7c11-49c0-ba9a-dc498e8eb39d
30-
N = 128
30+
N = 256
3131

3232
# ╔═╡ 64b448ee-5ccc-4f87-8ee0-20d2d6a41a3b
3333
sz = (N, N)
3434

3535
# ╔═╡ 90286b89-aedd-4ece-b9d6-e5c26c6ad635
36-
α = 0 * deg2rad(10f0)
36+
α = deg2rad(10f0)
3737

3838
# ╔═╡ dc01bc87-ffd7-400f-bbf2-3b00a3a84b78
3939
L = 50f-6
@@ -60,7 +60,7 @@ md"# Compare AS and shifted AS"
6060
res_AS = AngularSpectrum(field, z, λ, L)[1](field)[1];
6161

6262
# ╔═╡ 9cbafe25-3af6-4bcf-833c-8d3d7ca428a2
63-
res = shifted_angular_spectrum(field, z, λ, L, (α , 0), bandlimit=true)
63+
res = ShiftedAngularSpectrum(field, z, λ, L, (α , 0), bandlimit=true)[1](field)
6464

6565
# ╔═╡ 4efdc02b-4f69-4893-a410-6c6bbb765bab
6666
shift = (z .* tan.(α) ./ L .* N)[1]
@@ -100,6 +100,12 @@ all(.≈(1 .+ FourierTools.shift(res[1], (shift, 0))[round(Int, shift)+1:end, :]
100100
# ╔═╡ d7eac41e-c5c0-46f8-9b2f-d2f116b65d95
101101

102102

103+
# ╔═╡ 4c3d7581-df26-4bec-9e0e-44279158d8b9
104+
105+
106+
# ╔═╡ ed3ec3b6-4825-4cd8-9d95-13d978d64584
107+
108+
103109
# ╔═╡ Cell order:
104110
# ╠═b11e7be2-b315-11ee-27e7-abecfdbe64b6
105111
# ╠═3a5d9f20-a01d-481b-9858-b8e523ba7a20
@@ -129,3 +135,5 @@ all(.≈(1 .+ FourierTools.shift(res[1], (shift, 0))[round(Int, shift)+1:end, :]
129135
# ╠═154b48f2-58b8-4e4f-8648-8ea771125be5
130136
# ╠═f3f7bc9e-a16f-49d3-920f-031326f5f1af
131137
# ╠═d7eac41e-c5c0-46f8-9b2f-d2f116b65d95
138+
# ╠═4c3d7581-df26-4bec-9e0e-44279158d8b9
139+
# ╟─ed3ec3b6-4825-4cd8-9d95-13d978d64584

src/shifted_angular_spectrum.jl

Lines changed: 120 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,70 +53,164 @@ function _prepare_shifted_angular_spectrum(field::AbstractArray{CT}, z, λ, L,
5353
shift = txy .* z
5454

5555
ya = similar(field_new, real(eltype(field)), (size(field_new, 1), 1))
56-
ya .= (fftpos(L_new[1], size(field_new, 1), CenterFT)) .+ shift[1]
56+
Zygote.@ignore ya .= (fftpos(L_new[1], size(field_new, 1), CenterFT)) .+ shift[1]
5757
xa = similar(field_new, real(eltype(field)), (1, size(field_new, 2)))
58-
xa .= (fftpos(L_new[2], size(field_new, 2), CenterFT))' .+ shift[2]
58+
Zygote.@ignore xa .= (fftpos(L_new[2], size(field_new, 2), CenterFT))' .+ shift[2]
5959

6060
ramp_before = ifftshift(exp.(1im .* 2 .* T(π) ./ λ .* (sxy[2] .* x .+ sxy[1] .* y)), (1,2))
6161
ramp_after = ifftshift(exp.(1im .* 2 .* T(π) ./ λ .* (sxy[2] .* xa .+ sxy[1] .* ya)), (1,2))
6262
return (;field_new, H, W, fftdims, ramp_before, ramp_after)
6363
end
6464

6565

66+
function shifted_angular_spectrum(field::AbstractArray{CT, 2}, z, λ, L, α;
67+
padding=true, pad_factor=2,
68+
bandlimit=true,
69+
bandlimit_border=(0.8, 1.0)) where {CT<:Complex}
70+
71+
@assert size(field, 1) == size(field, 2) "input field needs to be quadradically shaped and not $(size(field, 1)), $(size(field, 2))"
72+
73+
(; field_new, H, W, fftdims, ramp_before, ramp_after) = _prepare_shifted_angular_spectrum(field, z, λ, L, real(CT).(α); padding,
74+
pad_factor, bandlimit, bandlimit_border)
75+
76+
# propagate field
77+
field_new_is = ifftshift(field_new, fftdims) ./ (ramp_before)
78+
field_out = fftshift(ramp_after .* ifft(fft(field_new_is, fftdims) .* H .* W, fftdims), fftdims)
79+
field_out_cropped = padding ? crop_center(field_out, size(field)) : field_out
80+
shift = z .* tan.(α) ./ L
81+
# return final field and some other variables
82+
return field_out_cropped, (; L, shift)
83+
end
84+
85+
86+
# highly optimized version with pre-planning
87+
struct ShiftedAngularSpectrum{A, T, T2, P, R}
88+
HW::A
89+
buffer::A
90+
buffer2::A
91+
L::T
92+
shift::T2
93+
p::P
94+
padding::Bool
95+
pad_factor::Int
96+
ramp_before::R
97+
ramp_after::R
98+
end
99+
100+
66101
"""
67-
shifted_angular_spectrum(field, z, λ, L, α; kwargs...)
102+
ShiftedAngularSpectrum(field, z, λ, L, α; kwargs...)
68103
69-
Returns the electrical field with physical length `L` and wavelength `λ` propagated with the shifted angular spectrum
104+
Returns a method to propagate the electrical field with physical length `L` and wavelength `λ` with the shifted angular spectrum
70105
method of plane waves (AS) by the propagation distance `z`.
71-
`α` is the shift angle with respect to the optical axis.
106+
`α` should be a tuple containing the offset angles with respect to the optical axis.
72107
73-
This method is efficient but to avoid recalculating some arrays (such as the phase kernel), see [`ShiftedAngularSpectrum`](@ref).
74108
75109
# Arguments
76110
* `field`: Input field
77111
* `z`: propagation distance. Can be a single number or a vector of `z`s (Or `CuVector`). In this case the returning array has one dimension more.
78112
* `λ`: wavelength of field
79113
* `L`: field size (can be a scalar or a tuple) indicating field size
80-
* `α` is the shift angle with respect to the optical axis.
114+
* `α` is the tuple of shift angles with respect to the optical axis.
81115
82116
83117
# Keyword Arguments
84-
* `padding=true`: applies padding to avoid convolution wraparound
85118
* `pad_factor=2`: padding of 2. Larger numbers are not recommended since they don't provide better results.
86119
* `bandlimit=true`: applies the bandlimit to avoid circular wraparound due to undersampling
87120
of the complex propagation kernel [1]
88-
* `bandlimit_border=(0.8, 1)`: applies a smooth bandlimit cut-off instead of hard-edge.
89-
121+
* `extract_ramp=true`: divides the field by phase ramp `exp.(1im * 2π / λ * (sin(α[2]) .* x .+ sin(α[1]) .* y))` and multiplies after
122+
propagation the ramp (with new real space coordinates) back to the field
90123
91124
# Examples
92125
```jldoctest
93126
julia> field = zeros(ComplexF32, (4,4)); field[3,3] = 1
94-
```
95127
128+
julia> AS, _ = ShiftedAngularSpectrum(field, 100e-6, 633e-9, 100e-6, (deg2rad(10), 0));
129+
130+
julia> AS(field)
131+
(ComplexF32[1.5269792f-5 + 1.7594219f-5im -3.996831f-5 - 7.624799f-5im -0.0047351345f0 + 0.002100923f0im -3.996831f-5 - 7.624799f-5im; -8.294997f-5 - 1.8230454f-5im 0.00028230582f0 + 8.1745195f-5im 0.0051693693f0 - 0.016958509f0im 0.00028230582f0 + 8.1745195f-5im; 0.0029884572f0 + 0.0040671355f0im -0.009686601f0 - 0.014245203f0im -0.82990384f0 + 0.5566719f0im -0.009686601f0 - 0.014245203f0im; -1.4191573f-7 + 9.41665f-5im 2.111472f-5 - 0.00031620878f0im -0.017670793f0 - 0.0014212304f0im 2.111472f-5 - 0.00031620878f0im], (L = 0.0001, shift = (0.17632698070846498, 0.0)))
132+
```
96133
97134
# References
98135
* Matsushima, Kyoji. "Shifted angular spectrum method for off-axis numerical propagation." Optics Express 18.17 (2010): 18453-18463.
99136
"""
100-
function shifted_angular_spectrum(field::AbstractArray{CT, 2}, z, λ, L, α;
137+
function ShiftedAngularSpectrum(field::AbstractArray{CT, N}, z::Number, λ, L, α;
138+
extract_ramp=true,
101139
padding=true, pad_factor=2,
102140
bandlimit=true,
103-
bandlimit_border=(0.8, 1.0)) where {CT<:Complex}
104-
105-
@assert size(field, 1) == size(field, 2) "input field needs to be quadradically shaped and not $(size(field, 1)), $(size(field, 2))"
106-
107-
(; field_new, H, W, fftdims, ramp_before, ramp_after) =
141+
bandlimit_border=(0.8, 1)) where {CT, N}
142+
143+
(; field_new, H, W, fftdims, ramp_before, ramp_after) =
108144
_prepare_shifted_angular_spectrum(field, z, λ, L, real(CT).(α); padding,
109145
pad_factor, bandlimit, bandlimit_border)
146+
147+
148+
buffer2 = similar(field, complex(eltype(field_new)), (size(field_new, 1), size(field_new, 2)))
149+
buffer = copy(buffer2)
150+
151+
p = plan_fft!(buffer, (1, 2))
152+
H .= H .* W
153+
HW = H
154+
shift = z .* tan.(α) ./ L
155+
if !extract_ramp
156+
ramp_before = nothing
157+
ramp_after = nothing
158+
end
110159

111-
# propagate field
112-
field_new_is = ifftshift(field_new, fftdims) ./ (ramp_before)
113-
# ramp_after = 1
114-
field_out = fftshift(ramp_after .* ifft(fft(field_new_is, fftdims) .* H .* W, fftdims), fftdims)
115-
field_out_cropped = padding ? crop_center(field_out, size(field)) : field_out
116-
shift = z .* tan.(α) ./ L
117-
# return final field and some other variables
118-
return field_out_cropped, (; L, shift)
160+
161+
return ShiftedAngularSpectrum{typeof(H), typeof(L), typeof(shift), typeof(p), typeof(ramp_before)}(HW,
162+
buffer, buffer2, L, shift, p, padding, pad_factor, ramp_before, ramp_after), (;L, shift)
163+
end
164+
165+
166+
167+
"""
168+
(shifted_as::ShiftedAngularSpectrum)(field)
169+
170+
Uses the struct to efficiently store some pre-calculated objects.
171+
Propagate the field.
172+
"""
173+
function (as::ShiftedAngularSpectrum{A, T, T2, P, R})(field) where {A,T,T2,P,R}
174+
fill!(as.buffer2, 0)
175+
field_new = set_center!(as.buffer2, field, broadcast=true)
176+
ifftshift!(as.buffer, field_new, (1, 2))
177+
if !(R === Nothing)
178+
as.buffer ./= as.ramp_before
179+
end
180+
field_imd = as.p * as.buffer
181+
field_imd .*= as.HW
182+
field_imd = inv(as.p) * field_imd
183+
if !(R === Nothing)
184+
field_imd .*= as.ramp_after
185+
end
186+
field_out = fftshift!(as.buffer2, field_imd, (1, 2))
187+
field_out_cropped = as.padding ? crop_center(field_out, size(field), return_view=true) : field_out
188+
return field_out_cropped, (; as.L, as.shift)
119189
end
120190

121191

122-
scalable_angular_spectrum(field::AbstractArray, z, λ, L; kwargs...) = throw(ArgumentError("Provided field needs to have a complex elementype"))
192+
function ChainRulesCore.rrule(as::ShiftedAngularSpectrum{A, T, T2, P, R}, field) where {A,T,T2,P,R}
193+
field_and_tuple = as(field)
194+
function as_pullback(ȳ)
195+
= NoTangent()
196+
y2 =.backing[1]
197+
198+
fill!(as.buffer2, 0)
199+
field_new = as.padding ? set_center!(as.buffer2, y2, broadcast=true) : y2
200+
ifftshift!(as.buffer, field_new, (1, 2))
201+
if !(R === Nothing)
202+
as.buffer .*= conj.(as.ramp_after)
203+
end
204+
field_imd = as.p * as.buffer
205+
field_imd .*= conj.(as.HW)
206+
207+
field_imd = inv(as.p) * field_imd
208+
if !(R === Nothing)
209+
field_imd ./= conj.(as.ramp_before)
210+
end
211+
field_out = fftshift!(as.buffer2, field_imd, (1, 2))
212+
field_out_cropped = as.padding ? crop_center(field_out, size(field), return_view=true) : field_out
213+
return f̄, field_out_cropped
214+
end
215+
return field_and_tuple, as_pullback
216+
end

test/shifted_angular_spectrum.jl

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,76 @@
88
y = fftpos(L, N, CenterFT)
99
field = box(Float32, sz, (20,20)) .* exp.(1im .* 2f0 * π ./ λ .* y .* sin(α));
1010
res_AS = AngularSpectrum(field, z, λ, L)[1](field)[1];
11-
res = shifted_angular_spectrum(field, z, λ, L, (α , 0), bandlimit=true)
11+
res = WaveOpticsPropagation.shifted_angular_spectrum(field, z, λ, L, (α , 0), bandlimit=true)
1212
shift = z * tan(α) / L * N
1313
shift2 = z * tan(α) / L
14+
res2 = ShiftedAngularSpectrum(field, z, λ, L, (α, 0), bandlimit=true)[1](field)
1415
@test all(.≈(1 .+ FourierTools.shift(res[1], (shift, 0))[round(Int, shift)+1:end, :], 1 .+ res_AS[round(Int, shift)+1:end, :], rtol=5f-2))
16+
@test all(.≈(1 .+ FourierTools.shift(res2[1], (shift, 0))[round(Int, shift)+1:end, :], 1 .+ res_AS[round(Int, shift)+1:end, :], rtol=5f-2))
1517

1618

1719
field = box(Float32, sz, (20,20)) .* exp.(1im .* 2f0 * π ./ λ .* y' .* sin(α));
1820
res_AS = AngularSpectrum(field, z, λ, L)[1](field)[1];
19-
res = shifted_angular_spectrum(field, z, λ, L, (0, α), bandlimit=true)
21+
res = WaveOpticsPropagation.shifted_angular_spectrum(field, z, λ, L, (0, α), bandlimit=true)
2022
shift = z * tan(α) / L * N
2123
shift2 = z * tan(α) / L
24+
res2 = ShiftedAngularSpectrum(field, z, λ, L, (0, α), bandlimit=true)[1](field)
2225
@test all(.≈(1 .+ FourierTools.shift(res[1], (0, shift))[:, round(Int, shift)+1:end], 1 .+ res_AS[:, round(Int, shift)+1:end], rtol=5f-2))
26+
@test all(.≈(1 .+ FourierTools.shift(res2[1], (0, shift))[:, round(Int, shift)+1:end], 1 .+ res_AS[:, round(Int, shift)+1:end], rtol=5f-2))
2327

2428

2529
res_AS = AngularSpectrum(field, z, λ, L)[1](field)[1];
26-
res = shifted_angular_spectrum(field, z, λ, L, (0, 0), bandlimit=true)[1]
30+
res = WaveOpticsPropagation.shifted_angular_spectrum(field, z, λ, L, (0, 0), bandlimit=true)[1]
31+
res2 = ShiftedAngularSpectrum(field, z, λ, L, (0, 0), bandlimit=true)[1](field)[1]
2732
@test all(.≈(1 .+ res, 1 .+ res_AS, rtol=1f-1))
2833
@test (1 .+ res, 1 .+ res_AS, rtol=1f-2)
34+
@test all(.≈(1 .+ res2, 1 .+ res_AS, rtol=1f-1))
35+
@test (1 .+ res2, 1 .+ res_AS, rtol=1f-2)
36+
37+
38+
39+
40+
@testset "Test gradient with Finite Differences" begin
41+
field = zeros(ComplexF64, (24, 24))
42+
field[14:16, 14:16] .= 1
43+
44+
α = deg2rad(10)
45+
gg(x) = sum(abs2.(x .- WaveOpticsPropagation.shifted_angular_spectrum(cis.(x), 100e-6, 633e-9, 100e-6, (α, 0))[1]))
46+
47+
out2 = FiniteDifferences.grad(central_fdm(5, 1), gg, field)[1]
48+
49+
out1 = gradient(gg, field)[1]
50+
@test out1 .+ cis(1) out2 .+ cis(1)
51+
AS, _ = ShiftedAngularSpectrum(field, 100e-6, 633e-9, 100e-6, (α, 0))
52+
53+
f_AS(x) = sum(abs2.(x .- AS(cis.(x))[1]))
54+
55+
out3 = gradient(f_AS, field)[1]
56+
57+
@test out3 out1
58+
59+
60+
field = zeros(ComplexF64, (15, 15))
61+
field[5:6, 3:8] .= 1
62+
gg(x) = sum(abs2.(x .- WaveOpticsPropagation.shifted_angular_spectrum(cis.(x), 100e-6, 633e-9, 100e-6, (α, 0))[1]))
63+
out2 = FiniteDifferences.grad(central_fdm(5, 1), gg, field)[1]
64+
out1 = gradient(gg, field)[1]
65+
@test out1 .+ cis(1) out2 .+ cis(1)
66+
AS, _ = ShiftedAngularSpectrum(field, 100e-6, 633e-9, 100e-6, (α, 0))
67+
f_AS(x) = sum(abs2.(x .- AS(cis.(x))[1]))
68+
out3 = gradient(f_AS, field)[1]
69+
@test out3 out1
70+
71+
72+
field = zeros(ComplexF64, (15, 15))
73+
field[5:6, 3:8] .= 1
74+
AS, _ = ShiftedAngularSpectrum(field, 100e-6, 633e-9, 100e-6, (α, 0); extract_ramp=false)
75+
f_AS(x) = sum(abs2.(x .- AS(cis.(x))[1]))
76+
out2 = FiniteDifferences.grad(central_fdm(5, 1), f_AS, field)[1]
77+
out3 = gradient(f_AS, field)[1]
78+
@test out3 out2
79+
80+
81+
end
2982

3083
end

0 commit comments

Comments
 (0)