ProCreations's picture
Publish validated GP-FVM reproduction
c711202 verified
Raw
History Blame Contribute Delete
14.8 kB
"""
Burgers Equation Problem Definition
Defines the viscous Burgers equation, initial condition families with randomized
parameters, and reference solutions (Cole-Hopf exact + high-resolution FVM).
PDE: ∂u/∂t + u ∂u/∂x = ν ∂²u/∂x²
Domain: x ∈ [0, 1], t ∈ [0, T]
"""
using Random
using QuadGK
using SpecialFunctions: erf
# ==============================================================================
# Problem Parameters
# ==============================================================================
"""
BurgersProblem
Defines the Burgers equation problem setup.
"""
Base.@kwdef struct BurgersProblem
# Spatial domain
x_min::Float64 = 0.0
x_max::Float64 = 1.0
# Temporal domain
T_end::Float64 = 0.5
# Physics
ν::Float64 = 0.01 # Viscosity
# Boundary conditions (Dirichlet)
u_left::Float64 = 0.0
u_right::Float64 = 0.0
end
domain_length(p::BurgersProblem) = p.x_max - p.x_min
# ==============================================================================
# Initial Condition Families
# ==============================================================================
"""
ICFamily
Abstract type for initial condition families.
Each family defines a parametric form with randomizable parameters.
"""
abstract type ICFamily end
"""
sample_parameters(family::ICFamily, rng::AbstractRNG)
Sample random parameters for an IC family.
"""
function sample_parameters end
"""
evaluate_ic(family::ICFamily, params, x)
Evaluate the initial condition at point x with given parameters.
"""
function evaluate_ic end
"""
evaluate_ic_dx(family::ICFamily, params, x)
Evaluate the derivative of the initial condition at point x.
"""
function evaluate_ic_dx end
"""
evaluate_ic_int(family::ICFamily, params, a, b)
Evaluate the integral of the initial condition over [a, b].
"""
function evaluate_ic_int(family::ICFamily, params, a, b)
# Default: numerical integration
return quadgk(x -> evaluate_ic(family, params, x), a, b)[1]
end
# ------------------------------------------------------------------------------
# Sine Wave IC
# ------------------------------------------------------------------------------
"""
SineIC <: ICFamily
Sine wave initial condition: u₀(x) = A sin(2πkx + φ)
Parameters:
- A ∈ [0.8, 1.2]: Amplitude
- k ∈ {1, 2}: Wave number
- φ ∈ [0, 2π): Phase
"""
struct SineIC <: ICFamily end
struct SineParams
A::Float64
k::Int
φ::Float64
end
function sample_parameters(::SineIC, rng::AbstractRNG)
A = 0.8 + 0.4 * rand(rng)
k = rand(rng, [1, 2])
# Phase must be 0 or π to satisfy u(0) = u(1) = 0 Dirichlet BCs
# sin(2πkx + φ) = 0 at x=0,1 requires φ = 0 or π
φ = rand(rng, [0.0, π])
return SineParams(A, k, φ)
end
function evaluate_ic(::SineIC, p::SineParams, x)
return p.A * sin(2π * p.k * x + p.φ)
end
function evaluate_ic_dx(::SineIC, p::SineParams, x)
return p.A * 2π * p.k * cos(2π * p.k * x + p.φ)
end
function evaluate_ic_int(::SineIC, p::SineParams, a, b)
# Analytical integral of A*sin(2πkx + φ)
coeff = -p.A / (2π * p.k)
return coeff * (cos(2π * p.k * b + p.φ) - cos(2π * p.k * a + p.φ))
end
# ------------------------------------------------------------------------------
# Gaussian Pulse IC
# ------------------------------------------------------------------------------
"""
GaussianIC <: ICFamily
Gaussian pulse initial condition: u₀(x) = A exp(-(x-μ)²/(2σ²))
Parameters:
- A ∈ [0.8, 1.2]: Amplitude
- μ ∈ [0.3, 0.7]: Center position
- σ ∈ [0.05, 0.15]: Width
"""
struct GaussianIC <: ICFamily end
struct GaussianParams
A::Float64
μ::Float64
σ::Float64
end
function sample_parameters(::GaussianIC, rng::AbstractRNG)
A = 0.8 + 0.4 * rand(rng)
μ = 0.3 + 0.4 * rand(rng)
σ = 0.05 + 0.1 * rand(rng)
return GaussianParams(A, μ, σ)
end
function evaluate_ic(::GaussianIC, p::GaussianParams, x)
return p.A * exp(-(x - p.μ)^2 / (2 * p.σ^2))
end
function evaluate_ic_dx(::GaussianIC, p::GaussianParams, x)
return -p.A * (x - p.μ) / p.σ^2 * exp(-(x - p.μ)^2 / (2 * p.σ^2))
end
function evaluate_ic_int(::GaussianIC, p::GaussianParams, a, b)
# Analytical integral using error function
coeff = p.A * p.σ * sqrt(π / 2)
return coeff * (erf((b - p.μ) / (p.σ * sqrt(2))) - erf((a - p.μ) / (p.σ * sqrt(2))))
end
# ------------------------------------------------------------------------------
# Step/Tanh IC
# ------------------------------------------------------------------------------
"""
StepIC <: ICFamily
Smoothed step initial condition: u₀(x) = A (1 - tanh((x-μ)/ε)) / 2
Parameters:
- A ∈ [0.8, 1.2]: Amplitude
- μ ∈ [0.3, 0.7]: Step location
- ε ∈ [0.08, 0.15]: Transition width (wider for GP compatibility)
"""
struct StepIC <: ICFamily end
struct StepParams
A::Float64
μ::Float64
ε::Float64
end
function sample_parameters(::StepIC, rng::AbstractRNG)
A = 0.8 + 0.4 * rand(rng)
μ = 0.3 + 0.4 * rand(rng)
ε = 0.08 + 0.07 * rand(rng) # Wider transition for GP compatibility
return StepParams(A, μ, ε)
end
function evaluate_ic(::StepIC, p::StepParams, x)
return p.A * (1 - tanh((x - p.μ) / p.ε)) / 2
end
function evaluate_ic_dx(::StepIC, p::StepParams, x)
sech_val = sech((x - p.μ) / p.ε)
return -p.A * sech_val^2 / (2 * p.ε)
end
function evaluate_ic_int(::StepIC, p::StepParams, a, b)
# Analytical integral: ∫ (1 - tanh(y))/2 dy = y/2 - ln(cosh(y))/2
# With substitution y = (x - μ)/ε
function antiderivative(x)
y = (x - p.μ) / p.ε
return p.A * p.ε * (y / 2 - log(cosh(y)) / 2)
end
return antiderivative(b) - antiderivative(a)
end
# ------------------------------------------------------------------------------
# Multi-mode IC
# ------------------------------------------------------------------------------
"""
MultiModeIC <: ICFamily
Multi-mode initial condition: u₀(x) = A₁ sin(2πx) + A₂ sin(4πx)
Parameters:
- A₁ ∈ [0.8, 1.2]: First mode amplitude
- A₂ ∈ [0.3, 0.7]: Second mode amplitude
"""
struct MultiModeIC <: ICFamily end
struct MultiModeParams
A₁::Float64
A₂::Float64
end
function sample_parameters(::MultiModeIC, rng::AbstractRNG)
A₁ = 0.8 + 0.4 * rand(rng)
A₂ = 0.3 + 0.4 * rand(rng)
return MultiModeParams(A₁, A₂)
end
function evaluate_ic(::MultiModeIC, p::MultiModeParams, x)
return p.A₁ * sin(2π * x) + p.A₂ * sin(4π * x)
end
function evaluate_ic_dx(::MultiModeIC, p::MultiModeParams, x)
return p.A₁ * 2π * cos(2π * x) + p.A₂ * 4π * cos(4π * x)
end
function evaluate_ic_int(::MultiModeIC, p::MultiModeParams, a, b)
# Analytical integral
term1 = -p.A₁ / (2π) * (cos(2π * b) - cos(2π * a))
term2 = -p.A₂ / (4π) * (cos(4π * b) - cos(4π * a))
return term1 + term2
end
# ------------------------------------------------------------------------------
# IC Family Registry
# ------------------------------------------------------------------------------
const IC_FAMILIES = Dict{Symbol, ICFamily}(
:sine => SineIC(),
:gaussian => GaussianIC(),
:step => StepIC(),
:multimode => MultiModeIC()
)
"""
get_ic_family(name::Symbol)
Get an IC family by name.
"""
get_ic_family(name::Symbol) = IC_FAMILIES[name]
"""
list_ic_families()
List available IC family names.
"""
list_ic_families() = collect(keys(IC_FAMILIES))
# ==============================================================================
# Initial Condition Instance
# ==============================================================================
"""
InitialCondition
A concrete initial condition instance with sampled parameters.
"""
struct InitialCondition{F<:ICFamily, P}
family::F
params::P
seed::Int
end
function InitialCondition(family::ICFamily, seed::Int)
rng = MersenneTwister(seed)
params = sample_parameters(family, rng)
return InitialCondition(family, params, seed)
end
function InitialCondition(family_name::Symbol, seed::Int)
family = get_ic_family(family_name)
return InitialCondition(family, seed)
end
# Evaluation interface
(ic::InitialCondition)(x) = evaluate_ic(ic.family, ic.params, x)
evaluate_dx(ic::InitialCondition, x) = evaluate_ic_dx(ic.family, ic.params, x)
evaluate_int(ic::InitialCondition, a, b) = evaluate_ic_int(ic.family, ic.params, a, b)
function Base.show(io::IO, ic::InitialCondition)
print(io, "InitialCondition($(typeof(ic.family).name.name), seed=$(ic.seed))")
end
# ==============================================================================
# Reference Solutions
# ==============================================================================
"""
AbstractReferenceSolution
Abstract type for reference solutions.
"""
abstract type AbstractReferenceSolution end
"""
evaluate_reference(ref::AbstractReferenceSolution, x, t)
Evaluate the reference solution at point (x, t).
"""
function evaluate_reference end
# ------------------------------------------------------------------------------
# High-Resolution FVM Reference
# ------------------------------------------------------------------------------
"""
HighResFVMReference
Reference solution computed via high-resolution classical FVM.
"""
struct HighResFVMReference <: AbstractReferenceSolution
xs::Vector{Float64} # Grid points
ts::Vector{Float64} # Time points
solution::Matrix{Float64} # Solution array [x_idx, t_idx]
end
"""
compute_highres_reference(problem::BurgersProblem, ic::InitialCondition;
N_ref=2000, N_t=1000)
Compute a high-resolution reference solution using Godunov FVM.
"""
function compute_highres_reference(problem::BurgersProblem, ic::InitialCondition;
N_ref::Int=2000, N_t::Int=1000)
(; x_min, x_max, T_end, ν, u_left, u_right) = problem
# Grid setup
xs = range(x_min, x_max, length=N_ref)
Δx = xs[2] - xs[1]
# CFL condition for stability
u_max = maximum(abs.(ic.(xs)))
Δt_advection = 0.5 * Δx / max(u_max, 1e-10)
Δt_diffusion = 0.25 * Δx^2 / ν
Δt = min(Δt_advection, Δt_diffusion, T_end / N_t)
# Actual number of time steps
N_t_actual = ceil(Int, T_end / Δt)
Δt = T_end / N_t_actual
ts = range(0, T_end, length=N_t_actual + 1)
# Initialize solution
u = ic.(xs)
solution = zeros(N_ref, N_t_actual + 1)
solution[:, 1] = u
# Time stepping (Godunov + central diff for viscosity)
u_new = similar(u)
for n in 1:N_t_actual
# Interior points
for i in 2:(N_ref-1)
# Godunov flux for advection: F = u²/2
# Upwind based on characteristic speed
u_L = u[i-1]
u_R = u[i]
F_left = godunov_flux(u_L, u_R)
u_L = u[i]
u_R = u[i+1]
F_right = godunov_flux(u_L, u_R)
# Central difference for viscosity
visc = ν * (u[i+1] - 2*u[i] + u[i-1]) / Δx^2
u_new[i] = u[i] - Δt / Δx * (F_right - F_left) + Δt * visc
end
# Boundary conditions
u_new[1] = u_left
u_new[N_ref] = u_right
u .= u_new
solution[:, n+1] = u
end
return HighResFVMReference(collect(xs), collect(ts), solution)
end
"""
godunov_flux(u_L, u_R)
Godunov numerical flux for Burgers equation: F(u) = u²/2
"""
function godunov_flux(u_L, u_R)
if u_L >= u_R
# Shock or expansion with s ≥ 0
if u_L + u_R >= 0
return 0.5 * u_L^2
else
return 0.5 * u_R^2
end
else
# Rarefaction
if u_L >= 0
return 0.5 * u_L^2
elseif u_R <= 0
return 0.5 * u_R^2
else
return 0.0 # Sonic point
end
end
end
function evaluate_reference(ref::HighResFVMReference, x, t)
# Linear interpolation in space and time
xs, ts, sol = ref.xs, ref.ts, ref.solution
# Find bracketing indices
i_x = searchsortedlast(xs, x)
i_t = searchsortedlast(ts, t)
# Clamp to valid range
i_x = clamp(i_x, 1, length(xs) - 1)
i_t = clamp(i_t, 1, length(ts) - 1)
# Interpolation weights
α_x = (x - xs[i_x]) / (xs[i_x + 1] - xs[i_x])
α_t = (t - ts[i_t]) / (ts[i_t + 1] - ts[i_t])
# Bilinear interpolation
u_00 = sol[i_x, i_t]
u_10 = sol[i_x + 1, i_t]
u_01 = sol[i_x, i_t + 1]
u_11 = sol[i_x + 1, i_t + 1]
u_0 = (1 - α_x) * u_00 + α_x * u_10
u_1 = (1 - α_x) * u_01 + α_x * u_11
return (1 - α_t) * u_0 + α_t * u_1
end
"""
evaluate_reference(ref::HighResFVMReference, xs::AbstractVector, t)
Evaluate reference solution at multiple x points for a single time t.
"""
function evaluate_reference(ref::HighResFVMReference, xs::AbstractVector, t)
return [evaluate_reference(ref, x, t) for x in xs]
end
# ==============================================================================
# Problem Instance
# ==============================================================================
"""
ProblemInstance
A complete problem instance with problem definition, IC, and reference solution.
"""
struct ProblemInstance
problem::BurgersProblem
ic::InitialCondition
reference::HighResFVMReference
end
function ProblemInstance(problem::BurgersProblem, ic::InitialCondition;
N_ref::Int=2000, N_t::Int=1000)
reference = compute_highres_reference(problem, ic; N_ref=N_ref, N_t=N_t)
return ProblemInstance(problem, ic, reference)
end
"""
generate_problem_instances(problem::BurgersProblem, ic_families, n_samples::Int;
base_seed::Int=42, N_ref::Int=2000)
Generate multiple problem instances with randomized ICs.
"""
function generate_problem_instances(problem::BurgersProblem,
ic_families::Vector{Symbol},
n_samples::Int;
base_seed::Int=42,
N_ref::Int=2000,
verbose::Bool=true)
instances = ProblemInstance[]
for (i_fam, family_name) in enumerate(ic_families)
verbose && println("Generating $n_samples instances for IC family: $family_name")
for i_sample in 1:n_samples
# Deterministic seed from family index and sample index
seed = base_seed + (i_fam - 1) * 1000 + i_sample
ic = InitialCondition(family_name, seed)
instance = ProblemInstance(problem, ic; N_ref=N_ref)
push!(instances, instance)
verbose && print(".")
end
verbose && println(" done")
end
return instances
end