| """ |
| 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 |
|
|
| |
| |
| |
|
|
| """ |
| BurgersProblem |
| |
| Defines the Burgers equation problem setup. |
| """ |
| Base.@kwdef struct BurgersProblem |
| |
| x_min::Float64 = 0.0 |
| x_max::Float64 = 1.0 |
|
|
| |
| T_end::Float64 = 0.5 |
|
|
| |
| ν::Float64 = 0.01 |
|
|
| |
| u_left::Float64 = 0.0 |
| u_right::Float64 = 0.0 |
| end |
|
|
| domain_length(p::BurgersProblem) = p.x_max - p.x_min |
|
|
| |
| |
| |
|
|
| """ |
| 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) |
| |
| return quadgk(x -> evaluate_ic(family, params, x), a, b)[1] |
| end |
|
|
| |
| |
| |
|
|
| """ |
| 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]) |
| |
| |
| φ = 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) |
| |
| coeff = -p.A / (2π * p.k) |
| return coeff * (cos(2π * p.k * b + p.φ) - cos(2π * p.k * a + p.φ)) |
| end |
|
|
| |
| |
| |
|
|
| """ |
| 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) |
| |
| coeff = p.A * p.σ * sqrt(π / 2) |
| return coeff * (erf((b - p.μ) / (p.σ * sqrt(2))) - erf((a - p.μ) / (p.σ * sqrt(2)))) |
| end |
|
|
| |
| |
| |
|
|
| """ |
| 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) |
| 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) |
| |
| |
| function antiderivative(x) |
| y = (x - p.μ) / p.ε |
| return p.A * p.ε * (y / 2 - log(cosh(y)) / 2) |
| end |
| return antiderivative(b) - antiderivative(a) |
| end |
|
|
| |
| |
| |
|
|
| """ |
| 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) |
| |
| term1 = -p.A₁ / (2π) * (cos(2π * b) - cos(2π * a)) |
| term2 = -p.A₂ / (4π) * (cos(4π * b) - cos(4π * a)) |
| return term1 + term2 |
| end |
|
|
| |
| |
| |
|
|
| 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)) |
|
|
| |
| |
| |
|
|
| """ |
| 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 |
|
|
| |
| (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 |
|
|
| |
| |
| |
|
|
| """ |
| 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 |
|
|
| |
| |
| |
|
|
| """ |
| HighResFVMReference |
| |
| Reference solution computed via high-resolution classical FVM. |
| """ |
| struct HighResFVMReference <: AbstractReferenceSolution |
| xs::Vector{Float64} |
| ts::Vector{Float64} |
| solution::Matrix{Float64} |
| 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 |
|
|
| |
| xs = range(x_min, x_max, length=N_ref) |
| Δx = xs[2] - xs[1] |
|
|
| |
| 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) |
|
|
| |
| N_t_actual = ceil(Int, T_end / Δt) |
| Δt = T_end / N_t_actual |
| ts = range(0, T_end, length=N_t_actual + 1) |
|
|
| |
| u = ic.(xs) |
| solution = zeros(N_ref, N_t_actual + 1) |
| solution[:, 1] = u |
|
|
| |
| u_new = similar(u) |
| for n in 1:N_t_actual |
| |
| for i in 2:(N_ref-1) |
| |
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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 |
| |
| if u_L + u_R >= 0 |
| return 0.5 * u_L^2 |
| else |
| return 0.5 * u_R^2 |
| end |
| else |
| |
| if u_L >= 0 |
| return 0.5 * u_L^2 |
| elseif u_R <= 0 |
| return 0.5 * u_R^2 |
| else |
| return 0.0 |
| end |
| end |
| end |
|
|
| function evaluate_reference(ref::HighResFVMReference, x, t) |
| |
| xs, ts, sol = ref.xs, ref.ts, ref.solution |
|
|
| |
| i_x = searchsortedlast(xs, x) |
| i_t = searchsortedlast(ts, t) |
|
|
| |
| i_x = clamp(i_x, 1, length(xs) - 1) |
| i_t = clamp(i_t, 1, length(ts) - 1) |
|
|
| |
| α_x = (x - xs[i_x]) / (xs[i_x + 1] - xs[i_x]) |
| α_t = (t - ts[i_t]) / (ts[i_t + 1] - ts[i_t]) |
|
|
| |
| 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 |
|
|
| |
| |
| |
|
|
| """ |
| 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 |
| |
| 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 |
|
|