Skip to content

Commit 88a8760

Browse files
authored
Implement FourierBandForcing (#32)
* Allow copying coefficients from Fourier field * Implement FourierBandForcing (untested) * Update Timestepping module * Small fixes + add tests * Improve from_fourier_grid! * Some fixes + tests * Update CHANGELOG [skip ci] * Update test * Fix nonperiodic tests * Allow α′ term in FourierBandForcing * Add option to use coarse-grained vorticity field * Simpler variant of coarse-grained vorticity case * Treat case where coarse-grained vorticity is zero * Test with smooth_vorticity = true * smoothed_vorticity -> filtered_vorticity * Update docs * Add SyntheticFields.remove_zeros! * Fixes to docs * Parallelise forcing computations
1 parent 1cde5e0 commit 88a8760

File tree

15 files changed

+766
-176
lines changed

15 files changed

+766
-176
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55

66
## Unreleased
77

8+
### Added
9+
10+
- Implement `FourierBandForcing` as an alternative to `NormalFluidForcing`.
11+
The new forcing is expected to influence only (or mostly) the scales where it
12+
is defined, letting the other lengthscales "free" to evolve.
13+
814
## [0.26.4] - 2025-03-04
915

1016
### Changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
1919
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
2020
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
2121
NonuniformFFTs = "cd96f58b-6017-4a02-bb9e-f4d81626177f"
22+
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
2223
Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588"
2324
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
2425
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@@ -62,6 +63,7 @@ LinearAlgebra = "1.9"
6263
MakieCore = "0.6, 0.7, 0.8, 0.9"
6364
NonuniformFFTs = "0.5.1, 0.6, 0.7"
6465
Observables = "0.5"
66+
OhMyThreads = "0.7.0"
6567
Polyester = "0.7"
6668
PrecompileTools = "1"
6769
Random = "1.10"

docs/src/modules/Forcing.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ Forcing
1414
```@docs
1515
NormalFluidForcing
1616
apply!
17-
get_velocities!
17+
```
18+
19+
## Fourier band forcing
20+
21+
```@docs
22+
FourierBandForcing
1823
```
1924

2025
## Abstract types

docs/src/modules/SyntheticFields.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ UniformVectorField
2020
```@docs
2121
FourierBandVectorField
2222
init_coefficients!
23+
from_fourier_grid!
24+
remove_zeros!
2325
```
2426

2527
## Saving and loading data

src/Forcing/Forcing.jl

Lines changed: 15 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ Defines methods for injecting energy onto a system of vortices.
66
module Forcing
77

88
using ..Filaments: AbstractFilament, UnitTangent
9-
using ..SyntheticFields: SyntheticFields # for docs only
9+
using ..BiotSavart: BiotSavart, BiotSavartCache
10+
using ..SyntheticFields: SyntheticFields, FourierBandVectorField
1011
using LinearAlgebra: ×
12+
using OhMyThreads: Scheduler, SerialScheduler, tforeach
1113

12-
export AbstractForcing, NormalFluidForcing
14+
using Adapt: adapt
15+
16+
export AbstractForcing, NormalFluidForcing, FourierBandForcing
1317

1418
"""
1519
AbstractForcing
@@ -18,157 +22,28 @@ Abstract type representing a forcing method.
1822
"""
1923
abstract type AbstractForcing end
2024

25+
init_cache(forcing::Nothing, args...) = nothing # called when forcing is disabled
26+
2127
"""
22-
Forcing.apply!(forcing::AbstractForcing, vs::AbstractVector{<:Vec3}, f::AbstractFilament)
28+
Forcing.apply!(forcing::AbstractForcing, vs::AbstractVector{<:Vec3}, f::AbstractFilament; [scheduler])
2329
2430
Apply forcing to a single filament `f` with self-induced velocities `vs`.
2531
2632
At output, the `vs` vector is overwritten with the actual vortex line velocities.
2733
34+
The optional `scheduler` keyword can be used to parallelise computations using one of the
35+
[schedulers defined in OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/refs/api/#Schedulers).
36+
2837
---
2938
30-
Forcing.apply!(forcing::NormalFluidForcing, vs, vn, tangents)
39+
Forcing.apply!(forcing::NormalFluidForcing, vs, vn, tangents; [scheduler])
3140
3241
This variant can be used in the case of a [`NormalFluidForcing`](@ref) if one already has
3342
precomputed values of the normal fluid velocity and local unit tangents at filament points.
34-
35-
The normal fluid velocities may be computed using [`Forcing.get_velocities!`](@ref).
3643
"""
3744
function apply! end
3845

39-
@doc raw"""
40-
NormalFluidForcing <: AbstractForcing
41-
NormalFluidForcing(vn::Function; α, α′ = 0)
42-
43-
Forcing due to mutual friction with a normal fluid.
44-
45-
The normal fluid is represented by a function `vn` which should take a position
46-
`x⃗::SVector{N, T}` and return a velocity `v⃗::SVector{N, T}` (`N` is the number of
47-
dimensions, usually `N = 3`).
48-
49-
In particular, the function could be a synthetic velocity field from the
50-
[`SyntheticFields`](@ref) module (see below for examples).
51-
52-
This type of forcing defines an external velocity ``\bm{v}_{\text{f}}`` affecting vortex
53-
motion, so that the actual vortex velocity ``\bm{v}_{\text{L}}`` is
54-
55-
```math
56-
\frac{\mathrm{d}\bm{s}}{\mathrm{d}t} = \bm{v}_{\text{L}} = \bm{v}_{\text{s}} + \bm{v}_{\text{f}}.
57-
```
58-
59-
Here ``\bm{v}_{\text{s}}`` is the self-induced velocity obtained by applying Biot–Savart's law.
60-
61-
The forcing velocity is of the form:
62-
63-
```math
64-
\bm{v}_{\text{f}} =
65-
α \bm{s}' × (\bm{v}_{\text{n}} - \bm{v}_{\text{s}})
66-
- α' \bm{s}' × \left[ \bm{s}' × (\bm{v}_{\text{n}} - \bm{v}_{\text{s}}) \right]
67-
```
68-
69-
where ``\bm{s}'`` is the local unit tangent vector, and ``α`` and ``α'`` are non-dimensional
70-
coefficients representing the intensity of Magnus and drag forces.
71-
72-
# Example
73-
74-
Define a mutual friction forcing based on a large-scale normal fluid velocity field
75-
(see [`SyntheticFields.FourierBandVectorField`](@ref)):
76-
77-
```jldoctest
78-
julia> using VortexPasta.Forcing: NormalFluidForcing
79-
80-
julia> using VortexPasta.SyntheticFields: SyntheticFields, FourierBandVectorField
81-
82-
julia> using Random: Xoshiro
83-
84-
julia> rng = Xoshiro(42); # initialise random number generator (optional, but recommended)
85-
86-
julia> Ls = (2π, 2π, 2π); # domain dimensions
87-
88-
julia> vn_rms = 1.0; # typical magnitude (rms value) of normal fluid velocity components
89-
90-
julia> vn = FourierBandVectorField(undef, Ls; kmin = 0.1, kmax = 1.5) # create field with non-zero Fourier wavevectors kmin ≤ |k⃗| ≤ kmax
91-
FourierBandVectorField{Float64, 3} with 9 independent Fourier coefficients in |k⃗| ∈ [1.0, 1.4142]
92-
93-
julia> SyntheticFields.init_coefficients!(rng, vn, vn_rms); # randomly set non-zero Fourier coefficients of the velocity field
94-
95-
julia> forcing = NormalFluidForcing(vn; α = 0.8, α′ = 0)
96-
NormalFluidForcing{Float64} with:
97-
├─ Magnus force coefficient: α = 0.8
98-
├─ Drag force coefficient: α′ = 0.0
99-
└─ Normal velocity field: FourierBandVectorField{Float64, 3} with 9 independent Fourier coefficients in |k⃗| ∈ [1.0, 1.4142]
100-
```
101-
102-
"""
103-
struct NormalFluidForcing{
104-
T,
105-
VelocityField <: Function, # should return an SVector{N, T}
106-
} <: AbstractForcing
107-
vn :: VelocityField
108-
α :: T
109-
α′ :: T
110-
end
111-
112-
function NormalFluidForcing(vn::F; α::T, α′::Real = 0) where {T <: AbstractFloat, F <: Function}
113-
NormalFluidForcing(vn, T(α), T(α′))
114-
end
115-
116-
function Base.show(io::IO, f::NormalFluidForcing{T}) where {T}
117-
(; vn, α, α′,) = f
118-
indent = get(io, :indent, 0)
119-
nspaces = max(indent, 1)
120-
spaces = " "^nspaces
121-
print(io, "NormalFluidForcing{$T} with:")
122-
print(io, "\n$(spaces)├─ Magnus force coefficient: α = ", α)
123-
print(io, "\n$(spaces)├─ Drag force coefficient: α′ = ", α′)
124-
print(io, "\n$(spaces)└─ Normal velocity field: ", vn)
125-
end
126-
127-
"""
128-
Forcing.get_velocities!(forcing::NormalFluidForcing, vn::AbstractVector{<:Vec3}, f::AbstractFilament)
129-
130-
Evaluate normal fluid velocity at filament nodes.
131-
132-
Results are written to `vn`, which should have the same length as the filament `f`.
133-
"""
134-
function get_velocities!(forcing::NormalFluidForcing, vn::AbstractVector, f::AbstractFilament)
135-
for i eachindex(vn, f)
136-
vn[i] = forcing.vn(f[i])
137-
end
138-
vn
139-
end
140-
141-
function apply!(forcing::NormalFluidForcing, vs::AbstractVector, f::AbstractFilament)
142-
eachindex(vs) == eachindex(f) || throw(DimensionMismatch("lengths of filament and velocity vectors don't match"))
143-
@inline get_at_node(i) = (v⃗ₙ = forcing.vn(f[i]), s⃗′ = f[i, UnitTangent()])
144-
_apply!(get_at_node, forcing, vs)
145-
end
146-
147-
function apply!(forcing::NormalFluidForcing, vs::AbstractVector, vn::AbstractVector, tangents::AbstractVector)
148-
eachindex(vs) == eachindex(vn) == eachindex(tangents) || throw(DimensionMismatch("lengths of vectors don't match"))
149-
@inline get_at_node(i) = @inbounds (v⃗ₙ = vn[i], s⃗′ = tangents[i],)
150-
_apply!(get_at_node, forcing, vs)
151-
end
152-
153-
# The first argument is a `get_at_node(i)` function which returns a NamedTuple with fields
154-
# (v⃗ₛ, v⃗ₙ, s⃗′) with the superfluid velocity, normal fluid velocity and the local unit
155-
# tangent at the node `i` of a filament. This is useful if those quantities have been
156-
# precomputed.
157-
function _apply!(get_at_node::F, forcing::NormalFluidForcing, vs::AbstractVector) where {F <: Function}
158-
(; α, α′,) = forcing
159-
V = eltype(vs) # usually Vec3{T} = SVector{3, T}
160-
for i eachindex(vs)
161-
(; v⃗ₙ, s⃗′,) = @inline get_at_node(i) # superfluid velocity, normal fluid velocity and unit tangent
162-
v⃗ₛ = vs[i]
163-
vₙₛ = V(v⃗ₙ) - V(v⃗ₛ) # slip velocity
164-
v_perp = s⃗′ × vₙₛ
165-
vf = α * v_perp # velocity due to Magnus force
166-
if !iszero(α′)
167-
vf = vf - α′ * s⃗′ × v_perp # velocity due to drag force (it's quite common to set α′ = 0)
168-
end
169-
vs[i] = v⃗ₛ + vf
170-
end
171-
vs
172-
end
46+
include("normal_fluid.jl")
47+
include("fourier_band.jl")
17348

17449
end

src/Forcing/fourier_band.jl

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
@doc raw"""
2+
FourierBandForcing <: AbstractForcing
3+
FourierBandForcing(vn::FourierBandVectorField; α, α′ = 0, filtered_vorticity = false)
4+
5+
Forcing due to mutual friction of a normal fluid with a Fourier-filtered superfluid velocity.
6+
7+
This forcing is similar to [`NormalFluidForcing`](@ref), but tries to only affect scales
8+
within a given band `[kmin, kmax]` in Fourier space. This is achieved by a normal fluid velocity
9+
field represented by a [`FourierBandVectorField`](@ref), and by a modified Schwarz's
10+
equation in which only a coarse-grained superfluid flow is taken into account in the
11+
estimation of the mutual friction term.
12+
13+
Concretely, the vortex line velocity according to this forcing type is:
14+
15+
```math
16+
\frac{\mathrm{d}\bm{s}}{\mathrm{d}t} = \bm{v}_{\text{L}} = \bm{v}_{\text{s}} + \bm{v}_{\text{f}}
17+
```
18+
19+
The forcing velocity is of the form:
20+
21+
```math
22+
\bm{v}_{\text{f}} = α \bm{s}' × \bm{v}_{\text{ns}}^{>} - α' \bm{s}' × \left( \bm{s}' × \bm{v}_{\text{ns}}^{>} \right)
23+
```
24+
25+
where ``\bm{v}_{\text{ns}}^{>} = \bm{v}_{\text{n}} - \bm{v}_{\text{s}}^{>}`` is a filtered slip velocity.
26+
In practice, the filtered velocity is active within the same wavenumber range `[kmin, kmax]` where `vn` is
27+
defined. See [`NormalFluidForcing`](@ref) for other definitions.
28+
29+
## Using a filtered vorticity field (experimental)
30+
31+
To further ensure that this forcing only affects the chosen range of scales, one can pass
32+
`filtered_vorticity = true`, which will replace the local unit tangent ``\bm{s}'`` with a
33+
normalised coarse-grained vorticity. This corresponds to setting ``\bm{s}' = \bm{ω}^{>} / |\bm{ω}^{>}|``
34+
where ``\bm{ω}^{>}`` is the Fourier-filtered vorticity field.
35+
"""
36+
struct FourierBandForcing{
37+
T <: AbstractFloat,
38+
N, # number of dimensions (usually 3)
39+
VelocityField <: FourierBandVectorField{T, N},
40+
} <: AbstractForcing
41+
vn :: VelocityField
42+
α :: T
43+
α′ :: T
44+
filtered_vorticity :: Bool
45+
end
46+
47+
with_filtered_vorticity(f::FourierBandForcing) = f.filtered_vorticity
48+
49+
function FourierBandForcing(
50+
vn::FourierBandVectorField{T, 3}; α::Real, α′::Real = 0,
51+
filtered_vorticity::Bool = false,
52+
) where {T <: AbstractFloat}
53+
FourierBandForcing(vn, T(α), T(α′), filtered_vorticity)
54+
end
55+
56+
function Base.show(io::IO, f::FourierBandForcing{T}) where {T}
57+
(; vn, α, α′,) = f
58+
indent = get(io, :indent, 0)
59+
nspaces = max(indent, 1)
60+
spaces = " "^nspaces
61+
print(io, "FourierBandForcing{$T} with:")
62+
print(io, "\n$(spaces)├─ Normal velocity field: ", vn)
63+
print(io, "\n$(spaces)├─ Filtered superfluid vorticity: ", with_filtered_vorticity(f))
64+
print(io, "\n$(spaces)└─ Friction coefficients: α = ", α, " and α′ = ", α′)
65+
end
66+
67+
# Here vs_d is the superfluid velocity in Fourier space, optionally on a device (GPU).
68+
function init_cache(f::FourierBandForcing{T, N}, cache_bs::BiotSavartCache) where {T, N}
69+
(; vn,) = f
70+
vs_d = BiotSavart.get_longrange_field_fourier(cache_bs).field
71+
A = eltype(vs_d).name.wrapper # e.g. Array, CuArray, ... (XXX: relies on Julia internals!)
72+
vn_d = adapt(A, vn)::FourierBandVectorField # vn on the device
73+
vtmp_h = similar(vn)::FourierBandVectorField # temporary buffer (on host)
74+
vtmp_d = adapt(A, vtmp_h)::FourierBandVectorField # temporary buffer (on device)
75+
ω_h = similar(vtmp_h) # coarse-grained superfluid vorticity
76+
if !with_filtered_vorticity(f)
77+
empty!(ω_h) # we don't use this field
78+
end
79+
ω_d = adapt(A, ω_h)
80+
(; vn_d, vtmp_d, vtmp_h, ω_h, ω_d)
81+
end
82+
83+
function update_cache!(cache, f::FourierBandForcing{T, N}, cache_bs::BiotSavartCache) where {T, N}
84+
(; vtmp_d, vn_d, vtmp_h, ω_h, ω_d) = cache
85+
86+
vs_d, ks_grid = let data = BiotSavart.get_longrange_field_fourier(cache_bs)
87+
local (; state, field, wavenumbers,) = data
88+
@assert state.quantity == :velocity
89+
@assert state.smoothed == true
90+
field, wavenumbers
91+
end
92+
α_ewald = cache_bs.params.α
93+
94+
# (1) Copy normal fluid velocity onto buffer (device -> device copy)
95+
@assert vtmp_d.cs !== vn_d.cs # coefficients are not aliased
96+
copyto!(vtmp_d, vn_d)
97+
98+
# (2) Retrieve values from coarse-grained velocity field (Gaussian filtered with α_ewald parameter).
99+
# We "unfilter" the values, similarly to the way we compute energy spectra from the long-range velocity.
100+
inv_four_α² = 1 / (4 * α_ewald * α_ewald)
101+
@inline function op(vn, vs_filtered, k⃗)
102+
= sum(abs2, k⃗)
103+
φ = @fastmath exp(k² * inv_four_α²)
104+
vs = φ * vs_filtered
105+
vn - vs
106+
end
107+
SyntheticFields.from_fourier_grid!(op, vtmp_d, vs_d, ks_grid) # vtmp_d now contains vn_d(k⃗) - vs_d(k⃗) in [kmin, kmax]
108+
109+
# Optionally compute coarse-grained vorticity.
110+
@inline function op_vorticity(_, vs_filtered, k⃗)
111+
= sum(abs2, k⃗)
112+
φ = @fastmath exp(k² * inv_four_α²)
113+
vs = φ * vs_filtered
114+
im * (k⃗ × vs)
115+
end
116+
if with_filtered_vorticity(f)
117+
@assert length(ω_h) == length(ω_d) == length(vtmp_d)
118+
SyntheticFields.from_fourier_grid!(op_vorticity, ω_d, vs_d, ks_grid)
119+
end
120+
121+
# (3) Copy results to CPU if needed (avoided if the "device" is the CPU).
122+
if vtmp_h !== vtmp_d
123+
copyto!(vtmp_h, vtmp_d)
124+
end
125+
if ω_h !== ω_d && with_filtered_vorticity(f)
126+
copyto!(ω_h, ω_d)
127+
end
128+
129+
nothing
130+
end
131+
132+
function apply!(forcing::FourierBandForcing, cache, vs::AbstractVector, f::AbstractFilament; scheduler = SerialScheduler())
133+
(; α, α′,) = forcing
134+
(; vtmp_h, ω_h,) = cache # contains vₙ - vₛ at large scale
135+
V = eltype(vs) # usually Vec3{T} = SVector{3, T}
136+
tforeach(eachindex(vs); scheduler) do i
137+
@inline
138+
s⃗ = f[i]
139+
if with_filtered_vorticity(forcing)
140+
ω⃗ = V(ω_h(s⃗)) # coarse-grained vorticity at s⃗
141+
ω_norm = sqrt(sum(abs2, ω⃗))
142+
# Note: in the case that the coarse-grained vorticity is exactly zero (very very
143+
# unlikely), we simply set s⃗′ to zero which disables the forcing.
144+
ω_invnorm = ifelse(iszero(ω_norm), ω_norm, 1/ω_norm) # = 1/|ω⃗| (or zero, if |ω⃗| = 0)
145+
s⃗′ = (ω⃗ * ω_invnorm)::V # replace s⃗′ with direction of coarse-grained vorticity
146+
else
147+
s⃗′ = f[i, UnitTangent()]::V
148+
end
149+
v⃗ₙₛ = V(vtmp_h(s⃗))
150+
v_perp = s⃗′ × v⃗ₙₛ
151+
vf = α * v_perp
152+
if !iszero(α′)
153+
vf = vf - α′ * s⃗′ × v_perp
154+
end
155+
vs[i] = vs[i] + vf
156+
end
157+
vs
158+
end

0 commit comments

Comments
 (0)