ProCreations's picture
Publish validated GP-FVM reproduction
c711202 verified
Raw
History Blame Contribute Delete
10.6 kB
"""
Visualize Cholesky factor sparsity patterns for 2D GP-FVM.
Creates a publication-quality figure showing the precision Cholesky factor
for different orderings, demonstrating how entry magnitudes decay and
why sparse approximation is effective.
Usage:
julia --project=. experiments/sparsity_pattern_2d.jl
julia --project=. experiments/sparsity_pattern_2d.jl --nxy 12
"""
using FunctionalGPs, GaussianMarkovRandomFields
using LinearAlgebra, SparseArrays
using CairoMakie
using TuePlots
using GPFiniteVolume
using ArgParse
# ------------------------------------------------------------------------------
# CLI
# ------------------------------------------------------------------------------
function parse_commandline()
s = ArgParseSettings(description = "Visualize 2D Cholesky sparsity patterns")
@add_arg_table! s begin
"--nxy"
help = "Grid size (N × N)"
arg_type = Int
default = 10
"--lengthscale"
help = "Kernel lengthscale"
arg_type = Float64
default = 0.1
"--output", "-o"
help = "Output path (default: figures/cholesky_sparsity_2d.pdf)"
arg_type = String
default = ""
end
return parse_args(s)
end
# ------------------------------------------------------------------------------
# Setup
# ------------------------------------------------------------------------------
"""
setup_2d_problem(; N_xy, kernel_lengthscale)
Create 2D measurement setup with evaluations, derivatives, and integrals.
Returns covariance matrix K and metadata.
"""
function setup_2d_problem(; N_xy=10, kernel_lengthscale=0.1)
# Build 2D product kernel
k_base = HalfIntegerMaternKernel(1, [kernel_lengthscale]) # Matérn-3/2
k_prod = k_base ⊗ k_base
# Create 2D grid
Xs_base = range(0.0, 1.0, length=N_xy)
Xs = FactorizedGrid(Xs_base, Xs_base)
# Evaluation functional
L_eval = EvaluationFunctional(Xs)
n_eval = prod(output_shape(L_eval))
# Derivative functional: ∂/∂x + ∂/∂y
d_sum = PartialDerivative((1, 0)) + PartialDerivative((0, 1))
L_deriv = L_eval ∘ d_sum
n_deriv = prod(output_shape(L_deriv))
# Integral functional over 2D cells
base_intervals = intervals_from_endpoints(collect(Xs_base))
domains_2d = base_intervals ⊗ base_intervals
L_integ = VectorizedLebesgueIntegral(domains_2d)
n_int = prod(output_shape(L_integ))
# Stack functionals
L_stack = StackedLinearFunctional(L_eval, L_deriv, L_integ)
# State layout
state_layout = layout((f = n_eval, f_dx = n_deriv, f_int = n_int))
# Compute full covariance matrix
K = Matrix(L_stack(L_stack(k_prod)))
# Build coordinate matrices
N_total = N_xy^2
X_eval = zeros(2, N_total)
idx = 1
for iy in 1:N_xy, ix in 1:N_xy
X_eval[1, idx] = Xs_base[ix]
X_eval[2, idx] = Xs_base[iy]
idx += 1
end
X_deriv = copy(X_eval)
n_cells_1d = length(base_intervals)
X_int = zeros(2, n_cells_1d^2)
idx = 1
for iy in 1:n_cells_1d, ix in 1:n_cells_1d
X_int[1, idx] = midpoint(base_intervals[ix])
X_int[2, idx] = midpoint(base_intervals[iy])
idx += 1
end
return (
K = K,
state_layout = state_layout,
X_eval = X_eval,
X_deriv = X_deriv,
X_int = X_int,
n_eval = n_eval,
n_deriv = n_deriv,
n_int = n_int,
)
end
"""
create_ordering(setup, order_type::Symbol)
Create global ordering for :eval_first or :int_first.
Returns permutation P and block boundaries.
"""
function create_ordering(setup, order_type::Symbol)
(; state_layout, X_eval, X_int, n_eval, n_deriv, n_int) = setup
# Compute maximin orderings
P_eval, _ = reverse_maximin_ordering(X_eval)
P_int, _ = reverse_maximin_ordering(X_int)
# Map to global indices
P_eval_global = indices(state_layout, :f)[P_eval]
P_int_global = indices(state_layout, :f_int)[P_int]
P_deriv_global = indices(state_layout, :f_dx)
if order_type == :eval_first
# Order: [derivatives; integrals; evaluations]
P_global = [P_deriv_global; P_int_global; P_eval_global]
block_sizes = (n_deriv, n_int, n_eval)
block_labels = ("Derivatives", "Integrals", "Evaluations")
elseif order_type == :int_first
# Order: [derivatives; evaluations; integrals]
P_global = [P_deriv_global; P_eval_global; P_int_global]
block_sizes = (n_deriv, n_eval, n_int)
block_labels = ("Derivatives", "Evaluations", "Integrals")
else
error("Unknown order_type: $order_type")
end
return P_global, block_sizes, block_labels
end
"""
compute_precision_cholesky(K, P)
Compute the exact Cholesky factor L of the precision matrix K⁻¹[P,P] = LL'.
"""
function compute_precision_cholesky(K::AbstractMatrix, P::Vector{Int})
K_perm = K[P, P]
K_perm_reg = K_perm + 1e-10 * I
K_inv = inv(Symmetric(K_perm_reg))
C = cholesky(Symmetric(K_inv))
return Matrix(C.L)
end
# ------------------------------------------------------------------------------
# Plotting
# ------------------------------------------------------------------------------
"""
create_sparsity_figure(setup; output_path)
Create publication-quality figure comparing sparsity patterns.
"""
function create_sparsity_figure(setup; output_path="figures/cholesky_sparsity_2d.pdf")
(; K, n_eval, n_deriv, n_int) = setup
n_total = n_eval + n_deriv + n_int
println("Matrix size: $n_total × $n_total")
println(" Evaluations: $n_eval")
println(" Derivatives: $n_deriv")
println(" Integrals: $n_int")
# Compute Cholesky factors for both orderings
println("\nComputing Cholesky factors...")
P_int, blocks_int, labels_int = create_ordering(setup, :int_first)
L_int = compute_precision_cholesky(K, P_int)
println(" Integrals-first: done")
P_eval, blocks_eval, labels_eval = create_ordering(setup, :eval_first)
L_eval = compute_precision_cholesky(K, P_eval)
println(" Evaluations-first: done")
# Set up TuePlots theme (appendix full-width, 2 panels)
theme = Theme(
TuePlots.SETTINGS[:ICML];
font=true, fontsize=true, figsize=true,
single_column=false,
nrows=1, ncols=2,
subplot_height_to_width_ratio=1.0,
)
set_theme!(theme)
fig = Figure()
# Colorbar range
crange = (-12, 0)
# Panel (a): Integrals-first
ax1 = Axis(fig[1, 1],
xlabel="Column index",
ylabel="Row index",
title="Integrals coarsest",
yreversed=true,
aspect=DataAspect(),
)
L_log_int = log10.(abs.(L_int) .+ 1e-16)
hm1 = heatmap!(ax1, 1:n_total, 1:n_total, L_log_int',
colormap=:viridis, colorrange=crange)
# Add block boundaries
add_block_boundaries!(ax1, blocks_int, labels_int, n_total)
# Panel (b): Evaluations-first
ax2 = Axis(fig[1, 2],
xlabel="Column index",
ylabel="Row index",
title="Evaluations coarsest",
yreversed=true,
aspect=DataAspect(),
)
L_log_eval = log10.(abs.(L_eval) .+ 1e-16)
hm2 = heatmap!(ax2, 1:n_total, 1:n_total, L_log_eval',
colormap=:viridis, colorrange=crange)
add_block_boundaries!(ax2, blocks_eval, labels_eval, n_total)
# Panel labels (top right)
text!(ax1, 0.98, 0.98, text="(a)", align=(:right, :top),
space=:relative, fontsize=9, font=:bold, color=:white)
text!(ax2, 0.98, 0.98, text="(b)", align=(:right, :top),
space=:relative, fontsize=9, font=:bold, color=:white)
# Shared colorbar
Colorbar(fig[1, 3], hm1, label=L"\log_{10} |L_{ij}|", width=10)
# Adjust layout
colgap!(fig.layout, 1, 8)
colgap!(fig.layout, 2, 5)
# Save
mkpath(dirname(output_path))
save(output_path, fig, pt_per_unit=1)
println("\nSaved: $output_path")
# Also save PNG for quick viewing
png_path = replace(output_path, ".pdf" => ".png")
save(png_path, fig, px_per_unit=3)
println("Saved: $png_path")
# Reset theme
set_theme!()
# Print sparsity statistics
println("\nSparsity statistics (entries with |L_ij| > threshold):")
for (name, L) in [("Integrals-first", L_int), ("Evaluations-first", L_eval)]
max_nnz = n_total * (n_total + 1) ÷ 2
nnz_1e3 = count(x -> abs(x) > 1e-3, L)
nnz_1e6 = count(x -> abs(x) > 1e-6, L)
nnz_1e9 = count(x -> abs(x) > 1e-9, L)
println(" $name:")
println(" |L| > 1e-3: $nnz_1e3 ($(round(100*nnz_1e3/max_nnz, digits=1))%)")
println(" |L| > 1e-6: $nnz_1e6 ($(round(100*nnz_1e6/max_nnz, digits=1))%)")
println(" |L| > 1e-9: $nnz_1e9 ($(round(100*nnz_1e9/max_nnz, digits=1))%)")
end
return fig
end
"""
add_block_boundaries!(ax, block_sizes, block_labels, n_total)
Add dashed lines and labels for block boundaries.
"""
function add_block_boundaries!(ax, block_sizes, block_labels, n_total)
cumsum_blocks = cumsum(collect(block_sizes))
for (i, boundary) in enumerate(cumsum_blocks[1:end-1])
b = boundary + 0.5
# Horizontal and vertical lines
hlines!(ax, [b], color=:white, linewidth=0.8, linestyle=:dash)
vlines!(ax, [b], color=:white, linewidth=0.8, linestyle=:dash)
end
# Add block labels in top-right corner of each block
block_starts = [0; cumsum_blocks[1:end-1]]
block_ends = cumsum_blocks
for (i, (start, finish, label)) in enumerate(zip(block_starts, block_ends, block_labels))
# Top-right corner: x near end, y near start (since y is reversed)
x_pos = finish - 2
y_pos = start + 3
text!(ax, x_pos, y_pos, text=label, align=(:right, :top),
fontsize=6, color=:white)
end
end
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
function main()
args = parse_commandline()
N_xy = args["nxy"]
lengthscale = args["lengthscale"]
if isempty(args["output"])
output_path = joinpath(@__DIR__, "..", "figures", "cholesky_sparsity_2d.pdf")
else
output_path = args["output"]
end
println("Setting up 2D problem ($(N_xy) × $(N_xy) grid)...")
setup = setup_2d_problem(; N_xy=N_xy, kernel_lengthscale=lengthscale)
create_sparsity_figure(setup; output_path=output_path)
end
if abspath(PROGRAM_FILE) == @__FILE__
main()
end