repro-adam-zero-sum-games-ode-dynamics / official_code /experiments /accuracy_vs_compute /hyperparam_sweep.jl
| """ | |
| 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" | |
| (" N=%3d: error=%.4f\n", N, best.rel_l2_error) | |
| else | |
| (" 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") | |
| ! 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) == | |
| main() | |
| end | |