ProCreations's picture
Publish validated GP-FVM reproduction
c711202 verified
Raw
History Blame Contribute Delete
8.5 kB
"""
Hyperparameter Sweep Experiment
For a single problem instance, sweep over:
- Grid sizes N
- Smoothness (GP-FVM: 1,2,3 for Matérn 3/2,5/2,7/2; Collocation: 2,3)
- Sparsity parameter ρ
- Lengthscale (pick best for each config)
Goal: Find optimal hyperparameters for GP-FVM vs GP-Collocation comparison.
"""
using Printf
using DataFrames
using CSV
using Dates
using ProgressMeter
using LinearAlgebra
include("problem.jl")
include("metrics.jl")
include("methods/sparse_fvm.jl")
include("methods/sparse_collocation.jl")
include("methods/classical_fvm.jl")
"""
sweep_lengthscale(solver, instance, N, n_timesteps, smoothness, ρ, lengthscales)
Try different lengthscales and return the one with lowest error.
"""
function sweep_lengthscale(solver, instance, N, n_timesteps, smoothness, ρ, lengthscales)
best_error = Inf
best_ls = lengthscales[1]
best_result = nothing
for ls in lengthscales
try
result = solver(instance, N;
n_timesteps=n_timesteps,
ρ=ρ,
smoothness=smoothness,
lengthscale=ls) # absolute lengthscale
# Compute error against reference
ref = instance.reference
T_end = instance.problem.T_end
u_ref = evaluate_reference(ref, result.xs, T_end)
u_pred = result.mean[:, end]
rel_error = norm(u_pred - u_ref) / norm(u_ref)
if rel_error < best_error
best_error = rel_error
best_ls = ls
best_result = result
end
catch e
# Skip failed configs
continue
end
end
return best_result, best_ls, best_error
end
"""
run_hyperparam_sweep(; kwargs...)
Run hyperparameter sweep for a single problem instance.
"""
function run_hyperparam_sweep(;
ic_family::Symbol = :sine,
ic_seed::Int = 43,
grid_sizes::Vector{Int} = [20, 40, 60, 80, 100, 150, 200],
smoothness_fvm::Vector{Int} = [1, 2, 3], # Matérn 3/2, 5/2, 7/2
smoothness_colloc::Vector{Int} = [2, 3], # Need 2nd deriv, so min 5/2
rhos::Vector{Float64} = [2.0, 3.0, 4.0, 5.0],
lengthscales::Vector{Float64} = [0.05, 0.1, 0.15, 0.2, 0.3],
output_dir::String = "results/hyperparam_sweep",
verbose::Bool = true
)
mkpath(output_dir)
timestamp = Dates.format(now(), "yyyy-mm-dd_HHMMSS")
# Setup problem
problem = BurgersProblem(
x_min = 0.0,
x_max = 1.0,
T_end = 0.3,
ν = 0.01,
u_left = 0.0,
u_right = 0.0
)
ic = InitialCondition(ic_family, ic_seed)
instance = ProblemInstance(problem, ic; N_ref=2000)
verbose && println("="^70)
verbose && println("Hyperparameter Sweep")
verbose && println("="^70)
verbose && println("IC: $ic_family (seed=$ic_seed)")
verbose && println("Grid sizes: $grid_sizes")
verbose && println("Smoothness (FVM): $smoothness_fvm")
verbose && println("Smoothness (Colloc): $smoothness_colloc")
verbose && println("ρ values: $rhos")
verbose && println("Lengthscales: $lengthscales")
verbose && println()
results = DataFrame(
method = String[],
N = Int[],
smoothness = Int[],
rho = Float64[],
best_lengthscale = Float64[],
rel_l2_error = Float64[],
time_s = Float64[]
)
# Classical FVM baseline (no hyperparams to tune)
verbose && println("Running Classical FVM baseline...")
for N in grid_sizes
n_timesteps = max(10, N ÷ 2) # More timesteps for better temporal resolution
result = solve_classical_fvm(instance, N; n_timesteps=n_timesteps)
ref = instance.reference
u_ref = evaluate_reference(ref, result.xs, problem.T_end)
u_pred = result.mean[:, end]
rel_error = norm(u_pred - u_ref) / norm(u_ref)
push!(results, ("classical_fvm", N, 0, 0.0, 0.0, rel_error, result.wall_time_s))
end
# Count total GP configurations
n_fvm_configs = length(grid_sizes) * length(smoothness_fvm) * length(rhos)
n_colloc_configs = length(grid_sizes) * length(smoothness_colloc) * length(rhos)
n_total = n_fvm_configs + n_colloc_configs
verbose && println("\nRunning GP-FVM sweep ($n_fvm_configs configs)...")
progress = Progress(n_total, desc="Sweeping hyperparams...")
# GP-FVM sweep
for N in grid_sizes
n_timesteps = max(10, N ÷ 2)
for smooth in smoothness_fvm
for ρ in rhos
result, best_ls, best_error = sweep_lengthscale(
solve_sparse_fvm, instance, N, n_timesteps,
smooth, ρ, lengthscales
)
if result !== nothing
push!(results, (
"sparse_fvm", N, smooth, ρ, best_ls,
best_error, result.wall_time_s
))
end
next!(progress)
end
end
end
verbose && println("\nRunning GP-Collocation sweep ($n_colloc_configs configs)...")
# GP-Collocation sweep
for N in grid_sizes
n_timesteps = max(10, N ÷ 2)
for smooth in smoothness_colloc
for ρ in rhos
result, best_ls, best_error = sweep_lengthscale(
solve_sparse_collocation, instance, N, n_timesteps,
smooth, ρ, lengthscales
)
if result !== nothing
push!(results, (
"sparse_collocation", N, smooth, ρ, best_ls,
best_error, result.wall_time_s
))
end
next!(progress)
end
end
end
# Save results
csv_path = joinpath(output_dir, "sweep_$(timestamp).csv")
CSV.write(csv_path, results)
verbose && println("\nSaved: $csv_path")
# Print summary: best config per method per N
verbose && println("\n" * "="^70)
verbose && println("Best configurations per method per N:")
verbose && println("="^70)
for method in ["sparse_fvm", "sparse_collocation", "classical_fvm"]
verbose && println("\n[$method]")
method_df = filter(row -> row.method == method, results)
for N in grid_sizes
n_df = filter(row -> row.N == N, method_df)
if nrow(n_df) > 0
best_idx = argmin(n_df.rel_l2_error)
best = n_df[best_idx, :]
if method == "classical_fvm"
@printf(" N=%3d: error=%.4f\n", N, best.rel_l2_error)
else
@printf(" N=%3d: error=%.4f (ν=%d, ρ=%.1f, ls=%.2f)\n",
N, best.rel_l2_error, best.smoothness, best.rho, best.best_lengthscale)
end
end
end
end
return results
end
# CLI
using ArgParse
function parse_args_sweep()
s = ArgParseSettings(description = "Hyperparameter sweep for GP-FVM vs Collocation")
@add_arg_table! s begin
"--ic-family"
help = "IC family (sine, gaussian, multimode)"
arg_type = Symbol
default = :sine
"--ic-seed"
help = "IC random seed"
arg_type = Int
default = 43
"--grid-sizes"
help = "Comma-separated grid sizes"
arg_type = String
default = "20,40,60,80,100,150,200"
"--rhos"
help = "Comma-separated ρ values"
arg_type = String
default = "2.0,3.0,4.0,5.0"
"--lengthscales"
help = "Comma-separated lengthscale values"
arg_type = String
default = "0.05,0.1,0.15,0.2,0.3"
"--output", "-o"
help = "Output directory"
arg_type = String
default = "results/hyperparam_sweep"
end
return parse_args(s)
end
function main()
args = parse_args_sweep()
grid_sizes = parse.(Int, split(args["grid-sizes"], ","))
rhos = parse.(Float64, split(args["rhos"], ","))
lengthscales = parse.(Float64, split(args["lengthscales"], ","))
run_hyperparam_sweep(
ic_family = args["ic-family"],
ic_seed = args["ic-seed"],
grid_sizes = grid_sizes,
rhos = rhos,
lengthscales = lengthscales,
output_dir = args["output"]
)
end
if abspath(PROGRAM_FILE) == @__FILE__
main()
end