From 183d192261e6262d29087027816b2b6ce4199f4e Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 5 Jan 2023 17:17:45 +0000 Subject: [PATCH 01/72] freespace inputs with both polarisation components --- src/Fields.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Fields.jl b/src/Fields.jl index 1191c4d1..ab08e012 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -333,6 +333,7 @@ struct SpatioTemporalField{iT} <: AbstractField τ0::Float64 Ishape::iT propz::Float64 + θ::Float64 end "Gaussian temporal-spatial field defined radially" @@ -353,10 +354,10 @@ superGaussian parameter `m=1` and Gaussian shaped spatial profile with waist `w0 propagation distance from the waist of `propz`, and other parameters as defined for [`TimeField`](@ref). """ -function GaussGaussField(;λ0, τfwhm, energy, w0, ϕ=0.0, τ0=0.0, m=1, propz=0.0) +function GaussGaussField(;λ0, τfwhm, energy, w0, ϕ=0.0, τ0=0.0, m=1, propz=0.0, θ=0.0) SpatioTemporalField(λ0, energy, ϕ, τ0, (t, xs) -> GaussGauss(t, xs, τfwhm, m, w0), - propz) + propz, θ) end function make_Etr(s::SpatioTemporalField, grid::Grid.RealGrid, spacegrid) @@ -385,6 +386,10 @@ function (s::SpatioTemporalField)(grid, spacegrid, FT) Etr = make_Etr(s, grid, spacegrid) energy_t = Fields.energyfuncs(grid, spacegrid)[1] Etr .*= sqrt(s.energy)/sqrt(energy_t(Etr)) + Etr = permutedims( + cat(Etr .* sin(s.θ), Etr .* cos(s.θ); dims=3), + (1, 3, 2) + ) Eωk = transform(spacegrid, FT, Etr) if s.propz != 0.0 prop!(Eωk, s.propz, grid, spacegrid) From 282ec028075f5e32c7151afc7b3dad634186ce1c Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 25 Jan 2023 08:41:04 +0000 Subject: [PATCH 02/72] starting free-space polarisation --- .../freespace/radial_xypol_env.jl | 109 ++++++++++++++++++ src/LinearOps.jl | 35 +++++- src/Luna.jl | 10 +- src/NonlinearRHS.jl | 41 ++++--- 4 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 examples/low_level_interface/freespace/radial_xypol_env.jl diff --git a/examples/low_level_interface/freespace/radial_xypol_env.jl b/examples/low_level_interface/freespace/radial_xypol_env.jl new file mode 100644 index 00000000..01fda781 --- /dev/null +++ b/examples/low_level_interface/freespace/radial_xypol_env.jl @@ -0,0 +1,109 @@ +using Luna +using HiSol +import FFTW +import Luna: Hankel +import PyPlot: plt + +λ0 = 800e-9 +τfwhm = 10e-15 +w0 = 50e-6 +energy = 1e-6 + +thickness = 20e-6 +material = :SiO2 + +R = 4*w0 +N = 2^8 + +grid = Grid.EnvGrid(thickness, λ0, (200e-9, 4e-6), 200e-15) +q = Hankel.QDHT(R, N, dim=3) + +χ3 = PhysData.χ3(material) +responses = (Nonlinear.Kerr_env(χ3),) + +nfun = PhysData.ref_index_fun(material) +function nfunreal(λ) + nr = real(nfun(λ)) + nr, nr +end +linop = LinearOps.make_const_linop(grid, q, nfunreal) + +normfun = NonlinearRHS.const_norm_radial(grid, q, nfunreal) +densityfun = z -> 1 + +inputs = (Fields.GaussGaussField(;λ0, τfwhm, energy, w0), + Fields.GaussGaussField(;λ0=1200e-9, τfwhm=50e-15, energy=energy*10, w0, θ=π/4)) +# inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0) +Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) + +output = Output.MemoryOutput(0, grid.zmax, 101) +Luna.run(Eω, grid, linop, transform, FT, output; init_dz=1e-6) + +## +z = output["z"] +Eωk = output["Eω"] # (ω, pol, k, z) + +Eωr = q \ Eωk # (ω, pol, r, z) +Etr = FFTW.ifft(Eωr, 1) # (t, pol, r, z) +Iωr = abs2.(Eωr) # (ω, pol, r, z) +Itr = abs2.(Etr) # (t, pol, r, z) + +Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, r, z) +Iωxy = FFTW.fftshift(dropdims(sum(Iωr; dims=3); dims=3), 1) # (ω, pol, z) +Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (r, z) + +Itxy = dropdims(sum(Itr; dims=3); dims=3) # (t, pol, z) + +ω = FFTW.fftshift(grid.ω) + +## +plt.figure() +plt.pcolormesh(z*1e3, q.r*1e6, Ir) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") + +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, q.r*1e6, Irxy[1, :, :]) +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, q.r*1e6, Irxy[2, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") +plt.suptitle("Frequency domain") +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 1, :]) +plt.title("X Pol") +plt.xlabel("Distance (mm)") +plt.ylabel("Time (fs)") +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 2, :]) +plt.title("Y Pol") +plt.xlabel("Distance (mm)") +plt.suptitle("Time domain") + +## +plt.figure() +plt.subplot(2, 1, 1) +plt.plot(grid.t*1e15, Itxy[:, 1, 1]; label="input X") +plt.plot(grid.t*1e15, Itxy[:, 2, 1]; label="input Y") +plt.legend() +plt.subplot(2, 1, 2) +plt.plot(grid.t*1e15, Itxy[:, 1, end]; label="output X") +plt.plot(grid.t*1e15, Itxy[:, 2, end]; label="output Y") +plt.xlabel("Time (fs)") +plt.legend() + +## +plt.figure() +plt.subplot(2, 1, 1) +plt.semilogy(ω*1e-15, Iωxy[:, 1, 1]; label="input X") +plt.semilogy(ω*1e-15, Iωxy[:, 2, 1]; label="input Y") +plt.legend() +plt.subplot(2, 1, 2) +plt.semilogy(ω*1e-15, Iωxy[:, 1, end]; label="output X") +plt.semilogy(ω*1e-15, Iωxy[:, 2, end]; label="output Y") +plt.xlabel("Frequency (rad/fs)") +plt.legend() \ No newline at end of file diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 13ed5030..db59744a 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -144,20 +144,24 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) - n = zero(grid.ω) - n[grid.sidx] = nfun.(2π*PhysData.c./grid.ω[grid.sidx]) - β1 = PhysData.dispersion_func(1, nfun)(grid.referenceλ) + n = zeros(Float64, (length(grid.ω), 2)) + for (ii, si) in enumerate(grid.sidx) + if si + n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + end + end + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) if thg β0const = 0.0 else - β0const = grid.ω0/PhysData.c * nfun(2π*PhysData.c./grid.ω0) + β0const = grid.ω0/PhysData.c * nfun(2π*PhysData.c./grid.ω0)[1] end make_const_linop(grid, q, n, β1, β0const; thg=thg) end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, - n::AbstractArray, β1::Number, β0ref::Number; thg=false) - out = Array{ComplexF64}(undef, (length(grid.ω), q.N)) + n::AbstractMatrix, β1::Number, β0ref::Number; thg=false) + out = Array{ComplexF64}(undef, (length(grid.ω), 2, q.N)) k2 = @. (n*grid.ω/PhysData.c)^2 kr2 = q.k.^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N, β0ref, thg) @@ -225,6 +229,25 @@ function _fill_linop_r!(out, grid::Grid.EnvGrid, β1, k2, kr2, Nr, βref, thg) end end +function _fill_linop_r!(out, grid::Grid.EnvGrid, β1, k2::AbstractMatrix, kr2, Nr, βref, thg) + for ir = 1:Nr + for ip = 1:2 + for iω = 1:length(grid.ω) + βsq = k2[iω, ip] - kr2[ir] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end + if !thg + out[iω, ip, ir] -= -im*βref + end + end + end + end +end + #=================================================# #=============== MODE AVERAGE ================# #=================================================# diff --git a/src/Luna.jl b/src/Luna.jl index 90628e4f..f0818a4b 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -264,14 +264,16 @@ end function setup(grid::Grid.EnvGrid, q::Hankel.QDHT, densityfun, normfun, responses, inputs) Utils.loadFFTwisdom() - xt = zeros(ComplexF64, length(grid.t), length(q.r)) + shape = size(normfun(0)) + xt = zeros(ComplexF64, shape) FT = FFTW.plan_fft(xt, 1, flags=settings["fftw_flag"]) - Eω = zeros(ComplexF64, length(grid.ω), length(q.k)) + Eω = zeros(ComplexF64, shape) Eωk = q * Eω doinputs_fs!(Eωk, grid, q, FT, inputs) - xo = Array{ComplexF64}(undef, length(grid.to), length(q.r)) + oshape = shape[2:end] + xo = Array{ComplexF64}(undef, length(grid.to), oshape...) FTo = FFTW.plan_fft(xo, 1, flags=settings["fftw_flag"]) - transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun) + transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun, true) inv(FT) # create inverse FT plans now, so wisdom is saved inv(FTo) Utils.saveFFTwisdom() diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index ed4dd674..a3406ea8 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -7,6 +7,7 @@ import LinearAlgebra: mul!, ldiv! import NumericalIntegration: integrate, SimpsonEven import Luna: PhysData, Modes, Maths, Grid import Luna.PhysData: wlfreq +using EllipsisNotation """ to_time!(Ato, Aω, Aωo, IFTplan) @@ -136,7 +137,7 @@ end function Et_to_Pt!(Pt, Et, responses, density, idcs) for i in idcs - Et_to_Pt!(view(Pt, :, i), view(Et, :, i), responses, density) + Et_to_Pt!(view(Pt, .., i), view(Et, .., i), responses, density) end end @@ -404,10 +405,10 @@ struct TransRadial{TT, HTT, FTT, nT, rT, gT, dT, iT} resp::rT # nonlinear responses (tuple of callables) grid::gT # time grid densityfun::dT # callable which returns density - Pto::Array{TT,2} # Buffer array for NL polarisation on oversampled time grid - Eto::Array{TT,2} # Buffer array for field on oversampled time grid - Eωo::Array{ComplexF64,2} # Buffer array for field on oversampled frequency grid - Pωo::Array{ComplexF64,2} # Buffer array for NL polarisation on oversampled frequency grid + Pto::Array{TT, 3} # Buffer array for NL polarisation on oversampled time grid + Eto::Array{TT, 3} # Buffer array for field on oversampled time grid + Eωo::Array{ComplexF64, 3} # Buffer array for field on oversampled frequency grid + Pωo::Array{ComplexF64, 3} # Buffer array for NL polarisation on oversampled frequency grid idcs::iT # CartesianIndices for Et_to_Pt! to iterate over end @@ -421,12 +422,13 @@ function show(io::IO, t::TransRadial) print(io, out) end -function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun) - Eωo = zeros(ComplexF64, (length(grid.ωo), HT.N)) - Eto = zeros(TT, (length(grid.to), HT.N)) +function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false) + shape = (length(grid.ωo), pol ? 2 : 1, HT.N) + Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, HT.N)) + Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, HT.N)) Pto = similar(Eto) Pωo = similar(Eωo) - idcs = CartesianIndices(size(Pto)[2:end]) + idcs = CartesianIndices(size(Pto)[3:end]) TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto, Eto, Eωo, Pωo, idcs) end @@ -493,23 +495,24 @@ Make function to return normalisation factor for radial symmetry. """ function norm_radial(grid, q, nfun) ω = grid.ω - out = zeros(Float64, (length(ω), q.N)) + out = zeros(Float64, (length(ω), 2, q.N)) kr2 = q.k.^2 - k2 = zeros(Float64, length(ω)) function norm(z) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./PhysData.c).^2 for ir = 1:q.N for iω in eachindex(ω) - if ω[iω] == 0 - out[iω, ir] = 1.0 + if ω[iω] == 0 || ~grid.sidx[iω] + out[iω, :, ir] .= 1.0 continue end - βsq = k2[iω] - kr2[ir] - if βsq <= 0 - out[iω, ir] = 1.0 - continue + for (ip, n) in enumerate(nfun(ω[iω]; z)) + k2 = (n*ω[iω]/PhysData.c)^2 + βsq = k2 - kr2[ir] + if βsq <= 0 + out[iω, ip, ir] = 1.0 + continue + end + out[iω, ip, ir] = sqrt(βsq)/(PhysData.μ_0*ω[iω]) end - out[iω, ir] = sqrt(βsq)/(PhysData.μ_0*ω[iω]) end end return out From 7ffa1770e223f904f1d4e0d3334ec1e79f3fee21 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Sat, 16 Nov 2024 13:16:50 +0000 Subject: [PATCH 03/72] add quartz, calcite, MgF2 --- src/PhysData.jl | 76 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 9e0784d8..11f39b90 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -353,6 +353,59 @@ function sellmeier_crystal(material, axis) else throw(DomainError(axis, "Unknown BBO axis $axis")) end + elseif material == :MgF2 + if axis == :o + return μm -> @. sqrt(complex(1 + + 0.27620 + + 0.60967/(1-(0.08636/μm)^2) + + 0.0080/(1-(18.0/μm)^2) + + 2.14973/(1-(25.0/μm)^2) + )) + elseif axis == :e + return μm -> @. sqrt(complex(1 + + 0.25385 + + 0.66405/(1-(0.08504/μm)^2) + + 1.0899/(1-(22.2/μm)^2) + + 0.1816/(1-(24.4/μm)^2) + + 2.1227/(1-(40.6/μm)^2) + )) + else + throw(DomainError(axis, "Unknown MgF2 axis $axis")) + end + elseif material == :SiO2 + if axis == :o + return μm -> @. sqrt(complex( + 1 + + 0.28604141 + + 1.07044083/(1-1.00585997e-2/μm^2) + + 1.10202242/(1-100/μm^2) + )) + elseif axis == :e + return μm -> @. sqrt(complex( + 1 + + 0.28851804 + + 1.09509924/(1-1.02101864e-2/μm^2) + + 1.15662475/(1-100/μm^2) + )) + else + throw(DomainError(axis, "Unknown SiO2 axis $axis")) + end + elseif material == :CaCO3 + if axis == :o + return μm -> @. sqrt(complex(1 + + 0.73358749 + + 0.96464345/(1-1.94325203e-2/μm^2) + + 1.82831454/(1-120/μm^2) + )) + elseif axis == :e + return μm -> @. sqrt(complex(1 + + 0.35859695 + + 0.82427830/(1-1.06689543e-2/μm^2) + + 0.14429128/(1-120/μm^2) + )) + else + throw(DomainError(axis, "Unknown CaCO3 axis $axis")) + end elseif material == :LBO # C Chen et al., J Opt. Soc. Am. 6, 616-621 (1989) # F. Hanson and D. Dick., Opt. Lett. 16, 205-207 (1991). @@ -427,8 +480,8 @@ end Get refractive index for any material at wavelength given in SI units. """ -function ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing) - return ref_index_fun(material, P, T; lookup=lookup)(λ) +function ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) + return ref_index_fun(material, P, T; lookup)(λ) end """ @@ -436,10 +489,14 @@ end Get function which returns refractive index. """ -function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; lookup=nothing) +function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; + lookup=nothing, axis=nothing) if material in gas χ1 = χ1_fun(material, P, T) return λ -> sqrt(1 + complex(χ1(λ))) + elseif ~isnothing(axis) + sell = sellmeier_crystal(material, axis) + return λ -> sell(λ*1e6) elseif material in glass if isnothing(lookup) lookup = (material == :SiO2) @@ -506,8 +563,7 @@ end """ dispersion_func(order, n) -Get a function that calculates dispersion of order `order` for a refractive index given by -`n(λ)`. +Get a function that calculates dispersion of order `order` for a refractive index given by `n(λ)`. """ function dispersion_func(order, n) β(ω) = @. ω/c * real(n(wlfreq(ω))) @@ -520,8 +576,9 @@ end Get a function to calculate dispersion. Arguments are the same as for [`dispersion`](@ref). """ -function dispersion_func(order, material::Symbol, P=1.0, T=roomtemp; lookup=nothing) - n = ref_index_fun(material, P, T, lookup=lookup) +function dispersion_func(order, material::Symbol, P=1.0, T=roomtemp; + lookup=nothing, axis=nothing) + n = ref_index_fun(material, P, T; lookup, axis) dispersion_func(order, n) end @@ -540,8 +597,9 @@ julia> dispersion(2, :BK7, 400e-9) * 1e30 * 1e-3 # convert to fs^2/mm 122.03632107303108 ``` """ -function dispersion(order, material::Symbol, λ, P=1.0, T=roomtemp; lookup=nothing) - return dispersion_func(order, material, P, T; lookup=lookup).(λ) +function dispersion(order, material::Symbol, λ, P=1.0, T=roomtemp; + lookup=nothing, axis=nothing) + return dispersion_func(order, material, P, T; lookup, axis).(λ) end """ From e5637663c512194edfaf437e3af96bfe768df09a Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Sat, 16 Nov 2024 13:19:07 +0000 Subject: [PATCH 04/72] add ADP, KDP --- src/PhysData.jl | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/PhysData.jl b/src/PhysData.jl index 11f39b90..76cd156f 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -406,6 +406,38 @@ function sellmeier_crystal(material, axis) else throw(DomainError(axis, "Unknown CaCO3 axis $axis")) end + elseif material == :ADP + if axis == :o + return μm -> @. sqrt(complex( + 2.302842 + + 15.102464*μm^2/(μm^2-400) + + 0.011125165/(μm^2-0.01325366) + )) + elseif axis == :e + return μm -> @. sqrt(complex( + 2.163510 + + 5.919896*μm^2/(μm^2-400) + + 0.009616676/(μm^2-0.01298912) + )) + else + throw(DomainError(axis, "Unknown ADP axis $axis")) + end + elseif material == :KDP + if axis == :o + return μm -> @. sqrt(complex( + 2.259276 + + 13.00522*μm^2/(μm^2-400) + + 0.01008956/(μm^2-0.0129426) + )) + elseif axis == :e + return μm -> @. sqrt(complex( + 2.132668 + + 3.2279924*μm^2/(μm^2-400) + + 0.008637494/(μm^2-0.0122810) + )) + else + throw(DomainError(axis, "Unknown KDP axis $axis")) + end elseif material == :LBO # C Chen et al., J Opt. Soc. Am. 6, 616-621 (1989) # F. Hanson and D. Dick., Opt. Lett. 16, 205-207 (1991). From d56b165357826e3f078897010af7243583915a5b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Sat, 16 Nov 2024 13:19:38 +0000 Subject: [PATCH 05/72] remove ADP and KDP from other sellmeier --- src/PhysData.jl | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 76cd156f..1b266ec2 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -301,30 +301,6 @@ function sellmeier_glass(material::Symbol) + 0.96464345/(1-1.94325203e-2/μm^2) + 1.82831454/(1-120/μm^2) )) - elseif material == :ADPo - return μm -> @. sqrt(complex( - 2.302842 - + 15.102464*μm^2/(μm^2-400) - + 0.011125165/(μm^2-0.01325366) - )) - elseif material == :ADPe - return μm -> @. sqrt(complex( - 2.163510 - + 5.919896*μm^2/(μm^2-400) - + 0.009616676/(μm^2-0.01298912) - )) - elseif material == :KDPo - return μm -> @. sqrt(complex( - 2.259276 - + 13.00522*μm^2/(μm^2-400) - + 0.01008956/(μm^2-0.0129426) - )) - elseif material == :KDPe - return μm -> @. sqrt(complex( - 2.132668 - + 3.2279924*μm^2/(μm^2-400) - + 0.008637494/(μm^2-0.0122810) - )) else throw(DomainError(material, "Unknown glass $material")) end From 9aafcda0fec41407bca8afb931d30f1162666d01 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:13:56 +0000 Subject: [PATCH 06/72] add Chi2 response type --- src/Nonlinear.jl | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Nonlinear.jl b/src/Nonlinear.jl index 4de039d6..024cb814 100644 --- a/src/Nonlinear.jl +++ b/src/Nonlinear.jl @@ -4,6 +4,8 @@ import Luna.PhysData: ε_0, e_ratio import Luna: Maths, Utils import FFTW import LinearAlgebra: mul!, ldiv! +import Rotations: RotZY, RotYZ, RotMatrix, RotMatrix3 +import StaticArrays: SMatrix, MArray function KerrScalar!(out, E, fac) @. out += fac*E^3 @@ -83,6 +85,49 @@ function Kerr_env_thg(γ3, ω0, t) end end +struct Chi2Field{χT} + χ2::χT + toCrystal::RotMatrix3{Float64} + toLab::RotMatrix3{Float64} + El::MArray{Tuple{3}, Float64} # field in the lab frame + Ec::MArray{Tuple{3}, Float64} # field in the crystal frame + Enl::MArray{Tuple{6}, Float64} # field products in the crystal frame + Pc::MArray{Tuple{3}, Float64} # polarisation in the crystal frame + Pl::MArray{Tuple{3}, Float64} # polarisation in the lab frame +end + +function Chi2Field(θ, ϕ, χ2) + toCrystal = RotMatrix(RotZY(-ϕ, -θ)) + toLab = RotMatrix(RotYZ(θ, ϕ)) + sv3 = MArray{Tuple{3}}(zeros(3)) + sv6 = MArray{Tuple{6}}(zeros(6)) + χ2 = SMatrix{3, 6}(χ2) + Chi2Field(χ2, toCrystal, toLab, sv3, copy(sv3), sv6, copy(sv3), copy(sv3)) +end + +function (c::Chi2Field)(out, E, ρ) + for i in axes(E, 1) + @inbounds c.El[1] = E[i, 1] + @inbounds c.El[2] = E[i, 2] + # c.El[3] = E[i, 3] + mul!(c.Ec, c.toCrystal, c.El) + @inbounds field_products!(c.Enl, c.Ec) + mul!(c.Pc, c.χ2, c.Enl) + mul!(c.Pl, c.toLab, c.Pc) + out[i, 1] += ε_0*c.Pl[1] + out[i, 2] += ε_0*c.Pl[2] + end +end + +function field_products!(Enl, Ec) + Enl[1] = Ec[1]^2 + Enl[2] = Ec[2]^2 + Enl[3] = Ec[3]^2 + Enl[4] = 2*Ec[2]*Ec[3] + Enl[5] = 2*Ec[1]*Ec[3] + Enl[6] = 2*Ec[1]*Ec[2] +end + "Response type for cumtrapz-based plasma polarisation, adapted from: M. Geissler, G. Tempea, A. Scrinzi, M. Schnürer, F. Krausz, and T. Brabec, Physical Review Letters 83, 2930 (1999)." struct PlasmaCumtrapz{R, EType, tType} From cf6ea8a0934e99f1f4ae2968f02450937eca7a0a Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:41:42 +0000 Subject: [PATCH 07/72] deps --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 0cd8a104..57fd807d 100644 --- a/Project.toml +++ b/Project.toml @@ -45,6 +45,7 @@ QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" Scratch = "6c6a2e73-6563-6170-7368-637461726353" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -94,6 +95,7 @@ QuadGK = "2.4" Random = "1.9" Reexport = "1.2" Roots = "2" +Rotations = "1.7.1" Scratch = "1" SpecialFunctions = "0.10.3, 1, 2" StaticArrays = "1" From 7f4a87b494100338ed738574bafb1d9d1c868b6b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:42:12 +0000 Subject: [PATCH 08/72] linearops for radial symmetric with polarisation --- src/LinearOps.jl | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index db59744a..f4c085be 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -128,8 +128,8 @@ Make constant linear operator for radial free-space. `n` is the refractive index and β1 is 1/velocity of the reference frame. """ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, - n::AbstractArray, β1::Number) - out = Array{ComplexF64}(undef, (length(grid.ω), q.N)) + n::AbstractMatrix, β1::Number) + out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/PhysData.c)^2 kr2 = q.k.^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N) @@ -137,14 +137,22 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, end function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) - n = zero(grid.ω) - n[grid.sidx] = nfun.(2π*PhysData.c./grid.ω[grid.sidx]) - β1 = PhysData.dispersion_func(1, nfun)(grid.referenceλ) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + n = zeros(Float64, (length(grid.ω), np)) + for (ii, si) in enumerate(grid.sidx) + if si + n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + end + end + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) make_const_linop(grid, q, n, β1) end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) - n = zeros(Float64, (length(grid.ω), 2)) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) @@ -161,7 +169,7 @@ end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, n::AbstractMatrix, β1::Number, β0ref::Number; thg=false) - out = Array{ComplexF64}(undef, (length(grid.ω), 2, q.N)) + out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/PhysData.c)^2 kr2 = q.k.^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N, β0ref, thg) @@ -188,13 +196,15 @@ end function _fill_linop_r!(out, grid::Grid.RealGrid, β1, k2, kr2, Nr) for ir = 1:Nr - for iω = 1:length(grid.ω) - βsq = k2[iω] - kr2[ir] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + for ip in axes(k2, 2) + for iω = 1:length(grid.ω) + βsq = k2[iω, ip] - kr2[ir] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end end end end From 174dd04f6f2035db983e9a700fce5c8d55c70e21 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:42:29 +0000 Subject: [PATCH 09/72] setup for radial symmetric with polarisation --- src/Luna.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Luna.jl b/src/Luna.jl index 07e4261b..5f536e54 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -221,14 +221,18 @@ end function setup(grid::Grid.RealGrid, q::Hankel.QDHT, densityfun, normfun, responses, inputs) Utils.loadFFTwisdom() - xt = zeros(Float64, length(grid.t), length(q.r)) + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) + tshape = (length(grid.t), np, length(q.r)) + ωshape = (length(grid.ω), np, length(q.r)) + xt = zeros(Float64, tshape) FT = FFTW.plan_rfft(xt, 1, flags=settings["fftw_flag"]) - Eω = zeros(ComplexF64, length(grid.ω), length(q.k)) + Eω = zeros(ComplexF64, ωshape) Eωk = q * Eω doinputs_fs!(Eωk, grid, q, FT, inputs) - xo = Array{Float64}(undef, length(grid.to), length(q.r)) + oshape = tshape[2:end] + xo = Array{Float64}(undef, length(grid.to), oshape...) FTo = FFTW.plan_rfft(xo, 1, flags=settings["fftw_flag"]) - transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun) + transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun, np > 1) inv(FT) # create inverse FT plans now, so wisdom is saved inv(FTo) Utils.saveFFTwisdom() From dea4aff478c01ac865d0cea67c9e0443c6fd26cb Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:42:36 +0000 Subject: [PATCH 10/72] faster Chi2 response --- src/Nonlinear.jl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Nonlinear.jl b/src/Nonlinear.jl index 024cb814..6a9bca6c 100644 --- a/src/Nonlinear.jl +++ b/src/Nonlinear.jl @@ -89,33 +89,33 @@ struct Chi2Field{χT} χ2::χT toCrystal::RotMatrix3{Float64} toLab::RotMatrix3{Float64} - El::MArray{Tuple{3}, Float64} # field in the lab frame - Ec::MArray{Tuple{3}, Float64} # field in the crystal frame - Enl::MArray{Tuple{6}, Float64} # field products in the crystal frame - Pc::MArray{Tuple{3}, Float64} # polarisation in the crystal frame - Pl::MArray{Tuple{3}, Float64} # polarisation in the lab frame + χ2_toLab::χT # combined matrix to multiply by toLab * χ2 + El::Vector{Float64} # field in the lab frame + Ec::Vector{Float64} # field in the crystal frame + Enl::Vector{Float64} # field products in the crystal frame + Pl::Vector{Float64} # polarisation in the lab frame end function Chi2Field(θ, ϕ, χ2) - toCrystal = RotMatrix(RotZY(-ϕ, -θ)) + toCrystal = RotMatrix(RotZY(-ϕ, -θ)) # RotMatrix converts to static matrix toLab = RotMatrix(RotYZ(θ, ϕ)) - sv3 = MArray{Tuple{3}}(zeros(3)) - sv6 = MArray{Tuple{6}}(zeros(6)) - χ2 = SMatrix{3, 6}(χ2) - Chi2Field(χ2, toCrystal, toLab, sv3, copy(sv3), sv6, copy(sv3), copy(sv3)) + sv3 = zeros(3) + sv6 = zeros(6) + χ2 = SMatrix{3, 6}(χ2) # just χ2 + χ2_toLab = SMatrix{3, 6}(toLab * χ2) # χ2 and coordinate transform in one step + Chi2Field(χ2, toCrystal, toLab, χ2_toLab, sv3, copy(sv3), sv6, copy(sv3)) end function (c::Chi2Field)(out, E, ρ) for i in axes(E, 1) @inbounds c.El[1] = E[i, 1] @inbounds c.El[2] = E[i, 2] - # c.El[3] = E[i, 3] + # note c.El[3] (Ez in the lab frame) is always zero here mul!(c.Ec, c.toCrystal, c.El) @inbounds field_products!(c.Enl, c.Ec) - mul!(c.Pc, c.χ2, c.Enl) - mul!(c.Pl, c.toLab, c.Pc) - out[i, 1] += ε_0*c.Pl[1] - out[i, 2] += ε_0*c.Pl[2] + mul!(c.Pl, c.χ2_toLab, c.Enl) + @inbounds out[i, 1] += ε_0*c.Pl[1] + @inbounds out[i, 2] += ε_0*c.Pl[2] end end From 7d03078b3dd5c61bbee4058615c670232e245d33 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:42:57 +0000 Subject: [PATCH 11/72] fix norm with birefringence --- src/NonlinearRHS.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index c83c9e4e..0ab27c6c 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -423,7 +423,6 @@ function show(io::IO, t::TransRadial) end function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false) - shape = (length(grid.ωo), pol ? 2 : 1, HT.N) Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, HT.N)) Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, HT.N)) Pto = similar(Eto) @@ -495,7 +494,9 @@ Make function to return normalisation factor for radial symmetry. """ function norm_radial(grid, q, nfun) ω = grid.ω - out = zeros(Float64, (length(ω), 2, q.N)) + ωfirst = ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + out = zeros(Float64, (length(ω), np, q.N)) kr2 = q.k.^2 function norm(z) for ir = 1:q.N From 22aa1e87e3a0867efb96b1ca81ef16dea8a35cd6 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Dec 2024 10:43:15 +0000 Subject: [PATCH 12/72] more systematic treatment of crystals in PhysData --- src/PhysData.jl | 112 +++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 9e0784d8..917a877b 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -65,7 +65,8 @@ const gas_str = Dict( :N2O => "NitrousOxide", :D2 => "Deuterium" ) -const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si, :MgF2, :ADPo, :ADPe, :KDPo, :KDPe, :CaCO3) +const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si, :MgF2, :CaCO3) +const crystal = (:ADP, :KPD, :BBO) const metal = (:Ag,:Al) """ @@ -301,30 +302,6 @@ function sellmeier_glass(material::Symbol) + 0.96464345/(1-1.94325203e-2/μm^2) + 1.82831454/(1-120/μm^2) )) - elseif material == :ADPo - return μm -> @. sqrt(complex( - 2.302842 - + 15.102464*μm^2/(μm^2-400) - + 0.011125165/(μm^2-0.01325366) - )) - elseif material == :ADPe - return μm -> @. sqrt(complex( - 2.163510 - + 5.919896*μm^2/(μm^2-400) - + 0.009616676/(μm^2-0.01298912) - )) - elseif material == :KDPo - return μm -> @. sqrt(complex( - 2.259276 - + 13.00522*μm^2/(μm^2-400) - + 0.01008956/(μm^2-0.0129426) - )) - elseif material == :KDPe - return μm -> @. sqrt(complex( - 2.132668 - + 3.2279924*μm^2/(μm^2-400) - + 0.008637494/(μm^2-0.0122810) - )) else throw(DomainError(material, "Unknown glass $material")) end @@ -336,8 +313,9 @@ end Sellmeier for crystals. Returns function of wavelength in μm which in turn returns the refractive index directly. Possible values for `axis` depend on the type of crystal. """ -function sellmeier_crystal(material, axis) +function sellmeier_crystal(material, axis=nothing) if material == :BBO + isnothing(axis) && (axis = :o) if axis == :o return μm -> sqrt(complex(1 + 0.90291/(1-0.003926/μm^2) @@ -356,6 +334,7 @@ function sellmeier_crystal(material, axis) elseif material == :LBO # C Chen et al., J Opt. Soc. Am. 6, 616-621 (1989) # F. Hanson and D. Dick., Opt. Lett. 16, 205-207 (1991). + isnothing(axis) && (axis = :x) if axis == :x return μm -> sqrt(complex( 2.45768 @@ -377,6 +356,40 @@ function sellmeier_crystal(material, axis) else throw(DomainError(axis, "Unknown LBO axis $axis")) end + elseif material == :ADP + isnothing(axis) && (axis = :o) + if axis == :o + return μm -> @. sqrt(complex( + 2.302842 + + 15.102464*μm^2/(μm^2-400) + + 0.011125165/(μm^2-0.01325366) + )) + elseif axis == :e + return μm -> @. sqrt(complex( + 2.163510 + + 5.919896*μm^2/(μm^2-400) + + 0.009616676/(μm^2-0.01298912) + )) + else + throw(DomainError(axis, "Unknown ADP axis $axis")) + end + elseif material == :KDP + isnothing(axis) && (axis = :o) + if axis == :o + return μm -> @. sqrt(complex( + 2.259276 + + 13.00522*μm^2/(μm^2-400) + + 0.01008956/(μm^2-0.0129426) + )) + elseif axis == :e + return μm -> @. sqrt(complex( + 2.132668 + + 3.2279924*μm^2/(μm^2-400) + + 0.008637494/(μm^2-0.0122810) + )) + else + throw(DomainError(axis, "Unknown KDP axis $axis")) + end else throw(DomainError(material, "Unknown crystal $material")) end @@ -389,6 +402,20 @@ function ref_index_fun_uniax(material; axes=(:o, :e)) return n end +function χ2(material) + if material == :BBO + # Shoji, I. et al. J. Opt. Soc. Am. B, JOSAB 16, 620–624 (1999) + χ2 = 2e-12.*[ # note factor of 2 to convert d to χ2 + 0 0 0 0 0.03 2.2; + 2.2 -2.2 0 -0.08 0 0; + 0.04 0.04 0.04 0 0 0 + ] + else + error("Unknown material for χ2: $material") + end + return χ2 +end + """ χ1_fun(gas::Symbol) @@ -423,11 +450,11 @@ end """ - ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing) + ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) Get refractive index for any material at wavelength given in SI units. """ -function ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing) +function ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) return ref_index_fun(material, P, T; lookup=lookup)(λ) end @@ -436,7 +463,7 @@ end Get function which returns refractive index. """ -function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; lookup=nothing) +function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) if material in gas χ1 = χ1_fun(material, P, T) return λ -> sqrt(1 + complex(χ1(λ))) @@ -451,6 +478,9 @@ function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; lookup=nothing) sell = sellmeier_glass(material) return λ -> sell(λ*1e6) end + elseif material in crystal + sell = sellmeier_crystal(material) + return λ -> sell(λ*1e6) elseif material in metal nmetal = let spl = lookup_metal(material) function nmetal(λ) @@ -516,17 +546,17 @@ function dispersion_func(order, n) end """ - dispersion_func(order, material, P=1, T=roomtemp; lookup=nothing) + dispersion_func(order, material, P=1, T=roomtemp; lookup=nothing, axis=nothing) Get a function to calculate dispersion. Arguments are the same as for [`dispersion`](@ref). """ -function dispersion_func(order, material::Symbol, P=1.0, T=roomtemp; lookup=nothing) - n = ref_index_fun(material, P, T, lookup=lookup) +function dispersion_func(order, material::Symbol, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) + n = ref_index_fun(material, P, T; lookup, axis) dispersion_func(order, n) end """ - dispersion(order, material, λ, P=1.0, T=roomtemp; lookup=nothing) + dispersion(order, material, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) Calculate the dispersion of order `order` of a given `material` at a wavelength `λ`. @@ -540,8 +570,8 @@ julia> dispersion(2, :BK7, 400e-9) * 1e30 * 1e-3 # convert to fs^2/mm 122.03632107303108 ``` """ -function dispersion(order, material::Symbol, λ, P=1.0, T=roomtemp; lookup=nothing) - return dispersion_func(order, material, P, T; lookup=lookup).(λ) +function dispersion(order, material::Symbol, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) + return dispersion_func(order, material, P, T; lookup, axis).(λ) end """ @@ -648,8 +678,8 @@ function γ3_gas(material::Symbol; source=nothing) end function χ3(material::Symbol, P=1.0, T=roomtemp; source=nothing) - if material in glass - n2 = n2_glass(material, λ=1030e-9) + if material in glass || material in crystal + n2 = n2_solid(material, λ=1030e-9) n0 = real(ref_index(material, 1030e-9)) return 4/3 * n2 * (ε_0*c*n0^2) end @@ -657,13 +687,14 @@ function χ3(material::Symbol, P=1.0, T=roomtemp; source=nothing) end function n2(material::Symbol, P=1.0, T=roomtemp; λ=nothing, source=nothing) - material in glass && return n2_glass(material::Symbol, λ=λ) + material in glass && return n2_solid(material::Symbol, λ=λ) + material in crystal && return n2_solid(material::Symbol, λ=λ) λ = isnothing(λ) ? 800e-9 : λ n0 = ref_index(material, λ, P, T) return @. 3/4 * χ3(material, P, T, source=source) / (ε_0*c*n0^2) end -function n2_glass(material::Symbol; λ=nothing) +function n2_solid(material::Symbol; λ=nothing) if material == :SiO2 return 2.7e-20 elseif material == :MgF2 @@ -672,6 +703,9 @@ function n2_glass(material::Symbol; λ=nothing) elseif material == :CaCO3 # Kabaciński et al., 10.1364/OE.27.011018 return 3.22e-20 + elseif material == :BBO + # M. Bache, et al., Opt. Mater. Express, OME 3(3), 357–382 (2013). + return 5.1e-20 else error("Unkown glass $material") end From dc6753720c6e869e98e0cc01ee2999ba9f870b4a Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 10 Dec 2024 08:22:31 +0000 Subject: [PATCH 13/72] add 2D free-space propagation --- .../freespace/free2D_bbo.jl | 164 ++++++++++++++++++ .../freespace/radial_xypol_bbo.jl | 154 ++++++++++++++++ src/Fields.jl | 41 ++++- src/Grid.jl | 27 +++ src/LinearOps.jl | 142 ++++++++++++++- src/Luna.jl | 46 ++++- src/NonlinearRHS.jl | 138 +++++++++++++++ 7 files changed, 707 insertions(+), 5 deletions(-) create mode 100644 examples/low_level_interface/freespace/free2D_bbo.jl create mode 100644 examples/low_level_interface/freespace/radial_xypol_bbo.jl diff --git a/examples/low_level_interface/freespace/free2D_bbo.jl b/examples/low_level_interface/freespace/free2D_bbo.jl new file mode 100644 index 00000000..46a37a0e --- /dev/null +++ b/examples/low_level_interface/freespace/free2D_bbo.jl @@ -0,0 +1,164 @@ +using Luna +import FFTW +import Luna: Hankel +import PyPlot: plt +import NumericalIntegration: integrate + +λ0 = 1030e-9 +τfwhm = 250e-15 +w0 = 50e-6 +energy = 1e-6 / 8 + +thickness = 2000e-6 +material = :BBO + +R = 4*w0 +N = 2^6 + +grid = Grid.RealGrid(thickness, λ0, (300e-9, 4e-6), 500e-15) +xgrid = Grid.Free2DGrid(R, N) + +θ = deg2rad(23.3717) +ϕ = deg2rad(30) + + +responses = (Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)), + Nonlinear.Kerr_field(PhysData.χ3(material))) + +nfun = PhysData.ref_index_fun_uniax(material) +function nfunreal(λ; z=0) + # n_e, n_o + real(nfun(λ, θ)), real(nfun(λ, 0)) +end +linop = LinearOps.make_const_linop(grid, xgrid, nfunreal) + +normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, nfunreal) +densityfun = z -> 1 +## +inputs = Fields.GaussGaussField(;λ0, τfwhm, energy=energy/(sqrt(π/2)*w0), w0) +# inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0) +Eω, transform, FT = Luna.setup(grid, xgrid, densityfun, normfun, responses, inputs) + +## +output = Output.MemoryOutput(0, grid.zmax, 101) +Luna.run(Eω, grid, linop, transform, FT, output; init_dz=1e-6) + +## +z = output["z"] +Eωk = output["Eω"] # (ω, pol, k, z) +x = xgrid.x + +ωprefac = 2π*PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) + +Eωr = FFTW.ifft(Eωk, 3) # (ω, pol, x, z) +Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, x, z) +Etr = Maths.hilbert(Etr) +Iωr = abs2.(Eωr) # (ω, pol, x, z) +Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(Etr) # (t, pol, x, z) + +Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, x, z) +# Iωxy = dropdims(integrate(x, Iωr; dims=3); dims=3)*ωprefac # (ω, pol, z) +Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (x, z) + +# Itxy = dropdims(integrate(x, Itr; dims=3); dims=3) # (t, pol, z) + +Eω0 = Eωr[:, :, length(x)÷2+1, :] +Et0 = FFTW.irfft(Eω0, 2*(length(grid.ω)-1), 1) # (t, pol, z) +Et0 = Maths.hilbert(Et0) +It0 = 0.5*PhysData.c*PhysData.ε_0*abs2.(Et0) + +et, eω = Fields.energyfuncs(grid, xgrid) + +energy_x = eω(Eωk[:, 1, :, 1])*sqrt(π/2)*w0 +energy_y = eω(Eωk[:, 2, :, 1])*sqrt(π/2)*w0 + +ω = grid.ω + +## +I1 = 0.94*energy/τfwhm / (π*w0^2) +d31 = 0.16e-12 +d22 = -2.3e-12 +deff = d31*sin(θ) - d22*cos(θ)*sin(3ϕ) + +ω3 = PhysData.wlfreq(λ0/2) +n1 = real(nfun(λ0, 0)) +n2 = n1 +n3 = real(nfun(λ0/2, θ)) + +χ2eff = 2deff + +I3 = 2*χ2eff^2*ω3^2/(n1*n2*n3*PhysData.ε_0 * PhysData.c^3) * I1^2 * z.^2 + +## +plt.figure() +# plt.plot(z*1e6, I3*1e-4; label="Calculated") +plt.plot(z*1e6, dropdims(maximum(It0[:, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") +# plt.plot(z*1e6, dropdims(maximum(Itr[:, 1, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") + +## +plt.figure() +plt.pcolormesh(z*1e3, x*1e6, Ir) +plt.xlabel("Distance (mm)") +plt.ylabel("X (μm)") + +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, x*1e6, Irxy[1, :, :]) +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, x*1e6, Irxy[2, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") +plt.suptitle("Frequency domain") + +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 1, :]) +plt.title("X Pol") +plt.xlabel("Distance (mm)") +plt.ylabel("Time (fs)") +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 2, :]) +plt.title("Y Pol") +plt.xlabel("Distance (mm)") +plt.suptitle("Time domain") + +## +plt.figure() +plt.subplot(2, 1, 1) +plt.plot(grid.t*1e15, Itxy[:, 1, 1]; label="input X") +plt.plot(grid.t*1e15, Itxy[:, 2, 1]; label="input Y") +plt.legend() +plt.subplot(2, 1, 2) +plt.plot(grid.t*1e15, Itxy[:, 1, end]; label="output X") +plt.plot(grid.t*1e15, Itxy[:, 2, end]; label="output Y") +plt.xlabel("Time (fs)") +plt.legend() + +## +mm = 2π*maximum(Iωxy)*1e12 +plt.figure() +plt.subplot(2, 1, 1) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, 1]; label="input X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 800) +plt.legend() +plt.subplot(2, 1, 2) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, end]; label="output X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, end]; label="output Y") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; c="C1", linestyle="--", label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 800) +plt.xlabel("Frequency (THz)") +plt.ylabel("SED (J/Hz)") +plt.legend() + +## +plt.figure() +plt.plot(grid.ω, imag(linop[:, 1, 32])) +plt.plot(grid.ω, imag(linop[:, 2, 32])) +plt.axvline(PhysData.wlfreq(λ0)) +plt.axvline(PhysData.wlfreq(λ0/2)) +plt.ylim(-1e6, 1e6) \ No newline at end of file diff --git a/examples/low_level_interface/freespace/radial_xypol_bbo.jl b/examples/low_level_interface/freespace/radial_xypol_bbo.jl new file mode 100644 index 00000000..6d176c2a --- /dev/null +++ b/examples/low_level_interface/freespace/radial_xypol_bbo.jl @@ -0,0 +1,154 @@ +using Luna +import FFTW +import Luna: Hankel +import PyPlot: plt +import NumericalIntegration: integrate + +λ0 = 1030e-9 +τfwhm = 250e-15 +w0 = 50e-6 +energy = 1e-6 + +thickness = 100e-6 +material = :BBO + +R = 4*w0 +N = 2^6 + +grid = Grid.RealGrid(thickness, λ0, (300e-9, 4e-6), 500e-15) +q = Hankel.QDHT(R, N, dim=3) + +θ = deg2rad(23.3717) +ϕ = deg2rad(30) + + +responses = (Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)), + Nonlinear.Kerr_field(PhysData.χ3(material))) + +nfun = PhysData.ref_index_fun_uniax(material) +function nfunreal(λ; z=0) + # n_e, n_o + real(nfun(λ, θ)), real(nfun(λ, 0)) +end +linop = LinearOps.make_const_linop(grid, q, nfunreal) + +normfun = NonlinearRHS.const_norm_radial(grid, q, nfunreal) +densityfun = z -> 1 + +inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0) +# inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0) +Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) + +## +output = Output.MemoryOutput(0, grid.zmax, 101) +Luna.run(Eω, grid, linop, transform, FT, output; init_dz=1e-6) + +## +z = output["z"] +Eωk = output["Eω"] # (ω, pol, k, z) + +ωprefac = 2π*PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) + +Eωr = q \ Eωk # (ω, pol, r, z) +Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, r, z) +Etr = Maths.hilbert(Etr) +Iωr = abs2.(Eωr) # (ω, pol, r, z) +Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(Etr) # (t, pol, r, z) + +Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, r, z) +Iωxy = dropdims(Hankel.integrateR(Iωr, q; dim=3); dims=3)*ωprefac # (ω, pol, z) +Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (r, z) + +Itxy = dropdims(Hankel.integrateR(Itr, q; dim=3); dims=3) # (t, pol, z) + +Eω0 = dropdims(Hankel.onaxis(Eωk, q); dims=q.dim) # (ω, pol, z) +Et0 = FFTW.irfft(Eω0, 2*(length(grid.ω)-1), 1) # (t, pol, z) +Et0 = Maths.hilbert(Et0) +It0 = 0.5*PhysData.c*PhysData.ε_0*abs2.(Et0) + +et, eω = Fields.energyfuncs(grid, q) + +energy_x = eω(Eωk[:, 1, :, end]) +energy_y = eω(Eωk[:, 2, :, end]) + +ω = grid.ω + +## +I1 = 0.94*energy/τfwhm / (π*w0^2) +d31 = 0.16e-12 +d22 = -2.3e-12 +deff = d31*sin(θ) - d22*cos(θ)*sin(3ϕ) + +ω3 = PhysData.wlfreq(λ0/2) +n1 = real(nfun(λ0, 0)) +n2 = n1 +n3 = real(nfun(λ0/2, θ)) + +χ2eff = 2deff + +I3 = 2*χ2eff^2*ω3^2/(n1*n2*n3*PhysData.ε_0 * PhysData.c^3) * I1^2 * z.^2 + +## +plt.figure() +plt.plot(z*1e6, I3*1e-4; label="Calculated") +plt.plot(z*1e6, dropdims(maximum(It0[:, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") +# plt.plot(z*1e6, dropdims(maximum(Itr[:, 1, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") + +## +plt.figure() +plt.pcolormesh(z*1e3, q.r*1e6, Ir) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") + +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, q.r*1e6, Irxy[1, :, :]) +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, q.r*1e6, Irxy[2, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") +plt.suptitle("Frequency domain") +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 1, :]) +plt.title("X Pol") +plt.xlabel("Distance (mm)") +plt.ylabel("Time (fs)") +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 2, :]) +plt.title("Y Pol") +plt.xlabel("Distance (mm)") +plt.suptitle("Time domain") + +## +plt.figure() +plt.subplot(2, 1, 1) +plt.plot(grid.t*1e15, Itxy[:, 1, 1]; label="input X") +plt.plot(grid.t*1e15, Itxy[:, 2, 1]; label="input Y") +plt.legend() +plt.subplot(2, 1, 2) +plt.plot(grid.t*1e15, Itxy[:, 1, end]; label="output X") +plt.plot(grid.t*1e15, Itxy[:, 2, end]; label="output Y") +plt.xlabel("Time (fs)") +plt.legend() + +## +mm = 2π*maximum(Iωxy)*1e12 +plt.figure() +plt.subplot(2, 1, 1) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, 1]; label="input X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 800) +plt.legend() +plt.subplot(2, 1, 2) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, end]; label="output X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, end]; label="output Y") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; c="C1", linestyle="--", label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 800) +plt.xlabel("Frequency (THz)") +plt.ylabel("SED (J/Hz)") +plt.legend() \ No newline at end of file diff --git a/src/Fields.jl b/src/Fields.jl index 2752dd28..def25412 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -379,8 +379,8 @@ function make_Etr(s::SpatioTemporalField, grid::Grid.EnvGrid, spacegrid) end transform(spacegrid::Hankel.QDHT, FT, Etr) = spacegrid * (FT * Etr) - transform(spacegrid::Grid.FreeGrid, FT, Etr) = FT * Etr +transform(spacegrid::Grid.Free2DGrid, FT, Etr) = FT * Etr """ (s::SpatioTemporalField)(grid, spacegrid, FT) @@ -390,7 +390,7 @@ and Fourier transform `FT` """ function (s::SpatioTemporalField)(grid, spacegrid, FT) Etr = make_Etr(s, grid, spacegrid) - energy_t = Fields.energyfuncs(grid, spacegrid)[1] + energy_t = energyfuncs(grid, spacegrid)[1] Etr .*= sqrt(s.energy)/sqrt(energy_t(Etr)) Etr = permutedims( cat(Etr .* sin(s.θ), Etr .* cos(s.θ); dims=3), @@ -611,6 +611,43 @@ function energyfuncs(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid) return energy_t, energy_ω end +function energyfuncs(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid) + δx = xgrid.x[2] - xgrid.x[1] + δt = grid.t[2] - grid.t[1] + prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δt + function energy_t(Et) + Eta = Maths.hilbert(Et) + return prefac_t * sum(abs2.(Eta)) + end + + δω = grid.ω[2] - grid.ω[1] + Δω = grid.ω[end] + δkx = xgrid.kx[2] - xgrid.kx[1] + Δkx = length(xgrid.kx)*δkx + prefac = PhysData.c*PhysData.ε_0/2 * 2π*δω/(Δω^2) * 2π*δkx/(Δkx^2) + energy_ω(Eω) = prefac * sum(abs2.(Eω)) + + return energy_t, energy_ω +end + +function energyfuncs(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid) + δx = xgrid.x[2] - xgrid.x[1] + δt = grid.t[2] - grid.t[1] + prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δt + function energy_t(Et) + return prefac_t * sum(abs2.(Et)) + end + + δω = grid.ω[2] - grid.ω[1] + Δω = length(grid.ω)*δω + δkx = xgrid.kx[2] - xgrid.kx[1] + Δkx = length(xgrid.kx)*δkx + prefac = PhysData.c*PhysData.ε_0/2 * 2π*δω/(Δω^2) * 2π*δkx/(Δkx^2) + energy_ω(Eω) = prefac * sum(abs2.(Eω)) + + return energy_t, energy_ω +end + """ prop_taylor!(Eω, grid, ϕs, λ0) prop_taylor!(Eω, grid::Grid.AbstractGrid, ϕs, λ0) diff --git a/src/Grid.jl b/src/Grid.jl index 9aa093ca..add2ae6b 100644 --- a/src/Grid.jl +++ b/src/Grid.jl @@ -234,6 +234,33 @@ end FreeGrid(R, N) = FreeGrid(R, N, R, N) +struct Free2DGrid + x::Vector{Float64} + kx::Vector{Float64} + xwin::Vector{Float64} + r::Vector{Float64} +end + +""" + Free2DGrid(R, N; window_factor=0.1) + +Spatial grid for 2D freespace propagation with `x` half-width `R` and +`N` samples. `window_factor` determines by how much the grid size is extended to fit +a filtering window. +""" +function Free2DGrid(R, N; window_factor=0.1) + Rw = R * (1 + window_factor) # size including window + + δx = 2Rw/N + n = collect(range(0, length=N)) + x = @. (n-N/2) * δx + kx = 2π*FFTW.fftfreq(N, 1/δx) + + xwin = Maths.planck_taper(x, -Rw, -R, R, Rw) + + Free2DGrid(x, kx, xwin, copy(x)) +end + function to_dict(g::GT) where GT <: AbstractGrid d = Dict{String, Any}() diff --git a/src/LinearOps.jl b/src/LinearOps.jl index f4c085be..007d5f08 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -8,7 +8,7 @@ import Luna.PhysData: wlfreq #=============== FREE SPACE ===============# #=================================================# """ - make_const_linop(grid, xygrid, n, frame_vel) + make_const_linop(grid, xygrid, n, β1) Make constant linear operator for full 3D propagation. `n` is the refractive index (array) and β1 is 1/velocity of the reference frame. @@ -118,6 +118,146 @@ function _fill_linop_xy!(out, grid::Grid.EnvGrid, β1::Float64, k2, kperp2, idcs end end +#=================================================# +#============ FREE SPACE (2D) ================# +#=================================================# +""" + make_const_linop(grid, xgrid, n, β1) + +Make constant linear operator for 2D free-space propagation. `n` is the refractive index (array) +and β1 is 1/velocity of the reference frame. +""" +function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, + n::AbstractArray, β1::Number) + kperp2 = xgrid.kx.^2 + idcs = CartesianIndices(xgrid.kx) + k2 = @. (n*grid.ω/PhysData.c)^2 + out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) + _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) + return out +end + +function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + n = zeros(Float64, (length(grid.ω), np)) + for (ii, si) in enumerate(grid.sidx) + if si + n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + end + end + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + make_const_linop(grid, xgrid, n, β1) +end + +function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, + n::AbstractArray, β1::Number, β0ref::Number; thg=false) + kperp2 = xgrid.kx.^2 + idcs = CartesianIndices(xgrid.kx) + k2 = @. (n*grid.ω/PhysData.c)^2 + out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) + _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) + return out +end + +function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun, + thg=false) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + n = zeros(Float64, (length(grid.ω), np)) + for (ii, si) in enumerate(grid.sidx) + if si + n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + end + end + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + if thg + β0const = 0.0 + else + β0const = grid.ω0/PhysData.c * nfun(wlfreq(grid.ω0)) + end + make_const_linop(grid, xgrid, n, β1, β0const; thg=thg) +end + +""" + make_linop(grid, xgrid, nfun) + +Make z-dependent linear operator for free-space propagation. `nfun(ω; z)` should return the +refractive index as a function of frequency `ω` and (kwarg) propagation distance `z`. +""" +function make_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) + kperp2 = xgrid.kx.^2 + idcs = CartesianIndices(xgrid.kx) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[1] + function linop!(out, z) + β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) + for (ii, si) in enumerate(grid.sidx) + if si + k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]; z)) .* grid.ω[ii]./PhysData.c).^2 + end + end + _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) + end +end + +# Internal routine -- function barrier aids with JIT compilation +function _fill_linop_x!(out, grid::Grid.RealGrid, β1::Float64, k2, kperp2, idcs) + for ii in idcs + for ip in axes(k2, 2) + for iω in eachindex(grid.ω) + βsq = k2[iω, ip] - kperp2[ii] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end + end + end + end +end + +function make_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun; thg=false) + kperp2 = xgrid.kx.^2 + idcs = CartesianIndices(xgrid.kx) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ); z)[1] + function linop!(out, z) + β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) + for (ii, si) in enumerate(grid.sidx) + if si + k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./PhysData.c).^2 + end + end + βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z)[1] + _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) + end +end + +function _fill_linop_x!(out, grid::Grid.EnvGrid, β1::Float64, k2, kperp2, idcs, βref; thg) + for ii in idcs + for ip in axes(k2, 2) + for iω in eachindex(grid.ω) + βsq = k2[iω, ip] - kperp2[ii] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end + if !thg + out[iω, ip, ii] -= -im*βref + end + end + end + end +end + #=================================================# #============== RADIAL SYMMETRY ==============# #=================================================# diff --git a/src/Luna.jl b/src/Luna.jl index 5f536e54..b8405ddd 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -206,14 +206,14 @@ function setup(grid::Grid.EnvGrid, densityfun, responses, inputs, Eω, transform, FT end -function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT,Grid.FreeGrid}, FT, +function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT, Grid.FreeGrid, Grid.Free2DGrid}, FT, inputs::Tuple{Vararg{T} where T <: Fields.SpatioTemporalField}) for field in inputs Eωk .+= field(grid, spacegrid, FT) end end -function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT,Grid.FreeGrid}, FT, +function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT, Grid.FreeGrid, Grid.Free2DGrid}, FT, inputs::Fields.SpatioTemporalField) doinputs_fs!(Eωk, grid, spacegrid, FT, (inputs,)) end @@ -296,6 +296,48 @@ function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, Eωk, transform, FT end +function setup(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, + densityfun, normfun, responses, inputs) + Utils.loadFFTwisdom() + x = xgrid.x + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) + tshape = (length(grid.t), np, length(x)) + ωshape = (length(grid.ω), np, length(x)) + xr = Array{Float64}(undef, tshape) + FT = FFTW.plan_rfft(xr, (1, 3), flags=settings["fftw_flag"]) + Eωk = zeros(ComplexF64, ωshape) + doinputs_fs!(Eωk, grid, xgrid, FT, inputs) + xo = Array{Float64}(undef, (length(grid.to), np, length(x))) + FTo = FFTW.plan_rfft(xo, (1, 3), flags=settings["fftw_flag"]) + transform = NonlinearRHS.TransFree2D(grid, xgrid, FTo, + responses, densityfun, normfun, np > 1) + inv(FT) # create inverse FT plans now, so wisdom is saved + inv(FTo) + Utils.saveFFTwisdom() + Eωk, transform, FT +end + +function setup(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, + densityfun, normfun, responses, inputs) + Utils.loadFFTwisdom() + x = xgrid.x + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) + tshape = (length(grid.t), np, length(x)) + ωshape = (length(grid.ω), np, length(x)) + xr = Array{ComplexF64}(undef, tshape) + FT = FFTW.plan_fft(xr, (1, 3), flags=settings["fftw_flag"]) + Eωk = zeros(ComplexF64, ωshape) + doinputs_fs!(Eωk, grid, xgrid, FT, inputs) + xo = Array{ComplexF64}(undef, (length(grid.to), np, length(x))) + FTo = FFTW.plan_fft(xo, (1, 3), flags=settings["fftw_flag"]) + transform = NonlinearRHS.TransFree2D(grid, xgrid, FTo, + responses, densityfun, normfun, np > 1) + inv(FT) # create inverse FT plans now, so wisdom is saved + inv(FTo) + Utils.saveFFTwisdom() + Eωk, transform, FT +end + linoptype(l::AbstractArray) = "constant" linoptype(l) = "variable" diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 0ab27c6c..dc25434a 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -652,4 +652,142 @@ function norm_free(grid, xygrid, nfun) end end +mutable struct TransFree2D{TT, FTT, nT, rT, gT, xgT, dT, iT} + FT::FTT # 2D Fourier transform (space to k-space and time to frequency) + normfun::nT # Function which returns normalisation factor + resp::rT # nonlinear responses (tuple of callables) + grid::gT # time grid + xgrid::xgT + densityfun::dT # callable which returns density + Pto::Array{TT, 3} # buffer for oversampled time-domain NL polarisation + Eto::Array{TT, 3} # buffer for oversampled time-domain field + Eωo::Array{ComplexF64, 3} # buffer for oversampled frequency-domain field + Pωo::Array{ComplexF64, 3} # buffer for oversampled frequency-domain NL polarisation + scale::Float64 # scale factor to be applied during oversampling + idcs::iT # iterating over these slices Eto/Pto into Vectors, one at each position +end + +function show(io::IO, t::TransFree2D) + grid = "grid type: $(typeof(t.grid))" + samples = "time grid size: $(length(t.grid.t)) / $(length(t.grid.to))" + resp = "responses: "*join([string(typeof(ri)) for ri in t.resp], "\n ") + x = "x grid: $(minimum(t.xgrid.x)) to $(maximum(t.xgrid.x)), N=$(length(t.xgrid.x))" + out = join(["TransFree", grid, samples, x, resp], "\n ") + print(io, out) +end + +function TransFree2D(TT, scale, grid, xgrid, FT, responses, densityfun, normfun, pol=false) + Nx = length(xgrid.x) + Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, Nx)) + Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, Nx)) + Pto = similar(Eto) + Pωo = similar(Eωo) + idcs = CartesianIndices(size(Pto)[3:end]) + TransFree2D(FT, normfun, responses, grid, xgrid, densityfun, + Pto, Eto, Eωo, Pωo, scale, idcs) +end + +""" + TransFree2D(grid, xygrid, FT, responses, densityfun, normfun) + +Construct a `TransFree2D` to calculate the reciprocal-domain nonlinear polarisation. + +# Arguments +- `grid::AbstractGrid` : the grid used in the simulation +- `xygrid` : the spatial grid (instances of [`Grid.FreeGrid`](@ref)) +- `FT::FFTW.Plan` : the full 3D (t-y-x) Fourier transform for the oversampled time grid +- `responses` : `Tuple` of response functions +- `densityfun` : callable which returns the gas density as a function of `z` +- `normfun` : normalisation factor as fctn of `z`, can be created via [`norm_free`](@ref) +""" +function TransFree2D(grid::Grid.RealGrid, args...) + N = length(grid.ω) + No = length(grid.ωo) + scale = (No-1)/(N-1) + TransFree2D(Float64, scale, grid, args...) +end + +function TransFree2D(grid::Grid.EnvGrid, args...) + N = length(grid.ω) + No = length(grid.ωo) + scale = No/N + TransFree2D(ComplexF64, scale, grid, args...) +end + +""" + (t::TransFree2D)(nl, Eω, z) + +Calculate the reciprocal-domain (ω-kx-space) nonlinear response due to the field `Eω` +and place the result in `nl`. +""" +function (t::TransFree2D)(nl, Eωk, z) + # TODO: this can probably be combined with the case for TransFree + fill!(t.Eωo, 0) + copy_scale!(t.Eωo, Eωk, length(t.grid.ω), t.scale) + ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, ky, kx) -> (t, y, x) + Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses + @. t.Pto *= t.grid.towin # apodisation + mul!(t.Pωo, t.FT, t.Pto) # transform (t, y, x) -> (ω, ky, kx) + copy_scale!(nl, t.Pωo, length(t.grid.ω), 1/t.scale) + nl .*= t.grid.ωwin .* (-im.*t.grid.ω)./(2 .* t.normfun(z)) +end + +""" + const_norm_free2D(grid, xgrid, nfun) + +Make function to return normalisation factor for 3D propagation without re-calculating at +every step. +""" +function const_norm_free2D(grid, xgrid, nfun) + nfunω = (ω; z) -> nfun(wlfreq(ω)) + normfun = norm_free2D(grid, xgrid, nfunω) + out = copy(normfun(0.0)) + function norm(z) + return out + end + return norm +end + +""" + norm_free2D(grid, xgrid, nfun) + +Make function to return normalisation factor for 3D propagation. + +!!! note + Here, `nfun(ω; z)` needs to take frequency `ω` and a keyword argument `z`. +""" +function norm_free2D(grid, xgrid, nfun) + # TODO: this can probably be combined with the case for TransFree by adjusting idcs + kperp2 = xgrid.kx.^2 + ω = grid.ω + ωfirst = ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + out = zeros(Float64, (length(ω), np, length(xgrid.kx))) + function norm(z) + for (ii, si) in enumerate(grid.sidx) + if si + k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./PhysData.c).^2 + end + end + for ii in eachindex(xgrid.kx) + for iω in eachindex(ω) + if ω[iω] == 0 + out[iω, :, ii] .= 1.0 + continue + end + for (ip, n) in enumerate(nfun(ω[iω]; z)) + βsq = k2[iω, ip] - kperp2[ii] + if βsq <= 0 + out[iω, ip, ii] = 1.0 + continue + end + out[iω, ip, ii] = sqrt(βsq)/(PhysData.μ_0*ω[iω]) + end + end + end + return out + end +end + end \ No newline at end of file From b56a54f454dc5ee2958cacc3bc6d7cb3eb58891d Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 10 Dec 2024 09:31:31 +0000 Subject: [PATCH 14/72] proper birefringence in linop and norm --- .../freespace/free2D_bbo.jl | 12 +++-- src/LinearOps.jl | 37 +++++++++++-- src/NonlinearRHS.jl | 54 ++++++++++++++++++- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/examples/low_level_interface/freespace/free2D_bbo.jl b/examples/low_level_interface/freespace/free2D_bbo.jl index 46a37a0e..a1569b96 100644 --- a/examples/low_level_interface/freespace/free2D_bbo.jl +++ b/examples/low_level_interface/freespace/free2D_bbo.jl @@ -9,7 +9,7 @@ import NumericalIntegration: integrate w0 = 50e-6 energy = 1e-6 / 8 -thickness = 2000e-6 +thickness = 1000e-6 material = :BBO R = 4*w0 @@ -30,9 +30,11 @@ function nfunreal(λ; z=0) # n_e, n_o real(nfun(λ, θ)), real(nfun(λ, 0)) end -linop = LinearOps.make_const_linop(grid, xgrid, nfunreal) +nfunx(λ, δθ; z=0) = real(nfun(λ, θ+δθ)) +nfuny(λ; z=0) = real(nfun(λ, 0)) +linop = LinearOps.make_const_linop(grid, xgrid, nfunx, nfuny) -normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, nfunreal) +normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, nfunx, nfuny) densityfun = z -> 1 ## inputs = Fields.GaussGaussField(;λ0, τfwhm, energy=energy/(sqrt(π/2)*w0), w0) @@ -157,8 +159,8 @@ plt.legend() ## plt.figure() -plt.plot(grid.ω, imag(linop[:, 1, 32])) +# plt.plot(grid.ω, imag(linop[:, 1, 32])) plt.plot(grid.ω, imag(linop[:, 2, 32])) plt.axvline(PhysData.wlfreq(λ0)) plt.axvline(PhysData.wlfreq(λ0/2)) -plt.ylim(-1e6, 1e6) \ No newline at end of file +# plt.ylim(-1e6, 1e6) \ No newline at end of file diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 007d5f08..f5aaa397 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -2,7 +2,7 @@ module LinearOps import FFTW import Hankel import Luna: Modes, Grid, PhysData, Maths -import Luna.PhysData: wlfreq +import Luna.PhysData: wlfreq, c #=================================================# #=============== FREE SPACE ===============# @@ -146,10 +146,37 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) make_const_linop(grid, xgrid, n, β1) end +function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nfuny) + # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ) + # nfuny(λ; z) just takes wavelength + out = zeros(ComplexF64, (length(grid.ω), 2, length(xgrid.kx))) + β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) + for (iω, si) in enumerate(grid.sidx) + if si + ny = nfuny(wlfreq(grid.ω[iω])) + ksq_ypol = (ny*grid.ω[iω]/c)^2 + for (ik, kxi) in enumerate(xgrid.kx) + k0 = grid.ω[iω]/PhysData.c + δθ = asin(kxi/k0) + nx = nfunx(wlfreq(grid.ω[iω]), δθ) + ksq_xpol = (nx*grid.ω[iω]/c)^2 + βsq_xpol = ksq_xpol - kxi^2 + β_xpol = βsq_xpol < 0 ? -im*min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) + out[iω, 1, ik] = -im*(β_xpol - β1*grid.ω[iω]) + + βsq_ypol = ksq_ypol - kxi^2 + β_ypol = βsq_ypol < 0 ? -im*min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) + out[iω, 2, ik] = -im*(β_ypol - β1*grid.ω[iω]) + end + end + end + out +end + function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, n::AbstractArray, β1::Number, β0ref::Number; thg=false) kperp2 = xgrid.kx.^2 @@ -170,7 +197,7 @@ function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun, n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) if thg β0const = 0.0 else @@ -285,7 +312,7 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) make_const_linop(grid, q, n, β1) end @@ -298,7 +325,7 @@ function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[1])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) if thg β0const = 0.0 else diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index dc25434a..1f955d73 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -6,7 +6,7 @@ import Base: show import LinearAlgebra: mul!, ldiv! import NumericalIntegration: integrate, SimpsonEven import Luna: PhysData, Modes, Maths, Grid -import Luna.PhysData: wlfreq +import Luna.PhysData: wlfreq, c using EllipsisNotation """ @@ -748,6 +748,17 @@ function const_norm_free2D(grid, xgrid, nfun) return norm end +function const_norm_free2D(grid, xgrid, nfunx, nfuny) + nfunωx = (ω, δθ) -> nfunx(wlfreq(ω), δθ) + nfunωy = (ω) -> nfuny(wlfreq(ω)) + normfun = norm_free2D(grid, xgrid, nfunωx, nfunωy) + out = copy(normfun(0.0)) + function norm(z) + return out + end + return norm +end + """ norm_free2D(grid, xgrid, nfun) @@ -790,4 +801,45 @@ function norm_free2D(grid, xgrid, nfun) end end +function norm_free2D(grid, xgrid, nfunx, nfuny) + # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ) + # nfuny(λ; z) just takes wavelength + ω = grid.ω + out = zeros(Float64, (length(ω), 2, length(xgrid.kx))) + function norm(z) + for (iω, si) in enumerate(grid.sidx) + if ω[iω] == 0 + out[iω, :, :] .= 1.0 + continue + end + if si + ny = nfuny(wlfreq(grid.ω[iω])) + ksq_ypol = (ny*grid.ω[iω]/c)^2 + for (ik, kxi) in enumerate(xgrid.kx) + k0 = grid.ω[iω]/PhysData.c + δθ = asin(kxi/k0) + nx = nfunx(wlfreq(grid.ω[iω]), δθ) + ksq_xpol = (nx*grid.ω[iω]/c)^2 + βsq_xpol = ksq_xpol - kxi^2 + if βsq_xpol < 0 + out[iω, 1, ik] .= 1.0 + else + out[iω, 1, ik] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) + end + + βsq_ypol = ksq_ypol - kxi^2 + if βsq_ypol < 0 + out[iω, :, ik] .= 1.0 + else + out[iω, 2, ik] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) + end + end + else + out[iω, :, :] .= 1.0 + end + end + return out + end +end + end \ No newline at end of file From e41830b5f59b4935cb28429320cd955791d2b60b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 10 Dec 2024 12:15:19 +0000 Subject: [PATCH 15/72] multi-dim integral function for analysis --- src/Maths.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Maths.jl b/src/Maths.jl index 35d89dc5..613258ba 100644 --- a/src/Maths.jl +++ b/src/Maths.jl @@ -11,6 +11,7 @@ import Luna: settings import Roots: fzero import Dierckx import Peaks +import NumericalIntegration: integrate #= Pre-created finite difference methods for speed. Use (order+6)th order central finite differences with 2 adaptive steps up to @@ -350,6 +351,26 @@ function cumtrapz(y, x; dim=1) return out end +""" + integrateNd(x, y; dim=1) + +Integrate a multi-dimensional array `y` over `x` along array dimension `dim` using the +trapezoidal method. +""" +function integrateNd(x, y; dim=1) + idxlo = CartesianIndices(size(y)[1:dim - 1]) + idxhi = CartesianIndices(size(y)[dim + 1:end]) + outshape = collect(size(y)) + outshape[dim] = 1 + out = zeros(eltype(y), Tuple(outshape)) + for hi in idxhi + for lo in idxlo + out[lo, 1, hi] = integrate(x, y[lo, :, hi]) + end + end + out +end + """ normbymax(x, dims) From d2a2abeb48aab8be46525366fac0008f2f8623e9 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 10 Dec 2024 12:15:33 +0000 Subject: [PATCH 16/72] actually proper (?) birefringence --- src/LinearOps.jl | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index f5aaa397..d95c74af 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -3,6 +3,7 @@ import FFTW import Hankel import Luna: Modes, Grid, PhysData, Maths import Luna.PhysData: wlfreq, c +import Roots: fzero #=================================================# #=============== FREE SPACE ===============# @@ -160,11 +161,11 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf ny = nfuny(wlfreq(grid.ω[iω])) ksq_ypol = (ny*grid.ω[iω]/c)^2 for (ik, kxi) in enumerate(xgrid.kx) - k0 = grid.ω[iω]/PhysData.c - δθ = asin(kxi/k0) + δθ = crystal_internal_angle(nfunx, grid.ω[iω], kxi) nx = nfunx(wlfreq(grid.ω[iω]), δθ) - ksq_xpol = (nx*grid.ω[iω]/c)^2 - βsq_xpol = ksq_xpol - kxi^2 + k_xpol = nx*grid.ω[iω]/c + # kx_xpol = k_xpol*sin(δθ) + βsq_xpol = k_xpol^2 - kxi^2 β_xpol = βsq_xpol < 0 ? -im*min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) out[iω, 1, ik] = -im*(β_xpol - β1*grid.ω[iω]) @@ -177,6 +178,24 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf out end +function crystal_internal_angle(nfun, ω, kx) + # External wavevector is kx = ω/c*sin(θ_i) with θ_i the AOI of the plane wave + # Internal wavevector is kx2 = ω/c * n(θ+δθ) * sin(δθ) + # Momentum conservation requires kx = kx2 + # kx is given by the grid, so + # kx = ω/c * n(θ+δθ)*sin(δθ) + # Solve this numerically + try + global δθ = fzero(0.0) do δθi + ω/c * nfun(wlfreq(ω), δθi)*sin(δθi) - kx + end + catch + error("Crystal index could not be found for λ=$(1e9wlfreq(ω)) nm, kx=$kx") + end + δθ +end + + function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, n::AbstractArray, β1::Number, β0ref::Number; thg=false) kperp2 = xgrid.kx.^2 From 2b01c596a83657773793d68f64517e7d9fe211ef Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 10 Dec 2024 15:23:20 +0000 Subject: [PATCH 17/72] include internal crystal angle in normfun --- src/LinearOps.jl | 22 +--------------------- src/NonlinearRHS.jl | 13 ++++++------- src/PhysData.jl | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index d95c74af..8244a398 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -2,8 +2,7 @@ module LinearOps import FFTW import Hankel import Luna: Modes, Grid, PhysData, Maths -import Luna.PhysData: wlfreq, c -import Roots: fzero +import Luna.PhysData: wlfreq, c, crystal_internal_angle #=================================================# #=============== FREE SPACE ===============# @@ -164,7 +163,6 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf δθ = crystal_internal_angle(nfunx, grid.ω[iω], kxi) nx = nfunx(wlfreq(grid.ω[iω]), δθ) k_xpol = nx*grid.ω[iω]/c - # kx_xpol = k_xpol*sin(δθ) βsq_xpol = k_xpol^2 - kxi^2 β_xpol = βsq_xpol < 0 ? -im*min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) out[iω, 1, ik] = -im*(β_xpol - β1*grid.ω[iω]) @@ -178,24 +176,6 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf out end -function crystal_internal_angle(nfun, ω, kx) - # External wavevector is kx = ω/c*sin(θ_i) with θ_i the AOI of the plane wave - # Internal wavevector is kx2 = ω/c * n(θ+δθ) * sin(δθ) - # Momentum conservation requires kx = kx2 - # kx is given by the grid, so - # kx = ω/c * n(θ+δθ)*sin(δθ) - # Solve this numerically - try - global δθ = fzero(0.0) do δθi - ω/c * nfun(wlfreq(ω), δθi)*sin(δθi) - kx - end - catch - error("Crystal index could not be found for λ=$(1e9wlfreq(ω)) nm, kx=$kx") - end - δθ -end - - function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, n::AbstractArray, β1::Number, β0ref::Number; thg=false) kperp2 = xgrid.kx.^2 diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 1f955d73..0fffdc77 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -6,7 +6,7 @@ import Base: show import LinearAlgebra: mul!, ldiv! import NumericalIntegration: integrate, SimpsonEven import Luna: PhysData, Modes, Maths, Grid -import Luna.PhysData: wlfreq, c +import Luna.PhysData: wlfreq, c, crystal_internal_angle using EllipsisNotation """ @@ -802,7 +802,7 @@ function norm_free2D(grid, xgrid, nfun) end function norm_free2D(grid, xgrid, nfunx, nfuny) - # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ) + # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ+δθ) # nfuny(λ; z) just takes wavelength ω = grid.ω out = zeros(Float64, (length(ω), 2, length(xgrid.kx))) @@ -816,13 +816,12 @@ function norm_free2D(grid, xgrid, nfunx, nfuny) ny = nfuny(wlfreq(grid.ω[iω])) ksq_ypol = (ny*grid.ω[iω]/c)^2 for (ik, kxi) in enumerate(xgrid.kx) - k0 = grid.ω[iω]/PhysData.c - δθ = asin(kxi/k0) + δθ = crystal_internal_angle(nfunx, grid.ω[iω], kxi) nx = nfunx(wlfreq(grid.ω[iω]), δθ) - ksq_xpol = (nx*grid.ω[iω]/c)^2 - βsq_xpol = ksq_xpol - kxi^2 + k_xpol = nx*grid.ω[iω]/c + βsq_xpol = k_xpol^2 - kxi^2 if βsq_xpol < 0 - out[iω, 1, ik] .= 1.0 + out[iω, 1, ik] = 1.0 else out[iω, 1, ik] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) end diff --git a/src/PhysData.jl b/src/PhysData.jl index 917a877b..ad6acc21 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -7,6 +7,7 @@ import CSV import DelimitedFiles: readdlm import Polynomials import Luna: Maths, Utils +import Roots: fzero include("data/lookup_tables.jl") @@ -402,6 +403,23 @@ function ref_index_fun_uniax(material; axes=(:o, :e)) return n end +function crystal_internal_angle(nfun, ω, kx) + # External wavevector is kx = ω/c*sin(θ_i) with θ_i the AOI of the plane wave + # Internal wavevector is kx2 = ω/c * n(θ+δθ) * sin(δθ) + # Momentum conservation requires kx = kx2 + # kx is given by the grid, so + # kx = ω/c * n(θ+δθ)*sin(δθ) + # Solve this numerically + try + global δθ = fzero(0.0) do δθi + ω/c * nfun(wlfreq(ω), δθi)*sin(δθi) - kx + end + catch + error("Crystal index could not be found for λ=$(1e9wlfreq(ω)) nm, kx=$kx") + end + δθ +end + function χ2(material) if material == :BBO # Shoji, I. et al. J. Opt. Soc. Am. B, JOSAB 16, 620–624 (1999) From cd96bcfcb15cdabdbccb777b1cbb6ca23b9ec125 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 11 Dec 2024 09:59:13 +0000 Subject: [PATCH 18/72] add more crystals --- src/PhysData.jl | 60 ++++++++++++++++++++++++++++++------------- test/test_physdata.jl | 7 +++++ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index ad6acc21..b1ceebf2 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -66,9 +66,9 @@ const gas_str = Dict( :N2O => "NitrousOxide", :D2 => "Deuterium" ) -const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si, :MgF2, :CaCO3) -const crystal = (:ADP, :KPD, :BBO) -const metal = (:Ag,:Al) +const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si) +const crystal = (:ADP, :KPD, :BBO, :CaCO3, :MgF2) +const metal = (:Ag, :Al) """ wlfreq(ωλ) @@ -290,19 +290,6 @@ function sellmeier_glass(material::Symbol) + 0.0030434748/(1-(1.13475115/μm)^2) + 1.54133408/(1-(1104/μm)^2) )) - elseif material == :MgF2 - return μm -> @. sqrt(complex(1 - + 0.27620 - + 0.60967/(1-(0.08636/μm)^2) - + 0.0080/(1-(18.0/μm)^2) - + 2.14973/(1-(25.0/μm)^2) - )) - elseif material == :CaCO3 - return μm -> @. sqrt(complex(1 - + 0.73358749 - + 0.96464345/(1-1.94325203e-2/μm^2) - + 1.82831454/(1-120/μm^2) - )) else throw(DomainError(material, "Unknown glass $material")) end @@ -391,6 +378,43 @@ function sellmeier_crystal(material, axis=nothing) else throw(DomainError(axis, "Unknown KDP axis $axis")) end + elseif material == :CaCO3 + isnothing(axis) && (axis = :o) + if axis == :o + return μm -> @. sqrt(complex(1 + + 0.73358749 + + 0.96464345/(1-1.94325203e-2/μm^2) + + 1.82831454/(1-120/μm^2) + )) + elseif axis == :e + return μm -> @. sqrt(complex(1 + + 0.35859695 + + 0.82427830/(1-1.06689543e-2/μm^2) + + 0.14429128/(1-120/μm^2) + )) + else + throw(DomainError(axis, "Unknown CaCO3 axis $axis")) + end + elseif material == :MgF2 + isnothing(axis) && (axis = :o) + if axis == :o + return μm -> @. sqrt(complex(1 + + 0.27620 + + 0.60967/(1-(0.08636/μm)^2) + + 0.0080/(1-(18.0/μm)^2) + + 2.14973/(1-(25.0/μm)^2) + )) + elseif axis == :e + return μm -> @. sqrt(complex(1 + + 0.25385 + + 0.66405/(1-(0.08504/μm)^2) + + 1.0899/(1-(22.2/μm)^2) + + 0.1816/(1-(24.4/μm)^2) + + 2.1227/(1-(40.6/μm)^2) + )) + else + throw(DomainError(axis, "Unknown MgF2 axis $axis")) + end else throw(DomainError(material, "Unknown crystal $material")) end @@ -473,7 +497,7 @@ end Get refractive index for any material at wavelength given in SI units. """ function ref_index(material, λ, P=1.0, T=roomtemp; lookup=nothing, axis=nothing) - return ref_index_fun(material, P, T; lookup=lookup)(λ) + return ref_index_fun(material, P, T; lookup, axis)(λ) end """ @@ -497,7 +521,7 @@ function ref_index_fun(material::Symbol, P=1.0, T=roomtemp; lookup=nothing, axis return λ -> sell(λ*1e6) end elseif material in crystal - sell = sellmeier_crystal(material) + sell = sellmeier_crystal(material, axis) return λ -> sell(λ*1e6) elseif material in metal nmetal = let spl = lookup_metal(material) diff --git a/test/test_physdata.jl b/test/test_physdata.jl index 85308958..206cb4d2 100644 --- a/test/test_physdata.jl +++ b/test/test_physdata.jl @@ -16,6 +16,13 @@ end @test PhysData.ref_index(:SiO2, PhysData.eV_to_m(0.91018)) ≈ 1.44621 @test real(PhysData.ref_index(:SiO2, PhysData.eV_to_m(121.6))) ≈ 0.9865 @test imag(PhysData.ref_index(:SiO2, PhysData.eV_to_m(121.6))) ≈ 0.0085 + # Comparisons with ref index info + @test isapprox(PhysData.ref_index(:CaCO3, 800e-9), 1.6488; rtol=1e-4) + @test isapprox(PhysData.ref_index(:CaCO3, 800e-9; axis=:e), 1.4819; rtol=1e-4) + @test isapprox(PhysData.ref_index(:MgF2, 800e-9), 1.3751; rtol=1e-4) + @test isapprox(PhysData.ref_index(:MgF2, 800e-9; axis=:e), 1.3867; rtol=1e-4) + @test isapprox(PhysData.ref_index(:BBO, 800e-9), 1.6604; rtol=1e-4) + @test isapprox(PhysData.ref_index(:BBO, 800e-9; axis=:e), 1.5455; rtol=1e-4) end @testset "Function equivalence" begin From cf86528f2556c4d180d4facc26b8a819925bc69c Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 7 Jan 2025 11:24:54 +0000 Subject: [PATCH 19/72] add LBO to crystals --- src/PhysData.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index b1ceebf2..c582763b 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -67,7 +67,7 @@ const gas_str = Dict( :D2 => "Deuterium" ) const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si) -const crystal = (:ADP, :KPD, :BBO, :CaCO3, :MgF2) +const crystal = (:ADP, :KPD, :BBO, :CaCO3, :MgF2, :LBO) const metal = (:Ag, :Al) """ From f3de30f397ac98c3c76efb8c0209394393fbf27e Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 21 Feb 2025 13:18:44 +0000 Subject: [PATCH 20/72] minor fixes --- src/Fields.jl | 11 ++++++----- src/Modes.jl | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Fields.jl b/src/Fields.jl index def25412..10b0cb14 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -711,13 +711,14 @@ a certain `thickness` of a `material`. If the central wavelength `λ0` is given, delay at this wavelength. Keyword arguments `P` (pressure), `T` (temperature) and `lookup` (whether to use lookup table instead of Sellmeier expansion). """ -function propagator_material(material; P=1, T=PhysData.roomtemp, lookup=nothing) - n = PhysData.ref_index_fun(material, P, T; lookup=lookup) - β1 = PhysData.dispersion_func(1, n) - function prop!(Eω, ω, thickness, λ0=nothing) +function propagator_material(material; P=1, T=PhysData.roomtemp, lookup=nothing, axis=nothing) + n = PhysData.ref_index_fun(material, P, T; lookup, axis) + β1fun = PhysData.dispersion_func(1, n) + function prop!(Eω, ω, thickness, λ0=nothing; β1=nothing) β = ω./PhysData.c .* n.(wlfreq.(ω)) if !isnothing(λ0) - β .-= β1(λ0) .* (ω .- wlfreq(λ0)) + β1 = isnothing(β1) ? β1fun(λ0) : β1 + β .-= β1 .* (ω .- wlfreq(λ0)) end β[.!isfinite.(β)] .= 0 Eω .*= exp.(-1im.*real(β).*thickness) diff --git a/src/Modes.jl b/src/Modes.jl index 78e1ae88..f048aa10 100644 --- a/src/Modes.jl +++ b/src/Modes.jl @@ -189,7 +189,7 @@ function dispersion_func(m::AbstractMode, order; z=0.0) end """ - dispersion_func(m::AbstractMode, order; z=0.0) + dispersion(m::AbstractMode, order, ω; z=0.0) Calculate the dispersion of a given `order` at frequency `ω`. """ From 2929cd051bfde007db85ed62656690488230b558 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 21 Feb 2025 13:19:12 +0000 Subject: [PATCH 21/72] beam walk-off simulation (Not finished) --- .../freespace/free2D_bbo.jl | 115 ++++++++++++++---- 1 file changed, 93 insertions(+), 22 deletions(-) diff --git a/examples/low_level_interface/freespace/free2D_bbo.jl b/examples/low_level_interface/freespace/free2D_bbo.jl index a1569b96..9a65d526 100644 --- a/examples/low_level_interface/freespace/free2D_bbo.jl +++ b/examples/low_level_interface/freespace/free2D_bbo.jl @@ -7,7 +7,7 @@ import NumericalIntegration: integrate λ0 = 1030e-9 τfwhm = 250e-15 w0 = 50e-6 -energy = 1e-6 / 8 +energy = 1e-6 thickness = 1000e-6 material = :BBO @@ -15,10 +15,10 @@ material = :BBO R = 4*w0 N = 2^6 -grid = Grid.RealGrid(thickness, λ0, (300e-9, 4e-6), 500e-15) +grid = Grid.RealGrid(thickness, λ0, (300e-9, 2e-6), 1200e-15) xgrid = Grid.Free2DGrid(R, N) -θ = deg2rad(23.3717) +θ = deg2rad(23.3717) ϕ = deg2rad(30) @@ -26,10 +26,6 @@ responses = (Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)), Nonlinear.Kerr_field(PhysData.χ3(material))) nfun = PhysData.ref_index_fun_uniax(material) -function nfunreal(λ; z=0) - # n_e, n_o - real(nfun(λ, θ)), real(nfun(λ, 0)) -end nfunx(λ, δθ; z=0) = real(nfun(λ, θ+δθ)) nfuny(λ; z=0) = real(nfun(λ, 0)) linop = LinearOps.make_const_linop(grid, xgrid, nfunx, nfuny) @@ -50,19 +46,19 @@ z = output["z"] Eωk = output["Eω"] # (ω, pol, k, z) x = xgrid.x -ωprefac = 2π*PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) +ωprefac = PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) * sqrt(π/2)*w0 Eωr = FFTW.ifft(Eωk, 3) # (ω, pol, x, z) Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, x, z) -Etr = Maths.hilbert(Etr) +EtrH = Maths.hilbert(Etr) Iωr = abs2.(Eωr) # (ω, pol, x, z) -Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(Etr) # (t, pol, x, z) +Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(EtrH) # (t, pol, x, z) Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, x, z) -# Iωxy = dropdims(integrate(x, Iωr; dims=3); dims=3)*ωprefac # (ω, pol, z) +Iωxy = dropdims(Maths.integrateNd(x, Iωr; dim=3); dims=3)*ωprefac # (ω, pol, z) Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (x, z) -# Itxy = dropdims(integrate(x, Itr; dims=3); dims=3) # (t, pol, z) +Itxy = dropdims(Maths.integrateNd(x, Itr; dim=3); dims=3)*sqrt(π/2)*w0 # (t, pol, z) Eω0 = Eωr[:, :, length(x)÷2+1, :] Et0 = FFTW.irfft(Eω0, 2*(length(grid.ω)-1), 1) # (t, pol, z) @@ -77,7 +73,7 @@ energy_y = eω(Eωk[:, 2, :, 1])*sqrt(π/2)*w0 ω = grid.ω ## -I1 = 0.94*energy/τfwhm / (π*w0^2) +I1 = 2*0.94*energy/τfwhm / (π*w0^2) d31 = 0.16e-12 d22 = -2.3e-12 deff = d31*sin(θ) - d22*cos(θ)*sin(3ϕ) @@ -104,14 +100,20 @@ plt.xlabel("Distance (mm)") plt.ylabel("X (μm)") ## -plt.figure() +fig = plt.figure() +fig.set_size_inches(12, 6) plt.subplot(1, 2, 1) plt.pcolormesh(z*1e3, x*1e6, Irxy[1, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("radius (μm)") +plt.title("X Polarisation") plt.subplot(1, 2, 2) plt.pcolormesh(z*1e3, x*1e6, Irxy[2, :, :]) plt.xlabel("Distance (mm)") plt.ylabel("radius (μm)") -plt.suptitle("Frequency domain") +plt.title("Y Polarisation") +fig.tight_layout() + ## plt.figure() @@ -150,7 +152,7 @@ plt.legend() plt.subplot(2, 1, 2) plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, end]; label="output X") plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, end]; label="output Y") -plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; c="C1", linestyle="--", label="input Y") +# plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; c="C1", linestyle="--", label="input Y") plt.ylim(1e-5mm, 5mm) plt.xlim(200, 800) plt.xlabel("Frequency (THz)") @@ -158,9 +160,78 @@ plt.ylabel("SED (J/Hz)") plt.legend() ## -plt.figure() -# plt.plot(grid.ω, imag(linop[:, 1, 32])) -plt.plot(grid.ω, imag(linop[:, 2, 32])) -plt.axvline(PhysData.wlfreq(λ0)) -plt.axvline(PhysData.wlfreq(λ0/2)) -# plt.ylim(-1e6, 1e6) \ No newline at end of file +lwe = Utils.load_dict_h5(joinpath(@__DIR__, "field_for_luna.h5")) +fig = plt.figure() +fig.set_size_inches(12, 12) +plt.subplot(2, 2, 1) +plt.pcolormesh(grid.t*1e15, xgrid.x*1e6, (Etr[:, 1, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, :, end]))) +plt.xlim(-512, 512) +plt.ylim(-200, 200) + +plt.subplot(2, 2, 2) +plt.pcolormesh(grid.t*1e15, xgrid.x*1e6, (Etr[:, 2, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, :, end]))) +plt.xlim(-512, 512) +plt.ylim(-200, 200) + +plt.subplot(2, 2, 3) +plt.pcolormesh(lwe["t"]*1e15, lwe["x"]*1e6, lwe["Etx_x"]; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(lwe["Etx_x"]))) +plt.ylim(-200, 200) + +plt.subplot(2, 2, 4) +plt.pcolormesh(lwe["t"]*1e15, lwe["x"]*1e6, lwe["Etx_y"]; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(lwe["Etx_y"]))) +plt.ylim(-200, 200) + +## +fig = plt.figure() +fig.set_size_inches(12, 6) +plt.subplot(1, 2, 1) +plt.plot(grid.t*1e15, 1e-9*(Etr[:, 1, length(xgrid.x)÷2+1, end]); + label="Max: $(maximum(1e-9*(Etr[:, 1, length(xgrid.x)÷2+1, end])))") +plt.ylabel("E (GV/m)") +plt.legend() +plt.xlabel("Time (fs)") +plt.title("X Polarisation") +plt.subplot(1, 2, 2) +plt.plot(grid.t*1e15, 1e-9*(Etr[:, 2, length(xgrid.x)÷2+1, end]); + label="Max: $(maximum(1e-9*(Etr[:, 2, length(xgrid.x)÷2+1, end])))") +plt.legend() +plt.xlabel("Time (fs)") +plt.title("Y Polarisation") + + +# ## +# nfunx2(λ, δθ; z=0) = real(nfun(λ, θ)) +# plt.figure() +# plt.plot( +# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), +# LinearOps.crystal_internal_angle.(nfunx, PhysData.wlfreq(λ0), xgrid.kx), +# "."; +# label="With angle dependence" +# ) +# plt.plot( +# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), +# LinearOps.crystal_internal_angle.(nfunx2, PhysData.wlfreq(λ0), xgrid.kx), +# "."; +# label="No angle dependence" +# ) +# plt.plot( +# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), +# asin.(xgrid.kx*PhysData.c/PhysData.wlfreq(λ0)/nfunx(λ0, 0)), +# "."; +# label="Naive calculation" +# ) +# plt.legend() + +## +Utils.save_dict_h5( + joinpath(@__DIR__, "beam_walk_off.h5"), + Dict( + "x" => x, + "z" => z, + "Irxy" => Irxy + ) +) \ No newline at end of file From b39e5ed0711b2b2f86921259f4a9fab2c8e9af7d Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 3 Jun 2025 12:18:07 +0100 Subject: [PATCH 22/72] chi2 benchmark --- .../freespace/chi2_benchmarking.jl | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/low_level_interface/freespace/chi2_benchmarking.jl diff --git a/examples/low_level_interface/freespace/chi2_benchmarking.jl b/examples/low_level_interface/freespace/chi2_benchmarking.jl new file mode 100644 index 00000000..5a3a97be --- /dev/null +++ b/examples/low_level_interface/freespace/chi2_benchmarking.jl @@ -0,0 +1,37 @@ +using Luna +using BenchmarkTools +import Rotations: RotZY, RotYZ, RotMatrix, RotMatrix3 +import LinearAlgebra: mul! +using Metal + +θ = deg2rad(23.3717) +ϕ = deg2rad(30) +material = :BBO + +Et = rand(2^12, 2) +out = zero(Et) + +c = Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)) +# r = Nonlinear.Kerr_field(PhysData.χ3(material)) + +function toprofile(n) + for i in 1:n + c(out, Et, 1) + end +end +## +@profview toprofile(1) +@profview toprofile(100) +@btime c(out, Et, 1) + +## +Enl2 = rand(2^12, 6)' +Et2 = rand(2^12, 3)' +## +Et2_m = MtlArray(convert(Matrix{Float32}, Et2)) +χ2_m = MtlArray(convert(Matrix{Float32}, c.χ2_toLab)) +Enl2_m = MtlArray(convert(Matrix{Float32}, Enl2)) +@btime mul!(Et2_m, χ2_m, Enl2_m) + +## +@btime mul!(Et2, c.χ2_toLab, Enl2) From 1d36ef87b6a24563d05b86913abfdd5a1b906e2e Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 11:59:40 +0100 Subject: [PATCH 23/72] clean up crystals --- src/PhysData.jl | 95 +++++-------------------------------------------- 1 file changed, 8 insertions(+), 87 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 733f367b..747036e4 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -67,7 +67,7 @@ const gas_str = Dict( :D2 => "Deuterium" ) const glass = (:SiO2, :BK7, :KBr, :CaF2, :BaF2, :Si) -const crystal = (:ADP, :KPD, :BBO, :CaCO3, :MgF2, :LBO) +const crystal = (:ADP, :KPD, :BBO, :CaCO3, :MgF2, :LBO, :quartz) const metal = (:Ag, :Al) """ @@ -290,19 +290,6 @@ function sellmeier_glass(material::Symbol) + 0.0030434748/(1-(1.13475115/μm)^2) + 1.54133408/(1-(1104/μm)^2) )) - elseif material == :MgF2 - return μm -> @. sqrt(complex(1 - + 0.27620 - + 0.60967/(1-(0.08636/μm)^2) - + 0.0080/(1-(18.0/μm)^2) - + 2.14973/(1-(25.0/μm)^2) - )) - elseif material == :CaCO3 - return μm -> @. sqrt(complex(1 - + 0.73358749 - + 0.96464345/(1-1.94325203e-2/μm^2) - + 1.82831454/(1-120/μm^2) - )) else throw(DomainError(material, "Unknown glass $material")) end @@ -333,6 +320,7 @@ function sellmeier_crystal(material, axis=nothing) throw(DomainError(axis, "Unknown BBO axis $axis")) end elseif material == :MgF2 + isnothing(axis) && (axis = :o) if axis == :o return μm -> @. sqrt(complex(1 + 0.27620 @@ -351,7 +339,8 @@ function sellmeier_crystal(material, axis=nothing) else throw(DomainError(axis, "Unknown MgF2 axis $axis")) end - elseif material == :SiO2 + elseif material == :quartz + isnothing(axis) && (axis = :o) if axis == :o return μm -> @. sqrt(complex( 1 @@ -367,9 +356,10 @@ function sellmeier_crystal(material, axis=nothing) + 1.15662475/(1-100/μm^2) )) else - throw(DomainError(axis, "Unknown SiO2 axis $axis")) + throw(DomainError(axis, "Unknown quartz axis $axis")) end elseif material == :CaCO3 + isnothing(axis) && (axis = :o) if axis == :o return μm -> @. sqrt(complex(1 + 0.73358749 @@ -386,6 +376,7 @@ function sellmeier_crystal(material, axis=nothing) throw(DomainError(axis, "Unknown CaCO3 axis $axis")) end elseif material == :ADP + isnothing(axis) && (axis = :o) if axis == :o return μm -> @. sqrt(complex( 2.302842 @@ -402,6 +393,7 @@ function sellmeier_crystal(material, axis=nothing) throw(DomainError(axis, "Unknown ADP axis $axis")) end elseif material == :KDP + isnothing(axis) && (axis = :o) if axis == :o return μm -> @. sqrt(complex( 2.259276 @@ -442,77 +434,6 @@ function sellmeier_crystal(material, axis=nothing) else throw(DomainError(axis, "Unknown LBO axis $axis")) end - elseif material == :ADP - isnothing(axis) && (axis = :o) - if axis == :o - return μm -> @. sqrt(complex( - 2.302842 - + 15.102464*μm^2/(μm^2-400) - + 0.011125165/(μm^2-0.01325366) - )) - elseif axis == :e - return μm -> @. sqrt(complex( - 2.163510 - + 5.919896*μm^2/(μm^2-400) - + 0.009616676/(μm^2-0.01298912) - )) - else - throw(DomainError(axis, "Unknown ADP axis $axis")) - end - elseif material == :KDP - isnothing(axis) && (axis = :o) - if axis == :o - return μm -> @. sqrt(complex( - 2.259276 - + 13.00522*μm^2/(μm^2-400) - + 0.01008956/(μm^2-0.0129426) - )) - elseif axis == :e - return μm -> @. sqrt(complex( - 2.132668 - + 3.2279924*μm^2/(μm^2-400) - + 0.008637494/(μm^2-0.0122810) - )) - else - throw(DomainError(axis, "Unknown KDP axis $axis")) - end - elseif material == :CaCO3 - isnothing(axis) && (axis = :o) - if axis == :o - return μm -> @. sqrt(complex(1 - + 0.73358749 - + 0.96464345/(1-1.94325203e-2/μm^2) - + 1.82831454/(1-120/μm^2) - )) - elseif axis == :e - return μm -> @. sqrt(complex(1 - + 0.35859695 - + 0.82427830/(1-1.06689543e-2/μm^2) - + 0.14429128/(1-120/μm^2) - )) - else - throw(DomainError(axis, "Unknown CaCO3 axis $axis")) - end - elseif material == :MgF2 - isnothing(axis) && (axis = :o) - if axis == :o - return μm -> @. sqrt(complex(1 - + 0.27620 - + 0.60967/(1-(0.08636/μm)^2) - + 0.0080/(1-(18.0/μm)^2) - + 2.14973/(1-(25.0/μm)^2) - )) - elseif axis == :e - return μm -> @. sqrt(complex(1 - + 0.25385 - + 0.66405/(1-(0.08504/μm)^2) - + 1.0899/(1-(22.2/μm)^2) - + 0.1816/(1-(24.4/μm)^2) - + 2.1227/(1-(40.6/μm)^2) - )) - else - throw(DomainError(axis, "Unknown MgF2 axis $axis")) - end else throw(DomainError(material, "Unknown crystal $material")) end From 017cdcb3b9f9b3f9de46aca737221d0b77e60fb2 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 13:33:31 +0100 Subject: [PATCH 24/72] harmonise norm functions --- src/NonlinearRHS.jl | 137 +++++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 53 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 0fffdc77..400cb00a 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -528,10 +528,10 @@ mutable struct TransFree{TT, FTT, nT, rT, gT, xygT, dT, iT} grid::gT # time grid xygrid::xygT densityfun::dT # callable which returns density - Pto::Array{TT, 3} # buffer for oversampled time-domain NL polarisation - Eto::Array{TT, 3} # buffer for oversampled time-domain field - Eωo::Array{ComplexF64, 3} # buffer for oversampled frequency-domain field - Pωo::Array{ComplexF64, 3} # buffer for oversampled frequency-domain NL polarisation + Pto::Array{TT, 4} # buffer for oversampled time-domain NL polarisation + Eto::Array{TT, 4} # buffer for oversampled time-domain field + Eωo::Array{ComplexF64, 4} # buffer for oversampled frequency-domain field + Pωo::Array{ComplexF64, 4} # buffer for oversampled frequency-domain NL polarisation scale::Float64 # scale factor to be applied during oversampling idcs::iT # iterating over these slices Eto/Pto into Vectors, one at each position end @@ -546,11 +546,11 @@ function show(io::IO, t::TransFree) print(io, out) end -function TransFree(TT, scale, grid, xygrid, FT, responses, densityfun, normfun) +function TransFree(TT, scale, grid, xygrid, FT, responses, densityfun, normfun, pol=false) Ny = length(xygrid.y) Nx = length(xygrid.x) - Eωo = zeros(ComplexF64, (length(grid.ωo), Ny, Nx)) - Eto = zeros(TT, (length(grid.to), Ny, Nx)) + Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, Ny, Nx)) + Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, Ny, Nx)) Pto = similar(Eto) Pωo = similar(Eωo) idcs = CartesianIndices((Ny, Nx)) @@ -628,24 +628,65 @@ Make function to return normalisation factor for 3D propagation. """ function norm_free(grid, xygrid, nfun) ω = grid.ω + ωfirst = ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) - k2 = zero(grid.ω) - out = zeros(Float64, (length(grid.ω), length(xygrid.ky), length(xygrid.kx))) + out = zeros(Float64, (length(grid.ω), np, length(xygrid.ky), length(xygrid.kx))) function norm(z) - k2[grid.sidx] = (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./PhysData.c).^2 for ii in idcs for iω in eachindex(ω) - if ω[iω] == 0 - out[iω, ii] = 1.0 + if ω[iω] == 0 || ~grid.sidx[iω] + out[iω, :, ii] = 1.0 continue end - βsq = k2[iω] - kperp2[ii] - if βsq <= 0 - out[iω, ii] = 1.0 - continue + for (ip, n) in enumerate(nfun(ω[iω]; z)) + k2 = (n*ω[iω]/PhysData.c)^2 + βsq = k2 - kperp2[ii] + if βsq <= 0 + out[iω, ip, ii] = 1.0 + continue + end + out[iω, ip, ii] = sqrt(βsq)/(PhysData.μ_0*ω[iω]) + end + end + end + return out + end +end + +function norm_free(grid, xygrid, nfunx, nfuny) + # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ+δθ) + # nfuny(λ; z) just takes wavelength + ω = grid.ω + out = zeros(Float64, (length(ω), 2, length(xygrid.ky), length(xygrid.kx))) + function norm(z) + for iω in eachindex(ω) + if ω[iω] == 0 || ~grid.sidx[iω] + out[iω, :, :] .= 1.0 + continue + end + ny = nfuny(wlfreq(ω[iω])) + ksq_ypol = (ny*ω[iω]/c)^2 + for (ikx, kxi) in enumerate(xygrid.kx) + for (iky, kyi) in enumerate(xygrid.ky) + δθ = crystal_internal_angle(nfunx, ω[iω], kxi) + nx = nfunx(wlfreq(ω[iω]), δθ) + k_xpol = nx*grid.ω[iω]/c + βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 + if βsq_xpol < 0 + out[iω, 1, iky, ikx] = 1.0 + else + out[iω, 1, iky, ikx] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) + end + + βsq_ypol = ksq_ypol - kxi^2 - kyi^2 + if βsq_ypol < 0 + out[iω, 2, iky, ikx] .= 1.0 + else + out[iω, 2, iky, ikx] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) + end end - out[iω, ii] = sqrt(βsq)/(PhysData.μ_0*ω[iω]) end end return out @@ -694,8 +735,8 @@ Construct a `TransFree2D` to calculate the reciprocal-domain nonlinear polarisat # Arguments - `grid::AbstractGrid` : the grid used in the simulation -- `xygrid` : the spatial grid (instances of [`Grid.FreeGrid`](@ref)) -- `FT::FFTW.Plan` : the full 3D (t-y-x) Fourier transform for the oversampled time grid +- `xgrid` : the spatial grid (instances of [`Grid.FreeGrid`](@ref)) +- `FT::FFTW.Plan` : the 2D (t-x) Fourier transform for the oversampled time grid - `responses` : `Tuple` of response functions - `densityfun` : callable which returns the gas density as a function of `z` - `normfun` : normalisation factor as fctn of `z`, can be created via [`norm_free`](@ref) @@ -727,7 +768,7 @@ function (t::TransFree2D)(nl, Eωk, z) ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, ky, kx) -> (t, y, x) Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses @. t.Pto *= t.grid.towin # apodisation - mul!(t.Pωo, t.FT, t.Pto) # transform (t, y, x) -> (ω, ky, kx) + mul!(t.Pωo, t.FT, t.Pto) # transform (t, x) -> (ω, kx) copy_scale!(nl, t.Pωo, length(t.grid.ω), 1/t.scale) nl .*= t.grid.ωwin .* (-im.*t.grid.ω)./(2 .* t.normfun(z)) end @@ -768,27 +809,21 @@ Make function to return normalisation factor for 3D propagation. Here, `nfun(ω; z)` needs to take frequency `ω` and a keyword argument `z`. """ function norm_free2D(grid, xgrid, nfun) - # TODO: this can probably be combined with the case for TransFree by adjusting idcs kperp2 = xgrid.kx.^2 ω = grid.ω ωfirst = ω[findfirst(grid.sidx)] np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) out = zeros(Float64, (length(ω), np, length(xgrid.kx))) function norm(z) - for (ii, si) in enumerate(grid.sidx) - if si - k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./PhysData.c).^2 - end - end for ii in eachindex(xgrid.kx) for iω in eachindex(ω) - if ω[iω] == 0 + if ω[iω] == 0 || ~grid.sidx[iω] out[iω, :, ii] .= 1.0 continue end for (ip, n) in enumerate(nfun(ω[iω]; z)) - βsq = k2[iω, ip] - kperp2[ii] + k2 = (n*ω[iω]/PhysData.c)^2 + βsq = k2 - kperp2[ii] if βsq <= 0 out[iω, ip, ii] = 1.0 continue @@ -807,34 +842,30 @@ function norm_free2D(grid, xgrid, nfunx, nfuny) ω = grid.ω out = zeros(Float64, (length(ω), 2, length(xgrid.kx))) function norm(z) - for (iω, si) in enumerate(grid.sidx) - if ω[iω] == 0 + for iω in eachindex(ω) + if ω[iω] == 0 || ~grid.sidx[iω] out[iω, :, :] .= 1.0 continue end - if si - ny = nfuny(wlfreq(grid.ω[iω])) - ksq_ypol = (ny*grid.ω[iω]/c)^2 - for (ik, kxi) in enumerate(xgrid.kx) - δθ = crystal_internal_angle(nfunx, grid.ω[iω], kxi) - nx = nfunx(wlfreq(grid.ω[iω]), δθ) - k_xpol = nx*grid.ω[iω]/c - βsq_xpol = k_xpol^2 - kxi^2 - if βsq_xpol < 0 - out[iω, 1, ik] = 1.0 - else - out[iω, 1, ik] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) - end - - βsq_ypol = ksq_ypol - kxi^2 - if βsq_ypol < 0 - out[iω, :, ik] .= 1.0 - else - out[iω, 2, ik] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) - end + ny = nfuny(wlfreq(ω[iω])) + ksq_ypol = (ny*ω[iω]/c)^2 + for (ik, kxi) in enumerate(xgrid.kx) + δθ = crystal_internal_angle(nfunx, ω[iω], kxi) + nx = nfunx(wlfreq(ω[iω]), δθ) + k_xpol = nx*grid.ω[iω]/c + βsq_xpol = k_xpol^2 - kxi^2 + if βsq_xpol < 0 + out[iω, 1, ik] = 1.0 + else + out[iω, 1, ik] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) + end + + βsq_ypol = ksq_ypol - kxi^2 + if βsq_ypol < 0 + out[iω, 2, ik] .= 1.0 + else + out[iω, 2, ik] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) end - else - out[iω, :, :] .= 1.0 end end return out From 0125e19a1a47e5d031fef1055445e0bf8bf65f5b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 13:52:53 +0100 Subject: [PATCH 25/72] make linop for radial propagation handle (nx, ny) --- src/LinearOps.jl | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 8244a398..d32c0c95 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -288,13 +288,13 @@ end #============== RADIAL SYMMETRY ==============# #=================================================# """ - make_const_linop(grid, q::QDHT, n, frame_vel) + make_const_linop(grid, q::QDHT, n, β1) Make constant linear operator for radial free-space. `n` is the refractive index (array) and β1 is 1/velocity of the reference frame. """ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, - n::AbstractMatrix, β1::Number) + n::AbstractVecOrMat, β1::Number) out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/PhysData.c)^2 kr2 = q.k.^2 @@ -334,7 +334,7 @@ function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, - n::AbstractMatrix, β1::Number, β0ref::Number; thg=false) + n::AbstractVecOrMat, β1::Number, β0ref::Number; thg=false) out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/PhysData.c)^2 kr2 = q.k.^2 @@ -351,11 +351,13 @@ distance `z`. """ function make_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) kr2 = q.k.^2 - k2 = zero(grid.ω) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 + k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N) end end @@ -378,36 +380,21 @@ end function make_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) kr2 = q.k.^2 - k2 = zero(grid.ω) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 - βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z) + k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 + βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z)[2] _fill_linop_r!(out, grid, β1, k2, kr2, q.N, βref, thg) end end function _fill_linop_r!(out, grid::Grid.EnvGrid, β1, k2, kr2, Nr, βref, thg) for ir = 1:Nr - for iω = 1:length(grid.ω) - βsq = k2[iω] - kr2[ir] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - if !thg - out[iω, ir] -= -im*βref - end - end - end -end - -function _fill_linop_r!(out, grid::Grid.EnvGrid, β1, k2::AbstractMatrix, kr2, Nr, βref, thg) - for ir = 1:Nr - for ip = 1:2 + for ip in axes(k2, 2) for iω = 1:length(grid.ω) βsq = k2[iω, ip] - kr2[ir] if βsq < 0 From 24140b1223e61b580eca0931af8a38cd57a930ef Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 13:52:58 +0100 Subject: [PATCH 26/72] comments --- src/Nonlinear.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Nonlinear.jl b/src/Nonlinear.jl index 6a9bca6c..3d889cb2 100644 --- a/src/Nonlinear.jl +++ b/src/Nonlinear.jl @@ -111,9 +111,9 @@ function (c::Chi2Field)(out, E, ρ) @inbounds c.El[1] = E[i, 1] @inbounds c.El[2] = E[i, 2] # note c.El[3] (Ez in the lab frame) is always zero here - mul!(c.Ec, c.toCrystal, c.El) - @inbounds field_products!(c.Enl, c.Ec) - mul!(c.Pl, c.χ2_toLab, c.Enl) + mul!(c.Ec, c.toCrystal, c.El) # transform to crystal frame + @inbounds field_products!(c.Enl, c.Ec) # calculate nonlinear products + mul!(c.Pl, c.χ2_toLab, c.Enl) # multiply by χ2 tensor and transform to lab frame @inbounds out[i, 1] += ε_0*c.Pl[1] @inbounds out[i, 2] += ε_0*c.Pl[2] end From 16efa48da54f7d70a21a348a3a025d8012c0b12f Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 14:18:09 +0100 Subject: [PATCH 27/72] use imported c --- src/LinearOps.jl | 68 ++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index d32c0c95..d7e3b98f 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -18,16 +18,22 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) k2 = zero(grid.ω) - k2[grid.sidx] .= (n[grid.sidx] .* grid.ω[grid.sidx] ./ PhysData.c).^2 + k2[grid.sidx] .= (n[grid.sidx] .* grid.ω[grid.sidx] ./ c).^2 out = zeros(ComplexF64, (length(grid.ω), length(xygrid.ky), length(xygrid.kx))) _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs) return out end function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) - n = zero(grid.ω) - n[grid.sidx] = nfun.(2π*PhysData.c./grid.ω[grid.sidx]) - β1 = PhysData.dispersion_func(1, nfun)(grid.referenceλ) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(wlfreq(ωfirst); z=0)) # 1 if single ref index, 2 if nx, ny + n = zeros(Float64, (length(grid.ω), np)) + for (ii, si) in enumerate(grid.sidx) + if si + n[ii, :] .= nfun(wlfreq(grid.ω[ii])) + end + end + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) make_const_linop(grid, xygrid, n, β1) end @@ -36,7 +42,7 @@ function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) k2 = zero(grid.ω) - k2[grid.sidx] .= (n[grid.sidx].*grid.ω[grid.sidx]./PhysData.c).^2 + k2[grid.sidx] .= (n[grid.sidx].*grid.ω[grid.sidx]./c).^2 out = zeros(ComplexF64, (length(grid.ω), length(xygrid.ky), length(xygrid.kx))) _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) return out @@ -50,7 +56,7 @@ function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun, if thg β0const = 0.0 else - β0const = grid.ω0/PhysData.c * nfun(wlfreq(grid.ω0)) + β0const = grid.ω0/c * nfun(wlfreq(grid.ω0)) end make_const_linop(grid, xygrid, n, β1, β0const; thg=thg) end @@ -68,7 +74,7 @@ function make_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx] ./ PhysData.c).^2 + k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx] ./ c).^2 _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs) end end @@ -95,8 +101,8 @@ function make_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun; thg=false) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./PhysData.c).^2 - βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z) + k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./c).^2 + βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z) _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) end end @@ -131,7 +137,7 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, n::AbstractArray, β1::Number) kperp2 = xgrid.kx.^2 idcs = CartesianIndices(xgrid.kx) - k2 = @. (n*grid.ω/PhysData.c)^2 + k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) return out @@ -139,11 +145,11 @@ end function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + np = length(nfun(wlfreq(ωfirst); z=0)) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si - n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) @@ -180,7 +186,7 @@ function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, n::AbstractArray, β1::Number, β0ref::Number; thg=false) kperp2 = xgrid.kx.^2 idcs = CartesianIndices(xgrid.kx) - k2 = @. (n*grid.ω/PhysData.c)^2 + k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) return out @@ -193,14 +199,14 @@ function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun, n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si - n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) if thg β0const = 0.0 else - β0const = grid.ω0/PhysData.c * nfun(wlfreq(grid.ω0)) + β0const = grid.ω0/c * nfun(wlfreq(grid.ω0)) end make_const_linop(grid, xgrid, n, β1, β0const; thg=thg) end @@ -222,7 +228,7 @@ function make_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) for (ii, si) in enumerate(grid.sidx) if si - k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]; z)) .* grid.ω[ii]./PhysData.c).^2 + k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]; z)) .* grid.ω[ii]./c).^2 end end _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) @@ -257,10 +263,10 @@ function make_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun; thg=false) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) for (ii, si) in enumerate(grid.sidx) if si - k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./PhysData.c).^2 + k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./c).^2 end end - βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z)[1] + βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[1] _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) end end @@ -296,7 +302,7 @@ and β1 is 1/velocity of the reference frame. function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, n::AbstractVecOrMat, β1::Number) out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) - k2 = @. (n*grid.ω/PhysData.c)^2 + k2 = @. (n*grid.ω/c)^2 kr2 = q.k.^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N) return out @@ -308,7 +314,7 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si - n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) @@ -321,14 +327,14 @@ function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si - n[ii, :] .= nfun(2π*PhysData.c./grid.ω[ii]) + n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) if thg β0const = 0.0 else - β0const = grid.ω0/PhysData.c * nfun(2π*PhysData.c./grid.ω0)[1] + β0const = grid.ω0/c * nfun(2π*c./grid.ω0)[1] end make_const_linop(grid, q, n, β1, β0const; thg=thg) end @@ -336,7 +342,7 @@ end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, n::AbstractVecOrMat, β1::Number, β0ref::Number; thg=false) out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) - k2 = @. (n*grid.ω/PhysData.c)^2 + k2 = @. (n*grid.ω/c)^2 kr2 = q.k.^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N, β0ref, thg) return out @@ -357,7 +363,7 @@ function make_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 + k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 _fill_linop_r!(out, grid, β1, k2, kr2, q.N) end end @@ -386,8 +392,8 @@ function make_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./PhysData.c).^2 - βref = thg ? 0.0 : grid.ω0/PhysData.c * nfun(grid.ω0; z=z)[2] + k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 + βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[2] _fill_linop_r!(out, grid, β1, k2, kr2, q.N, βref, thg) end end @@ -437,7 +443,7 @@ from previous bugs. See https://github.com/LupoLab/Luna/pull/142. See also [`αlim!`](@ref). """ -conj_clamp(n, ω) = clamp(real(n), 1e-3, Inf) - im*clamp(imag(n), 0, 3000*PhysData.c/ω) +conj_clamp(n, ω) = clamp(real(n), 1e-3, Inf) - im*clamp(imag(n), 0, 3000*c/ω) function make_const_linop(grid::Grid.RealGrid, βfun!, αfun!, β1) β = similar(grid.ω) @@ -529,7 +535,7 @@ function make_linop(grid::Grid.RealGrid, mode::Modes.AbstractMode, λ0) β1 = Modes.dispersion(mode, 1, ω0, z=z)::Float64 for iω in sidcs nc = conj_clamp(neff(iω; z=z), ω[iω]) - out[iω] = -im*(ω[iω]/PhysData.c*nc - ω[iω]*β1) + out[iω] = -im*(ω[iω]/c*nc - ω[iω]*β1) end end end @@ -556,7 +562,7 @@ function make_linop(grid::Grid.EnvGrid, mode::Modes.AbstractMode, λ0; thg=false end for iω in sidcs nc = conj_clamp(neff(iω; z=z), ω[iω]) - out[iω] = -im*(ω[iω]/PhysData.c*nc - (ω[iω] - grid.ω0)*β1) + out[iω] = -im*(ω[iω]/c*nc - (ω[iω] - grid.ω0)*β1) if !thg out[iω] -= -im*βref end @@ -645,7 +651,7 @@ function make_linop(grid::Grid.RealGrid, modes, λ0; ref_mode=1) for i in eachindex(modes) for iω in sidcs nc = conj_clamp(neff(iω, i; z=z), ω[iω]) - out[iω, i] = -im*(ω[iω]/PhysData.c*nc - ω[iω]*β1) + out[iω, i] = -im*(ω[iω]/c*nc - ω[iω]*β1) end end end @@ -665,7 +671,7 @@ function make_linop(grid::Grid.EnvGrid, modes, λ0; ref_mode=1, thg=false) for i in eachindex(modes) for iω in sidcs nc = conj_clamp(neff(iω, i; z=z), ω[iω]) - out[iω, i] = -im*(ω[iω]/PhysData.c*nc - (ω[iω] - grid.ω0)*β1) + out[iω, i] = -im*(ω[iω]/c*nc - (ω[iω] - grid.ω0)*β1) if !thg out[iω, i] -= -im*βref end From 7c890e2ac76234f60989c4a2be43b9277e4924f3 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 20:50:17 +0100 Subject: [PATCH 28/72] fix ref_index() for mixtures --- src/PhysData.jl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 747036e4..783b64cf 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -446,6 +446,16 @@ function ref_index_fun_uniax(material; axes=(:o, :e)) return n end +function ref_index_fun_xy(material, θ; ordinary=:o, extraordinary=:e) + # y polarisation: ordinary polarisation + no = ref_index_fun(material; axis=ordinary) + # x polarisation: extraordinary polarisation, need extra δθ argument + ne = ref_index_fun_uniax(material; axes=(ordinary, extraordinary)) + nfunx(λ, δθ=0) = real(ne(λ, θ+δθ)) + nfuny(λ) = real(no(λ)) + nfunx, nfuny +end + function crystal_internal_angle(nfun, ω, kx) # External wavevector is kx = ω/c*sin(θ_i) with θ_i the AOI of the plane wave # Internal wavevector is kx2 = ω/c * n(θ+δθ) * sin(δθ) @@ -567,7 +577,8 @@ end Get function which returns refractive index for gas mixture. `gases` is a `Tuple` of gas identifiers (`Symbol`s) and `P` is a `Tuple` of equal length containing pressures. """ -function ref_index_fun(gases::NTuple{N, Symbol}, P::NTuple{N, Number}, T=roomtemp; lookup=nothing) where N +function ref_index_fun(gases::NTuple{N, Symbol}, P::NTuple{N, Number}, T=roomtemp; + lookup=nothing, axis=nothing) where N ngas = let funs=[χ1_fun(gi, Pi, T) for (gi, Pi) in zip(gases, P)] function ngas(λ) res = funs[1](λ) From c53fa3a9cccae6bec43c223d8452795cb4afcbdb Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 21:21:44 +0100 Subject: [PATCH 29/72] generalise field creation for free-space --- src/Fields.jl | 27 +++++++++++++++++++-------- src/Luna.jl | 15 ++++++++++----- test/test_fields.jl | 16 ++++++++-------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/Fields.jl b/src/Fields.jl index 10b0cb14..3bc49092 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -389,13 +389,10 @@ Get the field for the provided `grid`, `spacegrid` function and Fourier transform `FT` """ function (s::SpatioTemporalField)(grid, spacegrid, FT) - Etr = make_Etr(s, grid, spacegrid) + Etr = make_Etr(s, grid, spacegrid) # (t, r) or (t, x) or (t, y, x) energy_t = energyfuncs(grid, spacegrid)[1] Etr .*= sqrt(s.energy)/sqrt(energy_t(Etr)) - Etr = permutedims( - cat(Etr .* sin(s.θ), Etr .* cos(s.θ); dims=3), - (1, 3, 2) - ) + Etr = rotate(Etr, s.θ) Eωk = transform(spacegrid, FT, Etr) if s.propz != 0.0 prop!(Eωk, s.propz, grid, spacegrid) @@ -403,8 +400,22 @@ function (s::SpatioTemporalField)(grid, spacegrid, FT) Eωk end +function rotate(Etr::AbstractArray{T, 2}, θ) where T + Etr = permutedims( + cat(Etr .* sin(θ), Etr .* cos(θ); dims=3), + (1, 3, 2) + ) +end + +function rotate(Etr::AbstractArray{T, 3}, θ) where T + Etr = permutedims( + cat(Etr .* sin(θ), Etr .* cos(θ); dims=4), + (1, 4, 2, 3) + ) +end + function prop!(Eωk, z, grid, q::Hankel.QDHT) - kzsq = @. (grid.ω/PhysData.c)^2 - (q.k^2)' + kzsq = (grid.ω/PhysData.c).^2 .- reshape(q.k.^2, (1, 1, length(q.k))) kzsq[kzsq .< 0] .= 0 kz = sqrt.(kzsq) @. Eωk *= exp(-1im * z * (kz - grid.ω/PhysData.c)) @@ -412,8 +423,8 @@ end function prop!(Eωk, z, grid, xygrid) kzsq = ((grid.ω ./ PhysData.c).^2 - .- reshape(xygrid.ky.^2, (1, length(xygrid.ky), 1)) - .- reshape(xygrid.kx.^2, (1, 1, length(xygrid.kx))) + .- reshape(xygrid.ky.^2, (1, 1, length(xygrid.ky), 1)) + .- reshape(xygrid.kx.^2, (1, 1, 1, length(xygrid.kx))) ) kzsq[kzsq.<0] .= 0 kz = sqrt.(kzsq) diff --git a/src/Luna.jl b/src/Luna.jl index 89aebea8..33d128b5 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -209,7 +209,12 @@ end function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT, Grid.FreeGrid, Grid.Free2DGrid}, FT, inputs::Tuple{Vararg{T} where T <: Fields.SpatioTemporalField}) for field in inputs - Eωk .+= field(grid, spacegrid, FT) + Eωki = field(grid, spacegrid, FT) + if size(Eωk, 2) == 2 + Eωk .+= Eωki + else + Eωk .+= Eωki[:, 2] # take y-polarisation if only 1 polarisation specified + end end end @@ -264,11 +269,11 @@ function setup(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, x = xygrid.x y = xygrid.y xr = Array{Float64}(undef, length(grid.t), length(y), length(x)) - FT = FFTW.plan_rfft(xr, (1, 2, 3), flags=settings["fftw_flag"]) + FT = FFTW.plan_rfft(xr, (1, 3, 4), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, length(grid.ω), length(y), length(x)) doinputs_fs!(Eωk, grid, xygrid, FT, inputs) xo = Array{Float64}(undef, length(grid.to), length(y), length(x)) - FTo = FFTW.plan_rfft(xo, (1, 2, 3), flags=settings["fftw_flag"]) + FTo = FFTW.plan_rfft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, responses, densityfun, normfun) inv(FT) # create inverse FT plans now, so wisdom is saved @@ -283,11 +288,11 @@ function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, x = xygrid.x y = xygrid.y xr = Array{ComplexF64}(undef, length(grid.t), length(y), length(x)) - FT = FFTW.plan_fft(xr, (1, 2, 3), flags=settings["fftw_flag"]) + FT = FFTW.plan_fft(xr, (1, 3, 4), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, length(grid.ω), length(y), length(x)) doinputs_fs!(Eωk, grid, xygrid, FT, inputs) xo = Array{ComplexF64}(undef, length(grid.to), length(y), length(x)) - FTo = FFTW.plan_fft(xo, (1, 2, 3), flags=settings["fftw_flag"]) + FTo = FFTW.plan_fft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, responses, densityfun, normfun) inv(FT) # create inverse FT plans now, so wisdom is saved diff --git a/test/test_fields.jl b/test/test_fields.jl index f8dd6844..2bb3192d 100644 --- a/test/test_fields.jl +++ b/test/test_fields.jl @@ -743,7 +743,7 @@ end end end -@testset "free-space inputs: radial" begin +# @testset "free-space inputs: radial" begin λ0 = 800e-9 τfwhm = 10e-15 energy = 1e-6 @@ -759,9 +759,9 @@ end grid = Grid.EnvGrid(1, λ0, (400e-9, 6e-6), 100e-15) - q = Hankel.QDHT(R, N, dim=2) + q = Hankel.QDHT(R, N, dim=3) - xt = zeros(Float64, length(grid.t), length(q.r)) + xt = zeros(Float64, length(grid.t), 2, length(q.r)) FT = FFTW.plan_fft(xt, 1, flags=FFTW.ESTIMATE) Eωk = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz)(grid, q, FT) @@ -770,11 +770,11 @@ end Eωr = Hankel.symmetric(q \ Eωk, q) Iωr = abs2.(Eωr) - Ir = dropdims(sum(Iωr; dims=1); dims=1) + Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) w1q = 2Maths.rms_width(r, Ir) @test isapprox(w1q, w1; rtol=1e-3) -end +# end @testset "free-space inputs: full 3D" begin λ0 = 800e-9 @@ -792,14 +792,14 @@ end grid = Grid.EnvGrid(1, λ0, (400e-9, 6e-6), 100e-15) xygrid = Grid.FreeGrid(R, N) - xr = Array{ComplexF64}(undef, length(grid.t), length(xygrid.y), length(xygrid.x)) - FT = FFTW.plan_fft(xr, (1, 2, 3), flags=FFTW.ESTIMATE) + xr = Array{ComplexF64}(undef, length(grid.t), 2, length(xygrid.y), length(xygrid.x)) + FT = FFTW.plan_fft(xr, (1, 3, 4), flags=FFTW.ESTIMATE) Eωk = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz)(grid, xygrid, FT) Etxy = FT \ Eωk - Ixy = dropdims(sum(abs2.(Etxy); dims=1); dims=1) + Ixy = dropdims(sum(abs2.(Etxy); dims=(1, 2)); dims=(1, 2)) Ix = dropdims(sum(Ixy; dims=1); dims=1) Iy = dropdims(sum(Ixy; dims=2); dims=2) From 2a8028fd364ab1410829b85efa9a9141661e5692 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 20 Aug 2025 21:21:56 +0100 Subject: [PATCH 30/72] make linops work for single polarisation --- src/LinearOps.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index d7e3b98f..94ce5b92 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -33,7 +33,7 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) make_const_linop(grid, xygrid, n, β1) end @@ -152,7 +152,7 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) make_const_linop(grid, xgrid, n, β1) end @@ -202,7 +202,7 @@ function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun, n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) if thg β0const = 0.0 else @@ -228,7 +228,7 @@ function make_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) for (ii, si) in enumerate(grid.sidx) if si - k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]; z)) .* grid.ω[ii]./c).^2 + k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii]./c).^2 end end _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) @@ -263,10 +263,10 @@ function make_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun; thg=false) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) for (ii, si) in enumerate(grid.sidx) if si - k2[ii, :] .= (nfun(wlfreq(grid.ω[ii]); z) .* grid.ω[ii]./c).^2 + k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii]./c).^2 end end - βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[1] + βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) end end @@ -317,7 +317,7 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) make_const_linop(grid, q, n, β1) end @@ -330,7 +330,7 @@ function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) n[ii, :] .= nfun(wlfreq(grid.ω[ii])) end end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[2])(grid.referenceλ) + β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) if thg β0const = 0.0 else @@ -360,7 +360,7 @@ function make_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) ωfirst = grid.ω[findfirst(grid.sidx)] np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] + nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[end] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 @@ -389,11 +389,11 @@ function make_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) ωfirst = grid.ω[findfirst(grid.sidx)] np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[2] + nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[end] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 - βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[2] + βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] _fill_linop_r!(out, grid, β1, k2, kr2, q.N, βref, thg) end end From ddf2fef4cd29475cc13eb35f3a977131478d28f2 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 21 Aug 2025 13:20:23 +0100 Subject: [PATCH 31/72] update BBO example --- .../freespace/free2D_bbo.jl | 140 ++++++------------ 1 file changed, 48 insertions(+), 92 deletions(-) diff --git a/examples/low_level_interface/freespace/free2D_bbo.jl b/examples/low_level_interface/freespace/free2D_bbo.jl index 9a65d526..2bc18b70 100644 --- a/examples/low_level_interface/freespace/free2D_bbo.jl +++ b/examples/low_level_interface/freespace/free2D_bbo.jl @@ -1,40 +1,47 @@ +#= +Here we simulate type I SHG in a BBO crystal driven at 800 nm. The focus is small and the crystal +is reasonably thick, so we observe strong temporal and spatial walk-off effects. +=# using Luna import FFTW import Luna: Hankel import PyPlot: plt import NumericalIntegration: integrate -λ0 = 1030e-9 -τfwhm = 250e-15 -w0 = 50e-6 -energy = 1e-6 +λ0 = 800e-9 # driving wavelength +τfwhm = 30e-15 # pulse duration +w0 = 20e-6 # 1/e² beam radius +energy = 10e-9 # pulse energy -thickness = 1000e-6 material = :BBO +thickness = 200e-6 # BBO thickness -R = 4*w0 -N = 2^6 +R = 4*w0 # radius of the spatial window +N = 2^6 # number of spatial points -grid = Grid.RealGrid(thickness, λ0, (300e-9, 2e-6), 1200e-15) +grid = Grid.RealGrid(thickness, λ0, (250e-9, 2e-6), 120e-15) xgrid = Grid.Free2DGrid(R, N) -θ = deg2rad(23.3717) -ϕ = deg2rad(30) - +θ = deg2rad(29.2) # type I phase-matching angle +ϕ = deg2rad(30) # ϕ for type I phase-matching +## responses = (Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)), Nonlinear.Kerr_field(PhysData.χ3(material))) -nfun = PhysData.ref_index_fun_uniax(material) -nfunx(λ, δθ; z=0) = real(nfun(λ, θ+δθ)) -nfuny(λ; z=0) = real(nfun(λ, 0)) +#= +Ref index functions. Note that here nfunx takes (λ, δθ=0) as arguments, +which allows make_const_linop to calculate the actual internal angle depending +on frequency and transverse k-vector component. +=# +nfunx, nfuny = PhysData.ref_index_fun_xy(material, θ) linop = LinearOps.make_const_linop(grid, xgrid, nfunx, nfuny) normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, nfunx, nfuny) -densityfun = z -> 1 +densityfun = z -> 1 # density is unity because we're considering a solid. ## +# scaling factor in front of the energy corresponds to the integral over y inputs = Fields.GaussGaussField(;λ0, τfwhm, energy=energy/(sqrt(π/2)*w0), w0) -# inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0) Eω, transform, FT = Luna.setup(grid, xgrid, densityfun, normfun, responses, inputs) ## @@ -46,6 +53,7 @@ z = output["z"] Eωk = output["Eω"] # (ω, pol, k, z) x = xgrid.x +# normalisation prefactor for spectral intensity ωprefac = PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) * sqrt(π/2)*w0 Eωr = FFTW.ifft(Eωk, 3) # (ω, pol, x, z) @@ -67,31 +75,10 @@ It0 = 0.5*PhysData.c*PhysData.ε_0*abs2.(Et0) et, eω = Fields.energyfuncs(grid, xgrid) -energy_x = eω(Eωk[:, 1, :, 1])*sqrt(π/2)*w0 -energy_y = eω(Eωk[:, 2, :, 1])*sqrt(π/2)*w0 +energy_out = dropdims(mapslices(eω, Eωk; dims=(1, 3)); dims=(1, 3))*sqrt(π/2)*w0 ω = grid.ω -## -I1 = 2*0.94*energy/τfwhm / (π*w0^2) -d31 = 0.16e-12 -d22 = -2.3e-12 -deff = d31*sin(θ) - d22*cos(θ)*sin(3ϕ) - -ω3 = PhysData.wlfreq(λ0/2) -n1 = real(nfun(λ0, 0)) -n2 = n1 -n3 = real(nfun(λ0/2, θ)) - -χ2eff = 2deff - -I3 = 2*χ2eff^2*ω3^2/(n1*n2*n3*PhysData.ε_0 * PhysData.c^3) * I1^2 * z.^2 - -## -plt.figure() -# plt.plot(z*1e6, I3*1e-4; label="Calculated") -plt.plot(z*1e6, dropdims(maximum(It0[:, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") -# plt.plot(z*1e6, dropdims(maximum(Itr[:, 1, 1, :]; dims=1); dims=1)*1e-4; label="Simulated") ## plt.figure() @@ -99,6 +86,14 @@ plt.pcolormesh(z*1e3, x*1e6, Ir) plt.xlabel("Distance (mm)") plt.ylabel("X (μm)") +## +plt.figure() +plt.plot(z*1e6, 1e9energy_out[1, :]; label="X polarisation") +plt.plot(z*1e6, 1e9energy_out[2, :]; label="Y polarisation") +plt.xlabel("Distance (μm)") +plt.ylabel("Energy (nJ)") +plt.legend() + ## fig = plt.figure() fig.set_size_inches(12, 6) @@ -147,14 +142,14 @@ plt.subplot(2, 1, 1) plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, 1]; label="input X") plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; label="input Y") plt.ylim(1e-5mm, 5mm) -plt.xlim(200, 800) +plt.xlim(200, 900) plt.legend() plt.subplot(2, 1, 2) plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 1, end]; label="output X") plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, end]; label="output Y") # plt.semilogy(ω*1e-12/2π, 2π*1e12*Iωxy[:, 2, 1]; c="C1", linestyle="--", label="input Y") plt.ylim(1e-5mm, 5mm) -plt.xlim(200, 800) +plt.xlim(200, 900) plt.xlabel("Frequency (THz)") plt.ylabel("SED (J/Hz)") plt.legend() @@ -162,28 +157,23 @@ plt.legend() ## lwe = Utils.load_dict_h5(joinpath(@__DIR__, "field_for_luna.h5")) fig = plt.figure() -fig.set_size_inches(12, 12) -plt.subplot(2, 2, 1) +fig.set_size_inches(12, 7) +plt.subplot(1, 2, 1) plt.pcolormesh(grid.t*1e15, xgrid.x*1e6, (Etr[:, 1, :, end])'; cmap="seismic") plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, :, end]))) -plt.xlim(-512, 512) -plt.ylim(-200, 200) +plt.xlim(-50, 50) +plt.ylim(-100, 100) +plt.xlabel("Time (fs)") +plt.ylabel("X (μm)") +plt.title("X polarisation") -plt.subplot(2, 2, 2) +plt.subplot(1, 2, 2) plt.pcolormesh(grid.t*1e15, xgrid.x*1e6, (Etr[:, 2, :, end])'; cmap="seismic") plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, :, end]))) -plt.xlim(-512, 512) -plt.ylim(-200, 200) - -plt.subplot(2, 2, 3) -plt.pcolormesh(lwe["t"]*1e15, lwe["x"]*1e6, lwe["Etx_x"]; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(lwe["Etx_x"]))) -plt.ylim(-200, 200) - -plt.subplot(2, 2, 4) -plt.pcolormesh(lwe["t"]*1e15, lwe["x"]*1e6, lwe["Etx_y"]; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(lwe["Etx_y"]))) -plt.ylim(-200, 200) +plt.xlim(-50, 50) +plt.ylim(-100, 100) +plt.xlabel("Time (fs)") +plt.title("Y polarisation") ## fig = plt.figure() @@ -200,38 +190,4 @@ plt.plot(grid.t*1e15, 1e-9*(Etr[:, 2, length(xgrid.x)÷2+1, end]); label="Max: $(maximum(1e-9*(Etr[:, 2, length(xgrid.x)÷2+1, end])))") plt.legend() plt.xlabel("Time (fs)") -plt.title("Y Polarisation") - - -# ## -# nfunx2(λ, δθ; z=0) = real(nfun(λ, θ)) -# plt.figure() -# plt.plot( -# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), -# LinearOps.crystal_internal_angle.(nfunx, PhysData.wlfreq(λ0), xgrid.kx), -# "."; -# label="With angle dependence" -# ) -# plt.plot( -# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), -# LinearOps.crystal_internal_angle.(nfunx2, PhysData.wlfreq(λ0), xgrid.kx), -# "."; -# label="No angle dependence" -# ) -# plt.plot( -# asin.(xgrid.kx .* PhysData.c ./ PhysData.wlfreq(λ0)), -# asin.(xgrid.kx*PhysData.c/PhysData.wlfreq(λ0)/nfunx(λ0, 0)), -# "."; -# label="Naive calculation" -# ) -# plt.legend() - -## -Utils.save_dict_h5( - joinpath(@__DIR__, "beam_walk_off.h5"), - Dict( - "x" => x, - "z" => z, - "Irxy" => Irxy - ) -) \ No newline at end of file +plt.title("Y Polarisation") \ No newline at end of file From 0b1709b3d208c95211bcbbd13a06d51767989d40 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 22 Aug 2025 20:02:22 +0100 Subject: [PATCH 32/72] working full 3D bbo example --- .../freespace/free3D_bbo.jl | 243 ++++++++++++++++++ src/LinearOps.jl | 34 ++- src/Luna.jl | 20 +- src/NonlinearRHS.jl | 17 +- 4 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 examples/low_level_interface/freespace/free3D_bbo.jl diff --git a/examples/low_level_interface/freespace/free3D_bbo.jl b/examples/low_level_interface/freespace/free3D_bbo.jl new file mode 100644 index 00000000..d93c93a6 --- /dev/null +++ b/examples/low_level_interface/freespace/free3D_bbo.jl @@ -0,0 +1,243 @@ +#= +Here we simulate type I SHG in a BBO crystal driven at 800 nm. The focus is small and the crystal +is reasonably thick, so we observe strong temporal and spatial walk-off effects. +=# +using Luna +import FFTW +import Luna: Hankel +import PyPlot: plt +import NumericalIntegration: integrate + +λ0 = 800e-9 # driving wavelength +τfwhm = 30e-15 # pulse duration +w0 = 20e-6 # 1/e² beam radius +energy = 10e-9 # pulse energy + +material = :BBO +thickness = 200e-6 # BBO thickness + +R = 4*w0 # radius of the spatial window +N = 2^6 # number of spatial points + +grid = Grid.RealGrid(thickness, λ0, (250e-9, 2e-6), 120e-15) +xygrid = Grid.FreeGrid(R, N) + +θ = deg2rad(29.2) # type I phase-matching angle +ϕ = deg2rad(30) # ϕ for type I phase-matching + +## +responses = (Nonlinear.Chi2Field(θ, ϕ, PhysData.χ2(material)),) +# Nonlinear.Kerr_field(PhysData.χ3(material))) + +#= +Ref index functions. Note that here nfunx takes (λ, δθ=0) as arguments, +which allows make_const_linop to calculate the actual internal angle depending +on frequency and transverse k-vector component. +=# +nfunx, nfuny = PhysData.ref_index_fun_xy(material, θ) +linop = LinearOps.make_const_linop(grid, xygrid, nfunx, nfuny) + +normfun = NonlinearRHS.const_norm_free(grid, xygrid, nfunx, nfuny) +densityfun = z -> 1 # density is unity because we're considering a solid. +## +inputs = Fields.GaussGaussField(;λ0, τfwhm, energy=energy, w0) +Eω, transform, FT = Luna.setup(grid, xygrid, densityfun, normfun, responses, inputs) + +## +output = Output.MemoryOutput(0, grid.zmax, 101) +Luna.run(Eω, grid, linop, transform, FT, output; init_dz=1e-6) + +## +z = output["z"] +Eωk = output["Eω"] # (ω, pol, ky, kx, z) +Ik = FFTW.fftshift(dropdims(sum(abs2.(Eωk); dims=1); dims=1), (2, 3)) +x = xygrid.x +y = xygrid.y + +# normalisation prefactor for spectral intensity +ωprefac = PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) + +Eωr = FFTW.ifft(Eωk, (3, 4)) # (ω, pol, y, x, z) +Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, y, x, z) +EtrH = Maths.hilbert(Etr) +Iωr = abs2.(Eωr) # (ω, pol, y, x, z) +Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(EtrH) # (t, pol, y, x, z) + +Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, y, x, z) +Ix = dropdims(sum(Irxy; dims=2); dims=2) # (pol, x, z) +Iω = dropdims( + Maths.integrateNd( + y, + Maths.integrateNd(x, Iωr; dim=4); + dim=3), + dims=(3, 4) +)*ωprefac # (ω, pol, z) +Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (y, x, z) + +Itxy = dropdims( + Maths.integrateNd( + y, + Maths.integrateNd(x, Itr; dim=4); + dim=3), + dims=(3, 4) +) # (t, pol, z) + +Eω0 = Eωr[:, :, length(y)÷2+1, length(x)÷2+1, :] +Et0 = FFTW.irfft(Eω0, 2*(length(grid.ω)-1), 1) # (t, pol, z) +Et0 = Maths.hilbert(Et0) +It0 = 0.5*PhysData.c*PhysData.ε_0*abs2.(Et0) + +et, eω = Fields.energyfuncs(grid, xygrid) + +energy_out = dropdims(mapslices(eω, Eωk; dims=(1, 3, 4)); dims=(1, 3, 4)) + +ω = grid.ω + +## +kx = FFTW.fftshift(xygrid.kx) +ky = FFTW.fftshift(xygrid.ky) +fig = plt.figure() +fig.set_size_inches(12, 12) +plt.subplot(2, 2, 1) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, 1])) +plt.clim(-6, 0) +plt.title("Input, X pol") +plt.subplot(2, 2, 2) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, 1])) +plt.clim(-6, 0) +plt.title("Input, Y pol") +plt.subplot(2, 2, 3) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, end])) +plt.clim(-6, 0) +plt.title("Output, X pol") +plt.subplot(2, 2, 4) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, end])) +plt.clim(-6, 0) +plt.title("Output, Y pol") + +## +plt.figure() +plt.pcolormesh(z*1e3, x*1e6, Ir[length(y)÷2+1, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("X (μm)") + +## +plt.figure() +plt.plot(z*1e6, 1e9energy_out[1, :]; label="X polarisation") +plt.plot(z*1e6, 1e9energy_out[2, :]; label="Y polarisation") +plt.xlabel("Distance (μm)") +plt.ylabel("Energy (nJ)") +plt.legend() + +## +fig = plt.figure() +fig.set_size_inches(12, 6) +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, x*1e6, Ix[1, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("X (μm)") +plt.title("X Polarisation") +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, x*1e6, Ix[2, :, :]) +plt.xlabel("Distance (mm)") +plt.ylabel("X (μm)") +plt.title("Y Polarisation") +fig.tight_layout() + + +## +plt.figure() +plt.subplot(1, 2, 1) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 1, :]) +plt.title("X Pol") +plt.xlabel("Distance (mm)") +plt.ylabel("Time (fs)") +plt.subplot(1, 2, 2) +plt.pcolormesh(z*1e3, grid.t*1e15, Itxy[:, 2, :]) +plt.title("Y Pol") +plt.xlabel("Distance (mm)") +plt.suptitle("Time domain") + +## +plt.figure() +plt.subplot(2, 1, 1) +plt.plot(grid.t*1e15, Itxy[:, 1, 1]; label="input X") +plt.plot(grid.t*1e15, Itxy[:, 2, 1]; label="input Y") +plt.legend() +plt.subplot(2, 1, 2) +plt.plot(grid.t*1e15, Itxy[:, 1, end]; label="output X") +plt.plot(grid.t*1e15, Itxy[:, 2, end]; label="output Y") +plt.xlabel("Time (fs)") +plt.legend() + +## +mm = 2π*maximum(Iω)*1e12 +plt.figure() +plt.subplot(2, 1, 1) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iω[:, 1, 1]; label="input X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iω[:, 2, 1]; label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 900) +plt.legend() +plt.subplot(2, 1, 2) +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iω[:, 1, end]; label="output X") +plt.semilogy(ω*1e-12/2π, 2π*1e12*Iω[:, 2, end]; label="output Y") +# plt.semilogy(ω*1e-12/2π, 2π*1e12*Iω[:, 2, 1]; c="C1", linestyle="--", label="input Y") +plt.ylim(1e-5mm, 5mm) +plt.xlim(200, 900) +plt.xlabel("Frequency (THz)") +plt.ylabel("SED (J/Hz)") +plt.legend() + +## +fig = plt.figure() +fig.set_size_inches(12, 7) +plt.subplot(1, 2, 1) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 1, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, :, end]))) +plt.xlim(-50, 50) +plt.ylim(-100, 100) +plt.xlabel("Time (fs)") +plt.ylabel("X (μm)") +plt.title("X polarisation") + +plt.subplot(1, 2, 2) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 2, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, :, end]))) +plt.xlim(-50, 50) +plt.ylim(-100, 100) +plt.xlabel("Time (fs)") +plt.title("Y polarisation") + +## +fig = plt.figure() +fig.set_size_inches(12, 12) +plt.subplot(2, 2, 1) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, 1]) +plt.title("Input, X pol") +plt.subplot(2, 2, 2) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, 1]) +plt.title("Input, Y pol") +plt.subplot(2, 2, 3) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, end]) +plt.title("Output, X pol") +plt.subplot(2, 2, 4) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, end]) +plt.title("Output, Y pol") + +## +fig = plt.figure() +fig.set_size_inches(12, 6) +plt.subplot(1, 2, 1) +plt.plot(grid.t*1e15, 1e-9*(Etr[:, 1, length(xygrid.x)÷2+1, end]); + label="Max: $(maximum(1e-9*(Etr[:, 1, length(xygrid.x)÷2+1, end])))") +plt.ylabel("E (GV/m)") +plt.legend() +plt.xlabel("Time (fs)") +plt.title("X Polarisation") +plt.subplot(1, 2, 2) +plt.plot(grid.t*1e15, 1e-9*(Etr[:, 2, length(xygrid.x)÷2+1, end]); + label="Max: $(maximum(1e-9*(Etr[:, 2, length(xygrid.x)÷2+1, end])))") +plt.legend() +plt.xlabel("Time (fs)") +plt.title("Y Polarisation") \ No newline at end of file diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 94ce5b92..99174ef2 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -37,6 +37,34 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) make_const_linop(grid, xygrid, n, β1) end +function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfunx, nfuny) + # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) + # nfuny(λ; z) just takes wavelength + out = zeros(ComplexF64, (length(grid.ω), 2, length(xygrid.ky), length(xygrid.kx))) + β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) + for (iω, si) in enumerate(grid.sidx) + if si + ny = nfuny(wlfreq(grid.ω[iω])) + ksq_ypol = (ny*grid.ω[iω]/c)^2 + for (ikx, kxi) in enumerate(xygrid.kx) + δθ = crystal_internal_angle(nfunx, grid.ω[iω], kxi) + nx = nfunx(wlfreq(grid.ω[iω]), δθ) + for (iky, kyi) in enumerate(xygrid.ky) + k_xpol = nx*grid.ω[iω]/c + βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 + β_xpol = βsq_xpol < 0 ? -min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) + out[iω, 1, iky, ikx] = -im*(β_xpol - β1*grid.ω[iω]) + + βsq_ypol = ksq_ypol - kxi^2 - kyi^2 + β_ypol = βsq_ypol < 0 ? -min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) + out[iω, 2, iky, ikx] = -im*(β_ypol - β1*grid.ω[iω]) + end + end + end + end + out +end + function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, n::AbstractArray, β1::Number, β0ref::Number; thg=false) kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 @@ -157,7 +185,7 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) end function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nfuny) - # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ) + # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) # nfuny(λ; z) just takes wavelength out = zeros(ComplexF64, (length(grid.ω), 2, length(xgrid.kx))) β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) @@ -170,11 +198,11 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf nx = nfunx(wlfreq(grid.ω[iω]), δθ) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 - β_xpol = βsq_xpol < 0 ? -im*min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) + β_xpol = βsq_xpol < 0 ? -min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) out[iω, 1, ik] = -im*(β_xpol - β1*grid.ω[iω]) βsq_ypol = ksq_ypol - kxi^2 - β_ypol = βsq_ypol < 0 ? -im*min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) + β_ypol = βsq_ypol < 0 ? -min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) out[iω, 2, ik] = -im*(β_ypol - β1*grid.ω[iω]) end end diff --git a/src/Luna.jl b/src/Luna.jl index 33d128b5..9a0c2795 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -256,7 +256,7 @@ function setup(grid::Grid.EnvGrid, q::Hankel.QDHT, oshape = shape[2:end] xo = Array{ComplexF64}(undef, length(grid.to), oshape...) FTo = FFTW.plan_fft(xo, 1, flags=settings["fftw_flag"]) - transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun, true) + transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun, np > 1) inv(FT) # create inverse FT plans now, so wisdom is saved inv(FTo) Utils.saveFFTwisdom() @@ -266,16 +266,17 @@ end function setup(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, densityfun, normfun, responses, inputs) Utils.loadFFTwisdom() + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) x = xygrid.x y = xygrid.y - xr = Array{Float64}(undef, length(grid.t), length(y), length(x)) + xr = Array{Float64}(undef, length(grid.t), np, length(y), length(x)) FT = FFTW.plan_rfft(xr, (1, 3, 4), flags=settings["fftw_flag"]) - Eωk = zeros(ComplexF64, length(grid.ω), length(y), length(x)) + Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) doinputs_fs!(Eωk, grid, xygrid, FT, inputs) - xo = Array{Float64}(undef, length(grid.to), length(y), length(x)) + xo = Array{Float64}(undef, length(grid.to), np, length(y), length(x)) FTo = FFTW.plan_rfft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, - responses, densityfun, normfun) + responses, densityfun, normfun, np > 1) inv(FT) # create inverse FT plans now, so wisdom is saved inv(FTo) Utils.saveFFTwisdom() @@ -285,16 +286,17 @@ end function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, densityfun, normfun, responses, inputs) Utils.loadFFTwisdom() + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) x = xygrid.x y = xygrid.y - xr = Array{ComplexF64}(undef, length(grid.t), length(y), length(x)) + xr = Array{ComplexF64}(undef, length(grid.t), np, length(y), length(x)) FT = FFTW.plan_fft(xr, (1, 3, 4), flags=settings["fftw_flag"]) - Eωk = zeros(ComplexF64, length(grid.ω), length(y), length(x)) + Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) doinputs_fs!(Eωk, grid, xygrid, FT, inputs) - xo = Array{ComplexF64}(undef, length(grid.to), length(y), length(x)) + xo = Array{ComplexF64}(undef, length(grid.to), np, length(y), length(x)) FTo = FFTW.plan_fft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, - responses, densityfun, normfun) + responses, densityfun, normfun, np > 1) inv(FT) # create inverse FT plans now, so wisdom is saved inv(FTo) Utils.saveFFTwisdom() diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 400cb00a..783cc2e9 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -618,6 +618,17 @@ function const_norm_free(grid, xygrid, nfun) return norm end +function const_norm_free(grid, xygrid, nfunx, nfuny) + nfunωx = (ω, δθ) -> nfunx(wlfreq(ω), δθ) + nfunωy = (ω) -> nfuny(wlfreq(ω)) + normfun = norm_free(grid, xygrid, nfunωx, nfunωy) + out = copy(normfun(0.0)) + function norm(z) + return out + end + return norm +end + """ norm_free(grid, xygrid, nfun) @@ -663,15 +674,15 @@ function norm_free(grid, xygrid, nfunx, nfuny) function norm(z) for iω in eachindex(ω) if ω[iω] == 0 || ~grid.sidx[iω] - out[iω, :, :] .= 1.0 + out[iω, :, :, :] .= 1.0 continue end ny = nfuny(wlfreq(ω[iω])) ksq_ypol = (ny*ω[iω]/c)^2 for (ikx, kxi) in enumerate(xygrid.kx) + δθ = crystal_internal_angle(nfunx, ω[iω], kxi) + nx = nfunx(wlfreq(ω[iω]), δθ) for (iky, kyi) in enumerate(xygrid.ky) - δθ = crystal_internal_angle(nfunx, ω[iω], kxi) - nx = nfunx(wlfreq(ω[iω]), δθ) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 if βsq_xpol < 0 From 8ee4690df775ebc9f939baa4abd214d8792fa4f9 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 3 Sep 2025 09:02:25 +0100 Subject: [PATCH 33/72] remove extra z kwargs --- src/LinearOps.jl | 8 ++++---- src/NonlinearRHS.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 99174ef2..d58505ff 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -26,7 +26,7 @@ end function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(wlfreq(ωfirst); z=0)) # 1 if single ref index, 2 if nx, ny + np = length(nfun(wlfreq(ωfirst))) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si @@ -173,7 +173,7 @@ end function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(wlfreq(ωfirst); z=0)) # 1 if single ref index, 2 if nx, ny + np = length(nfun(wlfreq(ωfirst))) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si @@ -338,7 +338,7 @@ end function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + np = length(nfun(ωfirst)) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si @@ -351,7 +351,7 @@ end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + np = length(nfun(ωfirst)) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) for (ii, si) in enumerate(grid.sidx) if si diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 783cc2e9..40023006 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -506,7 +506,7 @@ function norm_radial(grid, q, nfun) continue end for (ip, n) in enumerate(nfun(ω[iω]; z)) - k2 = (n*ω[iω]/PhysData.c)^2 + k2 = (real(n)*ω[iω]/PhysData.c)^2 βsq = k2 - kr2[ir] if βsq <= 0 out[iω, ip, ir] = 1.0 @@ -652,7 +652,7 @@ function norm_free(grid, xygrid, nfun) continue end for (ip, n) in enumerate(nfun(ω[iω]; z)) - k2 = (n*ω[iω]/PhysData.c)^2 + k2 = (real(n)*ω[iω]/PhysData.c)^2 βsq = k2 - kperp2[ii] if βsq <= 0 out[iω, ip, ii] = 1.0 From 5ffcfb9e2b09dad973a9af305b9bf5ceba5ce8bc Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 2 Oct 2025 19:10:19 +0100 Subject: [PATCH 34/72] fix polarisation ellipse --- src/Polarisation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Polarisation.jl b/src/Polarisation.jl index 3fa9738b..76be9fc0 100644 --- a/src/Polarisation.jl +++ b/src/Polarisation.jl @@ -59,7 +59,7 @@ function ellipse(S) U = S[3] V = S[4] aL = sqrt(Q^2 + U^2) - θ = angle(aL)/2 + θ = angle(Q + 1im*U)/2 A = sqrt((I + aL)/2) B = sqrt((abs(I - aL))/2) h = sign(V) From 8ef23c4852639e118f9ac70e2e8164afcaf86795 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 12 Dec 2025 14:51:19 +0000 Subject: [PATCH 35/72] fix field creation when only one polarisation is used --- src/Fields.jl | 2 +- src/Luna.jl | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Fields.jl b/src/Fields.jl index 3bc49092..000f0ea5 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -378,7 +378,7 @@ function make_Etr(s::SpatioTemporalField, grid::Grid.EnvGrid, spacegrid) sqrt.(s.Ishape(t, spacegrid.r)) .* exp.(im .* (s.ϕ .+ Δω.*t)) end -transform(spacegrid::Hankel.QDHT, FT, Etr) = spacegrid * (FT * Etr) +transform(q::Hankel.QDHT, FT, Etr) = q * (FT * Etr) transform(spacegrid::Grid.FreeGrid, FT, Etr) = FT * Etr transform(spacegrid::Grid.Free2DGrid, FT, Etr) = FT * Etr diff --git a/src/Luna.jl b/src/Luna.jl index d3893462..3ad11402 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -231,7 +231,8 @@ function doinputs_fs!(Eωk, grid, spacegrid::Union{Hankel.QDHT, Grid.FreeGrid, G if size(Eωk, 2) == 2 Eωk .+= Eωki else - Eωk .+= Eωki[:, 2] # take y-polarisation if only 1 polarisation specified + # take y-polarisation if only 1 polarisation specified + Eωk .+= Eωki[:, [2], :, :] # use array index [2] to preserve dimensionality end end end @@ -253,7 +254,11 @@ function setup(grid::Grid.RealGrid, q::Hankel.QDHT, FT = FFTW.plan_rfft(xt, 1, flags=settings["fftw_flag"]) Eω = zeros(ComplexF64, ωshape) Eωk = q * Eω - doinputs_fs!(Eωk, grid, q, FT, inputs) + # plan FFT for xy polarisation for field creation + tshape_xy = (length(grid.t), 2, length(q.r)) + xt_xy = zeros(Float64, tshape_xy) + FT_xy = FFTW.plan_rfft(xt_xy, 1, flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, q, FT_xy, inputs) oshape = tshape[2:end] xo = Array{Float64}(undef, length(grid.to), oshape...) FTo = FFTW.plan_rfft(xo, 1, flags=settings["fftw_flag"]) @@ -271,13 +276,19 @@ function setup(grid::Grid.EnvGrid, q::Hankel.QDHT, Logging.@info("Setting up and planning FFTs...") flush(stderr) Utils.loadFFTwisdom() - shape = size(normfun(0)) - xt = zeros(ComplexF64, shape) + np = size(normfun(0), 2) # number of polarisation directions (1 or 2) + tshape = (length(grid.t), np, length(q.r)) + ωshape = (length(grid.ω), np, length(q.r)) + xt = zeros(Float64, tshape) FT = FFTW.plan_fft(xt, 1, flags=settings["fftw_flag"]) - Eω = zeros(ComplexF64, shape) + Eω = zeros(ComplexF64, ωshape) Eωk = q * Eω - doinputs_fs!(Eωk, grid, q, FT, inputs) - oshape = shape[2:end] + # plan FFT for xy polarisation for field creation + tshape_xy = (length(grid.t), 2, length(q.r)) + xt_xy = zeros(Float64, tshape_xy) + FT_xy = FFTW.plan_fft(xt_xy, 1, flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, q, FT_xy, inputs) + oshape = tshape[2:end] xo = Array{ComplexF64}(undef, length(grid.to), oshape...) FTo = FFTW.plan_fft(xo, 1, flags=settings["fftw_flag"]) transform = NonlinearRHS.TransRadial(grid, q, FTo, responses, densityfun, normfun, np > 1) From 9056967bc994aab69839a026c9e0bed5e574d3e5 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 12 Dec 2025 14:51:24 +0000 Subject: [PATCH 36/72] fix simple radial examples --- .../low_level_interface/freespace/radial.jl | 36 +++++++++--------- .../freespace/radial_env.jl | 38 +++++++++---------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/examples/low_level_interface/freespace/radial.jl b/examples/low_level_interface/freespace/radial.jl index 003f9d59..a737e053 100644 --- a/examples/low_level_interface/freespace/radial.jl +++ b/examples/low_level_interface/freespace/radial.jl @@ -18,7 +18,7 @@ R = 4e-3 N = 1024 grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, N, dim=2) +q = Hankel.QDHT(R, N, dim=3) energyfun, energyfun_ω = Fields.energyfuncs(grid, q) @@ -41,35 +41,33 @@ inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0, propz= Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) # statsfun = Stats.collect_stats(grid, Eω, Stats.ω0(grid)) -output = Output.MemoryOutput(0, grid.zmax, 201) +output = Output.MemoryOutput(0, grid.zmax, 51) Luna.run(Eω, grid, linop, transform, FT, output) - +## ω = grid.ω t = grid.t zout = output.data["z"] -Eout = output.data["Eω"] +Eout = output.data["Eω"] # dimensions: (Nt, Npol, Nr, Nz) Erout = (q \ Eout) Iωr = abs2.(Erout) # Iω0 = Iωr[:, 1, :] -Er0 = dropdims(Hankel.onaxis(Eout, q), dims=2); -Iω0 = abs2.(Er0); -Iω0log = log10.(Maths.normbymax(Iω0)); +Er0 = dropdims(Hankel.onaxis(Eout, q), dims=(2, 3)) +Iω0 = abs2.(Er0) +Iω0log = log10.(Maths.normbymax(Iω0)) Etout = FFTW.irfft(Erout, length(grid.t), 1) -Ilog = log10.(Maths.normbymax(abs2.(Eout))) - -It = PhysData.c * PhysData.ε_0/2 * abs2.(Maths.hilbert(Etout)); +It = PhysData.c * PhysData.ε_0/2 * abs2.(Maths.hilbert(Etout)) Itlog = log10.(Maths.normbymax(It)) Ir = zeros(Float64, (length(q.r), length(zout))) Et = Maths.hilbert(Etout) energy = zeros(length(zout)) -for ii = 1:size(Etout, 3) - energy[ii] = energyfun(Etout[:, :, ii]); - Ir[:, ii] = integrate(grid.ω, Iωr[:, :, ii], SimpsonEven()); +for ii = 1:size(Etout, 4) + energy[ii] = energyfun(Etout[:, 1, :, ii]); + Ir[:, ii] = integrate(grid.ω, Iωr[:, 1, :, ii], SimpsonEven()); end ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) @@ -78,12 +76,12 @@ zr = π*w0^2/λ0 points = L/2 .+ [-15, 3, 21].*zr idcs = [argmin(abs.(zout .- point)) for point in points] -Epoints = [Hankel.symmetric(Et[:, :, idxi], q) for idxi in idcs] +Epoints = [Hankel.symmetric(Et[:, :, :, idxi], q) for idxi in idcs] rsym = Hankel.Rsymmetric(q); - +## import PyPlot:pygui, plt pygui(true) -Iλ0 = Iωr[ω0idx, :, :] +Iλ0 = Iωr[ω0idx, 1, :, :] λ0 = 2π*PhysData.c/grid.ω[ω0idx] w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) # w1 = w0 @@ -93,7 +91,7 @@ plt.plot(q.r*1e3, Maths.normbymax(Iλ0[:, end])) plt.plot(q.r*1e3, Maths.normbymax(Iλ0_analytic), "--") plt.figure() -plt.pcolormesh(zout*1e2, q.r*1e3, Iωr[ω0idx, :, :]) +plt.pcolormesh(zout*1e2, q.r*1e3, Iωr[ω0idx, 1, :, :]) plt.colorbar() plt.ylim(0, 4) plt.xlabel("z (m)") @@ -101,7 +99,7 @@ plt.ylabel("r (m)") plt.title("I(r, ω=ω0)") plt.figure() -plt.pcolormesh(zout*1e2, q.r*1e3, It[length(grid.t)÷2, :, :]) +plt.pcolormesh(zout*1e2, q.r*1e3, It[length(grid.t)÷2, 1, :, :]) plt.colorbar() plt.ylim(0, 4) plt.xlabel("z (m)") @@ -136,7 +134,7 @@ fig = plt.figure() fig.set_size_inches(12, 4) for ii in 1:3 plt.subplot(1, 3, ii) - plt.pcolormesh(grid.t*1e15, rsym*1e3, abs2.(Epoints[ii]'), cmap=jw) + plt.pcolormesh(grid.t*1e15, rsym*1e3, abs2.(Epoints[ii][:, 1, :]'), cmap=jw) plt.xlim(-42, 42) plt.ylim(-1.8, 1.8) end diff --git a/examples/low_level_interface/freespace/radial_env.jl b/examples/low_level_interface/freespace/radial_env.jl index 5cbe74e8..85c9e1cb 100644 --- a/examples/low_level_interface/freespace/radial_env.jl +++ b/examples/low_level_interface/freespace/radial_env.jl @@ -1,4 +1,5 @@ using Luna +import Luna.PhysData: wlfreq import FFTW import Luna: Hankel import NumericalIntegration: integrate, SimpsonEven @@ -14,10 +15,10 @@ energy = 2e-9 L = 0.6 R = 4e-3 -N = 512 +N = 1024 grid = Grid.EnvGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, N, dim=2) +q = Hankel.QDHT(R, N, dim=3) energyfun, energyfun_ω = Fields.energyfuncs(grid, q) @@ -45,26 +46,23 @@ t = grid.t zout = output.data["z"] Eout = output.data["Eω"]; -Eout = FFTW.fftshift(Eout, 1); - +Eout = FFTW.fftshift(Eout, 1) +## Erout = q \ Eout Iωr = abs2.(Erout) -Er0 = dropdims(Hankel.onaxis(Eout, q), dims=2); -Iω0 = abs2.(Er0); -Iω0log = log10.(Maths.normbymax(Iω0)); +Er0 = dropdims(Hankel.onaxis(Eout, q), dims=(2, 3)) +Iω0 = abs2.(Er0) +Iω0log = log10.(Maths.normbymax(Iω0)) Etout = FFTW.ifft(FFTW.fftshift(Erout, 1), 1) -Ilog = log10.(Maths.normbymax(abs2.(Eout))) - -It = PhysData.c * PhysData.ε_0/2 * abs2.(Etout); +It = PhysData.c * PhysData.ε_0/2 * abs2.(Etout) Itlog = log10.(Maths.normbymax(It)) Ir = zeros(Float64, (length(q.r), length(zout))) - energy = zeros(length(zout)) -for ii = 1:size(Etout, 3) - energy[ii] = energyfun(Etout[:, :, ii]); - Ir[:, ii] = integrate(ω, Iωr[:, :, ii], SimpsonEven()); +for ii = 1:size(Etout, 4) + energy[ii] = energyfun(Etout[:, 1, :, ii]); + Ir[:, ii] = integrate(ω, Iωr[:, 1, :, ii], SimpsonEven()); end ω0idx = argmin(abs.(ω .- 2π*PhysData.c/λ0)) @@ -73,12 +71,12 @@ zr = π*w0^2/λ0 points = L/2 .+ [-15, 3, 21].*zr idcs = [argmin(abs.(zout .- point)) for point in points] -Epoints = [Hankel.symmetric(Etout[:, :, idxi], q) for idxi in idcs] +Epoints = [Hankel.symmetric(Etout[:, [1], :, idxi], q) for idxi in idcs] rsym = Hankel.Rsymmetric(q); - +## import PyPlot:pygui, plt pygui(true) -Iλ0 = Iωr[ω0idx, :, :] +Iλ0 = Iωr[ω0idx, 1, :, :] w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) # w1 = w0 Iλ0_analytic = Maths.gauss.(q.r, w1/2)*(w0/w1)^2 # analytical solution (in paraxial approx) @@ -87,7 +85,7 @@ plt.plot(q.r*1e3, Maths.normbymax(Iλ0[:, end])) plt.plot(q.r*1e3, Maths.normbymax(Iλ0_analytic), "--") plt.figure() -plt.pcolormesh(zout*1e2, q.r*1e3, Iωr[ω0idx, :, :]) +plt.pcolormesh(zout*1e2, q.r*1e3, Iωr[ω0idx, 1, :, :]) plt.colorbar() plt.ylim(0, 4) plt.xlabel("z (m)") @@ -95,7 +93,7 @@ plt.ylabel("r (m)") plt.title("I(r, ω=ω0)") plt.figure() -plt.pcolormesh(zout*1e2, q.r*1e3, It[length(grid.t)÷2, :, :]) +plt.pcolormesh(zout*1e2, q.r*1e3, It[length(grid.t)÷2, 1, :, :]) plt.colorbar() plt.ylim(0, 4) plt.xlabel("z (m)") @@ -130,7 +128,7 @@ fig = plt.figure() fig.set_size_inches(12, 4) for ii in 1:3 plt.subplot(1, 3, ii) - plt.pcolormesh(grid.t*1e15, rsym*1e3, abs2.(Epoints[ii]'), cmap=jw) + plt.pcolormesh(grid.t*1e15, rsym*1e3, abs2.(Epoints[ii][:, 1, :]'), cmap=jw) plt.xlim(-42, 42) plt.ylim(-1.8, 1.8) end From 2d43738cec2b83f63827003513cbb896dbc2f9a1 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 12 Dec 2025 15:54:42 +0000 Subject: [PATCH 37/72] fix linop for full 3D with one polarisation --- .../freespace/free3D_bbo.jl | 18 ++++---- src/LinearOps.jl | 45 ++++++++++--------- src/Luna.jl | 4 +- src/NonlinearRHS.jl | 2 +- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/examples/low_level_interface/freespace/free3D_bbo.jl b/examples/low_level_interface/freespace/free3D_bbo.jl index d93c93a6..947cc57c 100644 --- a/examples/low_level_interface/freespace/free3D_bbo.jl +++ b/examples/low_level_interface/freespace/free3D_bbo.jl @@ -193,8 +193,8 @@ plt.legend() fig = plt.figure() fig.set_size_inches(12, 7) plt.subplot(1, 2, 1) -plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 1, :, end])'; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, :, end]))) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 1, N÷2, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, N÷2, :, end]))) plt.xlim(-50, 50) plt.ylim(-100, 100) plt.xlabel("Time (fs)") @@ -202,8 +202,8 @@ plt.ylabel("X (μm)") plt.title("X polarisation") plt.subplot(1, 2, 2) -plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 2, :, end])'; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, :, end]))) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 2, 32, :, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, N÷2, :, end]))) plt.xlim(-50, 50) plt.ylim(-100, 100) plt.xlabel("Time (fs)") @@ -229,15 +229,17 @@ plt.title("Output, Y pol") fig = plt.figure() fig.set_size_inches(12, 6) plt.subplot(1, 2, 1) -plt.plot(grid.t*1e15, 1e-9*(Etr[:, 1, length(xygrid.x)÷2+1, end]); - label="Max: $(maximum(1e-9*(Etr[:, 1, length(xygrid.x)÷2+1, end])))") +Etx_00 = Etr[:, 1, length(xygrid.y)÷2+1, length(xygrid.x)÷2+1, end] +plt.plot(grid.t*1e15, 1e-9*(Etx_00); + label="Max: $(maximum(1e-9*Etx_00))") plt.ylabel("E (GV/m)") plt.legend() plt.xlabel("Time (fs)") plt.title("X Polarisation") plt.subplot(1, 2, 2) -plt.plot(grid.t*1e15, 1e-9*(Etr[:, 2, length(xygrid.x)÷2+1, end]); - label="Max: $(maximum(1e-9*(Etr[:, 2, length(xygrid.x)÷2+1, end])))") +Ety_00 = Etr[:, 2, length(xygrid.y)÷2+1, length(xygrid.x)÷2+1, end] +plt.plot(grid.t*1e15, 1e-9*(Ety_00); + label="Max: $(maximum(Ety_00))") plt.legend() plt.xlabel("Time (fs)") plt.title("Y Polarisation") \ No newline at end of file diff --git a/src/LinearOps.jl b/src/LinearOps.jl index d58505ff..06d7525a 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -14,12 +14,11 @@ Make constant linear operator for full 3D propagation. `n` is the refractive ind and β1 is 1/velocity of the reference frame. """ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, - n::AbstractArray, β1::Number) + n::AbstractVecOrMat, β1::Number) kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) - k2 = zero(grid.ω) - k2[grid.sidx] .= (n[grid.sidx] .* grid.ω[grid.sidx] ./ c).^2 - out = zeros(ComplexF64, (length(grid.ω), length(xygrid.ky), length(xygrid.kx))) + k2 = @. (n*grid.ω/c)^2 + out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs) return out end @@ -110,13 +109,15 @@ end # Internal routine -- function barrier aids with JIT compilation function _fill_linop_xy!(out, grid::Grid.RealGrid, β1::Float64, k2, kperp2, idcs) for ii in idcs - for iω in eachindex(grid.ω) - βsq = k2[iω] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + for ip in axes(k2, 2) + for iω in eachindex(grid.ω) + βsq = k2[iω, ip] - kperp2[ii] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end end end end @@ -137,16 +138,18 @@ end function _fill_linop_xy!(out, grid::Grid.EnvGrid, β1::Float64, k2, kperp2, idcs, βref; thg) for ii in idcs - for iω in eachindex(grid.ω) - βsq = k2[iω] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - if !thg - out[iω, ii] -= -im*βref + for ip in axes(k2, 2) + for iω in eachindex(grid.ω) + βsq = k2[iω, ip] - kperp2[ii] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) + end + if !thg + out[iω, ip, ii] -= -im*βref + end end end end diff --git a/src/Luna.jl b/src/Luna.jl index 3ad11402..8509eba7 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -311,7 +311,9 @@ function setup(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, xr = Array{Float64}(undef, length(grid.t), np, length(y), length(x)) FT = FFTW.plan_rfft(xr, (1, 3, 4), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) - doinputs_fs!(Eωk, grid, xygrid, FT, inputs) + xr_xy = Array{Float64}(undef, length(grid.t), 2, length(y), length(x)) + FT_xy = FFTW.plan_rfft(xr_xy, (1, 3, 4), flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, xygrid, FT_xy, inputs) xo = Array{Float64}(undef, length(grid.to), np, length(y), length(x)) FTo = FFTW.plan_rfft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 15ce9c01..e0988fde 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -654,7 +654,7 @@ function norm_free(grid, xygrid, nfun) for ii in idcs for iω in eachindex(ω) if ω[iω] == 0 || ~grid.sidx[iω] - out[iω, :, ii] = 1.0 + out[iω, :, ii] .= 1.0 continue end for (ip, n) in enumerate(nfun(ω[iω]; z)) From 26b438b4637c7bf59a2af98f0f374ca8813a9171 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 12 Dec 2025 20:26:12 +0000 Subject: [PATCH 38/72] fix full 3D setup and examples --- .../low_level_interface/freespace/full3D.jl | 18 +++++++++--------- .../freespace/full3D_env.jl | 18 +++++++++--------- src/LinearOps.jl | 7 +++---- src/Luna.jl | 4 +++- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/examples/low_level_interface/freespace/full3D.jl b/examples/low_level_interface/freespace/full3D.jl index 6bb58bc0..8824e2a0 100644 --- a/examples/low_level_interface/freespace/full3D.jl +++ b/examples/low_level_interface/freespace/full3D.jl @@ -52,8 +52,8 @@ zout = output.data["z"] Eout = output.data["Eω"] println("Transforming...") -Eωyx = FFTW.ifft(Eout, (2, 3)) -Etyx = FFTW.irfft(Eout, length(grid.t), (1, 2, 3)) +Eωyx = FFTW.ifft(Eout, (3, 4)) +Etyx = FFTW.irfft(Eout, length(grid.t), (1, 3, 4)) println("...done") Ilog = log10.(Maths.normbymax(abs2.(Eωyx))) @@ -62,18 +62,18 @@ Iωyx = abs2.(Eωyx); Iyx = zeros(Float64, (length(y), length(x), length(zout))); energy = zeros(length(zout)); -for ii = 1:size(Etyx, 4) - energy[ii] = energyfun(Etyx[:, :, :, ii]); - Iyx[:, :, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωyx[:, :, :, ii], dims=1); +for ii = 1:size(Etyx, 5) + energy[ii] = energyfun(Etyx[:, 1, :, :, ii]); + Iyx[:, :, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωyx[:, 1, :, :, ii], dims=1); end ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) -E0ωyx = FFTW.ifft(Eω[ω0idx, :, :], (1, 2)); +E0ωyx = FFTW.ifft(Eω[ω0idx, 1, :, :], (1, 2)) Iωyx = abs2.(Eωyx) Iωyxlog = log10.(Maths.normbymax(Iωyx)); - +## import PyPlot:pygui, plt pygui(true) plt.figure() @@ -84,14 +84,14 @@ plt.ylabel("Y") plt.title("I(ω=ω0, x, y, z=0)") plt.figure() -plt.pcolormesh(x, y, (abs2.(Eωyx[ω0idx, :, :, end]))) +plt.pcolormesh(x, y, (abs2.(Eωyx[ω0idx, 1, :, :, end]))) plt.colorbar() plt.xlabel("X") plt.ylabel("Y") plt.title("I(ω=ω0, x, y, z=L)") plt.figure() -plt.pcolormesh(zout, ω.*1e-15/2π, Iωyxlog[:, N÷2+1, N÷2+1, :]) +plt.pcolormesh(zout, ω.*1e-15/2π, Iωyxlog[:, 1, N÷2+1, N÷2+1, :]) plt.xlabel("Z (m)") plt.ylabel("f (PHz)") plt.title("I(ω, x=0, y=0, z)") diff --git a/examples/low_level_interface/freespace/full3D_env.jl b/examples/low_level_interface/freespace/full3D_env.jl index acffcc0c..b312c5ab 100644 --- a/examples/low_level_interface/freespace/full3D_env.jl +++ b/examples/low_level_interface/freespace/full3D_env.jl @@ -48,11 +48,11 @@ zout = output.data["z"] Eout = output.data["Eω"] println("Transforming...") -Eωyx = FFTW.fftshift(FFTW.ifft(Eout, (2, 3)), 1); -Etyx = FFTW.ifft(Eout, (1, 2, 3)) +Eωyx = FFTW.fftshift(FFTW.ifft(Eout, (3, 4)), 1); +Etyx = FFTW.ifft(Eout, (1, 3, 4)) println("...done") -Eout = FFTW.fftshift(Eout, (2, 3)) +Eout = FFTW.fftshift(Eout, (3, 4)) Ilog = log10.(Maths.normbymax(abs2.(Eωyx))) @@ -60,14 +60,14 @@ Iωyx = abs2.(Eωyx); Iyx = zeros(Float64, (length(y), length(x), length(zout))); energy = zeros(length(zout)); -for ii = 1:size(Etyx, 4) - energy[ii] = energyfun(Etyx[:, :, :, ii]); - Iyx[:, :, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωyx[:, :, :, ii], dims=1); +for ii = 1:size(Etyx, 5) + energy[ii] = energyfun(Etyx[:, 1, :, :, ii]); + Iyx[:, :, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωyx[:, 1, :, :, ii], dims=1); end ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) -E0ωyx = FFTW.ifft(Eω[ω0idx, :, :], (1, 2)); +E0ωyx = FFTW.ifft(Eω[ω0idx, 1, :, :], (1, 2)); Iωyx = abs2.(Eωyx); Iωyxlog = log10.(Maths.normbymax(Iωyx)); @@ -82,14 +82,14 @@ plt.ylabel("Y") plt.title("I(ω=ω0, x, y, z=0)") plt.figure() -plt.pcolormesh(x, y, (abs2.(Eωyx[ω0idx, :, :, end]))) +plt.pcolormesh(x, y, (abs2.(Eωyx[ω0idx, 1, :, :, end]))) plt.colorbar() plt.xlabel("X") plt.ylabel("Y") plt.title("I(ω=ω0, x, y, z=L)") plt.figure() -plt.pcolormesh(zout, ω.*1e-15/2π, Iωyxlog[:, N÷2+1, N÷2+1, :]) +plt.pcolormesh(zout, ω.*1e-15/2π, Iωyxlog[:, 1, N÷2+1, N÷2+1, :]) plt.xlabel("Z (m)") plt.ylabel("f (PHz)") plt.title("I(ω, x=0, y=0, z)") diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 06d7525a..bb158aef 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -65,12 +65,11 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfunx, nfu end function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, - n::AbstractArray, β1::Number, β0ref::Number; thg=false) + n::AbstractVecOrMat, β1::Number, β0ref::Number; thg=false) kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) - k2 = zero(grid.ω) - k2[grid.sidx] .= (n[grid.sidx].*grid.ω[grid.sidx]./c).^2 - out = zeros(ComplexF64, (length(grid.ω), length(xygrid.ky), length(xygrid.kx))) + k2 = @. (n*grid.ω/c)^2 + out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) return out end diff --git a/src/Luna.jl b/src/Luna.jl index 8509eba7..4907af4b 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -337,7 +337,9 @@ function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, xr = Array{ComplexF64}(undef, length(grid.t), np, length(y), length(x)) FT = FFTW.plan_fft(xr, (1, 3, 4), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) - doinputs_fs!(Eωk, grid, xygrid, FT, inputs) + xr_xy = Array{Float64}(undef, length(grid.t), 2, length(y), length(x)) + FT_xy = FFTW.plan_fft(xr_xy, (1, 3, 4), flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, xygrid, FT_xy, inputs) xo = Array{ComplexF64}(undef, length(grid.to), np, length(y), length(x)) FTo = FFTW.plan_fft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, From c5e1a450fde76623fa3aac0b04ad3356adbb11d0 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 18 Dec 2025 15:51:57 +0000 Subject: [PATCH 39/72] simplify linear ops --- src/LinearOps.jl | 153 ++++++++++------------------------------------- 1 file changed, 31 insertions(+), 122 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index bb158aef..3257730d 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -4,6 +4,22 @@ import Hankel import Luna: Modes, Grid, PhysData, Maths import Luna.PhysData: wlfreq, c, crystal_internal_angle +function fill_linop_matrix!(out, grid, β1::Number, βref::Number, k2, kperp2, idcs) + for ii in idcs + for ip in axes(k2, 2) + for iω in eachindex(grid.ω) + βsq = k2[iω, ip] - kperp2[ii] + if βsq < 0 + # negative βsq -> evanescent fields -> attenuation + out[iω, ip, ii] = -im*(-β1*grid.ω[iω] - βref) - min(sqrt(abs(βsq)), 200) + else + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω] - βref) + end + end + end + end +end + #=================================================# #=============== FREE SPACE ===============# #=================================================# @@ -19,7 +35,7 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) - _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs) + fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) return out end @@ -70,7 +86,7 @@ function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) - _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) + fill_linop_matrix!(out, grid, β1, β0ref, k2, kperp2, idcs) return out end @@ -101,24 +117,7 @@ function make_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx] ./ c).^2 - _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs) - end -end - -# Internal routine -- function barrier aids with JIT compilation -function _fill_linop_xy!(out, grid::Grid.RealGrid, β1::Float64, k2, kperp2, idcs) - for ii in idcs - for ip in axes(k2, 2) - for iω in eachindex(grid.ω) - βsq = k2[iω, ip] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - end - end + fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) end end @@ -131,26 +130,7 @@ function make_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun; thg=false) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./c).^2 βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z) - _fill_linop_xy!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) - end -end - -function _fill_linop_xy!(out, grid::Grid.EnvGrid, β1::Float64, k2, kperp2, idcs, βref; thg) - for ii in idcs - for ip in axes(k2, 2) - for iω in eachindex(grid.ω) - βsq = k2[iω, ip] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - if !thg - out[iω, ip, ii] -= -im*βref - end - end - end + fill_linop_matrix!(out, grid, β1, βref, k2, kperp2, idcs) end end @@ -169,7 +149,7 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, idcs = CartesianIndices(xgrid.kx) k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) - _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) + fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) return out end @@ -213,12 +193,12 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf end function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, - n::AbstractArray, β1::Number, β0ref::Number; thg=false) + n::AbstractArray, β1::Number, β0ref::Number) kperp2 = xgrid.kx.^2 idcs = CartesianIndices(xgrid.kx) k2 = @. (n*grid.ω/c)^2 out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) - _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, β0ref; thg=thg) + fill_linop_matrix!(out, grid, β1, β0ref, k2, kperp2, idcs) return out end @@ -261,24 +241,7 @@ function make_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii]./c).^2 end end - _fill_linop_x!(out, grid, β1, k2, kperp2, idcs) - end -end - -# Internal routine -- function barrier aids with JIT compilation -function _fill_linop_x!(out, grid::Grid.RealGrid, β1::Float64, k2, kperp2, idcs) - for ii in idcs - for ip in axes(k2, 2) - for iω in eachindex(grid.ω) - βsq = k2[iω, ip] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - end - end + fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) end end @@ -297,26 +260,7 @@ function make_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun; thg=false) end end βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] - _fill_linop_x!(out, grid, β1, k2, kperp2, idcs, βref; thg=thg) - end -end - -function _fill_linop_x!(out, grid::Grid.EnvGrid, β1::Float64, k2, kperp2, idcs, βref; thg) - for ii in idcs - for ip in axes(k2, 2) - for iω in eachindex(grid.ω) - βsq = k2[iω, ip] - kperp2[ii] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ii] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - if !thg - out[iω, ip, ii] -= -im*βref - end - end - end + fill_linop_matrix!(out, grid, β1, βref, k2, kperp2, idcs) end end @@ -334,7 +278,7 @@ function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/c)^2 kr2 = q.k.^2 - _fill_linop_r!(out, grid, β1, k2, kr2, q.N) + fill_linop_matrix!(out, grid, β1, 0.0, k2, kr2, eachindex(q.k)) return out end @@ -366,15 +310,15 @@ function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) else β0const = grid.ω0/c * nfun(2π*c./grid.ω0)[1] end - make_const_linop(grid, q, n, β1, β0const; thg=thg) + make_const_linop(grid, q, n, β1, β0const) end function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, - n::AbstractVecOrMat, β1::Number, β0ref::Number; thg=false) + n::AbstractVecOrMat, β1::Number, β0ref::Number) out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) k2 = @. (n*grid.ω/c)^2 kr2 = q.k.^2 - _fill_linop_r!(out, grid, β1, k2, kr2, q.N, β0ref, thg) + fill_linop_matrix!(out, grid, β1, β0ref, k2, kr2, eachindex(q.k)) return out end @@ -394,23 +338,7 @@ function make_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 - _fill_linop_r!(out, grid, β1, k2, kr2, q.N) - end -end - -function _fill_linop_r!(out, grid::Grid.RealGrid, β1, k2, kr2, Nr) - for ir = 1:Nr - for ip in axes(k2, 2) - for iω = 1:length(grid.ω) - βsq = k2[iω, ip] - kr2[ir] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - end - end + fill_linop_matrix!(out, grid, β1, 0.0, k2, kr2, eachindex(q.k)) end end @@ -424,26 +352,7 @@ function make_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] - _fill_linop_r!(out, grid, β1, k2, kr2, q.N, βref, thg) - end -end - -function _fill_linop_r!(out, grid::Grid.EnvGrid, β1, k2, kr2, Nr, βref, thg) - for ir = 1:Nr - for ip in axes(k2, 2) - for iω = 1:length(grid.ω) - βsq = k2[iω, ip] - kr2[ir] - if βsq < 0 - # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ir] = -im*(-β1*grid.ω[iω]) - min(sqrt(abs(βsq)), 200) - else - out[iω, ip, ir] = -im*(sqrt(βsq) - β1*grid.ω[iω]) - end - if !thg - out[iω, ip, ir] -= -im*βref - end - end - end + fill_linop_matrix!(out, grid, β1, βref, k2, kr2, eachindex(q.k)) end end From 7d8233a6ca407a77802b33b6ca6ff3aa705d929c Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 19 Dec 2025 11:40:50 +0000 Subject: [PATCH 40/72] further simplification of LinearOps --- src/LinearOps.jl | 192 +++++++++++++++----------------------------- src/Luna.jl | 10 ++- src/NonlinearRHS.jl | 2 +- 3 files changed, 71 insertions(+), 133 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 3257730d..eb5d21da 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -4,16 +4,20 @@ import Hankel import Luna: Modes, Grid, PhysData, Maths import Luna.PhysData: wlfreq, c, crystal_internal_angle +getω0(grid::Grid.EnvGrid) = grid.ω0 +getω0(grid::Grid.RealGrid) = 0.0 + function fill_linop_matrix!(out, grid, β1::Number, βref::Number, k2, kperp2, idcs) + ω0 = getω0(grid) for ii in idcs for ip in axes(k2, 2) for iω in eachindex(grid.ω) βsq = k2[iω, ip] - kperp2[ii] if βsq < 0 # negative βsq -> evanescent fields -> attenuation - out[iω, ip, ii] = -im*(-β1*grid.ω[iω] - βref) - min(sqrt(abs(βsq)), 200) + out[iω, ip, ii] = -im*(-β1*(grid.ω[iω] - ω0) - βref) - min(sqrt(abs(βsq)), 200) else - out[iω, ip, ii] = -im*(sqrt(βsq) - β1*grid.ω[iω] - βref) + out[iω, ip, ii] = -im*(sqrt(βsq) - β1*(grid.ω[iω] - ω0) - βref) end end end @@ -23,23 +27,54 @@ end #=================================================# #=============== FREE SPACE ===============# #=================================================# +function transverse_k2(xygrid::Grid.FreeGrid) + kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 + idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + kperp2, idcs +end + +function transverse_k2(xgrid::Grid.Free2DGrid) + kperp2 = xgrid.kx.^2 + idcs = CartesianIndices(xgrid.kx) + kperp2, idcs +end + +function transverse_k2(xygrid::Hankel.QDHT) + kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 + idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + kperp2, idcs +end + + """ make_const_linop(grid, xygrid, n, β1) Make constant linear operator for full 3D propagation. `n` is the refractive index (array) and β1 is 1/velocity of the reference frame. """ -function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, - n::AbstractVecOrMat, β1::Number) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) +function make_const_linop(grid::Grid.AbstractGrid, + xygrid::Union{Grid.FreeGrid, Grid.Free2DGrid, Hankel.QDHT}, + n::AbstractVecOrMat, β1::Number, β0::Number) + kperp2, idcs = transverse_k2(xygrid) k2 = @. (n*grid.ω/c)^2 - out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) - fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) + out = zeros(ComplexF64, (length(grid.ω), size(n, 2), size(idcs)...)) + fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) return out end -function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) +thg_default(grid::Grid.RealGrid) = true +thg_default(grid::Grid.EnvGrid) = false + +checkthg(grid::Grid.EnvGrid, thg) = nothing +checkthg(grid::Grid.RealGrid, thg) = thg || error("`thg` must be `true` for `RealGrid`s") + +getβ0_n(grid::Grid.RealGrid, nfun, thg) = 0.0 +getβ0_n(grid::Grid.EnvGrid, nfun, thg) = thg ? 0.0 : grid.ω0/c * nfun(wlfreq(grid.ω0))[end] + +function make_const_linop(grid::Grid.AbstractGrid, + xygrid::Union{Grid.FreeGrid, Grid.Free2DGrid, Hankel.QDHT}, + nfun, thg::Bool=thg_default(grid)) + checkthg(grid, thg) ωfirst = grid.ω[findfirst(grid.sidx)] np = length(nfun(wlfreq(ωfirst))) # 1 if single ref index, 2 if nx, ny n = zeros(Float64, (length(grid.ω), np)) @@ -49,10 +84,12 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) end end β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) - make_const_linop(grid, xygrid, n, β1) + β0 = getβ0_n(grid, nfun, thg) + make_const_linop(grid, xygrid, n, β1, β0) end -function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfunx, nfuny) +function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns) + nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) # nfuny(λ; z) just takes wavelength out = zeros(ComplexF64, (length(grid.ω), 2, length(xygrid.ky), length(xygrid.kx))) @@ -80,29 +117,6 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfunx, nfu out end -function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, - n::AbstractVecOrMat, β1::Number, β0ref::Number; thg=false) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) - k2 = @. (n*grid.ω/c)^2 - out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xygrid.ky), length(xygrid.kx))) - fill_linop_matrix!(out, grid, β1, β0ref, k2, kperp2, idcs) - return out -end - -function make_const_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun, - thg=false) - n = zero(grid.ω) - n[grid.sidx] = nfun.(wlfreq.(grid.ω[grid.sidx])) - β1 = PhysData.dispersion_func(1, nfun)(grid.referenceλ) - if thg - β0const = 0.0 - else - β0const = grid.ω0/c * nfun(wlfreq(grid.ω0)) - end - make_const_linop(grid, xygrid, n, β1, β0const; thg=thg) -end - """ make_linop(grid, xygrid, nfun) @@ -137,36 +151,9 @@ end #=================================================# #============ FREE SPACE (2D) ================# #=================================================# -""" - make_const_linop(grid, xgrid, n, β1) - -Make constant linear operator for 2D free-space propagation. `n` is the refractive index (array) -and β1 is 1/velocity of the reference frame. -""" -function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, - n::AbstractArray, β1::Number) - kperp2 = xgrid.kx.^2 - idcs = CartesianIndices(xgrid.kx) - k2 = @. (n*grid.ω/c)^2 - out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) - fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) - return out -end - -function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(wlfreq(ωfirst))) # 1 if single ref index, 2 if nx, ny - n = zeros(Float64, (length(grid.ω), np)) - for (ii, si) in enumerate(grid.sidx) - if si - n[ii, :] .= nfun(wlfreq(grid.ω[ii])) - end - end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) - make_const_linop(grid, xgrid, n, β1) -end -function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nfuny) +function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tuple) + nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) # nfuny(λ; z) just takes wavelength out = zeros(ComplexF64, (length(grid.ω), 2, length(xgrid.kx))) @@ -192,35 +179,6 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfunx, nf out end -function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, - n::AbstractArray, β1::Number, β0ref::Number) - kperp2 = xgrid.kx.^2 - idcs = CartesianIndices(xgrid.kx) - k2 = @. (n*grid.ω/c)^2 - out = zeros(ComplexF64, (length(grid.ω), size(n, 2), length(xgrid.kx))) - fill_linop_matrix!(out, grid, β1, β0ref, k2, kperp2, idcs) - return out -end - -function make_const_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun, - thg=false) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - n = zeros(Float64, (length(grid.ω), np)) - for (ii, si) in enumerate(grid.sidx) - if si - n[ii, :] .= nfun(wlfreq(grid.ω[ii])) - end - end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) - if thg - β0const = 0.0 - else - β0const = grid.ω0/c * nfun(wlfreq(grid.ω0)) - end - make_const_linop(grid, xgrid, n, β1, β0const; thg=thg) -end - """ make_linop(grid, xgrid, nfun) @@ -384,27 +342,20 @@ See also [`αlim!`](@ref). """ conj_clamp(n, ω) = clamp(real(n), 1e-3, Inf) - im*clamp(imag(n), 0, 3000*c/ω) -function make_const_linop(grid::Grid.RealGrid, βfun!, αfun!, β1) +function make_const_linop(grid::Grid.AbstractGrid, βfun!, αfun!, β1::Number, β0::Number) + ω0 = getω0(grid) β = similar(grid.ω) βfun!(β, 0) α = similar(grid.ω) αfun!(α, 0) αlim!(α) - linop = @. -im*(β-β1*grid.ω) - α/2 + linop = @. -im*(β - β1*(grid.ω - ω0) - β0) - α/2 linop[.!grid.sidx] .= 0 return linop end -function make_const_linop(grid::Grid.EnvGrid, βfun!, αfun!, β1, β0ref) - β = similar(grid.ω) - βfun!(β, 0) - α = similar(grid.ω) - αfun!(α, 0) - αlim!(α) - linop = -im.*(β .- β1.*(grid.ω .- grid.ω0) .- β0ref) .- α./2 - linop[.!grid.sidx] .= 0 - return linop -end +getβ0_mode(grid::Grid.RealGrid, mode, λ0, thg) = 0.0 +getβ0_mode(grid::Grid.EnvGrid, mode, λ0, thg) = thg ? 0.0 : Modes.β(mode, wlfreq(λ0)) """ make_const_linop(grid, mode, λ0) @@ -412,13 +363,11 @@ end Make constant linear operator for mode-averaged propagation in mode `mode` with a reference wavelength `λ0`. """ -function make_const_linop(grid::Grid.EnvGrid, mode::Modes.AbstractMode, λ0; thg=false) - β1const = Modes.dispersion(mode, 1, wlfreq(λ0)) - if thg - β0const = 0.0 - else - β0const = Modes.β(mode, wlfreq(λ0)) - end +function make_const_linop(grid::Grid.AbstractGrid, mode::Modes.AbstractMode, λ0; + thg::Bool=thg_default(grid)) + checkthg(grid, thg) + β1 = Modes.dispersion(mode, 1, wlfreq(λ0)) + β0 = getβ0_mode(grid, mode, λ0, thg) βconst = zero(grid.ω) βconst[grid.sidx] = Modes.β.(mode, grid.ω[grid.sidx]) βconst[.!grid.sidx] .= 1 @@ -430,24 +379,9 @@ function make_const_linop(grid::Grid.EnvGrid, mode::Modes.AbstractMode, λ0; thg function αfun!(out, z) out .= αconst end - make_const_linop(grid, βfun!, αfun!, β1const, β0const), βfun!, β1const, αfun! + make_const_linop(grid, βfun!, αfun!, β1, β0), βfun!, β1, αfun! end -function make_const_linop(grid::Grid.RealGrid, mode::Modes.AbstractMode, λ0) - β1const = Modes.dispersion(mode, 1, wlfreq(λ0)) - βconst = zero(grid.ω) - βconst[grid.sidx] = Modes.β.(mode, grid.ω[grid.sidx]) - βconst[.!grid.sidx] .= 1 - function βfun!(out, z) - out .= βconst - end - αconst = zero(grid.ω) - αconst[grid.sidx] = Modes.α.(mode, grid.ω[grid.sidx]) - function αfun!(out, z) - out .= αconst - end - make_const_linop(grid, βfun!, αfun!, β1const), βfun!, β1const, αfun! -end """ neff_β_grid(grid, mode, λ0; ref_mode=1) @@ -529,7 +463,7 @@ Make constant (z-invariant) linear operator for multimode propagation. The frame taken as the group velocity at wavelength `λ0` in the mode given by `ref_mode` (which indexes into `modes`) """ -function make_const_linop(grid::Grid.RealGrid, modes, λ0; ref_mode=1) +function make_const_linop(grid::Grid.RealGrid, modes::Modes.ModeCollection, λ0; ref_mode=1) β1 = Modes.dispersion(modes[ref_mode], 1, wlfreq(λ0)) nmodes = length(modes) linops = zeros(ComplexF64, length(grid.ω), nmodes) @@ -545,7 +479,7 @@ function make_const_linop(grid::Grid.RealGrid, modes, λ0; ref_mode=1) linops end -function make_const_linop(grid::Grid.EnvGrid, modes, λ0; ref_mode=1, thg=false) +function make_const_linop(grid::Grid.EnvGrid, modes::Modes.ModeCollection, λ0; ref_mode=1, thg=false) β1 = Modes.dispersion(modes[ref_mode], 1, wlfreq(λ0)) if thg βref = 0.0 diff --git a/src/Luna.jl b/src/Luna.jl index 4907af4b..6021a0cc 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -337,7 +337,7 @@ function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, xr = Array{ComplexF64}(undef, length(grid.t), np, length(y), length(x)) FT = FFTW.plan_fft(xr, (1, 3, 4), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) - xr_xy = Array{Float64}(undef, length(grid.t), 2, length(y), length(x)) + xr_xy = Array{ComplexF64}(undef, length(grid.t), 2, length(y), length(x)) FT_xy = FFTW.plan_fft(xr_xy, (1, 3, 4), flags=settings["fftw_flag"]) doinputs_fs!(Eωk, grid, xygrid, FT_xy, inputs) xo = Array{ComplexF64}(undef, length(grid.to), np, length(y), length(x)) @@ -360,7 +360,9 @@ function setup(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, xr = Array{Float64}(undef, tshape) FT = FFTW.plan_rfft(xr, (1, 3), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, ωshape) - doinputs_fs!(Eωk, grid, xgrid, FT, inputs) + xr_xy = Array{Float64}(undef, length(grid.t), 2, length(x)) + FT_xy = FFTW.plan_rfft(xr_xy, (1, 3), flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, xgrid, FT_xy, inputs) xo = Array{Float64}(undef, (length(grid.to), np, length(x))) FTo = FFTW.plan_rfft(xo, (1, 3), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree2D(grid, xgrid, FTo, @@ -381,7 +383,9 @@ function setup(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, xr = Array{ComplexF64}(undef, tshape) FT = FFTW.plan_fft(xr, (1, 3), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, ωshape) - doinputs_fs!(Eωk, grid, xgrid, FT, inputs) + xr_xy = Array{ComplexF64}(undef, length(grid.t), 2, length(x)) + FT_xy = FFTW.plan_fft(xr_xy, (1, 3), flags=settings["fftw_flag"]) + doinputs_fs!(Eωk, grid, xgrid, FT_xy, inputs) xo = Array{ComplexF64}(undef, (length(grid.to), np, length(x))) FTo = FFTW.plan_fft(xo, (1, 3), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree2D(grid, xgrid, FTo, diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index e0988fde..1d08a3cf 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -839,7 +839,7 @@ function norm_free2D(grid, xgrid, nfun) continue end for (ip, n) in enumerate(nfun(ω[iω]; z)) - k2 = (n*ω[iω]/PhysData.c)^2 + k2 = (real(n)*ω[iω]/PhysData.c)^2 βsq = k2 - kperp2[ii] if βsq <= 0 out[iω, ip, ii] = 1.0 From 58d42e16b18f8928675944b8039c29e2468bb12d Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 19 Dec 2025 12:02:43 +0000 Subject: [PATCH 41/72] fix norm for 2D/3D propagation with crystal optics --- .../freespace/free2D_bbo.jl | 7 ++-- src/NonlinearRHS.jl | 36 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/examples/low_level_interface/freespace/free2D_bbo.jl b/examples/low_level_interface/freespace/free2D_bbo.jl index 2bc18b70..bfb24542 100644 --- a/examples/low_level_interface/freespace/free2D_bbo.jl +++ b/examples/low_level_interface/freespace/free2D_bbo.jl @@ -17,7 +17,7 @@ material = :BBO thickness = 200e-6 # BBO thickness R = 4*w0 # radius of the spatial window -N = 2^6 # number of spatial points +N = 2^7 # number of spatial points grid = Grid.RealGrid(thickness, λ0, (250e-9, 2e-6), 120e-15) xgrid = Grid.Free2DGrid(R, N) @@ -35,9 +35,9 @@ which allows make_const_linop to calculate the actual internal angle depending on frequency and transverse k-vector component. =# nfunx, nfuny = PhysData.ref_index_fun_xy(material, θ) -linop = LinearOps.make_const_linop(grid, xgrid, nfunx, nfuny) +linop = LinearOps.make_const_linop(grid, xgrid, (nfunx, nfuny)) -normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, nfunx, nfuny) +normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, (nfunx, nfuny)) densityfun = z -> 1 # density is unity because we're considering a solid. ## # scaling factor in front of the energy corresponds to the integral over y @@ -155,7 +155,6 @@ plt.ylabel("SED (J/Hz)") plt.legend() ## -lwe = Utils.load_dict_h5(joinpath(@__DIR__, "field_for_luna.h5")) fig = plt.figure() fig.set_size_inches(12, 7) plt.subplot(1, 2, 1) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 1d08a3cf..54dd672c 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -624,10 +624,11 @@ function const_norm_free(grid, xygrid, nfun) return norm end -function const_norm_free(grid, xygrid, nfunx, nfuny) - nfunωx = (ω, δθ) -> nfunx(wlfreq(ω), δθ) - nfunωy = (ω) -> nfuny(wlfreq(ω)) - normfun = norm_free(grid, xygrid, nfunωx, nfunωy) +function const_norm_free(grid, xygrid, nfuns::Tuple) + nfunx, nfuny = nfuns + nfunzx = (λ, δθ; z) -> nfunx(λ, δθ) + nfunzy = (λ; z) -> nfuny(λ) + normfun = norm_free(grid, xygrid, (nfunzx, nfunzy)) out = copy(normfun(0.0)) function norm(z) return out @@ -672,7 +673,8 @@ function norm_free(grid, xygrid, nfun) end end -function norm_free(grid, xygrid, nfunx, nfuny) +function norm_free(grid, xygrid, nfuns::Tuple) + nfunx, nfuny = nfuns # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ+δθ) # nfuny(λ; z) just takes wavelength ω = grid.ω @@ -683,11 +685,11 @@ function norm_free(grid, xygrid, nfunx, nfuny) out[iω, :, :, :] .= 1.0 continue end - ny = nfuny(wlfreq(ω[iω])) + ny = nfuny(wlfreq(ω[iω]); z) ksq_ypol = (ny*ω[iω]/c)^2 for (ikx, kxi) in enumerate(xygrid.kx) - δθ = crystal_internal_angle(nfunx, ω[iω], kxi) - nx = nfunx(wlfreq(ω[iω]), δθ) + δθ = crystal_internal_angle((λ, δθ) -> nfunx(λ, δθ; z), ω[iω], kxi) + nx = nfunx(wlfreq(ω[iω]), δθ; z) for (iky, kyi) in enumerate(xygrid.ky) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 @@ -806,10 +808,11 @@ function const_norm_free2D(grid, xgrid, nfun) return norm end -function const_norm_free2D(grid, xgrid, nfunx, nfuny) - nfunωx = (ω, δθ) -> nfunx(wlfreq(ω), δθ) - nfunωy = (ω) -> nfuny(wlfreq(ω)) - normfun = norm_free2D(grid, xgrid, nfunωx, nfunωy) +function const_norm_free2D(grid, xgrid, nfuns::Tuple) + nfunx, nfuny = nfuns + nfunzx = (λ, δθ; z) -> nfunx(λ, δθ) + nfunzy = (λ; z) -> nfuny(λ) + normfun = norm_free2D(grid, xgrid, (nfunzx, nfunzy)) out = copy(normfun(0.0)) function norm(z) return out @@ -853,7 +856,8 @@ function norm_free2D(grid, xgrid, nfun) end end -function norm_free2D(grid, xgrid, nfunx, nfuny) +function norm_free2D(grid, xgrid, nfuns::Tuple) + nfunx, nfuny = nfuns # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ+δθ) # nfuny(λ; z) just takes wavelength ω = grid.ω @@ -864,11 +868,11 @@ function norm_free2D(grid, xgrid, nfunx, nfuny) out[iω, :, :] .= 1.0 continue end - ny = nfuny(wlfreq(ω[iω])) + ny = nfuny(wlfreq(ω[iω]); z) ksq_ypol = (ny*ω[iω]/c)^2 for (ik, kxi) in enumerate(xgrid.kx) - δθ = crystal_internal_angle(nfunx, ω[iω], kxi) - nx = nfunx(wlfreq(ω[iω]), δθ) + δθ = crystal_internal_angle((λ, δθ) -> nfunx(λ, δθ; z), ω[iω], kxi) + nx = nfunx(wlfreq(ω[iω]), δθ; z) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 if βsq_xpol < 0 From 08d39bd6613692a8356912ac4327ee07095b09af Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 7 Jan 2026 20:16:57 +0000 Subject: [PATCH 42/72] dramatically speed up envelope radial symmetry free-space propagation --- .../low_level_interface/freespace/radial.jl | 2 +- src/NonlinearRHS.jl | 62 ++++++++++++------- test/test_radial.jl | 4 +- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/examples/low_level_interface/freespace/radial.jl b/examples/low_level_interface/freespace/radial.jl index a737e053..71c9b9fe 100644 --- a/examples/low_level_interface/freespace/radial.jl +++ b/examples/low_level_interface/freespace/radial.jl @@ -65,7 +65,7 @@ Ir = zeros(Float64, (length(q.r), length(zout))) Et = Maths.hilbert(Etout) energy = zeros(length(zout)) -for ii = 1:size(Etout, 4) +for ii in axes(Etout, 4) energy[ii] = energyfun(Etout[:, 1, :, ii]); Ir[:, ii] = integrate(grid.ω, Iωr[:, 1, :, ii], SimpsonEven()); end diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 54dd672c..ca3004e2 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -14,22 +14,22 @@ using EllipsisNotation Transform ``A(ω)`` on normal grid to ``A(t)`` on oversampled time grid. """ -function to_time!(Ato::Array{<:Real, D}, Aω, Aωo, IFTplan) where D +function to_time!(Ato::Array{<:Real, D}, Aω, Aωo, FT) where D N = size(Aω, 1) No = size(Aωo, 1) scale = (No-1)/(N-1) # Scale factor makes up for difference in FFT array length fill!(Aωo, 0) copy_scale!(Aωo, Aω, N, scale) - mul!(Ato, IFTplan, Aωo) + ldiv!(Ato, FT, Aωo) end -function to_time!(Ato::Array{<:Complex, D}, Aω, Aωo, IFTplan) where D +function to_time!(Ato::Array{<:Complex, D}, Aω, Aωo, FT) where D N = size(Aω, 1) No = size(Aωo, 1) scale = No/N # Scale factor makes up for difference in FFT array length fill!(Aωo, 0) copy_scale_both!(Aωo, Aω, N÷2, scale) - mul!(Ato, IFTplan, Aωo) + ldiv!(Ato, FT, Aωo) end """ @@ -82,7 +82,7 @@ function copy_scale_both!(dest::Vector, source::Vector, N, scale) end function copy_scale!(dest, source, N, scale) - (size(dest)[2:end] == size(source)[2:end] + (size(dest)[2:end] == size(source)[2:end] || error("dest and source must be same size except along first dimension")) idcs = CartesianIndices(size(dest)[2:end]) _cpsc_core(dest, source, N, scale, idcs) @@ -97,7 +97,7 @@ function _cpsc_core(dest, source, N, scale, idcs) end function copy_scale_both!(dest, source, N, scale) - (size(dest)[2:end] == size(source)[2:end] + (size(dest)[2:end] == size(source)[2:end] || error("dest and source must be same size except along first dimension")) idcs = CartesianIndices(size(dest)[2:end]) _cpscb_core(dest, source, N, scale, idcs) @@ -273,7 +273,7 @@ end function Erω_to_Prω!(t, x) Modes.to_space!(t.Erω, t.Emω, x, t.ts, z=t.z) - to_time!(t.Er, t.Erω, t.Erωo, inv(t.FT)) + to_time!(t.Er, t.Erω, t.Erωo, t.FT) # get nonlinear pol at r,θ Et_to_Pt!(t.Pr, t.Er, t.resp, t.density) @. t.Pr *= t.grid.towin @@ -289,13 +289,13 @@ function (t::TransModal)(nl, Eω, z) val, err = Cubature.hcubature_v( length(Eω)*2, (x, fval) -> pointcalc!(fval, x, t), - ll, ul, + ll, ul, reltol=t.rtol, abstol=t.atol, maxevals=t.mfcn, error_norm=Cubature.L2) else val, err = Cubature.pcubature_v( length(Eω)*2, (x, fval) -> pointcalc!(fval, x, t), - (ll[1],), (ul[1],), + (ll[1],), (ul[1],), reltol=t.rtol, abstol=t.atol, maxevals=t.mfcn, error_norm=Cubature.L2) end t.err .= reshape(reinterpret(ComplexF64, err), size(nl)) @@ -361,7 +361,7 @@ end const nlscale = sqrt(PhysData.ε_0*PhysData.c/2) function (t::TransModeAvg)(nl, Eω, z) - to_time!(t.Eto, Eω, t.Eωo, inv(t.FT)) + to_time!(t.Eto, Eω, t.Eωo, t.FT) @. t.Eto /= nlscale*sqrt(t.aeff(z)) Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z)) @. t.Pto *= t.grid.towin @@ -416,6 +416,10 @@ struct TransRadial{TT, HTT, FTT, nT, rT, gT, dT, iT} Eωo::Array{ComplexF64, 3} # Buffer array for field on oversampled frequency grid Pωo::Array{ComplexF64, 3} # Buffer array for NL polarisation on oversampled frequency grid idcs::iT # CartesianIndices for Et_to_Pt! to iterate over + Tfwd::Matrix{TT} + Tbwd::Matrix{TT} + Qbuf1::Matrix{TT} + Qbuf2::Matrix{TT} end function show(io::IO, t::TransRadial) @@ -429,12 +433,18 @@ function show(io::IO, t::TransRadial) end function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false) - Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, HT.N)) - Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, HT.N)) + np = pol ? 2 : 1 + Eωo = zeros(ComplexF64, (length(grid.ωo), np, HT.N)) + Eto = zeros(TT, (length(grid.to), np, HT.N)) Pto = similar(Eto) Pωo = similar(Eωo) idcs = CartesianIndices(size(Pto)[3:end]) - TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto, Eto, Eωo, Pωo, idcs) + Tfwd = convert(Matrix{TT}, HT.T .* HT.scaleRK) + Tbwd = convert(Matrix{TT}, HT.T ./ HT.scaleRK) + Qbuf1 = zeros(TT, (HT.N, length(grid.to))) + Qbuf2 = zeros(TT, (HT.N, length(grid.to))) + TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto, Eto, Eωo, Pωo, idcs, + Tfwd, Tbwd, Qbuf1, Qbuf2) end """ @@ -465,11 +475,21 @@ Calculate the reciprocal-domain (ω-k-space) nonlinear response due to the field place the result in `nl` """ function (t::TransRadial)(nl, Eω, z) - to_time!(t.Eto, Eω, t.Eωo, inv(t.FT)) # transform ω -> t - ldiv!(t.Eto, t.QDHT, t.Eto) # transform k -> r + to_time!(t.Eto, Eω, t.Eωo, t.FT) # transform ω -> t + # ldiv!(t.Eto, t.QDHT, t.Eto) # transform k -> r + for ip in axes(t.Eto, 2) + permutedims!(t.Qbuf1, view(t.Eto, :, ip, :), (2, 1)) + mul!(t.Qbuf2, t.Tbwd, t.Qbuf1) + permutedims!(view(t.Eto, :, ip, :), t.Qbuf2, (2, 1)) + end Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses @. t.Pto *= t.grid.towin # apodisation - mul!(t.Pto, t.QDHT, t.Pto) # transform r -> k + # mul!(t.Pto, t.QDHT, t.Pto) # transform r -> k + for ip in axes(t.Eto, 2) + permutedims!(t.Qbuf1, view(t.Pto, :, ip, :), (2, 1)) + mul!(t.Qbuf2, t.Tfwd, t.Qbuf1) + permutedims!(view(t.Pto, :, ip, :), t.Qbuf2, (2, 1)) + end to_freq!(nl, t.Pωo, t.Pto, t.FT) # transform t -> ω nl .*= t.grid.ωwin .* (-im.*t.grid.ω)./(2 .* t.normfun(z)) end @@ -478,7 +498,7 @@ end const_norm_radial(ω, q, nfun) Make function to return normalisation factor for radial symmetry without re-calculating at -every step. +every step. """ function const_norm_radial(grid, q, nfun) nfunω = (ω; z) -> nfun(wlfreq(ω)) @@ -493,7 +513,7 @@ end """ norm_radial(ω, q, nfun) -Make function to return normalisation factor for radial symmetry. +Make function to return normalisation factor for radial symmetry. !!! note Here, `nfun(ω; z)` needs to take frequency `ω` and a keyword argument `z`. @@ -698,7 +718,7 @@ function norm_free(grid, xygrid, nfuns::Tuple) else out[iω, 1, iky, ikx] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) end - + βsq_ypol = ksq_ypol - kxi^2 - kyi^2 if βsq_ypol < 0 out[iω, 2, iky, ikx] .= 1.0 @@ -880,7 +900,7 @@ function norm_free2D(grid, xgrid, nfuns::Tuple) else out[iω, 1, ik] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) end - + βsq_ypol = ksq_ypol - kxi^2 if βsq_ypol < 0 out[iω, 2, ik] .= 1.0 @@ -893,4 +913,4 @@ function norm_free2D(grid, xgrid, nfuns::Tuple) end end -end \ No newline at end of file +end diff --git a/test/test_radial.jl b/test/test_radial.jl index aa1fbe48..fc2b9d28 100644 --- a/test/test_radial.jl +++ b/test/test_radial.jl @@ -19,7 +19,7 @@ R = 4e-3 N = 128 grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, N, dim=2) +q = Hankel.QDHT(R, N, dim=3) energyfun, energyfun_ω = Fields.energyfuncs(grid, q) @@ -55,7 +55,7 @@ Iωr = abs2.(Erout) ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) -Iλ0 = Iωr[ω0idx, :, :] +Iλ0 = Iωr[ω0idx, 1, :, :] λ0 = 2π*PhysData.c/grid.ω[ω0idx] w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) Iλ0_analytic = Maths.gauss.(q.r, w1/2)*(w0/w1)^2 # analytical solution (in paraxial approx) From 272d03e1109c04c0e46db2057749576cb3422c88 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 7 Jan 2026 20:22:50 +0000 Subject: [PATCH 43/72] comments to future self --- src/NonlinearRHS.jl | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index ca3004e2..969bb259 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -416,10 +416,10 @@ struct TransRadial{TT, HTT, FTT, nT, rT, gT, dT, iT} Eωo::Array{ComplexF64, 3} # Buffer array for field on oversampled frequency grid Pωo::Array{ComplexF64, 3} # Buffer array for NL polarisation on oversampled frequency grid idcs::iT # CartesianIndices for Et_to_Pt! to iterate over - Tfwd::Matrix{TT} - Tbwd::Matrix{TT} - Qbuf1::Matrix{TT} - Qbuf2::Matrix{TT} + Tfwd::Matrix{TT} # forward Hankel transform matrix + Tbwd::Matrix{TT} # backward Hankel transform matrix + Qbuf1::Matrix{TT} # buffer array for Hankel transform + Qbuf2::Matrix{TT} # buffer array for Hankel transform end function show(io::IO, t::TransRadial) @@ -476,15 +476,24 @@ place the result in `nl` """ function (t::TransRadial)(nl, Eω, z) to_time!(t.Eto, Eω, t.Eωo, t.FT) # transform ω -> t - # ldiv!(t.Eto, t.QDHT, t.Eto) # transform k -> r + # transform Eto k -> r + # iterate over polarisation directions (either 1:2 or just 1) for ip in axes(t.Eto, 2) + # transform matrix Tbwd has shape Nr × Nr, but t.Eto has Nto × Np × Nr + # for fast matrix multiplication, we need the inner dimensions to match + # so first permute the dimensions (into the buffer array) + # now its shape is Nr × Nto + # note the view() to avoid allocating by indexing permutedims!(t.Qbuf1, view(t.Eto, :, ip, :), (2, 1)) + # now do matrix multiplication (Nr × Nr) × (Nr × Nto) -> (Nr × Nto) mul!(t.Qbuf2, t.Tbwd, t.Qbuf1) + # permute dims back into the original array permutedims!(view(t.Eto, :, ip, :), t.Qbuf2, (2, 1)) end Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses @. t.Pto *= t.grid.towin # apodisation - # mul!(t.Pto, t.QDHT, t.Pto) # transform r -> k + # transform Pto r -> k + # same as above but with forward transform matrix Tfwd for ip in axes(t.Eto, 2) permutedims!(t.Qbuf1, view(t.Pto, :, ip, :), (2, 1)) mul!(t.Qbuf2, t.Tfwd, t.Qbuf1) From 697263ccddfd029e44b71d2f5841eb9a7172323a Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 8 Jan 2026 22:23:24 +0000 Subject: [PATCH 44/72] whitespace --- src/Maths.jl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Maths.jl b/src/Maths.jl index 09b66ef1..39972afc 100644 --- a/src/Maths.jl +++ b/src/Maths.jl @@ -151,7 +151,7 @@ end Find crossings of the curve `y` on the axis `x` with the value `level * maximum(y)`. `method` can be `:spline`, `:linear` or `:nearest`. `:spline` uses a [`CSpline`](@ref). -`:linear` finds the closest values either side of the crossing point and interpolates linearly. +`:linear` finds the closest values either side of the crossing point and interpolates linearly. `:nearest` just uses the closest values either side of the crossing point. If `baseline` is true, the width is not taken at @@ -234,7 +234,7 @@ end """ linterpx(x1, x2, y1, y2, val) -Given two points on a straight line, `(x1, y1)` and `(x2, y2)`, calculate the value of `x` +Given two points on a straight line, `(x1, y1)` and `(x2, y2)`, calculate the value of `x` at which this straight line intercepts with `val`. # Examples @@ -324,7 +324,7 @@ end function cumtrapz!(out, y::Union{SubArray, Vector}, x) out[1] = 0 for i in 2:length(y) - out[i] = out[i-1]+ 1//2*(y[i-1] + y[i])*_dx(x, i) + out[i] = out[i-1] + 1//2*(y[i-1] + y[i])*_dx(x, i) end end @@ -347,7 +347,7 @@ Calculate the cumulative trapezoidal integral of `y`. """ function cumtrapz(y, x; dim=1) out = similar(y) - cumtrapz!(out, y, x; dim=dim) + cumtrapz!(out, y, x; dim=dim) return out end @@ -521,7 +521,7 @@ function wigner(t, A::Vector{<:Complex}; downsample=1, crop=1) startidx = 1 endidx = length(t) end - + A = A[startidx:downsample:endidx] t = t[startidx:downsample:endidx] @@ -791,7 +791,7 @@ function make_spline_ifun(x, ifun) xmin = minimum(x) N = length(x) ffast(x0) = x0 <= xmin ? 2 : - x0 >= xmax ? N : + x0 >= xmax ? N : ceil(Int, (x0-xmin)/(xmax-xmin)*(N-1))+1 ifun = ffast else @@ -806,7 +806,7 @@ end CSpline Simple cubic spline, see e.g.: -http://mathworld.wolfram.com/CubicSpline.html Boundary +http://mathworld.wolfram.com/CubicSpline.html Boundary conditions extrapolate with initially constant gradient """ struct CSpline{Tx,Ty,Vx<:AbstractVector{Tx},Vy<:AbstractVector{Ty}, fT} @@ -862,9 +862,9 @@ function (c::CSpline)(x0) x0 == c.x[i] && return c.y[i] x0 == c.x[i-1] && return c.y[i-1] t = (x0 - c.x[i - 1])/(c.x[i] - c.x[i - 1]) - (c.y[i - 1] - + c.D[i - 1]*t - + (3*(c.y[i] - c.y[i - 1]) - 2*c.D[i - 1] - c.D[i])*t^2 + (c.y[i - 1] + + c.D[i - 1]*t + + (3*(c.y[i] - c.y[i - 1]) - 2*c.D[i - 1] - c.D[i])*t^2 + (2*(c.y[i - 1] - c.y[i]) + c.D[i - 1] + c.D[i])*t^3) end @@ -1205,11 +1205,11 @@ function fpbspl!(h, hh, t, k, x, l) li = l+i lj = li-j if t[li] != t[lj] - f = hh[i]/(t[li] - t[lj]) + f = hh[i]/(t[li] - t[lj]) h[i] = h[i] + f*(t[li] - x) h[i+1] = f*(x - t[lj]) else - h[i+1] = 0.0 + h[i+1] = 0.0 end end end @@ -1222,7 +1222,7 @@ end Find the full width of a peak in `y` over `x` centred at index `pki`. The default `level=0.5` requests the full width at half maximum. Setting `level` to something -different computes the corresponding width. E.g. `level=0.1` for the 10% width. +different computes the corresponding width. E.g. `level=0.1` for the 10% width. `skipnonmono=true` skips peaks which are not monotonically increaing/decreasing before/after the peak. From c748cc7ad0911b420400e15f9316837f45c564a6 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 8 Jan 2026 22:23:54 +0000 Subject: [PATCH 45/72] simplify speedup --- src/NonlinearRHS.jl | 54 ++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 969bb259..3f9f42cb 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -411,15 +411,15 @@ struct TransRadial{TT, HTT, FTT, nT, rT, gT, dT, iT} resp::rT # nonlinear responses (tuple of callables) grid::gT # time grid densityfun::dT # callable which returns density - Pto::Array{TT, 3} # Buffer array for NL polarisation on oversampled time grid - Eto::Array{TT, 3} # Buffer array for field on oversampled time grid + Pto_r::Array{TT, 3} # Buffer array for NL polarisation on oversampled time grid + Pto_k::Array{TT, 3} # Buffer array for NL polarisation on oversampled time grid + Eto_r::Array{TT, 3} # Buffer array for field on oversampled time grid + Eto_k::Array{TT, 3} # Buffer array for field on oversampled time grid Eωo::Array{ComplexF64, 3} # Buffer array for field on oversampled frequency grid Pωo::Array{ComplexF64, 3} # Buffer array for NL polarisation on oversampled frequency grid idcs::iT # CartesianIndices for Et_to_Pt! to iterate over Tfwd::Matrix{TT} # forward Hankel transform matrix Tbwd::Matrix{TT} # backward Hankel transform matrix - Qbuf1::Matrix{TT} # buffer array for Hankel transform - Qbuf2::Matrix{TT} # buffer array for Hankel transform end function show(io::IO, t::TransRadial) @@ -435,16 +435,16 @@ end function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false) np = pol ? 2 : 1 Eωo = zeros(ComplexF64, (length(grid.ωo), np, HT.N)) - Eto = zeros(TT, (length(grid.to), np, HT.N)) - Pto = similar(Eto) + Eto_r = zeros(TT, (length(grid.to), np, HT.N)) + Pto_r = similar(Eto_r) + Eto_k = similar(Eto_r) + Pto_k = similar(Eto_r) Pωo = similar(Eωo) - idcs = CartesianIndices(size(Pto)[3:end]) - Tfwd = convert(Matrix{TT}, HT.T .* HT.scaleRK) - Tbwd = convert(Matrix{TT}, HT.T ./ HT.scaleRK) - Qbuf1 = zeros(TT, (HT.N, length(grid.to))) - Qbuf2 = zeros(TT, (HT.N, length(grid.to))) - TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto, Eto, Eωo, Pωo, idcs, - Tfwd, Tbwd, Qbuf1, Qbuf2) + idcs = CartesianIndices(size(Pto_r)[3:end]) + Tfwd = convert(Matrix{TT}, transpose(HT.T) .* HT.scaleRK) + Tbwd = convert(Matrix{TT}, transpose(HT.T) ./ HT.scaleRK) + TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto_r, Pto_k, Eto_r, Eto_k, Eωo, Pωo, idcs, + Tfwd, Tbwd) end """ @@ -475,31 +475,19 @@ Calculate the reciprocal-domain (ω-k-space) nonlinear response due to the field place the result in `nl` """ function (t::TransRadial)(nl, Eω, z) - to_time!(t.Eto, Eω, t.Eωo, t.FT) # transform ω -> t + to_time!(t.Eto_k, Eω, t.Eωo, t.FT) # transform ω -> t # transform Eto k -> r # iterate over polarisation directions (either 1:2 or just 1) - for ip in axes(t.Eto, 2) - # transform matrix Tbwd has shape Nr × Nr, but t.Eto has Nto × Np × Nr - # for fast matrix multiplication, we need the inner dimensions to match - # so first permute the dimensions (into the buffer array) - # now its shape is Nr × Nto - # note the view() to avoid allocating by indexing - permutedims!(t.Qbuf1, view(t.Eto, :, ip, :), (2, 1)) - # now do matrix multiplication (Nr × Nr) × (Nr × Nto) -> (Nr × Nto) - mul!(t.Qbuf2, t.Tbwd, t.Qbuf1) - # permute dims back into the original array - permutedims!(view(t.Eto, :, ip, :), t.Qbuf2, (2, 1)) + for ip in axes(t.Eto_k, 2) + mul!(view(t.Eto_r, :, ip, :), view(t.Eto_k, :, ip, :), t.Tbwd) end - Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses - @. t.Pto *= t.grid.towin # apodisation + Et_to_Pt!(t.Pto_r, t.Eto_r, t.resp, t.densityfun(z), t.idcs) # add up responses + @. t.Pto_r *= t.grid.towin # apodisation # transform Pto r -> k - # same as above but with forward transform matrix Tfwd - for ip in axes(t.Eto, 2) - permutedims!(t.Qbuf1, view(t.Pto, :, ip, :), (2, 1)) - mul!(t.Qbuf2, t.Tfwd, t.Qbuf1) - permutedims!(view(t.Pto, :, ip, :), t.Qbuf2, (2, 1)) + for ip in axes(t.Pto_k, 2) + mul!(view(t.Pto_k, :, ip, :), view(t.Pto_r, :, ip, :), t.Tfwd) end - to_freq!(nl, t.Pωo, t.Pto, t.FT) # transform t -> ω + to_freq!(nl, t.Pωo, t.Pto_k, t.FT) # transform t -> ω nl .*= t.grid.ωwin .* (-im.*t.grid.ω)./(2 .* t.normfun(z)) end From 6e2d24a53cce4b4d6ebeb0bf957a60a3ac4c087d Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 14 Jan 2026 14:51:17 +0000 Subject: [PATCH 46/72] free 2D examples --- .../low_level_interface/freespace/free2D.jl | 85 ++++++++++++++++++ .../freespace/free2D_env.jl | 87 +++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 examples/low_level_interface/freespace/free2D.jl create mode 100644 examples/low_level_interface/freespace/free2D_env.jl diff --git a/examples/low_level_interface/freespace/free2D.jl b/examples/low_level_interface/freespace/free2D.jl new file mode 100644 index 00000000..3cb4160d --- /dev/null +++ b/examples/low_level_interface/freespace/free2D.jl @@ -0,0 +1,85 @@ +using Luna +Luna.set_fftw_mode(:estimate) +import FFTW +import PyPlot:pygui, plt +pygui(true) + +gas = :Ar +pres = 4 + +τ = 30e-15 +λ0 = 800e-9 + +w0 = 2e-3 +energy = 1.5e-3 +L = 2 + +R = 6e-3 +N = 128 + +grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) +xgrid = Grid.Free2DGrid(R, N) + +x = xgrid.x +energyfun, energyfunω = Fields.energyfuncs(grid, xgrid) + +densityfun = let dens0=PhysData.density(gas, pres) + z -> dens0 +end + +responses = (Nonlinear.Kerr_env(PhysData.γ3_gas(gas)),) + +linop = LinearOps.make_const_linop(grid, xgrid, PhysData.ref_index_fun(gas, pres)) +normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, PhysData.ref_index_fun(gas, pres)) + +inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0) + +Eω, transform, FT = Luna.setup(grid, xgrid, densityfun, normfun, responses, inputs) + +# statsfun = Stats.collect_stats(grid, Eω, Stats.ω0(grid)) +output = Output.MemoryOutput(0, grid.zmax, 21) + +Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) + +ω = grid.ω +t = grid.t + +zout = output.data["z"] +Eout = output.data["Eω"] + +println("Transforming...") +Eωx = FFTW.ifft(Eout, 3) +Etx = FFTW.irfft(Eout, length(grid.t), (1, 3)) +println("...done") + + +Ilog = log10.(Maths.normbymax(abs2.(Eωx))) + +Iωx = abs2.(Eωx); + +Ix = zeros(Float64, (length(x), length(zout))); +energy = zeros(length(zout)); +for ii in axes(Etx, 4) + energy[ii] = energyfun(Etx[:, 1, :, ii]); + Ix[:, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωx[:, 1, :, ii], dims=1); +end + +ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) + +E0ωyx = FFTW.ifft(Eω[ω0idx, 1, :, :], (1, 2)) + +Iωxlog = log10.(Maths.normbymax(Iωx)) + +plt.figure() +plt.pcolormesh(zout, ω.*1e-15/2π, Iωxlog[:, 1, N÷2+1, :]) +plt.xlabel("Z (m)") +plt.ylabel("f (PHz)") +plt.title("I(ω, x=0, z)") +plt.clim(-6, 0) +plt.colorbar() + + +plt.figure() +plt.plot(zout.*1e2, energy.*1e6) +plt.xlabel("Distance [cm]") +plt.ylabel("Energy [μJ]") diff --git a/examples/low_level_interface/freespace/free2D_env.jl b/examples/low_level_interface/freespace/free2D_env.jl new file mode 100644 index 00000000..e81a10f8 --- /dev/null +++ b/examples/low_level_interface/freespace/free2D_env.jl @@ -0,0 +1,87 @@ +using Luna +Luna.set_fftw_mode(:estimate) +import FFTW +import PyPlot:pygui, plt +pygui(true) + +gas = :Ar +pres = 4 + +τ = 30e-15 +λ0 = 800e-9 + +w0 = 2e-3 +energy = 1.5e-3 +L = 2 + +R = 6e-3 +N = 128 + +grid = Grid.EnvGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) +xgrid = Grid.Free2DGrid(R, N) + +x = xgrid.x +energyfun, energyfunω = Fields.energyfuncs(grid, xgrid) + +densityfun = let dens0=PhysData.density(gas, pres) + z -> dens0 +end + +responses = (Nonlinear.Kerr_env(PhysData.γ3_gas(gas)),) + +linop = LinearOps.make_const_linop(grid, xgrid, PhysData.ref_index_fun(gas, pres)) +normfun = NonlinearRHS.const_norm_free2D(grid, xgrid, PhysData.ref_index_fun(gas, pres)) + +inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0) + +Eω, transform, FT = Luna.setup(grid, xgrid, densityfun, normfun, responses, inputs) + +# statsfun = Stats.collect_stats(grid, Eω, Stats.ω0(grid)) +output = Output.MemoryOutput(0, grid.zmax, 21) + +Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) + +ω = FFTW.fftshift(grid.ω) +t = grid.t + +zout = output.data["z"] +Eout = output.data["Eω"] + +println("Transforming...") +Eωx = FFTW.fftshift(FFTW.ifft(Eout, 3), 1); +Etx = FFTW.ifft(Eout, (1, 3)) +println("...done") + +Eout = FFTW.fftshift(Eout, 3) + +Ilog = log10.(Maths.normbymax(abs2.(Eωx))) + +Iωx = abs2.(Eωx); + +Ix = zeros(Float64, (length(x), length(zout))); +energy = zeros(length(zout)); +for ii in axes(Etx, 4) + energy[ii] = energyfun(Etx[:, 1, :, ii]); + Ix[:, ii] = (grid.ω[2]-grid.ω[1]) .* sum(Iωx[:, 1, :, ii], dims=1); +end + +ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) + +E0ωyx = FFTW.ifft(Eω[ω0idx, 1, :, :], (1, 2)); + +Iωyx = abs2.(Eωx); +Iωxlog = log10.(Maths.normbymax(Iωyx)); + +plt.figure() +plt.pcolormesh(zout, ω.*1e-15/2π, Iωxlog[:, 1, N÷2+1, :]) +plt.xlabel("Z (m)") +plt.ylabel("f (PHz)") +plt.title("I(ω, x=0, z)") +plt.clim(-6, 0) +plt.colorbar() + + +plt.figure() +plt.plot(zout.*1e2, energy.*1e6) +plt.xlabel("Distance [cm]") +plt.ylabel("Energy [μJ]") From 9c56a2b1a23fdf56b8dd0b9a93a479391322ee3c Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 3 Mar 2026 21:27:21 +0000 Subject: [PATCH 47/72] change y-x ordering to x-y ordering in full 3D simulations --- .../freespace/free3D_bbo.jl | 69 ++++++++-------- src/Fields.jl | 24 +++--- src/Grid.jl | 25 +++--- src/LinearOps.jl | 38 ++++----- src/Luna.jl | 26 +++--- src/NonlinearRHS.jl | 28 +++---- src/Processing.jl | 22 +++--- test/test_fields.jl | 28 +++---- test/test_full_freespace.jl | 79 +++++++++++++++++-- 9 files changed, 204 insertions(+), 135 deletions(-) diff --git a/examples/low_level_interface/freespace/free3D_bbo.jl b/examples/low_level_interface/freespace/free3D_bbo.jl index 947cc57c..3c547daf 100644 --- a/examples/low_level_interface/freespace/free3D_bbo.jl +++ b/examples/low_level_interface/freespace/free3D_bbo.jl @@ -14,13 +14,14 @@ w0 = 20e-6 # 1/e² beam radius energy = 10e-9 # pulse energy material = :BBO -thickness = 200e-6 # BBO thickness +thickness = 200e-6 # BBO thickness: 200 μm R = 4*w0 # radius of the spatial window -N = 2^6 # number of spatial points +Nx = 2^6 # number of spatial points in x +Ny = 16 # number of spatial points in y grid = Grid.RealGrid(thickness, λ0, (250e-9, 2e-6), 120e-15) -xygrid = Grid.FreeGrid(R, N) +xygrid = Grid.FreeGrid(R, Nx, R, Ny) θ = deg2rad(29.2) # type I phase-matching angle ϕ = deg2rad(30) # ϕ for type I phase-matching @@ -35,9 +36,9 @@ which allows make_const_linop to calculate the actual internal angle depending on frequency and transverse k-vector component. =# nfunx, nfuny = PhysData.ref_index_fun_xy(material, θ) -linop = LinearOps.make_const_linop(grid, xygrid, nfunx, nfuny) +linop = LinearOps.make_const_linop(grid, xygrid, (nfunx, nfuny)) -normfun = NonlinearRHS.const_norm_free(grid, xygrid, nfunx, nfuny) +normfun = NonlinearRHS.const_norm_free(grid, xygrid, (nfunx, nfuny)) densityfun = z -> 1 # density is unity because we're considering a solid. ## inputs = Fields.GaussGaussField(;λ0, τfwhm, energy=energy, w0) @@ -49,7 +50,7 @@ Luna.run(Eω, grid, linop, transform, FT, output; init_dz=1e-6) ## z = output["z"] -Eωk = output["Eω"] # (ω, pol, ky, kx, z) +Eωk = output["Eω"] # (ω, pol, kx, ky, z) Ik = FFTW.fftshift(dropdims(sum(abs2.(Eωk); dims=1); dims=1), (2, 3)) x = xygrid.x y = xygrid.y @@ -57,32 +58,32 @@ y = xygrid.y # normalisation prefactor for spectral intensity ωprefac = PhysData.c*PhysData.ε_0/2 * 2π/(grid.ω[end]^2) -Eωr = FFTW.ifft(Eωk, (3, 4)) # (ω, pol, y, x, z) -Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, y, x, z) +Eωr = FFTW.ifft(Eωk, (3, 4)) # (ω, pol, x, y, z) +Etr = FFTW.irfft(Eωr, 2*(length(grid.ω)-1), 1) # (t, pol, x, y, z) EtrH = Maths.hilbert(Etr) -Iωr = abs2.(Eωr) # (ω, pol, y, x, z) -Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(EtrH) # (t, pol, y, x, z) +Iωr = abs2.(Eωr) # (ω, pol, x, y, z) +Itr = 0.5*PhysData.c*PhysData.ε_0*abs2.(EtrH) # (t, pol, x, y, z) -Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, y, x, z) -Ix = dropdims(sum(Irxy; dims=2); dims=2) # (pol, x, z) +Irxy = dropdims(sum(Iωr; dims=1); dims=1) # (pol, x, y, z) +Ix = dropdims(sum(Irxy; dims=3); dims=3) # (pol, x, z) Iω = dropdims( Maths.integrateNd( y, - Maths.integrateNd(x, Iωr; dim=4); - dim=3), + Maths.integrateNd(x, Iωr; dim=3); + dim=4), dims=(3, 4) )*ωprefac # (ω, pol, z) -Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (y, x, z) +Ir = dropdims(sum(Iωr; dims=(1, 2)); dims=(1, 2)) # (x, y, z) Itxy = dropdims( Maths.integrateNd( y, - Maths.integrateNd(x, Itr; dim=4); - dim=3), + Maths.integrateNd(x, Itr; dim=3); + dim=4), dims=(3, 4) ) # (t, pol, z) -Eω0 = Eωr[:, :, length(y)÷2+1, length(x)÷2+1, :] +Eω0 = Eωr[:, :, length(x)÷2+1, length(y)÷2+1, :] Et0 = FFTW.irfft(Eω0, 2*(length(grid.ω)-1), 1) # (t, pol, z) Et0 = Maths.hilbert(Et0) It0 = 0.5*PhysData.c*PhysData.ε_0*abs2.(Et0) @@ -99,25 +100,25 @@ ky = FFTW.fftshift(xygrid.ky) fig = plt.figure() fig.set_size_inches(12, 12) plt.subplot(2, 2, 1) -plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, 1])) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, 1])') plt.clim(-6, 0) plt.title("Input, X pol") plt.subplot(2, 2, 2) -plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, 1])) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, 1])') plt.clim(-6, 0) plt.title("Input, Y pol") plt.subplot(2, 2, 3) -plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, end])) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[1, :, :, end])') plt.clim(-6, 0) plt.title("Output, X pol") plt.subplot(2, 2, 4) -plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, end])) +plt.pcolormesh(kx*1e-6, ky*1e-6, Maths.log10_norm(Ik[2, :, :, end])') plt.clim(-6, 0) plt.title("Output, Y pol") ## plt.figure() -plt.pcolormesh(z*1e3, x*1e6, Ir[length(y)÷2+1, :, :]) +plt.pcolormesh(z*1e3, x*1e6, Ir[:, length(y)÷2+1, :]) plt.xlabel("Distance (mm)") plt.ylabel("X (μm)") @@ -193,8 +194,8 @@ plt.legend() fig = plt.figure() fig.set_size_inches(12, 7) plt.subplot(1, 2, 1) -plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 1, N÷2, :, end])'; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, N÷2, :, end]))) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 1, :, Ny÷2+1, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 1, Ny÷2+1, :, end]))) plt.xlim(-50, 50) plt.ylim(-100, 100) plt.xlabel("Time (fs)") @@ -202,8 +203,8 @@ plt.ylabel("X (μm)") plt.title("X polarisation") plt.subplot(1, 2, 2) -plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 2, 32, :, end])'; cmap="seismic") -plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, N÷2, :, end]))) +plt.pcolormesh(grid.t*1e15, xygrid.x*1e6, (Etr[:, 2, :, Ny÷2+1, end])'; cmap="seismic") +plt.clim([-1, 1].*maximum(abs.(Etr[:, 2, :, Ny÷2+1, end]))) plt.xlim(-50, 50) plt.ylim(-100, 100) plt.xlabel("Time (fs)") @@ -213,23 +214,23 @@ plt.title("Y polarisation") fig = plt.figure() fig.set_size_inches(12, 12) plt.subplot(2, 2, 1) -plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, 1]) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, 1]') plt.title("Input, X pol") plt.subplot(2, 2, 2) -plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, 1]) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, 1]') plt.title("Input, Y pol") plt.subplot(2, 2, 3) -plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, end]) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[1, :, :, end]') plt.title("Output, X pol") plt.subplot(2, 2, 4) -plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, end]) +plt.pcolormesh(xygrid.x*1e6, xygrid.y*1e6, Irxy[2, :, :, end]') plt.title("Output, Y pol") ## fig = plt.figure() fig.set_size_inches(12, 6) plt.subplot(1, 2, 1) -Etx_00 = Etr[:, 1, length(xygrid.y)÷2+1, length(xygrid.x)÷2+1, end] +Etx_00 = Etr[:, 1, length(xygrid.x)÷2+1, length(xygrid.y)÷2+1, end] plt.plot(grid.t*1e15, 1e-9*(Etx_00); label="Max: $(maximum(1e-9*Etx_00))") plt.ylabel("E (GV/m)") @@ -237,9 +238,9 @@ plt.legend() plt.xlabel("Time (fs)") plt.title("X Polarisation") plt.subplot(1, 2, 2) -Ety_00 = Etr[:, 2, length(xygrid.y)÷2+1, length(xygrid.x)÷2+1, end] +Ety_00 = Etr[:, 2, length(xygrid.x)÷2+1, length(xygrid.y)÷2+1, end] plt.plot(grid.t*1e15, 1e-9*(Ety_00); label="Max: $(maximum(Ety_00))") plt.legend() plt.xlabel("Time (fs)") -plt.title("Y Polarisation") \ No newline at end of file +plt.title("Y Polarisation") diff --git a/src/Fields.jl b/src/Fields.jl index e000b863..46086b6d 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -138,7 +138,7 @@ function (p::PulseField)(grid, FT) Pt = It(Et, grid) Et .*= sqrt(p.power)/sqrt(maximum(Pt)) end - + FT * Et end @@ -389,7 +389,7 @@ Get the field for the provided `grid`, `spacegrid` function and Fourier transform `FT` """ function (s::SpatioTemporalField)(grid, spacegrid, FT) - Etr = make_Etr(s, grid, spacegrid) # (t, r) or (t, x) or (t, y, x) + Etr = make_Etr(s, grid, spacegrid) # (t, r) or (t, x) or (t, x, y) energy_t = energyfuncs(grid, spacegrid)[1] Etr .*= sqrt(s.energy)/sqrt(energy_t(Etr)) Etr = rotate(Etr, s.θ) @@ -423,8 +423,8 @@ end function prop!(Eωk, z, grid, xygrid) kzsq = ((grid.ω ./ PhysData.c).^2 - .- reshape(xygrid.ky.^2, (1, 1, length(xygrid.ky), 1)) - .- reshape(xygrid.kx.^2, (1, 1, 1, length(xygrid.kx))) + .- reshape(xygrid.kx.^2, (1, 1, length(xygrid.kx), 1)) + .- reshape(xygrid.ky.^2, (1, 1, 1, length(xygrid.ky))) ) kzsq[kzsq.<0] .= 0 kz = sqrt.(kzsq) @@ -464,7 +464,7 @@ function int2D(field1, field2, lowerlim, upperlim) end val end - + function normalised_field(fieldfunc, rmax) scale = 1.0/sqrt(int2D(fieldfunc, fieldfunc, (0.0, 0.0), (rmax, 2π))) return let scale=scale, fieldfunc=fieldfunc @@ -482,7 +482,7 @@ end coupled_field(i, mode, E, fieldfunc; energy, kwargs...) Create an element of an input field tuple (for use in `Luna.setup`) based on coupling -field `E` into a `mode`. The index `i` species the mode index. The temporal fields are +field `E` into a `mode`. The index `i` species the mode index. The temporal fields are initialised using `fieldfunc` (e.g. one of `GaussField`, `SechField` etc.) with the same keyword arguments. """ @@ -587,7 +587,7 @@ function energyfuncs(grid::Grid.RealGrid, xygrid::Grid.FreeGrid) prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δy * δt function energy_t(Et) Eta = Maths.hilbert(Et) - return prefac_t * sum(abs2.(Eta)) + return prefac_t * sum(abs2.(Eta)) end δω = grid.ω[2] - grid.ω[1] @@ -608,7 +608,7 @@ function energyfuncs(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid) δt = grid.t[2] - grid.t[1] prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δy * δt function energy_t(Et) - return prefac_t * sum(abs2.(Et)) + return prefac_t * sum(abs2.(Et)) end δω = grid.ω[2] - grid.ω[1] @@ -629,7 +629,7 @@ function energyfuncs(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid) prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δt function energy_t(Et) Eta = Maths.hilbert(Et) - return prefac_t * sum(abs2.(Eta)) + return prefac_t * sum(abs2.(Eta)) end δω = grid.ω[2] - grid.ω[1] @@ -647,7 +647,7 @@ function energyfuncs(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid) δt = grid.t[2] - grid.t[1] prefac_t = PhysData.c*PhysData.ε_0/2 * δx * δt function energy_t(Et) - return prefac_t * sum(abs2.(Et)) + return prefac_t * sum(abs2.(Et)) end δω = grid.ω[2] - grid.ω[1] @@ -831,7 +831,7 @@ prop_mode(Eω, args...) = prop_mode!(copy(Eω), args...) optcomp_taylor(Eω, grid, λ0; order=2) Maximise the peak power of the field `Eω` by adding Taylor-expanded spectral phases up to -order `order`. +order `order`. """ function optcomp_taylor(Eω::AbstractVecOrMat, grid, λ0; order=2, boundfac=8) τ = length(grid.t) * (grid.t[2] - grid.t[1])/2 @@ -881,7 +881,7 @@ _It(Et::AbstractMatrix, grid) = dropdims(sum(It(Et, grid); dims=2); dims=2) """ optcomp_material(Eω, grid, material, λ0; kwargs...) -Maximise the peak power of the field `Eω` by linear propagation through the `material`. +Maximise the peak power of the field `Eω` by linear propagation through the `material`. Keyword arguments `kwargs` are the same as for [`prop_material`](@ref). """ function optcomp_material(Eω::AbstractVecOrMat, grid, material, λ0, diff --git a/src/Grid.jl b/src/Grid.jl index add2ae6b..a9a3c9e6 100644 --- a/src/Grid.jl +++ b/src/Grid.jl @@ -45,7 +45,7 @@ function RealGrid(zmax, referenceλ, λ_lims, trange, δt=1) trange/δto, samples, δto*1e18) Logging.@info @sprintf("Requested time window: %.1f fs, actual time window: %.1f fs", trange*1e15, trange_even*1e15) δωo = 2π/trange_even # frequency spacing for fine grid - # Make fine grid + # Make fine grid Nto = collect(range(0, length=samples)) to = @. (Nto-samples/2)*δto # centre on 0 Nωo = collect(range(0, length=Int(samples/2 +1))) @@ -64,7 +64,7 @@ function RealGrid(zmax, referenceλ, λ_lims, trange, δt=1) t = @. (Nt-tsamples/2)*δt # Make apodisation windows - ωwindow = Maths.planck_taper(ω, ωmin/2, ωmin, ωmax, ωmax_win) + ωwindow = Maths.planck_taper(ω, ωmin/2, ωmin, ωmax, ωmax_win) twindow = Maths.planck_taper(t, minimum(t), -trange/2, trange/2, maximum(t)) towindow = Maths.planck_taper(to, minimum(to), -trange/2, trange/2, maximum(to)) @@ -143,17 +143,17 @@ function EnvGrid(zmax, referenceλ, λ_lims, trange; δt=1, thg=false) Logging.@info @sprintf("Samples needed: %.2f, samples: %d, δt = %.2f as", trange/δto, samples, δto*1e18) δωo = 2π/trange_even # frequency spacing for grid - # Make fine grid + # Make fine grid No = collect(range(0, length=samples)) to = @. (No-samples/2)*δto # time grid, centre on 0 vo = @. (No-samples/2)*δωo # freq grid relative to ω0 vo = FFTW.fftshift(vo) ωo = vo .+ ω0 - + ωmin = 2π*fmin ωmax = 2π*fmax ωmax_win = 2π*fmax_win - + # Find cropping area for coarse grid (contains frequencies of interest + apodisation) if oversampling cropidx = findfirst(x -> x>=(ωmax_win-δωo), ωo) @@ -167,16 +167,16 @@ function EnvGrid(zmax, referenceλ, λ_lims, trange; δt=1, thg=false) tsamples = length(v) Nt = collect(range(0, length=tsamples)) t = @. (Nt - tsamples/2)*δt - + ω = v .+ ω0 # True frequency grid # Indices to select real frequencies (for dispersion relation) - sidx = (ω .> ωmin/2) .& (ω .< ωmax_win) - + sidx = (ω .> ωmin/2) .& (ω .< ωmax_win) + # Make apodisation windows ωwindow = Maths.planck_taper(ω, ωmin/2, ωmin, ωmax, ωmax_win) twindow = Maths.planck_taper(t, minimum(t), -trange/2, trange/2, maximum(t)) towindow = Maths.planck_taper(to, minimum(to), -trange/2, trange/2, maximum(to)) - + # Check that grids are correct @assert δt/δto ≈ length(to)/length(t) @assert δt/δto ≈ minimum(vo)/minimum(v) # FFT grid -> sample at -fs/2 but not +fs/2 @@ -204,10 +204,13 @@ end """ FreeGrid(Rx, Nx, Ry, Ny; window_factor=0.1) + FreeGrid(R, N; window_factor=0.1) Spatial grid for full 3D freespace propagation with `x`/`y` half-width `Rx`/`Ry` and `Nx`/`Ny` samples. `window_factor` determines by how much the grid size is extended to fit a filtering window. + +If only `R` and `N` are given, it is assumed that `Rx = Ry = R` and `Nx = Ny = N`. """ function FreeGrid(Rx, Nx, Ry, Ny; window_factor=0.1) Rxw = Rx * (1 + window_factor) @@ -223,11 +226,11 @@ function FreeGrid(Rx, Nx, Ry, Ny; window_factor=0.1) y = @. (ny-Ny/2) * δy ky = 2π*FFTW.fftfreq(Ny, 1/δy) - r = sqrt.(reshape(y, (1, Ny)).^2 .+ reshape(x, (1, 1, Nx)).^2) + r = sqrt.(reshape(x, (1, Nx)).^2 .+ reshape(y, (1, 1, Ny)).^2) xwin = Maths.planck_taper(x, -Rxw, -Rx, Rx, Rxw) ywin = Maths.planck_taper(y, -Ryw, -Ry, Ry, Ryw) - xywin = reshape(ywin, (1, length(ywin))) .* reshape(xwin, (1, 1, length(xwin))) + xywin = reshape(xwin, (1, length(xwin))) .* reshape(ywin, (1, 1, length(ywin))) FreeGrid(x, y, kx, ky, r, xywin) end diff --git a/src/LinearOps.jl b/src/LinearOps.jl index eb5d21da..f4ff205f 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -28,8 +28,8 @@ end #=============== FREE SPACE ===============# #=================================================# function transverse_k2(xygrid::Grid.FreeGrid) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' + idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) kperp2, idcs end @@ -41,16 +41,16 @@ end function transverse_k2(xygrid::Hankel.QDHT) kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) kperp2, idcs end """ - make_const_linop(grid, xygrid, n, β1) + make_const_linop(grid, xygrid, n, β1, β0) -Make constant linear operator for full 3D propagation. `n` is the refractive index (array) -and β1 is 1/velocity of the reference frame. +Make constant linear operator for free-space propagation. `n` is the refractive index (array), +β1 is 1/velocity of the reference frame and β0 is the wavevector at the reference wavelength. """ function make_const_linop(grid::Grid.AbstractGrid, xygrid::Union{Grid.FreeGrid, Grid.Free2DGrid, Hankel.QDHT}, @@ -88,11 +88,11 @@ function make_const_linop(grid::Grid.AbstractGrid, make_const_linop(grid, xygrid, n, β1, β0) end -function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns) +function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns::Tuple) nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) # nfuny(λ; z) just takes wavelength - out = zeros(ComplexF64, (length(grid.ω), 2, length(xygrid.ky), length(xygrid.kx))) + out = zeros(ComplexF64, (length(grid.ω), 2, length(xygrid.kx), length(xygrid.ky))) β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) for (iω, si) in enumerate(grid.sidx) if si @@ -105,11 +105,11 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 β_xpol = βsq_xpol < 0 ? -min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) - out[iω, 1, iky, ikx] = -im*(β_xpol - β1*grid.ω[iω]) - + out[iω, 1, ikx, iky] = -im*(β_xpol - β1*grid.ω[iω]) + βsq_ypol = ksq_ypol - kxi^2 - kyi^2 β_ypol = βsq_ypol < 0 ? -min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) - out[iω, 2, iky, ikx] = -im*(β_ypol - β1*grid.ω[iω]) + out[iω, 2, ikx, iky] = -im*(β_ypol - β1*grid.ω[iω]) end end end @@ -124,8 +124,8 @@ Make z-dependent linear operator for free-space propagation. `nfun(ω; z)` shoul refractive index as a function of frequency `ω` and (kwarg) propagation distance `z`. """ function make_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' + idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) k2 = zero(grid.ω) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) function linop!(out, z) @@ -136,8 +136,8 @@ function make_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) end function make_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun; thg=false) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) + kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' + idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) k2 = zero(grid.ω) nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) function linop!(out, z) @@ -169,7 +169,7 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tu βsq_xpol = k_xpol^2 - kxi^2 β_xpol = βsq_xpol < 0 ? -min(sqrt(abs(βsq_xpol)), 200) : sqrt(βsq_xpol) out[iω, 1, ik] = -im*(β_xpol - β1*grid.ω[iω]) - + βsq_ypol = ksq_ypol - kxi^2 β_ypol = βsq_ypol < 0 ? -min(sqrt(abs(βsq_ypol)), 200) : sqrt(βsq_ypol) out[iω, 2, ik] = -im*(β_ypol - β1*grid.ω[iω]) @@ -363,7 +363,7 @@ getβ0_mode(grid::Grid.EnvGrid, mode, λ0, thg) = thg ? 0.0 : Modes.β(mode, wlf Make constant linear operator for mode-averaged propagation in mode `mode` with a reference wavelength `λ0`. """ -function make_const_linop(grid::Grid.AbstractGrid, mode::Modes.AbstractMode, λ0; +function make_const_linop(grid::Grid.AbstractGrid, mode::Modes.AbstractMode, λ0; thg::Bool=thg_default(grid)) checkthg(grid, thg) β1 = Modes.dispersion(mode, 1, wlfreq(λ0)) @@ -460,7 +460,7 @@ end make_const_linop(grid, modes, λ0; ref_mode=1) Make constant (z-invariant) linear operator for multimode propagation. The frame velocity is -taken as the group velocity at wavelength `λ0` in the mode given by `ref_mode` (which +taken as the group velocity at wavelength `λ0` in the mode given by `ref_mode` (which indexes into `modes`) """ function make_const_linop(grid::Grid.RealGrid, modes::Modes.ModeCollection, λ0; ref_mode=1) @@ -555,4 +555,4 @@ function make_linop(grid::Grid.EnvGrid, modes, λ0; ref_mode=1, thg=false) end -end \ No newline at end of file +end diff --git a/src/Luna.jl b/src/Luna.jl index 6021a0cc..aa66de3b 100644 --- a/src/Luna.jl +++ b/src/Luna.jl @@ -36,7 +36,7 @@ end """ set_fftw_threads(nthr) -Set number of threads to be used by FFTW. If set to `0`, the number of threads used by +Set number of threads to be used by FFTW. If set to `0`, the number of threads used by FFTW is determined automatically (see [`Utils.FFTWthreads()`](@ref)) """ function set_fftw_threads(nthr=0) @@ -307,14 +307,14 @@ function setup(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, Utils.loadFFTwisdom() np = size(normfun(0), 2) # number of polarisation directions (1 or 2) x = xygrid.x - y = xygrid.y - xr = Array{Float64}(undef, length(grid.t), np, length(y), length(x)) + y = xygrid.y + xr = Array{Float64}(undef, length(grid.t), np, length(x), length(y)) FT = FFTW.plan_rfft(xr, (1, 3, 4), flags=settings["fftw_flag"]) - Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) - xr_xy = Array{Float64}(undef, length(grid.t), 2, length(y), length(x)) + Eωk = zeros(ComplexF64, length(grid.ω), np, length(x), length(y)) + xr_xy = Array{Float64}(undef, length(grid.t), 2, length(x), length(y)) FT_xy = FFTW.plan_rfft(xr_xy, (1, 3, 4), flags=settings["fftw_flag"]) doinputs_fs!(Eωk, grid, xygrid, FT_xy, inputs) - xo = Array{Float64}(undef, length(grid.to), np, length(y), length(x)) + xo = Array{Float64}(undef, length(grid.to), np, length(x), length(y)) FTo = FFTW.plan_rfft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, responses, densityfun, normfun, np > 1) @@ -333,14 +333,14 @@ function setup(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, Utils.loadFFTwisdom() np = size(normfun(0), 2) # number of polarisation directions (1 or 2) x = xygrid.x - y = xygrid.y - xr = Array{ComplexF64}(undef, length(grid.t), np, length(y), length(x)) + y = xygrid.y + xr = Array{ComplexF64}(undef, length(grid.t), np, length(x), length(y)) FT = FFTW.plan_fft(xr, (1, 3, 4), flags=settings["fftw_flag"]) - Eωk = zeros(ComplexF64, length(grid.ω), np, length(y), length(x)) - xr_xy = Array{ComplexF64}(undef, length(grid.t), 2, length(y), length(x)) + Eωk = zeros(ComplexF64, length(grid.ω), np, length(x), length(y)) + xr_xy = Array{ComplexF64}(undef, length(grid.t), 2, length(x), length(y)) FT_xy = FFTW.plan_fft(xr_xy, (1, 3, 4), flags=settings["fftw_flag"]) doinputs_fs!(Eωk, grid, xygrid, FT_xy, inputs) - xo = Array{ComplexF64}(undef, length(grid.to), np, length(y), length(x)) + xo = Array{ComplexF64}(undef, length(grid.to), np, length(x), length(y)) FTo = FFTW.plan_fft(xo, (1, 3, 4), flags=settings["fftw_flag"]) transform = NonlinearRHS.TransFree(grid, xygrid, FTo, responses, densityfun, normfun, np > 1) @@ -356,7 +356,7 @@ function setup(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, x = xgrid.x np = size(normfun(0), 2) # number of polarisation directions (1 or 2) tshape = (length(grid.t), np, length(x)) - ωshape = (length(grid.ω), np, length(x)) + ωshape = (length(grid.ω), np, length(x)) xr = Array{Float64}(undef, tshape) FT = FFTW.plan_rfft(xr, (1, 3), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, ωshape) @@ -379,7 +379,7 @@ function setup(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, x = xgrid.x np = size(normfun(0), 2) # number of polarisation directions (1 or 2) tshape = (length(grid.t), np, length(x)) - ωshape = (length(grid.ω), np, length(x)) + ωshape = (length(grid.ω), np, length(x)) xr = Array{ComplexF64}(undef, tshape) FT = FFTW.plan_fft(xr, (1, 3), flags=settings["fftw_flag"]) Eωk = zeros(ComplexF64, ωshape) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 3f9f42cb..166e9590 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -572,11 +572,11 @@ end function TransFree(TT, scale, grid, xygrid, FT, responses, densityfun, normfun, pol=false) Ny = length(xygrid.y) Nx = length(xygrid.x) - Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, Ny, Nx)) - Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, Ny, Nx)) + Eωo = zeros(ComplexF64, (length(grid.ωo), pol ? 2 : 1, Nx, Ny)) + Eto = zeros(TT, (length(grid.to), pol ? 2 : 1, Nx, Ny)) Pto = similar(Eto) Pωo = similar(Eωo) - idcs = CartesianIndices((Ny, Nx)) + idcs = CartesianIndices((Nx, Ny)) TransFree(FT, normfun, responses, grid, xygrid, densityfun, Pto, Eto, Eωo, Pωo, scale, idcs) end @@ -617,10 +617,10 @@ and place the result in `nl`. function (t::TransFree)(nl, Eωk, z) fill!(t.Eωo, 0) copy_scale!(t.Eωo, Eωk, length(t.grid.ω), t.scale) - ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, ky, kx) -> (t, y, x) + ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, kx, ky) -> (t, x, y) Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses @. t.Pto *= t.grid.towin # apodisation - mul!(t.Pωo, t.FT, t.Pto) # transform (t, y, x) -> (ω, ky, kx) + mul!(t.Pωo, t.FT, t.Pto) # transform (t, x, y) -> (ω, kx, ky) copy_scale!(nl, t.Pωo, length(t.grid.ω), 1/t.scale) nl .*= t.grid.ωwin .* (-im.*t.grid.ω)./(2 .* t.normfun(z)) end @@ -665,9 +665,9 @@ function norm_free(grid, xygrid, nfun) ω = grid.ω ωfirst = ω[findfirst(grid.sidx)] np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.ky), length(xygrid.kx))) - out = zeros(Float64, (length(grid.ω), np, length(xygrid.ky), length(xygrid.kx))) + kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' + idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) + out = zeros(Float64, (length(grid.ω), np, length(xygrid.kx), length(xygrid.ky))) function norm(z) for ii in idcs for iω in eachindex(ω) @@ -695,7 +695,7 @@ function norm_free(grid, xygrid, nfuns::Tuple) # here nfunx(λ, δθ; z) also takes the angle and returns n_x(λ, θ+δθ) # nfuny(λ; z) just takes wavelength ω = grid.ω - out = zeros(Float64, (length(ω), 2, length(xygrid.ky), length(xygrid.kx))) + out = zeros(Float64, (length(ω), 2, length(xygrid.kx), length(xygrid.ky))) function norm(z) for iω in eachindex(ω) if ω[iω] == 0 || ~grid.sidx[iω] @@ -711,16 +711,16 @@ function norm_free(grid, xygrid, nfuns::Tuple) k_xpol = nx*grid.ω[iω]/c βsq_xpol = k_xpol^2 - kxi^2 - kyi^2 if βsq_xpol < 0 - out[iω, 1, iky, ikx] = 1.0 + out[iω, 1, ikx, iky] = 1.0 else - out[iω, 1, iky, ikx] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) + out[iω, 1, ikx, iky] = sqrt(βsq_xpol)/(PhysData.μ_0*ω[iω]) end βsq_ypol = ksq_ypol - kxi^2 - kyi^2 if βsq_ypol < 0 - out[iω, 2, iky, ikx] .= 1.0 + out[iω, 2, ikx, iky] .= 1.0 else - out[iω, 2, iky, ikx] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) + out[iω, 2, ikx, iky] = sqrt(βsq_ypol)/(PhysData.μ_0*ω[iω]) end end end @@ -801,7 +801,7 @@ function (t::TransFree2D)(nl, Eωk, z) # TODO: this can probably be combined with the case for TransFree fill!(t.Eωo, 0) copy_scale!(t.Eωo, Eωk, length(t.grid.ω), t.scale) - ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, ky, kx) -> (t, y, x) + ldiv!(t.Eto, t.FT, t.Eωo) # transform (ω, kx) -> (t, x) Et_to_Pt!(t.Pto, t.Eto, t.resp, t.densityfun(z), t.idcs) # add up responses @. t.Pto *= t.grid.towin # apodisation mul!(t.Pωo, t.FT, t.Pto) # transform (t, x) -> (ω, kx) diff --git a/src/Processing.jl b/src/Processing.jl index 7ec50404..8fdeed43 100644 --- a/src/Processing.jl +++ b/src/Processing.jl @@ -247,7 +247,7 @@ end time_bandwidth(grid, Eω; bandpass=nothing, oversampling=1) Extract the time-bandwidth product, after bandpassing if required. The TBP -is defined here as ΔfΔt where Δx is the FWHM of x. (In this definition, the TBP of +is defined here as ΔfΔt where Δx is the FWHM of x. (In this definition, the TBP of a perfect Gaussian pulse is ≈0.44). If `oversampling` > 1, the time-domain field is oversampled before extracting the FWHM. """ @@ -595,7 +595,7 @@ end """ getEω(output[, zslice]) -Get frequency-domain modal field from `output` with correct normalisation (i.e. +Get frequency-domain modal field from `output` with correct normalisation (i.e. `abs2.(Eω)`` gives angular-frequency spectral energy density in J/(rad/s)). """ getEω(output::AbstractOutput, args...) = getEω(makegrid(output), output, args...) @@ -630,7 +630,7 @@ fftnorm(grid::EnvGrid) = Maths.fftnorm(grid.t[2] - grid.t[1]) getφ(ω, Eω, τ) Extract the unwrapped spectral phase from the field `Eω`, subtracting the linear phase ramp corresponding -to a pulse in the middle of the time window defined by the `grid`. +to a pulse in the middle of the time window defined by the `grid`. """ function getφ(grid::AbstractGrid, Eω) ω = grid.ω @@ -721,7 +721,7 @@ If `relative` is `true`, `width` is relative bandwidth instead of the wavelength `ndims` determines how many dimensions of the array to sum over. For a field array with size `(Nω, N1, N2, ...)`, the first dimension is always assumed to be frequency. `ndim=1` means each field to be analysed is 1-dimensional, so the window iterates over all of `(N1, N2, ...)`. -`ndim=2` means each field to be analysed is 2-dimensional, `(Nω, N1)` in size, and will be +`ndim=2` means each field to be analysed is 2-dimensional, `(Nω, N1)` in size, and will be summed over its second dimension before finding the central frequency. The window iterates over all other dimensions, `(N2, ...)`. @@ -765,7 +765,7 @@ end PeakWindow(width, λmin, λmax; relative=false, ndims=1) An [`AutoWindow`](@ref) which uses the peak of the spectral energy density as the central -frequency. +frequency. """ function PeakWindow(width, λmin, λmax; relative=false, ndims=1) ω0fun = (ω, Iω) -> ω[argmax(Iω)] @@ -776,7 +776,7 @@ end CentroidWindow(width, λmin, λmax; relative=false, ndims=1, power=1) An [`AutoWindow`](@ref) which uses the centroid (centre of mass or first moment) of the -spectral energy density as the central frequency. Before calculating the centroid, the +spectral energy density as the central frequency. Before calculating the centroid, the SED is raised to the `power` given. """ function CentroidWindow(width, λmin, λmax; relative=false, ndims=1, power=1) @@ -972,7 +972,7 @@ function beam(grid, Eωm, modes, x, y; z=0, components=:xy) xs = coords == :polar ? (hypot(xi, yi), atan(yi, xi)) : (xi, yi) Modes.to_space!(Eωxy, Eωm, xs, tospace; z) # integrate over time/frequency and multiply by ε₀c/2 -> fluence - fluence[yidx, xidx] = PhysData.ε_0*PhysData.c/2*sum(energy_ω(Eωxy)) + fluence[xidx, yidx] = PhysData.ε_0*PhysData.c/2*sum(energy_ω(Eωxy)) end end fluence @@ -1009,13 +1009,13 @@ end function getEtxy(Etm, modes, xs::Tuple{AbstractVector, AbstractVector}, z; components=:xy) tospace = Modes.ToSpace(modes; components) x1, x2 = xs - Etxy = zeros(eltype(Etm), (size(Etm, 1), length(x1), length(x2), tospace.npol)) + Etxy = zeros(eltype(Etm), (size(Etm, 1), tospace.npol, length(x1), length(x2))) for (x2idx, x2i) in enumerate(x2) for (x1idx, x1i) in enumerate(x1) - @views Modes.to_space!(Etxy[:, x1idx, x2idx, :], Etm[.., 1], (x1i, x2i), tospace; z) + @views Modes.to_space!(Etxy[:, :, x1idx, x2idx], Etm[.., 1], (x1i, x2i), tospace; z) end end - Etxy + Etxy end function polarisation_components(output) @@ -1094,4 +1094,4 @@ of the total propagation distance. nearest_z(output, z::Number) = z < 0 ? [round(Int, min(abs(z), 1)*length(output["z"]))] : [argmin(abs.(output["z"] .- z))] nearest_z(output, z) = [nearest_z(output, zi)[1] for zi in z] -end \ No newline at end of file +end diff --git a/test/test_fields.jl b/test/test_fields.jl index 2bb3192d..d735f145 100644 --- a/test/test_fields.jl +++ b/test/test_fields.jl @@ -81,7 +81,7 @@ end Eω = input(grid, FT) Et = FT \ Eω @test isapprox(energy_t(Et), energy, rtol=1e-14) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=ϕ) Eω = input(grid, FT) Et = FT \ Eω @@ -124,7 +124,7 @@ end Et = FT \ Eω It = abs2.(Maths.hilbert(Et)) @test isapprox(Maths.fwhm(grid.t, It), τfwhm, rtol=1e-5) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=ϕ) Eω = input(grid, FT) Et = FT \ Eω @@ -173,7 +173,7 @@ end Et = FT \ Eω It = abs2.(Maths.hilbert(Et)) @test isapprox(grid.t[argmax(It)], τ0, rtol=1e-15, atol=1e-15) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=ϕ) Eω = input(grid, FT) Et = FT \ Eω @@ -208,7 +208,7 @@ end # non zero τ0 = -564e-15 - #real + #real τfwhm = 30e-15 λ0 = 800e-9 energy = 1e-6 @@ -226,7 +226,7 @@ end Et = FT \ Eω It = abs2.(Maths.hilbert(Et)) @test isapprox(grid.t[argmax(It)], τ0, rtol=1e-15, atol=1e-15) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=ϕ) Eω = input(grid, FT) Et = FT \ Eω @@ -275,7 +275,7 @@ end Et = FT \ Eω It = abs2.(Maths.hilbert(Et)) @test isapprox(getceo(grid.t, Et, It, PhysData.wlfreq(λ0)), ϕCEO, rtol=1e-15, atol=1e-15) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=[ϕCEO]) Eω = input(grid, FT) Et = FT \ Eω @@ -306,7 +306,7 @@ end # non zero - #real + #real τfwhm = 30e-15 λ0 = 800e-9 energy = 1e-6 @@ -326,7 +326,7 @@ end Et = FT \ Eω It = abs2.(Maths.hilbert(Et)) @test isapprox(abs(getceo(grid.t, Et, It, PhysData.wlfreq(λ0))), ϕCEO, rtol=1e-10) - + input = Fields.SechField(λ0=λ0, τfwhm=τfwhm, energy=energy, ϕ=[ϕCEO]) Eω = input(grid, FT) Et = FT \ Eω @@ -388,14 +388,14 @@ end @test isapprox(mean(I[istart:iend]), Pavg, rtol=5e-16) # test coherence time @test isapprox(Processing.coherence_time(grid, Et), 3.35/(PhysData.c*(Δλ)/λ0^2*2π), rtol=1e-2) - idcs = sortperm(PhysData.wlfreq.(grid.ω)) + idcs = sortperm(PhysData.wlfreq.(grid.ω)) # test spectral width @test isapprox(Maths.fwhm(PhysData.wlfreq.(grid.ω)[idcs], abs2.(Eω[idcs])), Δλ, rtol=3e-3) # now do the same for a number of realisations Eωs = hcat([Fields.CWSech(λ0=λ0, Pavg=Pavg, Δλ=Δλ, rng=MersenneTwister(i))(grid, FT) for i = 1:5]...) Iωs = abs2.(Eωs) Iωav = mean(Iωs, dims=2)[:,1] - idcs = sortperm(PhysData.wlfreq.(grid.ω)) + idcs = sortperm(PhysData.wlfreq.(grid.ω)) # test average spectral width @test isapprox(Maths.fwhm(PhysData.wlfreq.(grid.ω)[idcs], Iωav[idcs], minmax=:max), Δλ, rtol=6e-4) Ets = FFTW.ifft(Eωs, 1) @@ -652,7 +652,7 @@ end @test inputs[7].fields[1].energy/energy < 1e-20 @test inputs[8].fields[1].energy/energy < 1e-20 - # Now test that overlap integrals also work for diverging beams and produce + # Now test that overlap integrals also work for diverging beams and produce # sensible results a = 100e-6 w0 = 0.64a @@ -753,7 +753,7 @@ end zr = π*w0^2/λ0 w1 = w0*sqrt(1 + (propz/zr)^2) - + R = 4w1 N = 1024 @@ -786,13 +786,13 @@ end zr = π*w0^2/λ0 w1 = w0*sqrt(1 + (propz/zr)^2) - + R = 2w1 N = 256 grid = Grid.EnvGrid(1, λ0, (400e-9, 6e-6), 100e-15) xygrid = Grid.FreeGrid(R, N) - xr = Array{ComplexF64}(undef, length(grid.t), 2, length(xygrid.y), length(xygrid.x)) + xr = Array{ComplexF64}(undef, length(grid.t), 2, length(xygrid.x), length(xygrid.y)) FT = FFTW.plan_fft(xr, (1, 3, 4), flags=FFTW.ESTIMATE) Eωk = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz)(grid, xygrid, FT) diff --git a/test/test_full_freespace.jl b/test/test_full_freespace.jl index c5824218..c83fef54 100644 --- a/test/test_full_freespace.jl +++ b/test/test_full_freespace.jl @@ -1,6 +1,6 @@ import Test: @test, @testset -@testset "Full 3D propagation" begin +@testset "Full 3D propagation - square grid" begin using Luna Luna.set_fftw_mode(:estimate) import FFTW @@ -43,19 +43,84 @@ output = Output.MemoryOutput(0, grid.zmax, 21) Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) -Eout = output.data["Eω"] # (ω, ky, kx, z) +Eout = output.data["Eω"] # (ω, kx, ky, z) ω0idx = argmin(abs.(grid.ω .- wlfreq(λ0))) λ0 = 2π*PhysData.c/grid.ω[ω0idx] w1 = w0*sqrt(1+(L*λ0/(π*w0^2))^2) Iω0_analytic = Maths.gauss.(xygrid.x, w1/2) # analytical solution (in paraxial approx) -Eω0yx = FFTW.ifft(Eout[ω0idx, :, :, end], (1, 2)) -Iω0yx = abs2.(Eω0yx) -Iω0y = Maths.normbymax(dropdims(sum(Iω0yx, dims=2), dims=2)) -Iω0x = Maths.normbymax(dropdims(sum(Iω0yx, dims=1), dims=1)) +Eω0xy = FFTW.ifft(Eout[ω0idx, 1, :, :, end], (1, 2)) +Iω0xy = abs2.(Eω0xy) +Iω0y = Maths.normbymax(dropdims(sum(Iω0xy, dims=2), dims=2)) +Iω0x = Maths.normbymax(dropdims(sum(Iω0xy, dims=1), dims=1)) @test maximum(abs.(Iω0x .- Iω0_analytic)/norm(Iω0x)) < 5e-5 @test maximum(abs.(Iω0y .- Iω0_analytic)/norm(Iω0y)) < 5e-5 -end \ No newline at end of file +end +## +@testset "Full 3D propagation - non-square grid" begin +using Luna +Luna.set_fftw_mode(:estimate) +import FFTW +import Luna.PhysData: wlfreq +import LinearAlgebra: norm + +gas = :Ar +pres = 1 + +τ = 10e-15 +λ0 = 800e-9 + +w0 = 500e-6 +energy = 1e-12 +L = 2 + +R = 4e-3 +Nx = 128 +Ny = 256 + +saveN = 21 + +grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 80e-15) +xygrid = Grid.FreeGrid(R, Nx, R, Ny) + +x = xygrid.x +y = xygrid.y +energyfun, energyfunω = Fields.energyfuncs(grid, xygrid) + +dens0 = PhysData.density(gas, pres) +densityfun(z) = dens0 + +responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)),) + +linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) +normfun = NonlinearRHS.const_norm_free(grid, xygrid, PhysData.ref_index_fun(gas, pres)) + +inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0) + +Eω, transform, FT = Luna.setup(grid, xygrid, densityfun, normfun, responses, inputs) + +output = Output.MemoryOutput(0, grid.zmax, saveN) + +Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) + +Eout = output.data["Eω"] # (ω, pol, kx, ky, z) +@test size(Eout) == (length(grid.ω), 1, Nx, Ny, saveN) + +ω0idx = argmin(abs.(grid.ω .- wlfreq(λ0))) +λ0 = 2π*PhysData.c/grid.ω[ω0idx] +w1 = w0*sqrt(1+(L*λ0/(π*w0^2))^2) +Iω0x_analytic = Maths.gauss.(xygrid.x, w1/2) # analytical solution (in paraxial approx) +Iω0y_analytic = Maths.gauss.(xygrid.y, w1/2) # analytical solution (in paraxial approx) + +Eω0xy = FFTW.ifft(Eout[ω0idx, 1, :, :, end]) +Iω0xy = abs2.(Eω0xy) +Iω0y = Maths.normbymax(dropdims(sum(Iω0xy, dims=1), dims=1)) +Iω0x = Maths.normbymax(dropdims(sum(Iω0xy, dims=2), dims=2)) + +@test maximum(abs.(Iω0x .- Iω0x_analytic)/norm(Iω0x)) < 5e-5 +@test maximum(abs.(Iω0y .- Iω0y_analytic)/norm(Iω0y)) < 5e-5 + +end From 34495482e98956af5937a83ff6ba306ed20fc199 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 08:34:11 +0000 Subject: [PATCH 48/72] fluence shape --- src/Processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Processing.jl b/src/Processing.jl index 8fdeed43..d62bbddf 100644 --- a/src/Processing.jl +++ b/src/Processing.jl @@ -963,7 +963,7 @@ end function beam(grid, Eωm, modes, x, y; z=0, components=:xy) tospace = Modes.ToSpace(modes; components) - fluence = zeros(length(y), length(x)) + fluence = zeros(length(x), length(y)) _, energy_ω = Fields.energyfuncs(grid) # energyfuncs include correct FFT normalisation Eωxy = zeros(ComplexF64, (length(grid.ω), tospace.npol)) coords = Modes.dimlimits(modes[1])[1] From 29921cce8e1cb15e3c58828703af222cfe994cf4 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 08:57:47 +0000 Subject: [PATCH 49/72] remove duplicated make_const_linop --- src/LinearOps.jl | 61 +++------------------------------------------ test/test_fields.jl | 4 +-- 2 files changed, 5 insertions(+), 60 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index f4ff205f..2233ca8c 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -39,9 +39,9 @@ function transverse_k2(xgrid::Grid.Free2DGrid) kperp2, idcs end -function transverse_k2(xygrid::Hankel.QDHT) - kperp2 = @. (xygrid.kx^2)' + xygrid.ky^2 - idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) +function transverse_k2(q::Hankel.QDHT) + kperp2 = @. q.k^2 + idcs = CartesianIndices(q.k) kperp2, idcs end @@ -225,61 +225,6 @@ end #=================================================# #============== RADIAL SYMMETRY ==============# #=================================================# -""" - make_const_linop(grid, q::QDHT, n, β1) - -Make constant linear operator for radial free-space. `n` is the refractive index (array) -and β1 is 1/velocity of the reference frame. -""" -function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, - n::AbstractVecOrMat, β1::Number) - out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) - k2 = @. (n*grid.ω/c)^2 - kr2 = q.k.^2 - fill_linop_matrix!(out, grid, β1, 0.0, k2, kr2, eachindex(q.k)) - return out -end - -function make_const_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst)) # 1 if single ref index, 2 if nx, ny - n = zeros(Float64, (length(grid.ω), np)) - for (ii, si) in enumerate(grid.sidx) - if si - n[ii, :] .= nfun(wlfreq(grid.ω[ii])) - end - end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) - make_const_linop(grid, q, n, β1) -end - -function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst)) # 1 if single ref index, 2 if nx, ny - n = zeros(Float64, (length(grid.ω), np)) - for (ii, si) in enumerate(grid.sidx) - if si - n[ii, :] .= nfun(wlfreq(grid.ω[ii])) - end - end - β1 = PhysData.dispersion_func(1, λ -> nfun(λ)[end])(grid.referenceλ) - if thg - β0const = 0.0 - else - β0const = grid.ω0/c * nfun(2π*c./grid.ω0)[1] - end - make_const_linop(grid, q, n, β1, β0const) -end - -function make_const_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, - n::AbstractVecOrMat, β1::Number, β0ref::Number) - out = Array{ComplexF64}(undef, (length(grid.ω), size(n, 2), q.N)) - k2 = @. (n*grid.ω/c)^2 - kr2 = q.k.^2 - fill_linop_matrix!(out, grid, β1, β0ref, k2, kr2, eachindex(q.k)) - return out -end - """ make_linop(grid, q::QDHT, nfun) diff --git a/test/test_fields.jl b/test/test_fields.jl index d735f145..d388c72c 100644 --- a/test/test_fields.jl +++ b/test/test_fields.jl @@ -743,7 +743,7 @@ end end end -# @testset "free-space inputs: radial" begin +@testset "free-space inputs: radial" begin λ0 = 800e-9 τfwhm = 10e-15 energy = 1e-6 @@ -774,7 +774,7 @@ end w1q = 2Maths.rms_width(r, Ir) @test isapprox(w1q, w1; rtol=1e-3) -# end +end @testset "free-space inputs: full 3D" begin λ0 = 800e-9 From c2513eae671f8af35937f238cac3bf94135018fa Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 09:27:02 +0000 Subject: [PATCH 50/72] unify make_linop for free-space grids --- src/LinearOps.jl | 194 ++++++++++++++++++-------------------------- test/test_radial.jl | 67 +++++++++++++++ 2 files changed, 148 insertions(+), 113 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 2233ca8c..d8f2f764 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -7,6 +7,10 @@ import Luna.PhysData: wlfreq, c, crystal_internal_angle getω0(grid::Grid.EnvGrid) = grid.ω0 getω0(grid::Grid.RealGrid) = 0.0 +#=================================================# +#=============== FREE SPACE ===============# +#=================================================# + function fill_linop_matrix!(out, grid, β1::Number, βref::Number, k2, kperp2, idcs) ω0 = getω0(grid) for ii in idcs @@ -24,9 +28,6 @@ function fill_linop_matrix!(out, grid, β1::Number, βref::Number, k2, kperp2, i end end -#=================================================# -#=============== FREE SPACE ===============# -#=================================================# function transverse_k2(xygrid::Grid.FreeGrid) kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) @@ -49,8 +50,16 @@ end """ make_const_linop(grid, xygrid, n, β1, β0) -Make constant linear operator for free-space propagation. `n` is the refractive index (array), -β1 is 1/velocity of the reference frame and β0 is the wavevector at the reference wavelength. +Low-level constructor for a constant (z-invariant) free-space linear operator. + +Arguments: +- `grid`: `Grid.AbstractGrid` (`RealGrid` or `EnvGrid`) +- `xygrid`: transverse grid (`Grid.FreeGrid`, `Grid.Free2DGrid`, or `Hankel.QDHT`) +- `n`: refractive-index table on `grid.ω`, with one column per polarisation +- `β1`: inverse reference-frame velocity +- `β0`: reference wavevector offset (typically zero for `RealGrid` and optional for `EnvGrid`) + +The output has shape `(Nω, Npol, N⊥...)`, where `N⊥...` matches the transverse grid. """ function make_const_linop(grid::Grid.AbstractGrid, xygrid::Union{Grid.FreeGrid, Grid.Free2DGrid, Hankel.QDHT}, @@ -71,6 +80,15 @@ checkthg(grid::Grid.RealGrid, thg) = thg || error("`thg` must be `true` for `Rea getβ0_n(grid::Grid.RealGrid, nfun, thg) = 0.0 getβ0_n(grid::Grid.EnvGrid, nfun, thg) = thg ? 0.0 : grid.ω0/c * nfun(wlfreq(grid.ω0))[end] +""" + make_const_linop(grid, xygrid, nfun; thg=thg_default(grid)) + +Build a constant free-space operator from a refractive-index function `nfun`. + +`nfun(λ)` must return either a scalar index (single polarisation) or a vector/tuple of indices +(e.g. both x and y polarisation). For `EnvGrid`, `thg=false` subtracts the reference propagation +constant at `grid.ω0`; `thg=true` keeps the full phase. +""" function make_const_linop(grid::Grid.AbstractGrid, xygrid::Union{Grid.FreeGrid, Grid.Free2DGrid, Hankel.QDHT}, nfun, thg::Bool=thg_default(grid)) @@ -88,6 +106,15 @@ function make_const_linop(grid::Grid.AbstractGrid, make_const_linop(grid, xygrid, n, β1, β0) end +""" + make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns::Tuple) + +Constant full-3D free-space operator for crystal optics with two polarisation branches. + +`nfuns = (nfunx, nfuny)`, where `nfunx(λ, δθ)` is the refractive index for x-polarisation +and `nfuny(λ)` is the refractive index for y-polarisation. The reference frame velocity +is calculated from `nfuny(λ)`. +""" function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns::Tuple) nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) @@ -120,38 +147,40 @@ end """ make_linop(grid, xygrid, nfun) -Make z-dependent linear operator for free-space propagation. `nfun(ω; z)` should return the -refractive index as a function of frequency `ω` and (kwarg) propagation distance `z`. +Create a z-dependent free-space linear operator closure. + +Applies to `xygrid::Grid.FreeGrid` (full 3D), `Grid.Free2DGrid` (x-z), and +`Hankel.QDHT` (radial symmetry). + +Returns `linop!(out, z)`, which fills `out` in-place for propagation distance `z`. +`nfun(ω; z)` may return one or multiple refractive indices; the last branch +defines the reference-frame velocity and, for `EnvGrid` with `thg=false`, the +reference phase subtraction at `grid.ω0`. """ -function make_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfun) - kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' - idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) - k2 = zero(grid.ω) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) +function make_linop(grid::Grid.AbstractGrid, + xygrid::Union{Grid.FreeGrid,Grid.Free2DGrid,Hankel.QDHT}, + nfun, thg::Bool=thg_default(grid)) + checkthg(grid, thg) + kperp2, idcs = transverse_k2(xygrid) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ); z)[end] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx] ./ c).^2 - fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) + β0 = getβ0_n(grid, nfun, thg) + k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z) .* grid.ω[grid.sidx] ./ c).^2 + fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) end end -function make_linop(grid::Grid.EnvGrid, xygrid::Grid.FreeGrid, nfun; thg=false) - kperp2 = @. xygrid.kx^2 + (xygrid.ky^2)' - idcs = CartesianIndices((length(xygrid.kx), length(xygrid.ky))) - k2 = zero(grid.ω) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z) - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z=z).*grid.ω[grid.sidx]./c).^2 - βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z) - fill_linop_matrix!(out, grid, β1, βref, k2, kperp2, idcs) - end -end +""" + make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tuple) -#=================================================# -#============ FREE SPACE (2D) ================# -#=================================================# +Constant free-space operator for 2D (`x-z`) crystal propagation with two polarisations. +`nfuns = (nfunx, nfuny)` follows the same convention as the full-3D overload. +""" function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tuple) nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) @@ -179,85 +208,6 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tu out end -""" - make_linop(grid, xgrid, nfun) - -Make z-dependent linear operator for free-space propagation. `nfun(ω; z)` should return the -refractive index as a function of frequency `ω` and (kwarg) propagation distance `z`. -""" -function make_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfun) - kperp2 = xgrid.kx.^2 - idcs = CartesianIndices(xgrid.kx) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[1] - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - for (ii, si) in enumerate(grid.sidx) - if si - k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii]./c).^2 - end - end - fill_linop_matrix!(out, grid, β1, 0.0, k2, kperp2, idcs) - end -end - -function make_linop(grid::Grid.EnvGrid, xgrid::Grid.Free2DGrid, nfun; thg=false) - kperp2 = xgrid.kx.^2 - idcs = CartesianIndices(xgrid.kx) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ); z)[1] - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - for (ii, si) in enumerate(grid.sidx) - if si - k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii]./c).^2 - end - end - βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] - fill_linop_matrix!(out, grid, β1, βref, k2, kperp2, idcs) - end -end - -#=================================================# -#============== RADIAL SYMMETRY ==============# -#=================================================# -""" - make_linop(grid, q::QDHT, nfun) - -Make z-dependent linear operator for radial free-space propagation. `nfun(ω; z)` should -return the refractive index as a function of frequency `ω` and (kwarg) propagation -distance `z`. -""" -function make_linop(grid::Grid.RealGrid, q::Hankel.QDHT, nfun) - kr2 = q.k.^2 - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[end] - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 - fill_linop_matrix!(out, grid, β1, 0.0, k2, kr2, eachindex(q.k)) - end -end - -function make_linop(grid::Grid.EnvGrid, q::Hankel.QDHT, nfun; thg=false) - kr2 = q.k.^2 - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ), z=z)[end] - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - k2[grid.sidx, :] .= (nfun.(grid.ω[grid.sidx]; z=z) .* grid.ω[grid.sidx]./c).^2 - βref = thg ? 0.0 : grid.ω0/c * nfun(grid.ω0; z=z)[end] - fill_linop_matrix!(out, grid, β1, βref, k2, kr2, eachindex(q.k)) - end -end #=================================================# #=============== MODE AVERAGE ================# @@ -329,7 +279,7 @@ end """ - neff_β_grid(grid, mode, λ0; ref_mode=1) + neff_β_grid(grid, mode, λ0) Create closures which return the effective index and propagation constant as a function of the frequency grid **index**, rather than the frequency itself. @@ -344,6 +294,14 @@ function neff_β_grid(grid, mode, λ0) end end +""" + make_linop(grid, mode, λ0; thg=false) + +Create z-dependent mode-averaged linear-operator closures. + +For `RealGrid`, returns `(linop!, βfun!)`; for `EnvGrid`, `thg=false` additionally +subtracts the reference phase at `λ0`. +""" function make_linop(grid::Grid.RealGrid, mode::Modes.AbstractMode, λ0) sidcs = (1:length(grid.ω))[grid.sidx] neff, β = neff_β_grid(grid, mode, λ0) @@ -406,7 +364,9 @@ end Make constant (z-invariant) linear operator for multimode propagation. The frame velocity is taken as the group velocity at wavelength `λ0` in the mode given by `ref_mode` (which -indexes into `modes`) +indexes into `modes`). + +Returns an array of shape `(Nω, Nmodes)`. """ function make_const_linop(grid::Grid.RealGrid, modes::Modes.ModeCollection, λ0; ref_mode=1) β1 = Modes.dispersion(modes[ref_mode], 1, wlfreq(λ0)) @@ -449,7 +409,7 @@ end Create a closure that returns the effective index as a function of the frequency grid and mode **index**, rather than the mode and frequency themselves. Any [`Modes.AbstractMode`](@ref) -may define its one method for `neff_grid` to accelerate repeated calculation on the same +may define its own method for `neff_grid` to accelerate repeated calculation on the same frequency grid. """ function neff_grid(grid, modes, λ0; ref_mode=1) @@ -459,7 +419,15 @@ function neff_grid(grid, modes, λ0; ref_mode=1) _neff end -function make_linop(grid::Grid.RealGrid, modes, λ0; ref_mode=1) +""" + make_linop(grid, modes, λ0; ref_mode=1, thg=false) + +Create a z-dependent multimode linear-operator closure `linop!(out, z)`. + +The output is filled in-place with shape `(Nω, Nmodes)`. For `EnvGrid`, setting +`thg=false` subtracts the reference phase of `modes[ref_mode]` at `λ0`. +""" +function make_linop(grid::Grid.RealGrid, modes::Modes.ModeCollection, λ0; ref_mode=1) sidcs = (1:length(grid.ω))[grid.sidx] neff = neff_grid(grid, modes, λ0; ref_mode=ref_mode) linop! = let neff=neff, ω=grid.ω, modes=modes, ω0=wlfreq(λ0), ref_mode=ref_mode @@ -476,7 +444,7 @@ function make_linop(grid::Grid.RealGrid, modes, λ0; ref_mode=1) end end -function make_linop(grid::Grid.EnvGrid, modes, λ0; ref_mode=1, thg=false) +function make_linop(grid::Grid.EnvGrid, modes::Modes.ModeCollection, λ0; ref_mode=1, thg=false) sidcs = (1:length(grid.ω))[grid.sidx] neff = neff_grid(grid, modes, λ0; ref_mode=ref_mode) linop! = let neff=neff, ω=grid.ω, modes=modes, ω0=wlfreq(λ0), ref_mode=ref_mode diff --git a/test/test_radial.jl b/test/test_radial.jl index 9f8ae59c..41ee70e1 100644 --- a/test/test_radial.jl +++ b/test/test_radial.jl @@ -63,3 +63,70 @@ Ir = Maths.normbymax(Iλ0[:, end]) Ira = Maths.normbymax(Iλ0_analytic) @test maximum(abs.(Ir .- Ira)/norm(Ir)) < 1.5e-4 end + +@testset "Radial propagation with gradient" begin +import Luna +import Luna: Grid, Maths, PhysData, Nonlinear, Ionisation, NonlinearRHS, Output, Stats, LinearOps, Plotting +import Luna.PhysData: wlfreq +import FFTW +import Hankel +import LinearAlgebra: norm + +gas = :Ar +pres = 1.2 +τ = 20e-15 +λ0 = 800e-9 +w0 = 40e-6 +energy = 1e-12 +L = 0.6 +R = 4e-3 +N = 128 + +grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) +q = Hankel.QDHT(R, N, dim=3) + +energyfun, energyfun_ω = Fields.energyfuncs(grid, q) + +function prop(E, z) + Eω = FFTW.rfft(E, 1) + Eωk = q * Eω + kzsq = @. (grid.ω/PhysData.c)^2 - (q.k^2)' + kzsq[kzsq .< 0] .= 0 + kz = sqrt.(kzsq) + @. Eωk *= exp(-1im * z * (kz - grid.ω/PhysData.c)) + Eω = q \ Eωk + E = FFTW.irfft(Eω, length(grid.t), 1) + return E +end + +nfun = PhysData.ref_index_fun(gas, pres) +nfunω(ω; z=0.0) = nfun(PhysData.wlfreq(ω)) + +dens0 = PhysData.density(gas, pres) +densityfun(z) = dens0 +ionpot = PhysData.ionisation_potential(gas) +ionrate = Ionisation.IonRatePPTCached(gas, λ0) +responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)), + Nonlinear.PlasmaCumtrapz(grid.to, grid.to, ionrate, ionpot)) +linop = LinearOps.make_linop(grid, q, nfunω) +normfun = NonlinearRHS.norm_radial(grid, q, nfunω) + +inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0, propz=-0.3) + +Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) +output = Output.MemoryOutput(0, grid.zmax, 201) +Luna.run(Eω, grid, linop, transform, FT, output) + +Erout = (q \ output.data["Eω"]) +Iωr = abs2.(Erout) + +ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) + +Iλ0 = Iωr[ω0idx, 1, :, :] +λ0 = 2π*PhysData.c/grid.ω[ω0idx] +w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) +Iλ0_analytic = Maths.gauss.(q.r, w1/2)*(w0/w1)^2 # analytical solution (in paraxial approx) +Ir = Maths.normbymax(Iλ0[:, end]) +Ira = Maths.normbymax(Iλ0_analytic) +@test maximum(abs.(Ir .- Ira)/norm(Ir)) < 1.5e-4 +end From a03df58414679fcd92cc45c92f0bd02d1dd30c35 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 09:32:02 +0000 Subject: [PATCH 51/72] group functions better --- src/LinearOps.jl | 63 ++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index d8f2f764..a96a2189 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -118,7 +118,7 @@ is calculated from `nfuny(λ)`. function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns::Tuple) nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) - # nfuny(λ; z) just takes wavelength + # nfuny(λ) just takes wavelength out = zeros(ComplexF64, (length(grid.ω), 2, length(xygrid.kx), length(xygrid.ky))) β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) for (iω, si) in enumerate(grid.sidx) @@ -144,35 +144,6 @@ function make_const_linop(grid::Grid.RealGrid, xygrid::Grid.FreeGrid, nfuns::Tup out end -""" - make_linop(grid, xygrid, nfun) - -Create a z-dependent free-space linear operator closure. - -Applies to `xygrid::Grid.FreeGrid` (full 3D), `Grid.Free2DGrid` (x-z), and -`Hankel.QDHT` (radial symmetry). - -Returns `linop!(out, z)`, which fills `out` in-place for propagation distance `z`. -`nfun(ω; z)` may return one or multiple refractive indices; the last branch -defines the reference-frame velocity and, for `EnvGrid` with `thg=false`, the -reference phase subtraction at `grid.ω0`. -""" -function make_linop(grid::Grid.AbstractGrid, - xygrid::Union{Grid.FreeGrid,Grid.Free2DGrid,Hankel.QDHT}, - nfun, thg::Bool=thg_default(grid)) - checkthg(grid, thg) - kperp2, idcs = transverse_k2(xygrid) - ωfirst = grid.ω[findfirst(grid.sidx)] - np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny - k2 = zeros(Float64, (length(grid.ω), np)) - nfunλ(z) = λ -> nfun(wlfreq(λ); z)[end] - function linop!(out, z) - β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - β0 = getβ0_n(grid, nfun, thg) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z) .* grid.ω[grid.sidx] ./ c).^2 - fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) - end -end """ make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tuple) @@ -184,7 +155,7 @@ Constant free-space operator for 2D (`x-z`) crystal propagation with two polaris function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tuple) nfunx, nfuny = nfuns # here nfunx(λ, δθ) also takes the angle and returns n_x(λ, θ) - # nfuny(λ; z) just takes wavelength + # nfuny(λ) just takes wavelength out = zeros(ComplexF64, (length(grid.ω), 2, length(xgrid.kx))) β1 = PhysData.dispersion_func(1, nfuny)(grid.referenceλ) for (iω, si) in enumerate(grid.sidx) @@ -208,6 +179,36 @@ function make_const_linop(grid::Grid.RealGrid, xgrid::Grid.Free2DGrid, nfuns::Tu out end +""" + make_linop(grid, xygrid, nfun) + +Create a z-dependent free-space linear operator closure. + +Applies to `xygrid::Grid.FreeGrid` (full 3D), `Grid.Free2DGrid` (x-z), and +`Hankel.QDHT` (radial symmetry). + +Returns `linop!(out, z)`, which fills `out` in-place for propagation distance `z`. +`nfun(ω; z)` may return one or multiple refractive indices; the last branch +defines the reference-frame velocity and, for `EnvGrid` with `thg=false`, the +reference phase subtraction at `grid.ω0`. +""" +function make_linop(grid::Grid.AbstractGrid, + xygrid::Union{Grid.FreeGrid,Grid.Free2DGrid,Hankel.QDHT}, + nfun, thg::Bool=thg_default(grid)) + checkthg(grid, thg) + kperp2, idcs = transverse_k2(xygrid) + ωfirst = grid.ω[findfirst(grid.sidx)] + np = length(nfun(ωfirst; z=0)) # 1 if single ref index, 2 if nx, ny + k2 = zeros(Float64, (length(grid.ω), np)) + nfunλ(z) = λ -> nfun(wlfreq(λ); z)[end] + function linop!(out, z) + β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) + β0 = getβ0_n(grid, nfun, thg) + k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z) .* grid.ω[grid.sidx] ./ c).^2 + fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) + end +end + #=================================================# #=============== MODE AVERAGE ================# From e68f9c503caa9f9269e7df11dc60dc91124186da Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 09:43:23 +0000 Subject: [PATCH 52/72] fix some tests --- src/LinearOps.jl | 2 +- test/test_interface.jl | 2 +- test/test_linops.jl | 76 +++++++++++++++++++++--------------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index a96a2189..13c1cb69 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -203,7 +203,7 @@ function make_linop(grid::Grid.AbstractGrid, nfunλ(z) = λ -> nfun(wlfreq(λ); z)[end] function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) - β0 = getβ0_n(grid, nfun, thg) + β0 = getβ0_n(grid, nfunλ(z), thg) k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z) .* grid.ω[grid.sidx] ./ c).^2 fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) end diff --git a/test/test_interface.jl b/test/test_interface.jl index f5276b8f..4827eb37 100644 --- a/test/test_interface.jl +++ b/test/test_interface.jl @@ -423,7 +423,7 @@ end _, _, _, t400, _, _ = Interface.prop_capillary_args(args...; temperature=400, kwargs...) Raman300 = t300.resp[1] Raman400 = t400.resp[1] - NonlinearRHS.to_time!(t300.Eto, Eω, t300.Eωo, inv(t300.FT)) + NonlinearRHS.to_time!(t300.Eto, Eω, t300.Eωo, t300.FT) ρ = t300.densityfun(0) # note: using same density to compare only NL response Pto300 = zero(t300.Eto) Pto300_2 = zero(t300.Eto) diff --git a/test/test_linops.jl b/test/test_linops.jl index e40c6e38..a3b3def8 100644 --- a/test/test_linops.jl +++ b/test/test_linops.jl @@ -14,21 +14,21 @@ nfun = let rif=PhysData.ref_index_fun(gas, pres) end @testset "radial, field" begin -grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, Nr, dim=2) + grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) + q = Hankel.QDHT(R, Nr, dim=2) -linop = LinearOps.make_const_linop(grid, q, PhysData.ref_index_fun(gas, pres)) -linopf = LinearOps.make_linop(grid, q, nfun) -out = similar(linop) + linop = LinearOps.make_const_linop(grid, q, PhysData.ref_index_fun(gas, pres)) + linopf = LinearOps.make_linop(grid, q, nfun) + out = similar(linop) -@test size(linop) == (length(grid.ω), q.N) + @test size(linop) == (length(grid.ω), 1, q.N) -linopf(out, 0.0) -@test all(imag(out) .≈ imag(linop)) -@test all(real(out) .≈ real(linop)) -linopf(out, 0.5) -@test all(imag(out) .≈ imag(linop)) -@test all(real(out) .≈ real(linop)) + linopf(out, 0.0) + @test all(imag(out) .≈ imag(linop)) + @test all(real(out) .≈ real(linop)) + linopf(out, 0.5) + @test all(imag(out) .≈ imag(linop)) + @test all(real(out) .≈ real(linop)) end @testset "radial, env" begin @@ -51,34 +51,14 @@ end end @testset "3D, field" begin -grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) -xygrid = Grid.FreeGrid(R, Nx, R, Ny) - -linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) -linopf = LinearOps.make_linop(grid, xygrid, nfun) -out = similar(linop) - -@test size(linop) == (length(grid.ω), Ny, Nx) + grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) + xygrid = Grid.FreeGrid(R, Nx, R, Ny) -linopf(out, 0.0) -@test all(imag(out) .≈ imag(linop)) -@test all(real(out) .≈ real(linop)) -linopf(out, 0.5) -@test all(imag(out) .≈ imag(linop)) -@test all(real(out) .≈ real(linop)) -end - -@testset "3D, env" begin -grid = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) -grid_thg = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12; thg=true) -xygrid = Grid.FreeGrid(R, Nx, R, Ny) - -for gi in (grid, grid_thg) - linop = LinearOps.make_const_linop(gi, xygrid, PhysData.ref_index_fun(gas, pres)) - linopf = LinearOps.make_linop(gi, xygrid, nfun) + linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) + linopf = LinearOps.make_linop(grid, xygrid, nfun) out = similar(linop) - @test size(linop) == (length(gi.ω), Ny, Nx) + @test size(linop) == (length(grid.ω), 1, Nx, Ny) linopf(out, 0.0) @test all(imag(out) .≈ imag(linop)) @@ -87,6 +67,26 @@ for gi in (grid, grid_thg) @test all(imag(out) .≈ imag(linop)) @test all(real(out) .≈ real(linop)) end + +@testset "3D, env" begin + grid = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) + grid_thg = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12; thg=true) + xygrid = Grid.FreeGrid(R, Nx, R, Ny) + + for gi in (grid, grid_thg) + linop = LinearOps.make_const_linop(gi, xygrid, PhysData.ref_index_fun(gas, pres)) + linopf = LinearOps.make_linop(gi, xygrid, nfun) + out = similar(linop) + + @test size(linop) == (length(gi.ω), 1, Nx, Ny) + + linopf(out, 0.0) + @test all(imag(out) .≈ imag(linop)) + @test all(real(out) .≈ real(linop)) + linopf(out, 0.5) + @test all(imag(out) .≈ imag(linop)) + @test all(real(out) .≈ real(linop)) + end end @testset "equivalence for fast z-dependent linops" begin @@ -156,4 +156,4 @@ for zi in range(0, L, length=10) βdm!(outdm, zi) @test outm == outdm end -end \ No newline at end of file +end From cde7fe507722370199842841ffaf1caa40265395 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 09:47:40 +0000 Subject: [PATCH 53/72] fix modes tests --- test/test_modes.jl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/test_modes.jl b/test/test_modes.jl index bf83c834..f75d1ac7 100644 --- a/test/test_modes.jl +++ b/test/test_modes.jl @@ -1,12 +1,10 @@ import Test: @test, @testset, @test_throws -import FunctionZeros: besselj_zero -import SpecialFunctions: besselj -import HCubature: hquadrature -import LinearAlgebra: norm -import FFTW using Luna +import Luna.Capillary: besselj_zero, besselj, hquadrature +import LinearAlgebra: norm import Luna: Hankel import Luna.PhysData: wlfreq +import FFTW @testset "delegation" begin @@ -398,8 +396,8 @@ end # testset "makemodes" N, _ = hquadrature(r -> r*mode(r), 0, a) N *= 2π # azimuthal integral # mode(0) = 1.0, so peak fluence is just energy/N - @test isapprox(fluence[1, length(x)÷2+1], energy/N, rtol=1e-6) - @test isapprox(energy/N .* mode.(x), fluence[1, :], rtol=1e-6) + @test isapprox(fluence[length(x)÷2+1, 1], energy/N, rtol=1e-6) + @test isapprox(energy/N .* mode.(x), fluence[:, 1], rtol=1e-6) end out = prop_capillary(a, flength, gas, pressure; @@ -432,6 +430,6 @@ end # testset "makemodes" t, Etxy_grid = Processing.getEtxy(out, xs, flength; oversampling=1) @testset "comparing at $x1, $x2" for (x1idx, x1) in enumerate(xs[1]), (x2idx, x2) in enumerate(xs[2]) _, Etthis = Processing.getEtxy(out, (x1, x2), flength; oversampling=1) - @test Etthis ≈ Etxy_grid[:, x1idx, x2idx, :] + @test Etthis ≈ Etxy_grid[:, :, x1idx, x2idx] end end # testset "spatial field and fluence" From bc29120962afb5b573ed4ad5ed56795c6bf763d1 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 11:46:03 +0000 Subject: [PATCH 54/72] fix linearops for non-crystal birefringence --- src/LinearOps.jl | 6 ++- test/test_linops.jl | 92 ++++++++++++--------------------------------- 2 files changed, 30 insertions(+), 68 deletions(-) diff --git a/src/LinearOps.jl b/src/LinearOps.jl index 13c1cb69..dba94133 100644 --- a/src/LinearOps.jl +++ b/src/LinearOps.jl @@ -204,7 +204,11 @@ function make_linop(grid::Grid.AbstractGrid, function linop!(out, z) β1 = PhysData.dispersion_func(1, nfunλ(z))(grid.referenceλ) β0 = getβ0_n(grid, nfunλ(z), thg) - k2[grid.sidx] .= (nfun.(grid.ω[grid.sidx]; z) .* grid.ω[grid.sidx] ./ c).^2 + for (ii, si) in enumerate(grid.sidx) + if si + k2[ii, :] .= (nfun(grid.ω[ii]; z) .* grid.ω[ii] ./ c).^2 + end + end fill_linop_matrix!(out, grid, β1, β0, k2, kperp2, idcs) end end diff --git a/test/test_linops.jl b/test/test_linops.jl index a3b3def8..5bf2d7c4 100644 --- a/test/test_linops.jl +++ b/test/test_linops.jl @@ -8,77 +8,35 @@ Nr = 256 Nx = 128 Ny = 64 gas = :Ar -pres = 1 -nfun = let rif=PhysData.ref_index_fun(gas, pres) - (ω; z) -> rif(wlfreq(ω)) -end - -@testset "radial, field" begin - grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) - q = Hankel.QDHT(R, Nr, dim=2) - - linop = LinearOps.make_const_linop(grid, q, PhysData.ref_index_fun(gas, pres)) - linopf = LinearOps.make_linop(grid, q, nfun) - out = similar(linop) - - @test size(linop) == (length(grid.ω), 1, q.N) - - linopf(out, 0.0) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) - linopf(out, 0.5) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) -end - -@testset "radial, env" begin -grid = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) -grid_thg = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12; thg=true) -q = Hankel.QDHT(R, Nr, dim=2) - -for gi in (grid, grid_thg) - linop = LinearOps.make_const_linop(gi, q, PhysData.ref_index_fun(gas, pres)) - linopf = LinearOps.make_linop(gi, q, nfun) - out = similar(linop) - - linopf(out, 0.0) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) - linopf(out, 0.5) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) -end -end +pressure = 1 -@testset "3D, field" begin - grid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) +@testset "free space" begin + rgrid = Grid.RealGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) + egrid = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) + q = Hankel.QDHT(R, Nr, dim=3) xygrid = Grid.FreeGrid(R, Nx, R, Ny) - - linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) - linopf = LinearOps.make_linop(grid, xygrid, nfun) - out = similar(linop) - - @test size(linop) == (length(grid.ω), 1, Nx, Ny) - - linopf(out, 0.0) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) - linopf(out, 0.5) - @test all(imag(out) .≈ imag(linop)) - @test all(real(out) .≈ real(linop)) -end - -@testset "3D, env" begin - grid = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12) - grid_thg = Grid.EnvGrid(1, 800e-9, (400e-9, 2000e-9), 0.2e-12; thg=true) - xygrid = Grid.FreeGrid(R, Nx, R, Ny) - - for gi in (grid, grid_thg) - linop = LinearOps.make_const_linop(gi, xygrid, PhysData.ref_index_fun(gas, pres)) - linopf = LinearOps.make_linop(gi, xygrid, nfun) + xgrid = Grid.Free2DGrid(R, Nx) + + getshape(grid, q::Hankel.QDHT, pol) = (length(grid.ω), pol ? 2 : 1, q.N) + getshape(grid, sg::Grid.Free2DGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x)) + getshape(grid, sg::Grid.FreeGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x), length(sg.y)) + + @testset "$(typeof(grid)), $(typeof(sg)), pol = $pol" for sg in (q, xgrid, xygrid), + pol in (false, true), + grid in (rgrid, egrid) + nfunλ = PhysData.ref_index_fun(gas, pressure) + if pol + nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) + else + nfun = (λ; z=0.0) -> nfunλ(λ) + end + nfunω = (ω; z) -> nfun(wlfreq(ω); z) + + linop = LinearOps.make_const_linop(grid, sg, nfun) + linopf = LinearOps.make_linop(grid, sg, nfunω) out = similar(linop) - @test size(linop) == (length(gi.ω), 1, Nx, Ny) + @test size(linop) == getshape(grid, sg, pol) linopf(out, 0.0) @test all(imag(out) .≈ imag(linop)) From 67741b2b0935037034e8e868938adef1671fe3f5 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:08:04 +0000 Subject: [PATCH 55/72] Fields.prop! for 2D grids --- src/Fields.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Fields.jl b/src/Fields.jl index 46086b6d..b4ede343 100644 --- a/src/Fields.jl +++ b/src/Fields.jl @@ -421,7 +421,7 @@ function prop!(Eωk, z, grid, q::Hankel.QDHT) @. Eωk *= exp(-1im * z * (kz - grid.ω/PhysData.c)) end -function prop!(Eωk, z, grid, xygrid) +function prop!(Eωk, z, grid, xygrid::Grid.FreeGrid) kzsq = ((grid.ω ./ PhysData.c).^2 .- reshape(xygrid.kx.^2, (1, 1, length(xygrid.kx), 1)) .- reshape(xygrid.ky.^2, (1, 1, 1, length(xygrid.ky))) @@ -431,6 +431,15 @@ function prop!(Eωk, z, grid, xygrid) @. Eωk *= exp(-1im * z * (kz - grid.ω / PhysData.c)) end +function prop!(Eωk, z, grid, xygrid::Grid.Free2DGrid) + kzsq = ((grid.ω ./ PhysData.c).^2 + .- reshape(xygrid.kx.^2, (1, 1, length(xygrid.kx))) + ) + kzsq[kzsq.<0] .= 0 + kz = sqrt.(kzsq) + @. Eωk *= exp(-1im * z * (kz - grid.ω / PhysData.c)) +end + """ gauss_beam(k, ω0; z=0.0, pol=:y) From 9881fdd0f4599d6495d400124ae7dcb0debaa967 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:08:17 +0000 Subject: [PATCH 56/72] fix test_linops --- test/test_linops.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/test_linops.jl b/test/test_linops.jl index 5bf2d7c4..0bd23260 100644 --- a/test/test_linops.jl +++ b/test/test_linops.jl @@ -21,9 +21,13 @@ pressure = 1 getshape(grid, sg::Grid.Free2DGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x)) getshape(grid, sg::Grid.FreeGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x), length(sg.y)) - @testset "$(typeof(grid)), $(typeof(sg)), pol = $pol" for sg in (q, xgrid, xygrid), + @testset "$(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), pol in (false, true), + thg in (false, true), grid in (rgrid, egrid) + if grid isa Grid.RealGrid && ~thg + continue + end nfunλ = PhysData.ref_index_fun(gas, pressure) if pol nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) @@ -32,8 +36,8 @@ pressure = 1 end nfunω = (ω; z) -> nfun(wlfreq(ω); z) - linop = LinearOps.make_const_linop(grid, sg, nfun) - linopf = LinearOps.make_linop(grid, sg, nfunω) + linop = LinearOps.make_const_linop(grid, sg, nfun, thg) + linopf = LinearOps.make_linop(grid, sg, nfunω, thg) out = similar(linop) @test size(linop) == getshape(grid, sg, pol) @@ -51,7 +55,7 @@ end a = 125e-6 L = 1 grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.5e-12) -coren, densityfun = Capillary.gradient(gas, L, pres, 0) +coren, densityfun = Capillary.gradient(gas, L, pressure, 0) m = Capillary.MarcatiliMode(a, coren) dm = Modes.delegated(m) # delegated mode tricks make_linop into using the generic version @@ -75,7 +79,7 @@ L = 1 # NO THG thg = false grid = Grid.EnvGrid(L, 800e-9, (400e-9, 2000e-9), 0.5e-12; thg=thg) -coren, densityfun = Capillary.gradient(gas, L, pres, 0) +coren, densityfun = Capillary.gradient(gas, L, pressure, 0) m = Capillary.MarcatiliMode(a, coren) dm = Modes.delegated(m) # delegated mode tricks make_linop into using the generic version... @@ -96,7 +100,7 @@ end # WITH THG thg = true grid = Grid.EnvGrid(L, 800e-9, (400e-9, 2000e-9), 0.5e-12; thg=thg) -coren, densityfun = Capillary.gradient(gas, L, pres, 0) +coren, densityfun = Capillary.gradient(gas, L, pressure, 0) m = Capillary.MarcatiliMode(a, coren) dm = Modes.delegated(m) # delegated mode tricks make_linop into using the generic version... From 09978aca8a886aab8814b15ab43b0aca8770de20 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:08:22 +0000 Subject: [PATCH 57/72] unify free-space tests --- test/test_freespace.jl | 132 ++++++++++++++++++++++++++++++++++++ test/test_full_freespace.jl | 126 ---------------------------------- test/test_radial.jl | 132 ------------------------------------ 3 files changed, 132 insertions(+), 258 deletions(-) create mode 100644 test/test_freespace.jl delete mode 100644 test/test_full_freespace.jl delete mode 100644 test/test_radial.jl diff --git a/test/test_freespace.jl b/test/test_freespace.jl new file mode 100644 index 00000000..92ff8773 --- /dev/null +++ b/test/test_freespace.jl @@ -0,0 +1,132 @@ +#= here we test that everything runs without throwing errors for every combination of: + 1. Real and envelope grids + 2. Radial (QDHT), 2D cartesian and 3D cartesian spatial grids + 3. (For envelope) THG on/off + 4. Constant pressure and pressure gradient +=# +using Luna +import Luna.PhysData: wlfreq +import Luna: Hankel, FFTW +Luna.set_fftw_mode(:estimate) +import LinearAlgebra: norm +import Test: @test, @testset + +R = 1.5e-3 +Nr = 512 +Nx = 256 +Ny = 128 +gas = :Ar +pressure = 1 + +λ0 = 800e-9 +w0 = 40e-6 +τfwhm = 20e-15 +energy = 1e-12 +L = 0.3 + +rgrid = Grid.RealGrid(L, λ0, (400e-9, 2000e-9), 0.2e-12) +egrid = Grid.EnvGrid(L, λ0, (400e-9, 2000e-9), 0.2e-12) +q = Hankel.QDHT(R, Nr, dim=3) +xygrid = Grid.FreeGrid(R, Nx, R, Ny) +xgrid = Grid.Free2DGrid(R, Nx) + +getshape(grid, q::Hankel.QDHT, pol) = (length(grid.ω), pol ? 2 : 1, q.N) +getshape(grid, sg::Grid.Free2DGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x)) +getshape(grid, sg::Grid.FreeGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x), length(sg.y)) + +makekerr(grid::Grid.RealGrid, thg) = Nonlinear.Kerr_field(PhysData.γ3_gas(gas)) +makekerr(grid::Grid.EnvGrid, thg) = thg ? Nonlinear.Kerr_env(PhysData.γ3_gas(gas)) : Nonlinear.Kerr_env_thg(PhysData.γ3_gas(gas), grid.ω0, grid.to) + +makeconstnorm(grid, q::Hankel.QDHT, nfunλ) = NonlinearRHS.const_norm_radial(grid, q, nfunλ) +makeconstnorm(grid, sg::Grid.Free2DGrid, nfunλ) = NonlinearRHS.const_norm_free2D(grid, sg, nfunλ) +makeconstnorm(grid, sg::Grid.FreeGrid, nfunλ) = NonlinearRHS.const_norm_free(grid, sg, nfunλ) + +makenorm(grid, q::Hankel.QDHT, nfunω) = NonlinearRHS.norm_radial(grid, q, nfunω) +makenorm(grid, sg::Grid.Free2DGrid, nfunω) = NonlinearRHS.norm_free2D(grid, sg, nfunω) +makenorm(grid, sg::Grid.FreeGrid, nfunω) = NonlinearRHS.norm_free(grid, sg, nfunω) + +function testfocus(q::Hankel.QDHT, Eω, w0) + Eωfoc = Eω[:, :, :, end] + Eωr = q \ Eωfoc + Ir = Maths.normbymax(dropdims(sum(abs2.(Eωr); dims=(1, 2)); dims=(1, 2))) + Ir_analytical = Maths.gauss.(q.r, w0/2) + @test maximum(abs.(Ir .- Ir_analytical)/norm(Ir)) < 5e-3 +end + +function testfocus(sg::Grid.Free2DGrid, Eω, w0) + Eωfoc = Eω[:, :, :, end] + Eωx = FFTW.ifft(Eωfoc, 3) + Ix = Maths.normbymax(dropdims(sum(abs2.(Eωx); dims=(1, 2)); dims=(1, 2))) + Ix_analytical = Maths.gauss.(sg.x, w0/2) + @test maximum(abs.(Ix .- Ix_analytical)/norm(Ix)) < 2e-3 +end + +function testfocus(sg::Grid.FreeGrid, Eω, w0) + Eωfoc = Eω[:, :, :, :, end] + Eωxy = FFTW.ifft(Eωfoc, (3, 4)) + Iy = Maths.normbymax(dropdims(sum(abs2.(Eωxy); dims=(1, 2, 3)); dims=(1, 2, 3))) + Ix = Maths.normbymax(dropdims(sum(abs2.(Eωxy); dims=(1, 2, 4)); dims=(1, 2, 4))) + Ix_analytical = Maths.gauss.(sg.x, w0/2) + @test maximum(abs.(Ix .- Ix_analytical)/norm(Ix)) < 2e-3 + Iy_analytical = Maths.gauss.(sg.y, w0/2) + @test maximum(abs.(Iy .- Iy_analytical)/norm(Ix)) < 2e-3 +end + +@testset "Constant pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), + pol in (false, true), + thg in (false, true), + grid in (rgrid, egrid) + if grid isa Grid.RealGrid && ~thg + continue + end + nfunλ = PhysData.ref_index_fun(gas, pressure) + if pol + nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) + else + nfun = (λ; z=0.0) -> nfunλ(λ) + end + + linop = LinearOps.make_const_linop(grid, sg, nfun, thg) + dens0 = PhysData.density(gas, pressure) + densityfun(z) = dens0 + + responses = (makekerr(grid, thg),) + normfun = makeconstnorm(grid, sg, nfun) + + inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz=-L) + + Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) + output = Output.MemoryOutput(0, grid.zmax, 21) + Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) + testfocus(sg, output["Eω"], w0) +end +## +@testset "Gradient pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), + pol in (false, true), + thg in (false, true), + grid in (rgrid, egrid) + if grid isa Grid.RealGrid && ~thg + continue + end + nfunλ = PhysData.ref_index_fun(gas, pressure) + if pol + nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) + else + nfun = (λ; z=0.0) -> nfunλ(λ) + end + nfunω = (ω; z) -> nfun(wlfreq(ω); z) + + linop = LinearOps.make_linop(grid, sg, nfunω, thg) + dens0 = PhysData.density(gas, pressure) + densityfun(z) = dens0 + + responses = (makekerr(grid, thg),) + normfun = makenorm(grid, sg, nfunω) + + inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz=-L) + + Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) + output = Output.MemoryOutput(0, grid.zmax, 21) + Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) + testfocus(sg, output["Eω"], w0) +end diff --git a/test/test_full_freespace.jl b/test/test_full_freespace.jl deleted file mode 100644 index c83fef54..00000000 --- a/test/test_full_freespace.jl +++ /dev/null @@ -1,126 +0,0 @@ -import Test: @test, @testset - -@testset "Full 3D propagation - square grid" begin -using Luna -Luna.set_fftw_mode(:estimate) -import FFTW -import Luna.PhysData: wlfreq -import LinearAlgebra: norm - -gas = :Ar -pres = 1 - -τ = 10e-15 -λ0 = 800e-9 - -w0 = 500e-6 -energy = 1e-12 -L = 2 - -R = 4e-3 -N = 128 - -grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 80e-15) -xygrid = Grid.FreeGrid(R, N) - -x = xygrid.x -y = xygrid.y -energyfun, energyfunω = Fields.energyfuncs(grid, xygrid) - -dens0 = PhysData.density(gas, pres) -densityfun(z) = dens0 - -responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)),) - -linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) -normfun = NonlinearRHS.const_norm_free(grid, xygrid, PhysData.ref_index_fun(gas, pres)) - -inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0) - -Eω, transform, FT = Luna.setup(grid, xygrid, densityfun, normfun, responses, inputs) - -output = Output.MemoryOutput(0, grid.zmax, 21) - -Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) - -Eout = output.data["Eω"] # (ω, kx, ky, z) - -ω0idx = argmin(abs.(grid.ω .- wlfreq(λ0))) -λ0 = 2π*PhysData.c/grid.ω[ω0idx] -w1 = w0*sqrt(1+(L*λ0/(π*w0^2))^2) -Iω0_analytic = Maths.gauss.(xygrid.x, w1/2) # analytical solution (in paraxial approx) - -Eω0xy = FFTW.ifft(Eout[ω0idx, 1, :, :, end], (1, 2)) -Iω0xy = abs2.(Eω0xy) -Iω0y = Maths.normbymax(dropdims(sum(Iω0xy, dims=2), dims=2)) -Iω0x = Maths.normbymax(dropdims(sum(Iω0xy, dims=1), dims=1)) - -@test maximum(abs.(Iω0x .- Iω0_analytic)/norm(Iω0x)) < 5e-5 -@test maximum(abs.(Iω0y .- Iω0_analytic)/norm(Iω0y)) < 5e-5 - -end -## -@testset "Full 3D propagation - non-square grid" begin -using Luna -Luna.set_fftw_mode(:estimate) -import FFTW -import Luna.PhysData: wlfreq -import LinearAlgebra: norm - -gas = :Ar -pres = 1 - -τ = 10e-15 -λ0 = 800e-9 - -w0 = 500e-6 -energy = 1e-12 -L = 2 - -R = 4e-3 -Nx = 128 -Ny = 256 - -saveN = 21 - -grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 80e-15) -xygrid = Grid.FreeGrid(R, Nx, R, Ny) - -x = xygrid.x -y = xygrid.y -energyfun, energyfunω = Fields.energyfuncs(grid, xygrid) - -dens0 = PhysData.density(gas, pres) -densityfun(z) = dens0 - -responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)),) - -linop = LinearOps.make_const_linop(grid, xygrid, PhysData.ref_index_fun(gas, pres)) -normfun = NonlinearRHS.const_norm_free(grid, xygrid, PhysData.ref_index_fun(gas, pres)) - -inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0) - -Eω, transform, FT = Luna.setup(grid, xygrid, densityfun, normfun, responses, inputs) - -output = Output.MemoryOutput(0, grid.zmax, saveN) - -Luna.run(Eω, grid, linop, transform, FT, output, max_dz=Inf, init_dz=1e-1) - -Eout = output.data["Eω"] # (ω, pol, kx, ky, z) -@test size(Eout) == (length(grid.ω), 1, Nx, Ny, saveN) - -ω0idx = argmin(abs.(grid.ω .- wlfreq(λ0))) -λ0 = 2π*PhysData.c/grid.ω[ω0idx] -w1 = w0*sqrt(1+(L*λ0/(π*w0^2))^2) -Iω0x_analytic = Maths.gauss.(xygrid.x, w1/2) # analytical solution (in paraxial approx) -Iω0y_analytic = Maths.gauss.(xygrid.y, w1/2) # analytical solution (in paraxial approx) - -Eω0xy = FFTW.ifft(Eout[ω0idx, 1, :, :, end]) -Iω0xy = abs2.(Eω0xy) -Iω0y = Maths.normbymax(dropdims(sum(Iω0xy, dims=1), dims=1)) -Iω0x = Maths.normbymax(dropdims(sum(Iω0xy, dims=2), dims=2)) - -@test maximum(abs.(Iω0x .- Iω0x_analytic)/norm(Iω0x)) < 5e-5 -@test maximum(abs.(Iω0y .- Iω0y_analytic)/norm(Iω0y)) < 5e-5 - -end diff --git a/test/test_radial.jl b/test/test_radial.jl deleted file mode 100644 index 41ee70e1..00000000 --- a/test/test_radial.jl +++ /dev/null @@ -1,132 +0,0 @@ -import Test: @test, @testset - -@testset "Radial propagation" begin -import Luna -import Luna: Grid, Maths, PhysData, Nonlinear, Ionisation, NonlinearRHS, Output, Stats, LinearOps, Plotting -import Luna.PhysData: wlfreq -import FFTW -import Hankel -import LinearAlgebra: norm - -gas = :Ar -pres = 1.2 -τ = 20e-15 -λ0 = 800e-9 -w0 = 40e-6 -energy = 1e-12 -L = 0.6 -R = 4e-3 -N = 128 - -grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, N, dim=3) - -energyfun, energyfun_ω = Fields.energyfuncs(grid, q) - -function prop(E, z) - Eω = FFTW.rfft(E, 1) - Eωk = q * Eω - kzsq = @. (grid.ω/PhysData.c)^2 - (q.k^2)' - kzsq[kzsq .< 0] .= 0 - kz = sqrt.(kzsq) - @. Eωk *= exp(-1im * z * (kz - grid.ω/PhysData.c)) - Eω = q \ Eωk - E = FFTW.irfft(Eω, length(grid.t), 1) - return E -end - -dens0 = PhysData.density(gas, pres) -densityfun(z) = dens0 -ionpot = PhysData.ionisation_potential(gas) -ionrate = Ionisation.IonRatePPTCached(gas, λ0) -responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)), - Nonlinear.PlasmaCumtrapz(grid.to, grid.to, ionrate, ionpot)) -linop = LinearOps.make_const_linop(grid, q, PhysData.ref_index_fun(gas, pres)) -normfun = NonlinearRHS.const_norm_radial(grid, q, PhysData.ref_index_fun(gas, pres)) - -inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0, propz=-0.3) - -Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) -output = Output.MemoryOutput(0, grid.zmax, 201) -Luna.run(Eω, grid, linop, transform, FT, output) - -Erout = (q \ output.data["Eω"]) -Iωr = abs2.(Erout) - -ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) - -Iλ0 = Iωr[ω0idx, 1, :, :] -λ0 = 2π*PhysData.c/grid.ω[ω0idx] -w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) -Iλ0_analytic = Maths.gauss.(q.r, w1/2)*(w0/w1)^2 # analytical solution (in paraxial approx) -Ir = Maths.normbymax(Iλ0[:, end]) -Ira = Maths.normbymax(Iλ0_analytic) -@test maximum(abs.(Ir .- Ira)/norm(Ir)) < 1.5e-4 -end - -@testset "Radial propagation with gradient" begin -import Luna -import Luna: Grid, Maths, PhysData, Nonlinear, Ionisation, NonlinearRHS, Output, Stats, LinearOps, Plotting -import Luna.PhysData: wlfreq -import FFTW -import Hankel -import LinearAlgebra: norm - -gas = :Ar -pres = 1.2 -τ = 20e-15 -λ0 = 800e-9 -w0 = 40e-6 -energy = 1e-12 -L = 0.6 -R = 4e-3 -N = 128 - -grid = Grid.RealGrid(L, 800e-9, (400e-9, 2000e-9), 0.2e-12) -q = Hankel.QDHT(R, N, dim=3) - -energyfun, energyfun_ω = Fields.energyfuncs(grid, q) - -function prop(E, z) - Eω = FFTW.rfft(E, 1) - Eωk = q * Eω - kzsq = @. (grid.ω/PhysData.c)^2 - (q.k^2)' - kzsq[kzsq .< 0] .= 0 - kz = sqrt.(kzsq) - @. Eωk *= exp(-1im * z * (kz - grid.ω/PhysData.c)) - Eω = q \ Eωk - E = FFTW.irfft(Eω, length(grid.t), 1) - return E -end - -nfun = PhysData.ref_index_fun(gas, pres) -nfunω(ω; z=0.0) = nfun(PhysData.wlfreq(ω)) - -dens0 = PhysData.density(gas, pres) -densityfun(z) = dens0 -ionpot = PhysData.ionisation_potential(gas) -ionrate = Ionisation.IonRatePPTCached(gas, λ0) -responses = (Nonlinear.Kerr_field(PhysData.γ3_gas(gas)), - Nonlinear.PlasmaCumtrapz(grid.to, grid.to, ionrate, ionpot)) -linop = LinearOps.make_linop(grid, q, nfunω) -normfun = NonlinearRHS.norm_radial(grid, q, nfunω) - -inputs = Fields.GaussGaussField(λ0=λ0, τfwhm=τ, energy=energy, w0=w0, propz=-0.3) - -Eω, transform, FT = Luna.setup(grid, q, densityfun, normfun, responses, inputs) -output = Output.MemoryOutput(0, grid.zmax, 201) -Luna.run(Eω, grid, linop, transform, FT, output) - -Erout = (q \ output.data["Eω"]) -Iωr = abs2.(Erout) - -ω0idx = argmin(abs.(grid.ω .- 2π*PhysData.c/λ0)) - -Iλ0 = Iωr[ω0idx, 1, :, :] -λ0 = 2π*PhysData.c/grid.ω[ω0idx] -w1 = w0*sqrt(1+(L/2*λ0/(π*w0^2))^2) -Iλ0_analytic = Maths.gauss.(q.r, w1/2)*(w0/w1)^2 # analytical solution (in paraxial approx) -Ir = Maths.normbymax(Iλ0[:, end]) -Ira = Maths.normbymax(Iλ0_analytic) -@test maximum(abs.(Ir .- Ira)/norm(Ir)) < 1.5e-4 -end From 59847ab29ec6ae8a4d21b58aaba930e497173796 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:09:17 +0000 Subject: [PATCH 58/72] comment --- test/test_freespace.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 92ff8773..976bbaf7 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -3,6 +3,8 @@ 2. Radial (QDHT), 2D cartesian and 3D cartesian spatial grids 3. (For envelope) THG on/off 4. Constant pressure and pressure gradient +We also check that the spatial linear operators work correctly by testing the focusing +of a Gaussian beam. (This cross-checks LinearOps vs Fields.prop!) =# using Luna import Luna.PhysData: wlfreq From 784a6651605c8226dac82cc865ec36a89df16644 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:29:50 +0000 Subject: [PATCH 59/72] test chi2 --- test/runtests.jl | 5 +++ test/test_chi2.jl | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 test/test_chi2.jl diff --git a/test/runtests.jl b/test/runtests.jl index a2552f24..620c5f5c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -92,6 +92,11 @@ end include(joinpath(testdir, "test_kerr.jl")) end +@testset "Chi2" begin + @info("================= test_chi2.jl") + include(joinpath(testdir, "test_chi2.jl")) +end + @testset "LinearOps" begin @info("================= test_linops.jl") include(joinpath(testdir, "test_linops.jl")) diff --git a/test/test_chi2.jl b/test/test_chi2.jl new file mode 100644 index 00000000..786f2086 --- /dev/null +++ b/test/test_chi2.jl @@ -0,0 +1,107 @@ +import Test: @test, @testset +import Luna: Fields, Grid, Nonlinear, PhysData +import Luna.PhysData: ε_0 +import FFTW + +@testset "Chi2 field products" begin + Ec = [1.0, -2.0, 0.5] + Enl = zeros(6) + Nonlinear.field_products!(Enl, Ec) + @test Enl == [1.0, 4.0, 0.25, -2.0, 1.0, -4.0] +end + +@testset "Chi2Field without rotation" begin + χ2 = zeros(3, 6) + χ2[1,1] = 1.2 + χ2[1,2] = -0.7 + χ2[1,6] = 0.3 + χ2[2,2] = 0.5 + χ2[2,6] = -0.4 + + c = Nonlinear.Chi2Field(0.0, 0.0, χ2) + E = [1.0 2.0; + -0.5 0.25; + 0.0 -1.0] + + out = zeros(size(E)) + c(out, E, 1.0) + + expected = similar(out) + for i in axes(E, 1) + Ex = E[i, 1] + Ey = E[i, 2] + Enl1 = Ex^2 + Enl2 = Ey^2 + Enl6 = 2*Ex*Ey + + expected[i, 1] = ε_0*(χ2[1,1]*Enl1 + χ2[1,2]*Enl2 + χ2[1,6]*Enl6) + expected[i, 2] = ε_0*(χ2[2,2]*Enl2 + χ2[2,6]*Enl6) + end + + @test isapprox(out, expected, rtol=1e-14, atol=0.0) + + c(out, E, 1.0) + @test isapprox(out, 2 .* expected, rtol=1e-14, atol=0.0) +end + +@testset "Chi2Field with rotation" begin + χ2 = reshape(collect(0.1:0.1:1.8), 3, 6) + θ = 0.37 + ϕ = -1.1 + + c = Nonlinear.Chi2Field(θ, ϕ, χ2) + E = [0.8 -0.2; + -0.1 0.4; + 0.0 0.6; + -0.5 -0.3] + + out = zeros(size(E)) + c(out, E, 1.0) + + expected = zeros(size(E)) + El = zeros(3) + for i in axes(E, 1) + El[1] = E[i, 1] + El[2] = E[i, 2] + El[3] = 0.0 + + Ec = c.toCrystal * El + Enl = zeros(6) + Nonlinear.field_products!(Enl, Ec) + Pc = c.χ2 * Enl + Pl = c.toLab * Pc + + expected[i, 1] = ε_0*Pl[1] + expected[i, 2] = ε_0*Pl[2] + end + + @test isapprox(out, expected, rtol=1e-13, atol=1e-15) +end + +@testset "Chi2Field generates second harmonic" begin + λ0 = 800e-9 + grid = Grid.RealGrid(1.0, λ0, (200e-9, 1600e-9), 4e-12) + input = Fields.GaussField(λ0=λ0, τfwhm=800e-15, power=1.0) + Et = 1e10 .* Fields.make_Et(input, grid) + N = length(Et) + + E = zeros(N, 2) + E[:, 1] .= Et + + c = Nonlinear.Chi2Field(0.0, 0.0, PhysData.χ2(:BBO)) + + out = zeros(size(E)) + c(out, E, 1.0) + + ω = grid.ω + ω0 = PhysData.wlfreq(λ0) + fundband = @. (0.9ω0 < ω < 1.1ω0) + shband = @. (1.9ω0 < ω < 2.1ω0) + + Pωy = abs2.(FFTW.rfft(out[:, 2] ./ ε_0)) + fundamental = sum(Pωy[fundband]) + second_harmonic = sum(Pωy[shband]) + + @test second_harmonic > 1e8 * max(fundamental, eps()) + @test maximum(abs.(out[:, 1])) < 1e-15 +end From 164cfb17ed2a7521985ed5672da9db531ee630ed Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Wed, 4 Mar 2026 15:32:48 +0000 Subject: [PATCH 60/72] docstrings --- src/Nonlinear.jl | 24 +++++++++++++++++++----- src/PhysData.jl | 10 ++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Nonlinear.jl b/src/Nonlinear.jl index 49be8975..4da8c37b 100644 --- a/src/Nonlinear.jl +++ b/src/Nonlinear.jl @@ -96,6 +96,20 @@ struct Chi2Field{χT} Pl::Vector{Float64} # polarisation in the lab frame end +""" + Chi2Field(θ, ϕ, χ2) + +Construct a second-order nonlinear polarisation response for real, two-component +electric fields in the lab frame. + +`θ` and `ϕ` (radians) define the crystal orientation relative to the lab frame. +`χ2` must be a 3×6 second-order susceptibility tensor in contracted notation, +with column order `[xx, yy, zz, yz, xz, xy]` (where mixed terms are multiplied by 2 +in [`field_products!`](@ref)). + +The returned callable adds \$ε_0 P_{NL}\$ to `out` when invoked as +`response(out, E, ρ)`. Note that the density `ρ` is ignored. +""" function Chi2Field(θ, ϕ, χ2) toCrystal = RotMatrix(RotZY(-ϕ, -θ)) # RotMatrix converts to static matrix toLab = RotMatrix(RotYZ(θ, ϕ)) @@ -184,7 +198,7 @@ We take the magnitude of the electric field to calculate the ionization rate and fraction, and then solve the plasma polarisation component-wise for the vector field. -A similar approach was used in: C Tailliez et al 2020 New J. Phys. 22 103038. +A similar approach was used in: C Tailliez et al 2020 New J. Phys. 22 103038. """ function PlasmaVector!(Plas::PlasmaCumtrapz, E) Ex = E[:,1] @@ -227,7 +241,7 @@ abstract type RamanPolar end "Raman polarisation response type for a carrier resolved field" struct RamanPolarField{TR, Tt, Thv, Tω, Tv, FTt, HTt} <: RamanPolar r::TR # Raman response - h::Tt # doubled buffer to hold response + padding + h::Tt # doubled buffer to hold response + padding ht::Thv # buffer to hold time domain response hω::Tω # the frequency domain Raman response function Eω2::Tω # buffer to hold the Fourier transform of E^2 @@ -245,7 +259,7 @@ end "Raman polarisation response type for an envelope" struct RamanPolarEnv{TR, Tt, Thv, Tω, Tv, FTt} <: RamanPolar r::TR # Raman response - h::Tt # doubled buffer to hold response + padding + h::Tt # doubled buffer to hold response + padding ht::Thv # buffer to hold time domain response hω::Tω # the frequency domain Raman response function Eω2::Tω # buffer to hold the Fourier transform of E^2 @@ -347,7 +361,7 @@ function (R::RamanPolar)(out, Et, ρ) # i.e. only the part corresponding to the original time grid # note that the response function time 0 is put into the first element of the response array # this ensures that causality is maintained, and no artificial delay between the field and - # the start of the response function occurs, at each convolution point. + # the start of the response function occurs, at each convolution point. R.r(R.ht, ρ) R.hω .= R.FT * R.h @@ -368,7 +382,7 @@ function (R::RamanPolar)(out, Et, ρ) for i = 1:length(E) R.Pout[i] = ρ*E[i]*R.P[i] end - + # copy to output in dimensions requested if ndims(Et) > 1 out .+= reshape(R.Pout, size(Et)) diff --git a/src/PhysData.jl b/src/PhysData.jl index d6c4df73..64456b19 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -482,6 +482,16 @@ function crystal_internal_angle(nfun, ω, kx) δθ end +""" + χ2(material) + +Return the second-order nonlinear susceptibility tensor for `material` as a +3×6 matrix in SI units (m/V), using contracted notation +`[xx, yy, zz, yz, xz, xy]`. + +Currently implemented materials: +- `:BBO` +""" function χ2(material) if material == :BBO # Shoji, I. et al. J. Opt. Soc. Am. B, JOSAB 16, 620–624 (1999) From 9cb204cbdcb5a1af53030883c9934ceb7454a7d3 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 5 Mar 2026 11:13:01 +0000 Subject: [PATCH 61/72] remove tests scripts from runtests --- test/runtests.jl | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 620c5f5c..be7adfda 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -107,14 +107,9 @@ end include(joinpath(testdir, "test_modes.jl")) end -@testset "Radial Propagation" begin - @info("================= test_radial.jl") - include(joinpath(testdir, "test_radial.jl")) -end - -@testset "Full 3D Propagation" begin - @info("================= test_full_freespace.jl") - include(joinpath(testdir, "test_full_freespace.jl")) +@testset "Free-space Propagation" begin + @info("================= test_freespace.jl") + include(joinpath(testdir, "test_freespace.jl")) end @testset "Antiresonant modes" begin @@ -162,4 +157,4 @@ end include(joinpath(testdir, "test_gnlse.jl")) end -end \ No newline at end of file +end From cbcabaed90c02dc22121b7a522123c2a1d6b7e2d Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 5 Mar 2026 11:58:23 +0000 Subject: [PATCH 62/72] missing docstring --- src/Nonlinear.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Nonlinear.jl b/src/Nonlinear.jl index 4da8c37b..fe6401c4 100644 --- a/src/Nonlinear.jl +++ b/src/Nonlinear.jl @@ -133,6 +133,20 @@ function (c::Chi2Field)(out, E, ρ) end end + +""" + field_products!(Enl, Ec) + +Fill the contracted second-order field-product vector `Enl` from crystal-frame +field components `Ec`. + +The output ordering is +`[Ex^2, Ey^2, Ez^2, 2EyEz, 2ExEz, 2ExEy]`, matching the 3x6 `χ2` tensor +column order `[xx, yy, zz, yz, xz, xy]` used by [`Chi2Field`](@ref). + +Both `Enl` and `Ec` are mutated/read in place and are expected to have length 6 +and 3, respectively. +""" function field_products!(Enl, Ec) Enl[1] = Ec[1]^2 Enl[2] = Ec[2]^2 From 00e12187258e186d6526be621ed03891f57fdc41 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Thu, 5 Mar 2026 13:40:48 +0000 Subject: [PATCH 63/72] improve memory handling with function barrier --- test/test_freespace.jl | 47 +++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 976bbaf7..56738e6b 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -74,13 +74,7 @@ function testfocus(sg::Grid.FreeGrid, Eω, w0) @test maximum(abs.(Iy .- Iy_analytical)/norm(Ix)) < 2e-3 end -@testset "Constant pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), - pol in (false, true), - thg in (false, true), - grid in (rgrid, egrid) - if grid isa Grid.RealGrid && ~thg - continue - end +function runprop_const(grid, sg, thg, pol) nfunλ = PhysData.ref_index_fun(gas, pressure) if pol nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) @@ -100,16 +94,10 @@ end Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) output = Output.MemoryOutput(0, grid.zmax, 21) Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) - testfocus(sg, output["Eω"], w0) + output["Eω"] end -## -@testset "Gradient pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), - pol in (false, true), - thg in (false, true), - grid in (rgrid, egrid) - if grid isa Grid.RealGrid && ~thg - continue - end + +function runprop_grad(grid, sg, thg, pol) nfunλ = PhysData.ref_index_fun(gas, pressure) if pol nfun = (λ; z=0.0) -> (nfunλ(λ), nfunλ(λ)) @@ -130,5 +118,30 @@ end Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) output = Output.MemoryOutput(0, grid.zmax, 21) Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) - testfocus(sg, output["Eω"], w0) + output["Eω"] +end + +@testset "Constant pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), + pol in (false, true), + thg in (false, true), + grid in (rgrid, egrid) + if grid isa Grid.RealGrid && ~thg + continue + end + Eω = runprop_const(grid, sg, thg, pol) + + testfocus(sg, Eω, w0) +end +## +@testset "Gradient pressure: $(typeof(grid)), $(typeof(sg)), pol = $pol, thg = $thg" for sg in (q, xgrid, xygrid), + pol in (false, true), + thg in (false, true), + grid in (rgrid, egrid) + if grid isa Grid.RealGrid && ~thg + continue + end + Eω = runprop_grad(grid, sg, thg, pol) + + testfocus(sg, Eω, w0) + end From ce294306807ecd6ec3fc55271f7d41d30fdf2c5b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 09:33:15 +0000 Subject: [PATCH 64/72] copilot comments --- src/NonlinearRHS.jl | 2 +- test/test_freespace.jl | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 166e9590..f699df31 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -749,7 +749,7 @@ function show(io::IO, t::TransFree2D) samples = "time grid size: $(length(t.grid.t)) / $(length(t.grid.to))" resp = "responses: "*join([string(typeof(ri)) for ri in t.resp], "\n ") x = "x grid: $(minimum(t.xgrid.x)) to $(maximum(t.xgrid.x)), N=$(length(t.xgrid.x))" - out = join(["TransFree", grid, samples, x, resp], "\n ") + out = join(["TransFree2D", grid, samples, x, resp], "\n ") print(io, out) end diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 56738e6b..f19c1c09 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -37,7 +37,7 @@ getshape(grid, sg::Grid.Free2DGrid, pol) = (length(grid.ω), pol ? 2 : 1, length getshape(grid, sg::Grid.FreeGrid, pol) = (length(grid.ω), pol ? 2 : 1, length(sg.x), length(sg.y)) makekerr(grid::Grid.RealGrid, thg) = Nonlinear.Kerr_field(PhysData.γ3_gas(gas)) -makekerr(grid::Grid.EnvGrid, thg) = thg ? Nonlinear.Kerr_env(PhysData.γ3_gas(gas)) : Nonlinear.Kerr_env_thg(PhysData.γ3_gas(gas), grid.ω0, grid.to) +makekerr(grid::Grid.EnvGrid, thg) = thg ? Nonlinear.Kerr_env_thg(PhysData.γ3_gas(gas), grid.ω0, grid.to) : Nonlinear.Kerr_env(PhysData.γ3_gas(gas)) makeconstnorm(grid, q::Hankel.QDHT, nfunλ) = NonlinearRHS.const_norm_radial(grid, q, nfunλ) makeconstnorm(grid, sg::Grid.Free2DGrid, nfunλ) = NonlinearRHS.const_norm_free2D(grid, sg, nfunλ) @@ -143,5 +143,4 @@ end Eω = runprop_grad(grid, sg, thg, pol) testfocus(sg, Eω, w0) - end From ac1c1cc77cb6d15f78518b5b1831888eea4fde1e Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 09:35:08 +0000 Subject: [PATCH 65/72] fix error msg --- src/PhysData.jl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/PhysData.jl b/src/PhysData.jl index 64456b19..1b42fb4a 100644 --- a/src/PhysData.jl +++ b/src/PhysData.jl @@ -469,7 +469,7 @@ function crystal_internal_angle(nfun, ω, kx) # External wavevector is kx = ω/c*sin(θ_i) with θ_i the AOI of the plane wave # Internal wavevector is kx2 = ω/c * n(θ+δθ) * sin(δθ) # Momentum conservation requires kx = kx2 - # kx is given by the grid, so + # kx is given by the grid, so # kx = ω/c * n(θ+δθ)*sin(δθ) # Solve this numerically try @@ -601,7 +601,7 @@ function ref_index_fun(gases::NTuple{N, Symbol}, P::NTuple{N, Number}, T=roomtem ngas = let funs=[χ1_fun(gi, Pi, T) for (gi, Pi) in zip(gases, P)] function ngas(λ) res = funs[1](λ) - for ii in 2:length(gases) + for ii in 2:length(gases) res += funs[ii](λ) end return sqrt(1 + res) @@ -656,7 +656,7 @@ end Calculate the dispersion of order `order` of a given `material` at a wavelength `λ`. For gases the pressure `P` (default:atmosphere) and the temperature `T` (default: room temp) -can also be specified. `lookup::Bool` determines whether a lookup table or a Sellmeier +can also be specified. `lookup::Bool` determines whether a lookup table or a Sellmeier expansion is used for the refractive index (default is material dependent). # Examples @@ -803,7 +803,7 @@ function n2_solid(material::Symbol; λ=nothing) # M. Bache, et al., Opt. Mater. Express, OME 3(3), 357–382 (2013). return 5.1e-20 else - error("Unkown glass $material") + error("Unknown material $material") end end @@ -1157,11 +1157,11 @@ function raman_parameters(material) qJodd = 1, qJeven = 0, Δα = 10.2e-31, # [2] - # TODO τ2r = + # TODO τ2r = dαdQ = 1.46e-20, # [1] Ωv = 3e14, # [1] μ = 1.3e-26, # [1] - # TODO τ2v = + # TODO τ2v = ) elseif material == :N2O rp = (kind = :molecular, @@ -1173,10 +1173,10 @@ function raman_parameters(material) qJeven = 1, # [14] Δα = 28.1e-31, # [2] note that [14] uses twice this τ2r = 23.8e-12, # [14] - # TODO dαdQ = + # TODO dαdQ = Ωv = 2*π*1285*100.0*c, - # TODO μ = - # TODO τ2v = + # TODO μ = + # TODO τ2v = ) elseif material == :SiO2 # [18] rp = (kind = :intermediate, @@ -1198,7 +1198,7 @@ function raman_parameters(material) Bρv = 384e6, # [16] Aρv = 0.0, # [16] Cv = 8220e6 # [16] - ) + ) elseif material == :SF6 rp = (kind = :molecular, rotation = :none, @@ -1207,7 +1207,7 @@ function raman_parameters(material) Ωv = 2*π*775*100.0*c, # [6] μ = (18.998403*m_u)/6, τ2v = 6.6e-12, # [13] - ) + ) else throw(DomainError(material, "Unknown material $material")) end @@ -1275,7 +1275,7 @@ function lookup_mirror(type) elseif type == :PC1611 dat = readdlm(joinpath(Utils.datadir(), "PC1611.txt"); skipstart=1) λR = dat[:, 1] * 1e-9 - R = dat[:, 2] + R = dat[:, 2] rspl = Maths.BSpline(λR, sqrt.(R/100)) λGDD = dat[:, 3] * 1e-9 ω = wlfreq.(λGDD) @@ -1292,7 +1292,7 @@ function lookup_mirror(type) elseif type == :PC1821 dat = readdlm(joinpath(Utils.datadir(), "PC1821.txt"); skipstart=1) λR = dat[:, 1] * 1e-9 - R = dat[:, 2] + R = dat[:, 2] rspl = Maths.BSpline(λR, sqrt.(R/100)) λGDD = dat[:, 3] * 1e-9 ω = wlfreq.(λGDD) @@ -1309,7 +1309,7 @@ function lookup_mirror(type) elseif type == :HD120 dat = readdlm(joinpath(Utils.datadir(), "HD120.csv"), ','; skipstart=1) λR = dat[:, 1] * 1e-9 - R = dat[:, 2] # reflectivity per mirror + R = dat[:, 2] # reflectivity per mirror rspl = Maths.BSpline(λR, sqrt.(R/100)) λGDD = dat[:, 3] * 1e-9 ω = wlfreq.(λGDD) @@ -1383,4 +1383,4 @@ function process_mirror_data(λR, R, λGDD, GDD, λ0, λmin, λmax; fitorder=5, λ, λmin-windowwidth, λmin, λmax, λmax+windowwidth) end -end \ No newline at end of file +end From e214fd23447df23e6e1ce0c55b395fd8debfba94 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 13:06:17 +0000 Subject: [PATCH 66/72] simplify and shorten freespace tests --- test/test_freespace.jl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index f19c1c09..84b6a27b 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -13,15 +13,15 @@ Luna.set_fftw_mode(:estimate) import LinearAlgebra: norm import Test: @test, @testset -R = 1.5e-3 -Nr = 512 -Nx = 256 +R = 0.3e-3 +Nr = 128 +Nx = 64 Ny = 128 gas = :Ar pressure = 1 λ0 = 800e-9 -w0 = 40e-6 +w0 = 200e-6 τfwhm = 20e-15 energy = 1e-12 L = 0.3 @@ -50,28 +50,28 @@ makenorm(grid, sg::Grid.FreeGrid, nfunω) = NonlinearRHS.norm_free(grid, sg, nfu function testfocus(q::Hankel.QDHT, Eω, w0) Eωfoc = Eω[:, :, :, end] Eωr = q \ Eωfoc - Ir = Maths.normbymax(dropdims(sum(abs2.(Eωr); dims=(1, 2)); dims=(1, 2))) + Ir = dropdims(sum(abs2.(Eωr); dims=(1, 2)); dims=(1, 2)) Ir_analytical = Maths.gauss.(q.r, w0/2) - @test maximum(abs.(Ir .- Ir_analytical)/norm(Ir)) < 5e-3 + @test Ir/norm(Ir) ≈ Ir_analytical/norm(Ir_analytical) rtol=0.1 end function testfocus(sg::Grid.Free2DGrid, Eω, w0) Eωfoc = Eω[:, :, :, end] Eωx = FFTW.ifft(Eωfoc, 3) - Ix = Maths.normbymax(dropdims(sum(abs2.(Eωx); dims=(1, 2)); dims=(1, 2))) + Ix = dropdims(sum(abs2.(Eωx); dims=(1, 2)); dims=(1, 2)) Ix_analytical = Maths.gauss.(sg.x, w0/2) - @test maximum(abs.(Ix .- Ix_analytical)/norm(Ix)) < 2e-3 + @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=1e-2 end function testfocus(sg::Grid.FreeGrid, Eω, w0) Eωfoc = Eω[:, :, :, :, end] Eωxy = FFTW.ifft(Eωfoc, (3, 4)) - Iy = Maths.normbymax(dropdims(sum(abs2.(Eωxy); dims=(1, 2, 3)); dims=(1, 2, 3))) - Ix = Maths.normbymax(dropdims(sum(abs2.(Eωxy); dims=(1, 2, 4)); dims=(1, 2, 4))) + Iy = dropdims(sum(abs2.(Eωxy); dims=(1, 2, 3)); dims=(1, 2, 3)) + Ix = dropdims(sum(abs2.(Eωxy); dims=(1, 2, 4)); dims=(1, 2, 4)) Ix_analytical = Maths.gauss.(sg.x, w0/2) - @test maximum(abs.(Ix .- Ix_analytical)/norm(Ix)) < 2e-3 + @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=1e-2 Iy_analytical = Maths.gauss.(sg.y, w0/2) - @test maximum(abs.(Iy .- Iy_analytical)/norm(Ix)) < 2e-3 + @test Ix/norm(Iy) ≈ Ix_analytical/norm(Iy_analytical) rtol=1e-2 end function runprop_const(grid, sg, thg, pol) @@ -116,8 +116,8 @@ function runprop_grad(grid, sg, thg, pol) inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz=-L) Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) - output = Output.MemoryOutput(0, grid.zmax, 21) - Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) + output = Output.MemoryOutput(0, grid.zmax, 11) + Luna.run(Eω, grid, linop, transform, FT, output; init_dz=0.1) output["Eω"] end From 7d4673eaafc403952dcd2e7210a516a889e129b1 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 13:07:33 +0000 Subject: [PATCH 67/72] change const test --- test/test_freespace.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 84b6a27b..8b0ed7f2 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -92,8 +92,8 @@ function runprop_const(grid, sg, thg, pol) inputs = Fields.GaussGaussField(;λ0, τfwhm, energy, w0, propz=-L) Eω, transform, FT = Luna.setup(grid, sg, densityfun, normfun, responses, inputs) - output = Output.MemoryOutput(0, grid.zmax, 21) - Luna.run(Eω, grid, linop, transform, FT, output; init_dz=5e-3) + output = Output.MemoryOutput(0, grid.zmax, 11) + Luna.run(Eω, grid, linop, transform, FT, output; init_dz=0.1) output["Eω"] end From 206652e2836a1a37f16bc0a53bbc74c5400fa926 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 13:10:41 +0000 Subject: [PATCH 68/72] increase pupil radius, make tests even smaller --- test/test_freespace.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 8b0ed7f2..3ac18887 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -13,10 +13,10 @@ Luna.set_fftw_mode(:estimate) import LinearAlgebra: norm import Test: @test, @testset -R = 0.3e-3 -Nr = 128 +R = 0.6e-3 +Nr = 64 Nx = 64 -Ny = 128 +Ny = 32 gas = :Ar pressure = 1 From ce95b52b9aed92c46ee56317312f22b8ba974b54 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Fri, 6 Mar 2026 13:24:51 +0000 Subject: [PATCH 69/72] rtol --- test/test_freespace.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_freespace.jl b/test/test_freespace.jl index 3ac18887..3951d669 100644 --- a/test/test_freespace.jl +++ b/test/test_freespace.jl @@ -52,7 +52,7 @@ function testfocus(q::Hankel.QDHT, Eω, w0) Eωr = q \ Eωfoc Ir = dropdims(sum(abs2.(Eωr); dims=(1, 2)); dims=(1, 2)) Ir_analytical = Maths.gauss.(q.r, w0/2) - @test Ir/norm(Ir) ≈ Ir_analytical/norm(Ir_analytical) rtol=0.1 + @test Ir/norm(Ir) ≈ Ir_analytical/norm(Ir_analytical) rtol=0.01 end function testfocus(sg::Grid.Free2DGrid, Eω, w0) @@ -60,7 +60,7 @@ function testfocus(sg::Grid.Free2DGrid, Eω, w0) Eωx = FFTW.ifft(Eωfoc, 3) Ix = dropdims(sum(abs2.(Eωx); dims=(1, 2)); dims=(1, 2)) Ix_analytical = Maths.gauss.(sg.x, w0/2) - @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=1e-2 + @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=0.001 end function testfocus(sg::Grid.FreeGrid, Eω, w0) @@ -69,9 +69,9 @@ function testfocus(sg::Grid.FreeGrid, Eω, w0) Iy = dropdims(sum(abs2.(Eωxy); dims=(1, 2, 3)); dims=(1, 2, 3)) Ix = dropdims(sum(abs2.(Eωxy); dims=(1, 2, 4)); dims=(1, 2, 4)) Ix_analytical = Maths.gauss.(sg.x, w0/2) - @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=1e-2 Iy_analytical = Maths.gauss.(sg.y, w0/2) - @test Ix/norm(Iy) ≈ Ix_analytical/norm(Iy_analytical) rtol=1e-2 + @test Ix/norm(Ix) ≈ Ix_analytical/norm(Ix_analytical) rtol=0.001 + @test Iy/norm(Iy) ≈ Iy_analytical/norm(Iy_analytical) rtol=0.001 end function runprop_const(grid, sg, thg, pol) From 15af937fa867c86c249c97a34e96bebd83f0e4a9 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Mar 2026 17:07:29 +0000 Subject: [PATCH 70/72] remove function corpse --- src/NonlinearRHS.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index 34cd267d..d12dced7 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -523,20 +523,6 @@ function show(io::IO, t::TransRadial) print(io, out) end -function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false) - np = pol ? 2 : 1 - Eωo = zeros(ComplexF64, (length(grid.ωo), np, HT.N)) - Eto_r = zeros(TT, (length(grid.to), np, HT.N)) - Pto_r = similar(Eto_r) - Eto_k = similar(Eto_r) - Pto_k = similar(Eto_r) - Pωo = similar(Eωo) - idcs = CartesianIndices(size(Pto_r)[3:end]) - Tfwd = convert(Matrix{TT}, transpose(HT.T) .* HT.scaleRK) - Tbwd = convert(Matrix{TT}, transpose(HT.T) ./ HT.scaleRK) - TransRadial(HT, FT, normfun, responses, grid, densityfun, Pto_r, Pto_k, Eto_r, Eto_k, Eωo, Pωo, idcs, - Tfwd, Tbwd) - """ TransRadial(TT, grid, HT, FT, responses, densityfun, normfun; noise_field=nothing) From b78421037dc593af932352f342ab5d0fdfb7445b Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Mon, 9 Mar 2026 19:59:32 +0000 Subject: [PATCH 71/72] missed an inv(FT) --- src/NonlinearRHS.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index d12dced7..f2b60296 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -415,7 +415,7 @@ function TransModeAvg(TT, grid, FT, resp, densityfun, norm!, aeff; noise_field=n if !isnothing(noise_field) Eωo_noise = zeros(ComplexF64, length(grid.ωo)) Et_noise = zeros(TT, length(grid.to)) - to_time!(Et_noise, noise_field, Eωo_noise, inv(FT)) + to_time!(Et_noise, noise_field, Eωo_noise, FT) Et_nl = zeros(TT, length(grid.to)) else Et_noise = nothing From 4e7e6085ab7b96272123e1887a782cbec52f73d9 Mon Sep 17 00:00:00 2001 From: chrisbrahms Date: Tue, 17 Mar 2026 19:12:20 +0000 Subject: [PATCH 72/72] fix merge --- src/NonlinearRHS.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonlinearRHS.jl b/src/NonlinearRHS.jl index f2b60296..16014beb 100644 --- a/src/NonlinearRHS.jl +++ b/src/NonlinearRHS.jl @@ -534,7 +534,7 @@ Construct a `TransRadial` to calculate the reciprocal-domain nonlinear polarisat via inverse FFT and inverse Hankel transform, and stored as `Et_noise`. Generate with [`Fields.generate_noise_field`](@ref Luna.Fields.generate_noise_field). """ -function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun; noise_field=nothing) +function TransRadial(TT, grid, HT, FT, responses, densityfun, normfun, pol=false; noise_field=nothing) np = pol ? 2 : 1 Eωo = zeros(ComplexF64, (length(grid.ωo), np, HT.N)) Eto_r = zeros(TT, (length(grid.to), np, HT.N))