Advanced Usage Patterns
Advanced Techniques
Sophisticated patterns for experienced users and scientific computing
This guide covers sophisticated usage patterns and advanced techniques for experienced SHTnsKit.jl users.
This guide assumes familiarity with basic SHTnsKit.jl usage. See the Quick Start Guide first if you're new to the package.
Advanced Configuration Management
Configuration Factories
using SHTnsKit
# Create a factory for consistent configuration creation
struct SHTnsConfigFactory
default_lmax::Int
cache::Dict{Tuple{Int,Int}, SHTnsConfig}
end
function SHTnsConfigFactory(lmax::Int=64)
SHTnsConfigFactory(lmax, Dict())
end
function get_config(factory::SHTnsConfigFactory, lmax::Int, mmax::Int)
key = (lmax, mmax)
if !haskey(factory.cache, key)
cfg = create_gauss_config(lmax, mmax)
factory.cache[key] = cfg
end
return factory.cache[key]
end
function cleanup!(factory::SHTnsConfigFactory)
for cfg in values(factory.cache)
destroy_config(cfg)
end
empty!(factory.cache)
end
# Usage
factory = SHTnsConfigFactory()
cfg32 = get_config(factory, 32, 32)
cfg64 = get_config(factory, 64, 64)
# ... use configurations ...
cleanup!(factory)Dynamic Resolution Adaptation
using SHTnsKit
mutable struct AdaptiveSpectralTransform
current_lmax::Int
max_lmax::Int
configs::Dict{Int, SHTnsConfig}
tolerance::Float64
end
function AdaptiveSpectralTransform(max_lmax::Int, tolerance::Float64=1e-10)
AdaptiveSpectralTransform(16, max_lmax, Dict{Int, SHTnsConfig}(), tolerance)
end
function get_config!(transform::AdaptiveSpectralTransform, lmax::Int)
if !haskey(transform.configs, lmax)
transform.configs[lmax] = create_gauss_config(lmax, lmax)
end
return transform.configs[lmax]
end
function adaptive_analyze(transform::AdaptiveSpectralTransform, field::Matrix{Float64})
lmax = transform.current_lmax
while lmax <= transform.max_lmax
cfg = get_config!(transform, lmax)
# Interpolate field to current resolution if needed
field_resized = resize_spatial_field(field, cfg)
# Analyze
sh = analysis(cfg, field_resized)
# Check convergence by looking at high-degree coefficients
high_degree_power = sum(abs2, sh[end-min(10, div(length(sh), 4)):end])
total_power = sum(abs2, sh)
if high_degree_power / total_power < transform.tolerance
transform.current_lmax = lmax
return sh, lmax
end
lmax = min(lmax * 2, transform.max_lmax)
end
# Maximum resolution reached
cfg = get_config!(transform, transform.max_lmax)
field_resized = resize_spatial_field(field, cfg)
sh = analysis(cfg, field_resized)
transform.current_lmax = transform.max_lmax
return sh, transform.max_lmax
end
function cleanup!(transform::AdaptiveSpectralTransform)
for cfg in values(transform.configs)
destroy_config(cfg)
end
empty!(transform.configs)
endSpectral Domain Operations
Custom Spectral Filtering
using SHTnsKit
function create_spectral_filter(lmax::Int;
low_pass::Union{Int,Nothing}=nothing,
high_pass::Union{Int,Nothing}=nothing,
band_pass::Union{Tuple{Int,Int},Nothing}=nothing)
filter = ones(Float64, (lmax+1)*(lmax+2)÷2)
cfg_temp = create_gauss_config(lmax, lmax)
for i in 1:length(filter)
l, m = lm_from_index(cfg_temp, i)
if low_pass !== nothing && l > low_pass
filter[i] = 0.0
elseif high_pass !== nothing && l < high_pass
filter[i] = 0.0
elseif band_pass !== nothing
l_min, l_max = band_pass
if l < l_min || l > l_max
filter[i] = 0.0
end
end
end
destroy_config(cfg_temp)
return filter
end
function apply_spectral_filter!(sh::Vector{Float64}, filter::Vector{Float64})
@assert length(sh) == length(filter) "Filter size mismatch"
sh .*= filter
return sh
end
# Example: Smooth a noisy field
cfg = create_gauss_config(64, 64)
# Create bandlimited test field (smooth function suitable for SHT)
θ, φ = cfg.θ, cfg.φ
noisy_field = zeros(cfg.nlat, cfg.nlon)
for i in 1:cfg.nlat, j in 1:cfg.nlon
noisy_field[i,j] = 1.0 + 0.3 * sin(3*θ[i]) * cos(2*φ[j]) + 0.1 * cos(5*θ[i])
end
# Low-pass filter (keep only l ≤ 20)
sh = analysis(cfg, noisy_field)
lowpass_filter = create_spectral_filter(64, low_pass=20)
apply_spectral_filter!(sh, lowpass_filter)
smooth_field = synthesis(cfg, sh)
destroy_config(cfg)Spectral Derivative Operations
SHTnsKit provides spectral methods for computing derivatives, which are more accurate than finite differences. All operations support MPI parallelization with PencilArrays.
Serial Gradient Computation
using SHTnsKit
# Setup configuration
lmax = 32
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create test field: Y_2^1-like pattern
test_field = zeros(cfg.nlat, cfg.nlon)
for i in 1:cfg.nlat, j in 1:cfg.nlon
θ = cfg.θ[i]
φ = cfg.φ[j]
test_field[i,j] = sin(θ) * cos(θ) * cos(φ) # ∝ Y_2^1
end
# Transform to spectral coefficients
Alm = analysis(cfg, test_field)
# Compute gradient using spectral method (exact derivatives)
# synthesis_grad computes ∇f = (∂f/∂θ, (1/sinθ)∂f/∂φ)
∂f_∂θ, ∂f_∂φ_over_sinθ = synthesis_grad(cfg, Alm)
println("Gradient computed using spectral method:")
println(" ∂f/∂θ range: ", extrema(∂f_∂θ))
println(" (1/sinθ)∂f/∂φ range: ", extrema(∂f_∂φ_over_sinθ))
destroy_config(cfg)MPI-Parallel Gradient Computation
using MPI
MPI.Init()
using SHTnsKit, PencilArrays, PencilFFTs
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
# Setup configuration
lmax = 64
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create distributed array using PencilArrays
pen = Pencil((nlat, nlon), comm)
fθφ = PencilArray(pen, zeros(Float64, PencilArrays.size_local(pen)...))
# Fill local portion with test pattern
ranges = PencilArrays.range_local(pen)
for (i_local, i_global) in enumerate(ranges[1])
θ = cfg.θ[i_global]
for (j_local, j_global) in enumerate(ranges[2])
φ = cfg.φ[j_global]
fθφ[i_local, j_local] = sin(θ) * cos(θ) * cos(φ)
end
end
# Distributed analysis: spatial → spectral
Alm = SHTnsKit.dist_analysis(cfg, fθφ)
# Compute gradient using distributed synthesis with spheroidal transform
# Gradient = (∂f/∂θ, (1/sinθ)∂f/∂φ) via spheroidal synthesis
Tlm = zeros(ComplexF64, cfg.lmax+1, cfg.mmax+1) # Zero toroidal component
∂f_∂θ, ∂f_∂φ_over_sinθ = SHTnsKit.dist_synthesis_sphtor(cfg, Alm, Tlm; prototype_θφ=fθφ)
if rank == 0
println("Distributed gradient computed successfully")
end
destroy_config(cfg)
MPI.Finalize()Serial Laplacian
The Laplacian has a simple form in spectral space: ∇²f_lm = -l(l+1) f_lm
using SHTnsKit
lmax = 32
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create test field (Y_2^0)
test_field = zeros(cfg.nlat, cfg.nlon)
for i in 1:cfg.nlat
x = cfg.x[i] # cos(θ)
test_field[i, :] .= (3*x^2 - 1)/2
end
# Transform to spectral
Alm = analysis(cfg, test_field)
# Apply Laplacian in spectral space: multiply by -l(l+1)
Laplacian_Alm = zeros(ComplexF64, cfg.lmax+1, cfg.mmax+1)
for l in 0:cfg.lmax
for m in 0:min(l, cfg.mmax)
Laplacian_Alm[l+1, m+1] = -l * (l + 1) * Alm[l+1, m+1]
end
end
# Transform back to spatial domain
laplacian_field = synthesis(cfg, Laplacian_Alm)
# For Y_2^0, the Laplacian should be -2(2+1) = -6 times the original
println("Ratio (should be ≈ -6): ", laplacian_field[cfg.nlat÷2, 1] / test_field[cfg.nlat÷2, 1])
destroy_config(cfg)MPI-Parallel Laplacian
using MPI
MPI.Init()
using SHTnsKit, PencilArrays, PencilFFTs
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
lmax = 64
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create distributed array
pen = Pencil((nlat, nlon), comm)
fθφ = PencilArray(pen, zeros(Float64, PencilArrays.size_local(pen)...))
# Fill with Y_2^0 pattern
ranges = PencilArrays.range_local(pen)
for (i_local, i_global) in enumerate(ranges[1])
x = cfg.x[i_global]
for j_local in 1:length(ranges[2])
fθφ[i_local, j_local] = (3*x^2 - 1)/2
end
end
# Compute distributed Laplacian (uses spectral method internally)
laplacian_fθφ = SHTnsKit.dist_scalar_laplacian(cfg, fθφ; prototype_θφ=fθφ)
if rank == 0
println("Distributed Laplacian computed")
end
destroy_config(cfg)
MPI.Finalize()Serial Divergence and Vorticity
using SHTnsKit
lmax = 32
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create a velocity field
Vθ = zeros(cfg.nlat, cfg.nlon)
Vφ = zeros(cfg.nlat, cfg.nlon)
for i in 1:cfg.nlat, j in 1:cfg.nlon
θ = cfg.θ[i]
φ = cfg.φ[j]
Vθ[i,j] = cos(θ) * sin(φ)
Vφ[i,j] = cos(φ)
end
# Decompose into spheroidal/toroidal
Slm, Tlm = analysis_sphtor(cfg, Vθ, Vφ)
# Compute divergence and vorticity
div_field = divergence_from_spheroidal(cfg, Slm)
vort_field = vorticity_from_toroidal(cfg, Tlm)
println("Divergence range: ", extrema(div_field))
println("Vorticity range: ", extrema(vort_field))
destroy_config(cfg)MPI-Parallel Divergence and Vorticity
using MPI
MPI.Init()
using SHTnsKit, PencilArrays, PencilFFTs
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
lmax = 64
nlat = lmax + 2
nlon = 2*lmax + 1
cfg = create_gauss_config(lmax, nlat; nlon=nlon)
# Create distributed velocity field components
pen = Pencil((nlat, nlon), comm)
Vtθφ = PencilArray(pen, zeros(Float64, PencilArrays.size_local(pen)...))
Vpθφ = PencilArray(pen, zeros(Float64, PencilArrays.size_local(pen)...))
# Fill with test velocity field
ranges = PencilArrays.range_local(pen)
for (i_local, i_global) in enumerate(ranges[1])
θ = cfg.θ[i_global]
for (j_local, j_global) in enumerate(ranges[2])
φ = cfg.φ[j_global]
Vtθφ[i_local, j_local] = cos(θ) * sin(φ)
Vpθφ[i_local, j_local] = cos(φ)
end
end
# Compute distributed divergence directly
div_field = SHTnsKit.dist_spatial_divergence(cfg, Vtθφ, Vpθφ; prototype_θφ=Vtθφ)
# Compute distributed vorticity directly
vort_field = SHTnsKit.dist_spatial_vorticity(cfg, Vtθφ, Vpθφ; prototype_θφ=Vtθφ)
if rank == 0
println("Distributed divergence and vorticity computed")
end
destroy_config(cfg)
MPI.Finalize()Running MPI Examples
Save as gradient_mpi.jl and run with:
mpiexec -n 4 julia --project gradient_mpi.jlMulti-Field Processing Patterns
Coherent Transform Pipeline
using SHTnsKit
struct SpectralPipeline
cfg::SHTnsConfig
stages::Vector{Function}
buffers::Dict{Symbol, Any}
end
function SpectralPipeline(lmax::Int, mmax::Int)
cfg = create_gauss_config(lmax, mmax)
stages = Function[]
buffers = Dict{Symbol, Any}()
# Pre-allocate common buffers
buffers[:sh_temp] = allocate_spectral(cfg)
buffers[:spatial_temp] = allocate_spatial(cfg)
SpectralPipeline(cfg, stages, buffers)
end
function add_stage!(pipeline::SpectralPipeline, stage_func::Function)
push!(pipeline.stages, stage_func)
end
function process(pipeline::SpectralPipeline, input_field::Matrix{Float64})
# Initial analysis
analyze!(pipeline.cfg, input_field, pipeline.buffers[:sh_temp])
# Apply all stages
for stage in pipeline.stages
stage(pipeline.cfg, pipeline.buffers)
end
# Final synthesis
synthesize!(pipeline.cfg, pipeline.buffers[:sh_temp], pipeline.buffers[:spatial_temp])
return copy(pipeline.buffers[:spatial_temp])
end
function cleanup!(pipeline::SpectralPipeline)
destroy_config(pipeline.cfg)
end
# Example pipeline: smooth -> amplify low modes -> threshold
pipeline = SpectralPipeline(32, 32)
# Stage 1: Low-pass filter
add_stage!(pipeline, function(cfg, buffers)
sh = buffers[:sh_temp]
for i in 1:length(sh)
l, m = lm_from_index(cfg, i)
if l > 16
sh[i] = 0.0
end
end
end)
# Stage 2: Amplify low modes
add_stage!(pipeline, function(cfg, buffers)
sh = buffers[:sh_temp]
for i in 1:length(sh)
l, m = lm_from_index(cfg, i)
if l <= 8
sh[i] *= 2.0
end
end
end)
# Process data
test_field = rand(get_nlat(pipeline.cfg), get_nphi(pipeline.cfg))
result = process(pipeline, test_field)
cleanup!(pipeline)Batch Transform Manager
using SHTnsKit
using Base.Threads
struct BatchTransformManager
configs::Dict{Int, SHTnsConfig}
thread_buffers::Vector{Dict{Symbol, Any}}
max_lmax::Int
end
function BatchTransformManager(max_lmax::Int=128)
configs = Dict{Int, SHTnsConfig}()
thread_buffers = [Dict{Symbol, Any}() for _ in 1:nthreads()]
BatchTransformManager(configs, thread_buffers, max_lmax)
end
function get_config!(manager::BatchTransformManager, lmax::Int)
if !haskey(manager.configs, lmax)
manager.configs[lmax] = create_gauss_config(lmax, lmax)
end
return manager.configs[lmax]
end
function get_buffers!(manager::BatchTransformManager, thread_id::Int, lmax::Int)
buffers = manager.thread_buffers[thread_id]
key = Symbol("buffers_$lmax")
if !haskey(buffers, key)
cfg = get_config!(manager, lmax)
buffers[key] = Dict(
:sh => allocate_spectral(cfg),
:spatial => allocate_spatial(cfg)
)
end
return buffers[key]
end
function batch_process(manager::BatchTransformManager,
fields::Vector{Matrix{Float64}},
lmax::Int,
process_func::Function)
results = Vector{Any}(undef, length(fields))
cfg = get_config!(manager, lmax)
@threads for i in 1:length(fields)
thread_id = threadid()
buffers = get_buffers!(manager, thread_id, lmax)
# Resize field if necessary
field = fields[i]
if size(field) != (cfg.nlat, cfg.nlon)
field = resize_spatial_field(field, cfg)
end
# Transform
analyze!(cfg, field, buffers[:sh])
# Process in spectral domain
result_sh = process_func(cfg, buffers[:sh])
# Transform back
synthesize!(cfg, result_sh, buffers[:spatial])
results[i] = copy(buffers[:spatial])
end
return results
end
function cleanup!(manager::BatchTransformManager)
for cfg in values(manager.configs)
destroy_config(cfg)
end
empty!(manager.configs)
end
# Example: Batch low-pass filtering
manager = BatchTransformManager()
# Generate test data
test_fields = [rand(65, 129) for _ in 1:100]
# Process function: low-pass filter
function lowpass_filter(cfg, sh)
result = copy(sh)
for i in 1:length(result)
l, m = lm_from_index(cfg, i)
if l > 20
result[i] = 0.0
end
end
return result
end
# Process batch
filtered_fields = batch_process(manager, test_fields, 64, lowpass_filter)
println("Processed $(length(filtered_fields)) fields")
cleanup!(manager)Advanced Vector Field Analysis
Helmholtz Decomposition
using SHTnsKit
function helmholtz_decomposition(cfg::SHTnsConfig, u::Matrix{Float64}, v::Matrix{Float64})
# Decompose vector field into rotational and divergent parts
# u = u_rot + u_div, v = v_rot + v_div
# Get spheroidal and toroidal components
S_lm, T_lm = analyze_vector(cfg, u, v)
# Rotational part (from toroidal component)
u_rot, v_rot = synthesize_vector(cfg, zeros(length(S_lm)), T_lm)
# Divergent part (from spheroidal component)
u_div, v_div = synthesize_vector(cfg, S_lm, zeros(length(T_lm)))
return (u_rot, v_rot), (u_div, v_div), (S_lm, T_lm)
end
function vector_field_properties(cfg::SHTnsConfig, u::Matrix{Float64}, v::Matrix{Float64})
# Compute various properties of vector field
(u_rot, v_rot), (u_div, v_div), (S_lm, T_lm) = helmholtz_decomposition(cfg, u, v)
# Energy in each component
rot_energy = sum(u_rot.^2 + v_rot.^2)
div_energy = sum(u_div.^2 + v_div.^2)
total_energy = sum(u.^2 + v.^2)
# Spectral energies
spheroidal_energy = sum(abs2, S_lm)
toroidal_energy = sum(abs2, T_lm)
return Dict(
:rotational_fraction => rot_energy / total_energy,
:divergent_fraction => div_energy / total_energy,
:spheroidal_energy => spheroidal_energy,
:toroidal_energy => toroidal_energy,
:total_spectral_energy => spheroidal_energy + toroidal_energy
)
end
# Example analysis
cfg = create_gauss_config(48, 48)
θ, φ = SHTnsKit.create_coordinate_matrices(cfg)
# Create test vector field with known properties
u = @. 10 * sin(2θ) * cos(φ) # Mostly divergent
v = @. 5 * cos(θ) * sin(2φ) # Mixed
properties = vector_field_properties(cfg, u, v)
println("Vector Field Analysis:")
for (key, value) in properties
println(" $key: $value")
end
destroy_config(cfg)Enstrophy and Energy Cascade Analysis
using SHTnsKit
function compute_enstrophy_spectrum(cfg::SHTnsConfig, u::Matrix{Float64}, v::Matrix{Float64})
# Enstrophy Z(l) = l(l+1) * |ω_l|² where ω is vorticity
# Get toroidal component (related to vorticity)
S_lm, T_lm = analyze_vector(cfg, u, v)
# Compute enstrophy per degree
lmax = get_lmax(cfg)
enstrophy = zeros(lmax + 1)
for i in 1:length(T_lm)
l, m = lm_from_index(cfg, i)
enstrophy[l + 1] += l * (l + 1) * abs2(T_lm[i])
end
return enstrophy
end
function compute_energy_spectrum(cfg::SHTnsConfig, u::Matrix{Float64}, v::Matrix{Float64})
# Kinetic energy E(l) = |u_l|²
S_lm, T_lm = analyze_vector(cfg, u, v)
lmax = get_lmax(cfg)
energy = zeros(lmax + 1)
for i in 1:length(S_lm)
l, m = lm_from_index(cfg, i)
energy[l + 1] += abs2(S_lm[i]) + abs2(T_lm[i])
end
return energy
end
function analyze_turbulent_cascade(cfg::SHTnsConfig,
velocity_fields::Vector{Tuple{Matrix{Float64}, Matrix{Float64}}})
# Analyze energy cascade in turbulent flow
n_snapshots = length(velocity_fields)
lmax = get_lmax(cfg)
mean_energy = zeros(lmax + 1)
mean_enstrophy = zeros(lmax + 1)
for (u, v) in velocity_fields
energy = compute_energy_spectrum(cfg, u, v)
enstrophy = compute_enstrophy_spectrum(cfg, u, v)
mean_energy .+= energy
mean_enstrophy .+= enstrophy
end
mean_energy ./= n_snapshots
mean_enstrophy ./= n_snapshots
# Find inertial range (power law behavior)
degrees = 1:lmax
return Dict(
:degrees => degrees,
:energy_spectrum => mean_energy[2:end], # Skip l=0
:enstrophy_spectrum => mean_enstrophy[2:end],
:energy_slope => fit_power_law_slope(degrees[5:end÷2], mean_energy[6:end÷2+1]),
:enstrophy_slope => fit_power_law_slope(degrees[5:end÷2], mean_enstrophy[6:end÷2+1])
)
end
function fit_power_law_slope(x, y)
# Simple linear fit in log-log space
log_x = log.(x)
log_y = log.(y[y .> 0]) # Avoid log(0)
if length(log_y) < 2
return NaN
end
# Linear regression
n = length(log_x)
sum_x = sum(log_x)
sum_y = sum(log_y[1:length(log_x)])
sum_xy = sum(log_x .* log_y[1:length(log_x)])
sum_x2 = sum(log_x.^2)
slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x^2)
return slope
endTemporal Evolution and Time Series
Spectral Time Series Analysis
using SHTnsKit
using FFTW
struct SpectralTimeSeries
cfg::SHTnsConfig
time_series::Vector{Vector{Float64}} # Each element is sh coefficients
times::Vector{Float64}
lmax::Int
end
function SpectralTimeSeries(cfg::SHTnsConfig)
SpectralTimeSeries(cfg, Vector{Float64}[], Float64[], get_lmax(cfg))
end
function add_snapshot!(sts::SpectralTimeSeries, field::Matrix{Float64}, time::Float64)
sh = analyze(sts.cfg, field)
push!(sts.time_series, sh)
push!(sts.times, time)
end
function temporal_power_spectrum(sts::SpectralTimeSeries, l::Int, m::Int)
# Get time evolution of specific (l,m) mode
idx = lmidx(sts.cfg, l, m)
mode_evolution = [sh[idx] for sh in sts.time_series]
# Compute temporal Fourier transform
fft_result = fft(mode_evolution)
power_spectrum = abs2.(fft_result)
# Frequency axis
dt = length(sts.times) > 1 ? sts.times[2] - sts.times[1] : 1.0
frequencies = fftfreq(length(mode_evolution), 1/dt)
return frequencies, power_spectrum
end
function mode_correlation_matrix(sts::SpectralTimeSeries)
# Compute correlation between different spherical harmonic modes
n_modes = length(sts.time_series[1])
n_times = length(sts.time_series)
# Create matrix: modes × time
mode_matrix = zeros(n_modes, n_times)
for (i, sh) in enumerate(sts.time_series)
mode_matrix[:, i] = sh
end
# Compute correlation matrix
correlation_matrix = cor(mode_matrix')
return correlation_matrix
end
function dominant_mode_evolution(sts::SpectralTimeSeries, n_modes::Int=10)
# Find most energetic modes and track their evolution
# Compute mean energy per mode
mean_energies = zeros(length(sts.time_series[1]))
for sh in sts.time_series
mean_energies .+= abs2.(sh)
end
mean_energies ./= length(sts.time_series)
# Find dominant modes
dominant_indices = sortperm(mean_energies, rev=true)[1:n_modes]
# Extract evolution
evolutions = []
for idx in dominant_indices
l, m = lm_from_index(sts.cfg, idx)
evolution = [sh[idx] for sh in sts.time_series]
push!(evolutions, (l=l, m=m, evolution=evolution, mean_energy=mean_energies[idx]))
end
return evolutions
end
# Example usage
cfg = create_gauss_config(32, 32)
sts = SpectralTimeSeries(cfg)
# Generate synthetic time series (e.g., decaying turbulence)
for t in 0:0.1:10.0
# Synthetic field with time evolution
θ, φ = SHTnsKit.create_coordinate_matrices(cfg)
decay_factor = exp(-0.1 * t)
field = decay_factor * (
sin(3θ) .* cos(2φ) +
0.5 * sin(5θ) .* cos(4φ) * cos(0.5π * t) +
randn(size(θ)...) * 0.1
)
add_snapshot!(sts, field, t)
end
# Analysis
dominant_modes = dominant_mode_evolution(sts, 5)
for mode in dominant_modes
println("Mode l=$(mode.l), m=$(mode.m): mean energy = $(mode.mean_energy)")
end
# Temporal spectrum of dominant mode
if length(dominant_modes) > 0
l, m = dominant_modes[1].l, dominant_modes[1].m
freqs, power = temporal_power_spectrum(sts, l, m)
println("Temporal spectrum computed for mode ($l, $m)")
end
destroy_config(cfg)Custom Interpolation and Remapping
Adaptive Mesh Refinement Interface
using SHTnsKit
struct AdaptiveMesh
base_cfg::SHTnsConfig
refined_regions::Vector{Dict{Symbol, Any}}
global_field::Union{Vector{Float64}, Nothing}
end
function AdaptiveMesh(base_lmax::Int)
base_cfg = create_gauss_config(base_lmax, base_lmax)
AdaptiveMesh(base_cfg, Dict{Symbol, Any}[], nothing)
end
function add_refined_region!(mesh::AdaptiveMesh,
θ_center::Float64, φ_center::Float64,
radius::Float64, refinement_lmax::Int)
refined_cfg = create_gauss_config(refinement_lmax, refinement_lmax)
region = Dict(
:center => (θ_center, φ_center),
:radius => radius,
:cfg => refined_cfg,
:lmax => refinement_lmax,
:local_field => nothing
)
push!(mesh.refined_regions, region)
end
function interpolate_to_refined_region!(mesh::AdaptiveMesh, region_idx::Int)
if mesh.global_field === nothing
error("No global field set")
end
region = mesh.refined_regions[region_idx]
base_spatial = synthesize(mesh.base_cfg, mesh.global_field)
# Extract region from global field (simplified interpolation)
θ_global, φ_global = SHTnsKit.create_coordinate_matrices(mesh.base_cfg)
θ_local, φ_local = SHTnsKit.create_coordinate_matrices(region[:cfg])
# Simple nearest-neighbor interpolation (in practice, use proper interpolation)
local_spatial = zeros(size(θ_local))
for i in 1:size(θ_local, 1), j in 1:size(θ_local, 2)
# Find nearest point in global grid
distances = (θ_global .- θ_local[i,j]).^2 + (φ_global .- φ_local[i,j]).^2
min_idx = argmin(distances)
local_spatial[i,j] = base_spatial[min_idx]
end
# Analyze to get local spectral representation
region[:local_field] = analyze(region[:cfg], local_spatial)
end
function project_refined_to_global!(mesh::AdaptiveMesh, region_idx::Int)
region = mesh.refined_regions[region_idx]
if region[:local_field] === nothing
error("No refined field in region $region_idx")
end
# Convert refined solution back to global grid
local_spatial = synthesize(region[:cfg], region[:local_field])
# Project onto global spectral representation
# This requires careful handling of overlapping regions
global_spatial = synthesize(mesh.base_cfg, mesh.global_field)
# Weighted blending (simplified)
θ_center, φ_center = region[:center]
radius = region[:radius]
θ_global, φ_global = SHTnsKit.create_coordinate_matrices(mesh.base_cfg)
θ_local, φ_local = SHTnsKit.create_coordinate_matrices(region[:cfg])
# Apply refined solution in the local region
# (Proper implementation would use overlap integrals)
mesh.global_field = analyze(mesh.base_cfg, global_spatial)
end
function cleanup!(mesh::AdaptiveMesh)
destroy_config(mesh.base_cfg)
for region in mesh.refined_regions
destroy_config(region[:cfg])
end
endMemory-Mapped Large Dataset Processing
using SHTnsKit
using Mmap
struct MemoryMappedSpectralData
file_path::String
cfg::SHTnsConfig
n_snapshots::Int
nlm::Int
mmap_array::Array{Float64, 2} # nlm × n_snapshots
end
function create_mmap_spectral_data(file_path::String, cfg::SHTnsConfig, n_snapshots::Int)
nlm = cfg.nlm
# Create memory-mapped file
file_size = nlm * n_snapshots * sizeof(Float64)
open(file_path, "w+") do io
write(io, zeros(UInt8, file_size))
end
# Memory map the file
mmap_array = Mmap.mmap(file_path, Array{Float64, 2}, (nlm, n_snapshots))
MemoryMappedSpectralData(file_path, cfg, n_snapshots, nlm, mmap_array)
end
function add_snapshot!(mmsd::MemoryMappedSpectralData,
spatial_field::Matrix{Float64},
snapshot_idx::Int)
if snapshot_idx > mmsd.n_snapshots
error("Snapshot index $snapshot_idx exceeds capacity $(mmsd.n_snapshots)")
end
# Analyze and store directly in memory-mapped array
sh = analyze(mmsd.cfg, spatial_field)
mmsd.mmap_array[:, snapshot_idx] = sh
end
function process_snapshots_streaming(mmsd::MemoryMappedSpectralData,
process_func::Function,
chunk_size::Int=100)
results = []
n_chunks = div(mmsd.n_snapshots, chunk_size)
for chunk in 1:n_chunks
start_idx = (chunk - 1) * chunk_size + 1
end_idx = min(chunk * chunk_size, mmsd.n_snapshots)
# Process chunk
chunk_data = mmsd.mmap_array[:, start_idx:end_idx]
chunk_result = process_func(mmsd.cfg, chunk_data)
push!(results, chunk_result)
# Optional: trigger garbage collection
if chunk % 10 == 0
GC.gc()
end
end
return results
end
function compute_temporal_statistics(mmsd::MemoryMappedSpectralData)
# Compute statistics without loading all data into memory
mean_spectrum = zeros(mmsd.nlm)
var_spectrum = zeros(mmsd.nlm)
# First pass: compute mean
for i in 1:mmsd.n_snapshots
mean_spectrum .+= mmsd.mmap_array[:, i]
end
mean_spectrum ./= mmsd.n_snapshots
# Second pass: compute variance
for i in 1:mmsd.n_snapshots
diff = mmsd.mmap_array[:, i] - mean_spectrum
var_spectrum .+= diff.^2
end
var_spectrum ./= (mmsd.n_snapshots - 1)
return mean_spectrum, sqrt.(var_spectrum)
end
function cleanup!(mmsd::MemoryMappedSpectralData)
# Close memory mapping and optionally remove file
finalize(mmsd.mmap_array)
# rm(mmsd.file_path) # Uncomment to delete file
end
# Example: Process large climate dataset
# cfg = create_gauss_config(128, 128)
# n_years = 100
# n_snapshots_per_year = 365
# total_snapshots = n_years * n_snapshots_per_year
# mmsd = create_mmap_spectral_data("climate_data.bin", cfg, total_snapshots)
# # Add data (in practice, this would come from files)
# for i in 1:total_snapshots
# synthetic_field = generate_climate_snapshot(i) # User function
# add_snapshot!(mmsd, synthetic_field, i)
# end
# # Compute statistics
# mean_spec, std_spec = compute_temporal_statistics(mmsd)
# cleanup!(mmsd)
# destroy_config(cfg)Integration with External Libraries
Interfacing with Climate Models
using SHTnsKit
# using NCDatasets # For NetCDF files
function read_climate_model_output(file_path::String, variable::String, time_index::Int)
# Read data from NetCDF file (pseudo-code)
# In practice, use NCDatasets.jl or similar
# data = NCDatasets.Dataset(file_path) do ds
# ds[variable][:, :, time_index]
# end
# For demonstration, create synthetic data
nlat, nlon = 96, 192 # Typical climate model resolution
data = rand(nlat, nlon)
return data
end
function climate_model_to_shtns(data::Matrix{Float64}, target_lmax::Int)
# Convert climate model grid to SHTns format
input_nlat, input_nlon = size(data)
# Create appropriate configuration
cfg = create_regular_config(target_lmax, target_lmax)
target_nlat, target_nlon = cfg.nlat, cfg.nlon
# Interpolate to target grid (simplified)
if (input_nlat, input_nlon) != (target_nlat, target_nlon)
# Bilinear interpolation (in practice, use proper spherical interpolation)
data_interpolated = imresize(data, (target_nlat, target_nlon))
else
data_interpolated = data
end
# Analyze
sh = analysis(cfg, data_interpolated)
return cfg, sh
end
function process_climate_ensemble(file_paths::Vector{String},
variable::String,
target_lmax::Int)
ensemble_spectra = []
reference_cfg = nothing
for file_path in file_paths
println("Processing: $file_path")
# Read multiple time steps
n_time_steps = get_time_dimension_size(file_path) # User function
for t in 1:min(n_time_steps, 100) # Limit for example
data = read_climate_model_output(file_path, variable, t)
cfg, sh = climate_model_to_shtns(data, target_lmax)
if reference_cfg === nothing
reference_cfg = cfg
end
push!(ensemble_spectra, sh)
end
end
return reference_cfg, ensemble_spectra
end
# Example usage
# file_paths = ["model1_output.nc", "model2_output.nc", "model3_output.nc"]
# cfg, ensemble = process_climate_ensemble(file_paths, "temperature", 64)
# # Compute ensemble statistics
# n_members = length(ensemble)
# mean_spectrum = sum(ensemble) / n_members
# variance_spectrum = sum([(sp - mean_spectrum).^2 for sp in ensemble]) / (n_members - 1)
# println("Ensemble analysis complete")
# destroy_config(cfg)Automatic Differentiation Integration
SHTnsKit.jl provides seamless integration with Julia's automatic differentiation ecosystem through package extensions for ForwardDiff.jl and Zygote.jl.
ForwardDiff.jl Support (Forward-Mode AD)
Forward-mode automatic differentiation is ideal for functions with few inputs and many outputs, common in parameter estimation problems.
using SHTnsKit
using ForwardDiff
cfg = create_gauss_config(16, 16)
# Example: Parameter estimation for spherical harmonic coefficients
function objective(params)
# Create spherical harmonic field from parameters
sh = zeros(cfg.nlm)
sh[1:length(params)] = params
# Transform to spatial domain
spatial = synthesis(cfg, sh)
# Compute some objective (e.g., match target pattern)
target = rand(size(spatial))
return sum((spatial - target).^2)
end
# Initial parameters
params₀ = rand(10)
# Compute gradient using ForwardDiff
∇f = ForwardDiff.gradient(objective, params₀)
# Use gradient for optimization
params₁ = params₀ - 0.01 * ∇f
destroy_config(cfg)Zygote.jl Support (Reverse-Mode AD)
Reverse-mode AD is ideal for functions with many inputs and few outputs, common in neural networks and optimization problems.
using SHTnsKit
using Zygote
cfg = create_gauss_config(16, 16)
# Example: Optimizing spatial field patterns
function loss_function(spatial_field)
# Transform to spectral domain
sh = analysis(cfg, spatial_field)
# Regularize high-frequency components
high_freq_penalty = sum(sh[end-20:end].^2)
# Target smooth field
smooth_penalty = sum(abs2, spatial_field .- mean(spatial_field))
return 0.1 * high_freq_penalty + smooth_penalty
end
# Initial spatial field
spatial₀ = randn(cfg.nlat, cfg.nlon)
# Compute gradient using Zygote
∇L = Zygote.gradient(loss_function, spatial₀)[1]
# Update field
spatial₁ = spatial₀ - 0.01 * ∇L
destroy_config(cfg)Advanced AD Patterns
Differentiable Vector Field Operations
using SHTnsKit
using ForwardDiff
cfg = create_gauss_config(12, 12)
function vector_field_energy(params)
n = length(params) ÷ 2
S_lm = params[1:n] # Spheroidal coefficients
T_lm = params[n+1:end] # Toroidal coefficients
# Synthesize vector field
Vθ, Vφ = synthesize_vector(cfg, S_lm, T_lm)
# Compute kinetic energy
kinetic_energy = sum(Vθ.^2 + Vφ.^2)
# Add enstrophy (related to vorticity)
_, T_reconstructed = analyze_vector(cfg, Vθ, Vφ)
enstrophy = sum(abs2, T_reconstructed)
return 0.5 * kinetic_energy + 0.1 * enstrophy
end
# Optimize vector field parameters
nlm = cfg.nlm
vector_params = randn(2 * nlm)
# Gradient descent
for i in 1:10
grad = ForwardDiff.gradient(vector_field_energy, vector_params)
vector_params .-= 0.01 * grad
energy = vector_field_energy(vector_params)
println("Iteration $i: Energy = $energy")
end
destroy_config(cfg)Differentiable Field Rotations
using SHTnsKit
using ForwardDiff
cfg = create_gauss_config(16, 16)
# Optimize rotation angles to match target field
function rotation_objective(angles)
α, β, γ = angles
# Original field
# Create bandlimited test coefficients
sh_original = zeros(cfg.nlm)
sh_original[1] = 1.0
if cfg.nlm > 10
sh_original[2:5] = [0.5, 0.3, 0.2, 0.1]
end
# Rotate field (in-place helper for real-basis coefficients)
sh_rotated = copy(sh_original)
rotate_real!(cfg, sh_rotated; alpha=α, beta=β, gamma=γ)
# Target field (e.g., aligned with some axis)
sh_target = zeros(cfg.nlm)
sh_target[1] = 1.0 # Y₀⁰ mode only
return sum((sh_rotated - sh_target).^2)
end
# Find optimal rotation angles
angles₀ = [0.1, 0.1, 0.1]
for i in 1:20
grad = ForwardDiff.gradient(rotation_objective, angles₀)
angles₀ .-= 0.01 * grad
obj = rotation_objective(angles₀)
println("Iteration $i: Objective = $obj, Angles = $angles₀")
end
destroy_config(cfg)Neural Differential Equations on the Sphere
using SHTnsKit
using Zygote
# using DifferentialEquations # For ODE solving
cfg = create_gauss_config(20, 20)
# Define a neural ODE on the sphere using SHT
function sphere_neural_ode!(du, u, p, t)
# u contains spherical harmonic coefficients
# p contains neural network parameters
# Transform to spatial domain
spatial = synthesize(cfg, u)
# Apply nonlinear transformation (neural network layer)
# This is a simplified version - real neural networks would be more complex
W, b = p[1:length(spatial)], p[length(spatial)+1:end]
transformed = tanh.(W .* spatial .+ b[1:size(spatial, 1), 1:size(spatial, 2)])
# Transform back to spectral domain for time derivative
du .= analysis(cfg, transformed)
end
# Example usage would involve solving the ODE and differentiating through the solutionPerformance Considerations for AD
Memory Management
using SHTnsKit
using ForwardDiff
cfg = create_gauss_config(32, 32)
# Pre-allocate buffers to avoid repeated allocation during AD
struct ADBuffers
sh_buffer::Vector{Float64}
spatial_buffer::Matrix{Float64}
grad_buffer::Vector{Float64}
end
function create_ad_buffers(cfg)
ADBuffers(
allocate_spectral(cfg),
allocate_spatial(cfg),
zeros(cfg.nlm)
)
end
buffers = create_ad_buffers(cfg)
# Use buffers in AD computations to reduce allocation
function efficient_objective(params, buffers)
# Use pre-allocated buffers
buffers.sh_buffer[1:length(params)] = params
fill!(buffers.sh_buffer, 0.0)
buffers.sh_buffer[1:length(params)] = params
synthesize!(cfg, buffers.sh_buffer, buffers.spatial_buffer)
return sum(abs2, buffers.spatial_buffer)
end
# This version is more memory efficient
grad = ForwardDiff.gradient(p -> efficient_objective(p, buffers), rand(10))
destroy_config(cfg)Choosing Between Forward and Reverse Mode
using BenchmarkTools
# Rule of thumb:
# - Use ForwardDiff when: n_parameters < n_outputs
# - Use Zygote when: n_parameters > n_outputs
cfg = create_gauss_config(16, 16)
n_params = 50
n_spatial = cfg.nlat * cfg.nlon
println("Parameters: $n_params, Spatial points: $n_spatial")
function test_objective(params)
sh = zeros(cfg.nlm)
sh[1:n_params] = params
spatial = synthesis(cfg, sh)
return sum(abs2, spatial) # Single output
end
params = rand(n_params)
if n_params < n_spatial
println("ForwardDiff recommended (fewer parameters than outputs)")
@btime ForwardDiff.gradient($test_objective, $params)
else
println("Zygote recommended (more parameters than outputs)")
@btime Zygote.gradient($test_objective, $params)
end
destroy_config(cfg)Applications in Scientific Computing
Inverse Problems
# Parameter estimation from observations
function solve_inverse_problem(observations, initial_guess, cfg)
using ForwardDiff
using Optim
function forward_model(params)
# Convert parameters to spherical harmonic field
sh = param_to_sh(params, cfg)
return synthesis(cfg, sh)
end
function objective(params)
predicted = forward_model(params)
return sum((observations - predicted).^2)
end
# Use automatic differentiation for optimization
result = optimize(objective, initial_guess, BFGS(),
autodiff=:forward)
return result.minimizer
endData Assimilation
# Variational data assimilation with spherical harmonic background
function variational_assimilation(observations, background_sh, obs_locations, cfg)
using Zygote
function cost_function(analysis_sh)
# Background term
background_cost = sum((analysis_sh - background_sh).^2)
# Observation term
analysis_spatial = synthesize(cfg, analysis_sh)
obs_cost = sum((analysis_spatial[obs_locations] - observations).^2)
return 0.5 * (background_cost + obs_cost)
end
# Minimize cost function using gradients
analysis_sh = copy(background_sh)
for i in 1:100
grad = Zygote.gradient(cost_function, analysis_sh)[1]
analysis_sh .-= 0.01 * grad
cost = cost_function(analysis_sh)
println("Iteration $i: Cost = $cost")
if norm(grad) < 1e-6
break
end
end
return analysis_sh
endThis comprehensive advanced usage guide demonstrates sophisticated patterns for expert users of SHTnsKit.jl, covering everything from configuration management to large-scale data processing, automatic differentiation, and integration with external scientific computing workflows.