Heat/Burgers using rectangular collocation via ClassicalOrthogonalPolynomials.jl and OrdinaryDiffEq.jl

I recently created an example of solving time-evolution PDEs with rectangular collocation and thought this might be helpful for others. This uses ClassicalOrthogonalPolynomials.jl, which has similar underlying mathematical techniques as ApproxFun but built via quasi-arrays (on top of QuasiArrays.jl / ContinuumArrays.jl) which makes it more versatile, in particular, collocation can be done fairly naturally.

The example includes both a simple 1D ODE, heat equation, and Burger’s equation, using OrdinaryDiffEq.jl for the time-stepping:

Note that rectangular collocation is due to @tobydriscoll and Nick Hale:
https://academic.oup.com/imajna/article-abstract/36/1/108/2363563

7 Likes

This is helpful, thanks Sheehan!

If we have a nonlinear boundary condition at the right boundary for the heat equation, say,

u_x(1,t) = \beta(u^4(1, t)-u_0^4),

governed by the Stefan’s law where \beta and u_0 are some constant parameters. How can we deal with the nonlinearity and solve the heat equation with left Dirichlet boundary condition and right nonlinear power law boundary condition:

u_t = u_{xx}

with u(0, t) = u_0 and u_x(1, t) = \beta(u^4(1, t)-u_0^4)?

Thanks in advance!

I think you can differentiate these conditions wrt time and then modify the top/last row of the RHS accordingly.

So the right nonlinear boundary condition has a u_x term on the left hand side. If we differentiate it w.r.t time, there’ll be a mixed partial derivative term from the u_x part. Wouldn’t this make the problem harder?

I don’t know what you mean by “harder” but it just lets you phrase it in terms of a mass matrix

I’ll see if I can get it working

I added an example with a variable Neumann condition.

Your problem is harder since it’s nonlinear so not clear how to fit it into a mass matrix setup. Probably you can solve it via a DAE solver. Or it should be fairly straightforward using your own implicit time-stepper, eg., backward Euler.

1 Like

Using DAEProblem should be fine for that.

1 Like

Thanks for the comment, @ChrisRackauckas . And thanks @dlfivefifty for the detailed examples. You’ve been really helpful!

So I solved the 1D heat equation subject to three types of boundary conditions (Dirichlet, Neumann and Robin) using DAEProblem(), where my DAE residual functions have the same structure as the implicitly-defined DAEs example in Differential Algebraic Equations · DifferentialEquations.jl . Obviously, turning it into a DAE problem is an overkill for heat equation with linear BCs but I wanted to test that the DAE treatment works for these cases where the analytical solutions are known. The analytical solutions, boundary conditions and initial conditions used in my code are the same as the ones in Solving the Heat Equation · MethodOfLines.jl.

Here’s the full code:

# examples taken from: https://docs.sciml.ai/MethodOfLines/stable/tutorials/heat/

using ClassicalOrthogonalPolynomials, OrdinaryDiffEq, LinearAlgebra, Plots, Sundials

T = ChebyshevT()
n = 100
x₁_cheb = reverse(ChebyshevGrid{1}(n-2))
x₂_cheb = reverse(ChebyshevGrid{2}(n))

# shift the domain from default [-1,1] to [0,1]
x₁ = (x₁_cheb .+ 1) ./ 2
x₂ = (x₂_cheb .+ 1) ./ 2

V = T[x₂_cheb,1:n]
D₁ = 2*diff(T)[x₂_cheb, 1:n] / V
D₂ = 4*diff(T,2)[x₁_cheb,1:n] / V
R = T[x₁_cheb,1:n] / V

# DAE residual function
# reference: https://docs.sciml.ai/DiffEqDocs/stable/tutorials/dae_example/
function heat_dirichlet!(out, du, u, p, t)
    D₂ = p
    
    # left BC: u(0,t) = exp(-t)
    out[1] = u[1] - exp(-t)
    
    interior_d2u = D₂ * u
    for i in 2:n-1
        out[i] = interior_d2u[i-1] - du[i]
    end
    
    # right BC: u(1, t) = cos(1)exp(-t)
    out[end] = u[end] - cos(1)*exp(-t)
end

function heat_neumann!(out, du, u, p, t)
    D₁, D₂ = p
    
    # left BC: uₓ(0,1) = 0
    out[1] = dot(D₁[1,:],u)
    
    interior_d2u = D₂ * u
    for i in 2:n-1
        out[i] = interior_d2u[i-1] - du[i]
    end
    
    # right BC: uₓ(1,t) = -sin(1)exp(-t)
    out[end] = dot(D₁[end,:],u) + sin(1)*exp(-t)
end

function heat_robin!(out, du, u, p, t)
    D₁, D₂ = p
    
    # left BC: u(-1,t) + 3uₓ(−1,t) = (sin(−1)+3cos(−1))exp(−t),
    out[1] = u[1] + 3*dot((1/2)*D₁[1,:],u) - (sin(-1)+3*cos(-1))*exp(-t)
    
    # interior: uₓₓ - uₜ = 0
    interior_d2u = (1/4)*D₂ * u
    for i in 2:n-1
        out[i] = interior_d2u[i-1] - du[i]
    end
    
    # right BC: u(1,t) + uₓ(1,t) = (sin(1)+cos(1))exp(−t)
    out[end] = u[end] + dot((1/2)*D₁[end,:],u) - (sin(1)+cos(1))*exp(-t)
end

u_init = x -> cos(x) # initial condition for dirichlet and neumann BCs
u0 = u_init.(x₂)
du0 = -sin.(x₂)
du0[2:end-1] = D₂ * u0

u_init_robin = x -> sin(x)  # initial condition for robin BCs
u0_robin = u_init_robin.(x₂_cheb)
du0_robin = cos.(x₂_cheb)
du0_robin[2:end-1] = (1/4)*D₂ * u0_robin

differential_vars = [false; trues(n-2); false]

prob_dirichlet = DAEProblem(heat_dirichlet!, du0, u0, (0., 1.), D₂, differential_vars=differential_vars)
sol_dirichlet = solve(prob_dirichlet, IDA(), reltol=1e-8, abstol=1e-8)

prob_neumann = DAEProblem(heat_neumann!, du0, u0, (0., 1.), (D₁, D₂), differential_vars=differential_vars)
sol_neumann = solve(prob_neumann, IDA(), reltol=1e-8, abstol=1e-8)

prob_robin =  DAEProblem(heat_robin!, du0_robin, u0_robin, (0., 1.), (D₁, D₂), differential_vars=differential_vars)
sol_robin = solve(prob_robin, IDA(), reltol=1e-8, abstol=1e-8)

t = range(0,1,100)
display(contourf(t, x₂, hcat(sol_dirichlet.(t)...), xlabel="time", ylabel="space", title="Heat Equation with Dirichlet BCs"))
display(contourf(t, x₂, hcat(sol_neumann.(t)...), xlabel="time", ylabel="space", title="Heat Equation with Neumann BCs"))
display(contourf(t, x₂_cheb, hcat(sol_robin.(t)...), xlabel="time", ylabel="space", title="Heat Equation with Robin BCs"))



# ----------- compare numerical with analytical solns ----------------

u_exact(x, t) = cos.(x) * exp(-t)  # exact soln for dirichlet and neumann BCs
u_exact_robin(x, t) = sin.(x) * exp(-t)  # exact soln for robin BCs

t_points = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]

p_dirichlet = plot()
p_neumann = plot()
p_robin = plot()

colors = [:blue, :red, :green, :orange, :purple, :magenta]
markers = [:circle, :square, :diamond, :utriangle, :dtriangle, :star]

for (i, t_val) in enumerate(t_points)
    
    u_num_dirichlet = sol_dirichlet(t_val)
    u_num_neumann = sol_neumann(t_val)
    u_exact_vals = u_exact(x₂, t_val)
    
    plot!(p_dirichlet, x₂, u_num_dirichlet, label="Numerical, t=$(t_val)", color=colors[i], xlabel="x", ylabel="u(x, t)", title="numerical vs exact solution comparison - Dirichlet ")
    scatter!(x₂, u_exact_vals, label="Exact, t=$(t_val)", color=colors[i], marker=markers[i],markersize=3, alpha=0.5)
    
    plot!(p_neumann, x₂, u_num_neumann, label="Numerical, t=$(t_val)", color=colors[i], xlabel="x", ylabel="u(x, t)", title="numerical vs exact solution comparison - Neumann")
    scatter!(x₂, u_exact_vals, label="Exact, t=$(t_val)", color=colors[i], marker=markers[i],markersize=3, alpha=0.5)
    
    u_num_robin = sol_robin(t_val)
    u_exact_vals_robin = u_exact_robin(x₂_cheb, t_val)
    
    plot!(p_robin, x₂_cheb, u_num_robin, label="Numerical, t=$(t_val)", color=colors[i], xlabel="x", ylabel="u(x, t)", title="numerical vs exact solution comparison - Robin")
    scatter!(x₂_cheb, u_exact_vals_robin, label="Exact, t=$(t_val)", color=colors[i], marker=markers[i],markersize=3, alpha=0.5)
end

display(p_dirichlet)
display(p_neumann)
display(p_robin)

The plots I got are identical to those in Solving the Heat Equation · MethodOfLines.jl :


2 Likes