Acceleration via Real-FFT backend
The spectral step can be computed with either of two FFT backends. The default, ComplexFFTBackend, uses complex-valued plans (plan_fft / plan_ifft) over (nx, ny) complex arrays. RealFFTBackend uses real-valued plans (plan_rfft / plan_irfft) instead: because the displacement and load fields are real, the non-redundant half of the spectrum is of size (nx÷2+1, ny), which roughly halves the memory and arithmetic cost of every spectral step.
This is a purely numerical choice and is orthogonal to the rheology — both runs below use exactly the same SolidEarth with a ViscousMantle, and differ only in the fft field of SolverOptions.
This example verifies that both backends give identical results and measures the speed-up on a standard benchmark geometry (cylindrical load, same setup as the Analytical benchmark).
using FastIsostasy
W, n = 3f6, 8
domain = RegionalDomain(W, n)
H_ice_0 = zeros(domain)
H_ice_1 = 1f3 .* (domain.R .< 1f6)
t_ice = [0, 1, 50f3]
H_ice = [H_ice_0, H_ice_1, H_ice_1]
it = TimeInterpolatedIceThickness(t_ice, H_ice, domain)
bcs = BoundaryConditions(domain, ice_thickness = it)
sealevel = RegionalSeaLevel()
nout = NativeOutput(vars = [:u],
t = [100, 500, 1500, 5000, 10_000, 50_000f0])
solidearth = SolidEarth(
domain,
mantle = ViscousMantle(),
lithosphere = LaterallyVariableLithosphere(),
layer_boundaries = [88f3],
layer_viscosities = [1f21],
) Lithosphere: LaterallyVariableLithosphere
Mantle: ViscousMantle
Calibration: NoCalibration
Compressibility: CompressibleMantle
Viscosity lumping: FreqDomainViscosityLumping
Lithosphere column: IncompressibleLithosphereColumn
extrema(effective_viscosity): (1.17187504f21, 1.17187504f21)
extrema(litho_thickness): (88000.0f0, 88000.0f0)
active cells: 65536 / 65536
litho_youngmodulus: 6.6e10
litho_poissonratio, mantle_poissonratio: Float32[0.28, 0.28]
rho_uppermantle, rho_litho: Float32[3400.0, 3200.0]
Run the simulation with the default complex-FFT backend.
opts_complex = SolverOptions(
integ = RKCIntegrator(),
fft = ComplexFFTBackend(),
show_progress = false, # hide progress bar
)
sim_complex = Simulation(domain, bcs, sealevel, solidearth, (0, 50f3);
nout = nout, opts = opts_complex)
run!(sim_complex)┌ Warning: NetCDF filename does not end with '.nc' and is therefore ignored.
└ @ FastIsostasy ~/work/FastIsostasy.jl/FastIsostasy.jl/src/io.jl:333Run the same simulation with the real-FFT backend. Only opts changes.
opts_real = SolverOptions(
integ = RKCIntegrator(),
fft = RealFFTBackend(),
show_progress = false, # hide progress bar
)
sim_real = Simulation(domain, bcs, sealevel, solidearth, (0, 50f3);
nout = nout, opts = opts_real)
run!(sim_real)┌ Warning: NetCDF filename does not end with '.nc' and is therefore ignored.
└ @ FastIsostasy ~/work/FastIsostasy.jl/FastIsostasy.jl/src/io.jl:333
┌ Warning: RealFFTBackend is experimental: it may yield larger numerical errors than ComplexFFTBackend for laterally-variable lithosphere setups, and the expected performance gain may not materialise on all hardware. Prefer ComplexFFTBackend for production runs.
└ @ FastIsostasy ~/work/FastIsostasy.jl/FastIsostasy.jl/src/tools.jl:207Correctness check
The two displacement fields should be numerically identical up to floating-point round-off. We print the maximum absolute difference across all stored snapshots.
max_diff = maximum(maximum(abs, u_c .- u_r)
for (u_c, u_r) in zip(sim_complex.nout.vals[:u], sim_real.nout.vals[:u]))
println("Max |u_complex - u_real| across all snapshots: $max_diff m")Max |u_complex - u_real| across all snapshots: 0.053326607 mTiming comparison
The sim.timer.t_computation vector stores the wall time (in seconds) elapsed up to each output snapshot.
t_comp_complex = sim_complex.timer.t_computation
t_comp_real = sim_real.timer.t_computation
println("Total computation time — ComplexFFTBackend: $(round(t_comp_complex[end]; digits=3)) s")
println("Total computation time — RealFFTBackend: $(round(t_comp_real[end]; digits=3)) s")
println("Speed-up: $(round(t_comp_complex[end] / t_comp_real[end]; digits=2))×")Total computation time — ComplexFFTBackend: 11.201 s
Total computation time — RealFFTBackend: 9.76 s
Speed-up: 1.15×Typically yields something like:
Total computation time — ComplexFFTBackend: 12.279 s Total computation time — RealFFTBackend: 11.195 s Speed-up: 1.1×
Hinting towards the fact that the bulk of the computation is not spent in the spectral step.
Copy-pastable code
using FastIsostasy
W, n = 3f6, 8
domain = RegionalDomain(W, n)
H_ice_1 = 1f3 .* (domain.R .< 1f6)
it = TimeInterpolatedIceThickness([0, 1, 50f3],
[zeros(domain), H_ice_1, H_ice_1], domain)
bcs = BoundaryConditions(domain, ice_thickness = it)
nout = NativeOutput(vars = [:u], t = [100, 500, 1500, 5000, 10_000, 50_000f0])
solidearth = SolidEarth(domain,
mantle = ViscousMantle(), lithosphere = LaterallyVariableLithosphere(),
layer_boundaries = [88f3], layer_viscosities = [1f21])
# the *only* difference between the two runs is the `fft` field
function run_backend(backend)
opts = SolverOptions(integ = RKCIntegrator(), fft = backend,
show_progress = false)
sim = Simulation(domain, bcs, RegionalSeaLevel(), solidearth, (0, 50f3);
nout = nout, opts = opts)
run!(sim)
return sim
end
sim_complex = run_backend(ComplexFFTBackend())
sim_real = run_backend(RealFFTBackend())
maximum(maximum(abs, uc .- ur)
for (uc, ur) in zip(sim_complex.nout.vals[:u], sim_real.nout.vals[:u]))