Skip to article frontmatterSkip to article content

Chapter 10

Functions

Shooting method for a two-point boundary-value problem
shoot.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
    shoot(ϕ, xspan, g₁, g₂, init)

Shooting method to solve a two-point boundary value problem with
ODE u'' = `ϕ`(x, u, u') for x in `xspan`, left boundary condition
`g₁`(u,u')=0, and right boundary condition `g₂`(u,u')=0. The
value `init` is an initial estimate for vector [u,u'] at x=a.

Returns vectors for the nodes, the solution u, and derivative u'.
"""
function shoot(ϕ, xspan, g₁, g₂, init, tol = 1e-5)
    # ODE posed as a first-order equation in 2 variables.
    shootivp = (v, p, x) -> [v[2]; ϕ(x, v[1], v[2])]

    # Evaluate the difference between computed and target values at x=b.
    function objective(s)
        IVP = ODEProblem(shootivp, s, float.(xspan))
        sol = solve(IVP, Tsit5(), abstol = tol / 10, reltol = tol / 10)
        x = sol.t
        y = sol
        return [g₁(s...), g₂(y.u[end]...)]
    end

    # Find the unknown quantity at x=a by rootfinding.
    x = []
    y = []   # these values will be overwritten
    s = levenberg(objective, init, xtol = tol)[:, end]

    # Use the stored last solution of the IVP.
    u, du_dx = y[1, :], y[2, :]
    return x, u, du_dx
end
Second-order differentiation matrices
diffmats2.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
    diffmat2(n, xspan)

Compute 2nd-order-accurate differentiation matrices on `n`+1 points
in the interval `xspan`. Returns a vector of nodes and the matrices
for the first and second derivatives.
"""
function diffmat2(n, xspan)
    a, b = xspan
    h = (b - a) / n
    x = [a + i * h for i in 0:n]   # nodes

    # Define most of Dₓ by its diagonals.
    dp = fill(0.5 / h, n)        # superdiagonal
    dm = fill(-0.5 / h, n)       # subdiagonal
    Dₓ = diagm(-1 => dm, 1 => dp)

    # Fix first and last rows.
    Dₓ[1, 1:3] = [-1.5, 2, -0.5] / h
    Dₓ[n+1, n-1:n+1] = [0.5, -2, 1.5] / h

    # Define most of Dₓₓ by its diagonals.
    d0 = fill(-2 / h^2, n + 1)    # main diagonal
    dp = ones(n) / h^2         # super- and subdiagonal
    Dₓₓ = diagm(-1 => dp, 0 => d0, 1 => dp)

    # Fix first and last rows.
    Dₓₓ[1, 1:4] = [2, -5, 4, -1] / h^2
    Dₓₓ[n+1, n-2:n+1] = [-1, 4, -5, 2] / h^2

    return x, Dₓ, Dₓₓ
end
Chebyshev differentiation matrices
diffcheb.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"""
    diffcheb(n, xspan)

Compute Chebyshev differentiation matrices on `n`+1 points in the
interval `xspan`. Returns a vector of nodes and the matrices for the
first and second derivatives.
"""
function diffcheb(n, xspan)
    x = [-cos(k * π / n) for k in 0:n]    # nodes in [-1,1]

    # Off-diagonal entries.
    c = [2; ones(n - 1); 2]    # endpoint factors
    dij = (i, j) -> (-1)^(i + j) * c[i+1] / (c[j+1] * (x[i+1] - x[j+1]))
    Dₓ = [dij(i, j) for i in 0:n, j in 0:n]

    # Diagonal entries.
    Dₓ[isinf.(Dₓ)] .= 0         # fix divisions by zero on diagonal
    s = sum(Dₓ, dims = 2)
    Dₓ -= diagm(s[:, 1])         # "negative sum trick"

    # Transplant to [a,b].
    a, b = xspan
    x = @. a + (b - a) * (x + 1) / 2
    Dₓ = 2 * Dₓ / (b - a)             # chain rule

    # Second derivative.
    Dₓₓ = Dₓ^2
    return x, Dₓ, Dₓₓ
end
Solution of a linear boundary-value problem
bvplin.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
    bvplin(p, q, r, xspan, lval, rval, n)

Use finite differences to solve a linear bopundary value problem.
The ODE is u''+`p`(x)u'+`q`(x)u = `r`(x) on the interval `xspan`,
with endpoint function values given as `lval` and `rval`. There will
be `n`+1 equally spaced nodes, including the endpoints.

Returns vectors of the nodes and the solution values.
"""
function bvplin(p, q, r, xspan, lval, rval, n)
    x, Dₓ, Dₓₓ = diffmat2(n, xspan)

    P = diagm(p.(x))
    Q = diagm(q.(x))
    L = Dₓₓ + P * Dₓ + Q     # ODE expressed at the nodes

    # Replace first and last rows using boundary conditions.
    z = zeros(1, n)
    A = [[1 z]; L[2:n, :]; [z 1]]
    b = [lval; r.(x[2:n]); rval]

    # Solve the system.
    u = A \ b
    return x, u
end
Solution of a nonlinear boundary-value problem
bvp.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
    bvplin(p, q, r, xspan, lval, rval, n)

Use finite differences to solve a linear bopundary value problem.
The ODE is u''+`p`(x)u'+`q`(x)u = `r`(x) on the interval `xspan`,
with endpoint function values given as `lval` and `rval`. There will
be `n`+1 equally spaced nodes, including the endpoints.

Returns vectors of the nodes and the solution values.
"""
function bvplin(p, q, r, xspan, lval, rval, n)
    x, Dₓ, Dₓₓ = diffmat2(n, xspan)

    P = diagm(p.(x))
    Q = diagm(q.(x))
    L = Dₓₓ + P * Dₓ + Q     # ODE expressed at the nodes

    # Replace first and last rows using boundary conditions.
    z = zeros(1, n)
    A = [[1 z]; L[2:n, :]; [z 1]]
    b = [lval; r.(x[2:n]); rval]

    # Solve the system.
    u = A \ b
    return x, u
end
Piecewise linear finite elements for a linear BVP
fem.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""
    fem(c, s, f, a, b, n)

Use a piecewise linear finite element method to solve a two-point
boundary value problem. The ODE is (`c`(x)u')' + `s`(x)u = `f`(x) on
the interval [`a`,`b`], and the boundary values are zero. The
discretization uses `n` equal subintervals.

Return vectors for the nodes and the values of u.
"""
function fem(c, s, f, a, b, n)
    # Define the grid.
    h = (b - a) / n
    x = @. a + h * (0:n)

    # Templates for the subinterval matrix and vector contributions.
    Ke = [1 -1; -1 1]
    Me = (1 / 6) * [2 1; 1 2]
    fe = (1 / 2) * [1; 1]

    # Evaluate coefficent functions and find average values.
    cval = c.(x)
    cbar = (cval[1:n] + cval[2:n+1]) / 2
    sval = s.(x)
    sbar = (sval[1:n] + sval[2:n+1]) / 2
    fval = f.(x)
    fbar = (fval[1:n] + fval[2:n+1]) / 2

    # Assemble global system, one interval at a time.
    K = zeros(n - 1, n - 1)
    M = zeros(n - 1, n - 1)
    f = zeros(n - 1)
    K[1, 1] = cbar[1] / h
    M[1, 1] = sbar[1] * h / 3
    f[1] = fbar[1] * h / 2
    K[n-1, n-1] = cbar[n] / h
    M[n-1, n-1] = sbar[n] * h / 3
    f[n-1] = fbar[n] * h / 2
    for k in 2:n-1
        K[k-1:k, k-1:k] += (cbar[k] / h) * Ke
        M[k-1:k, k-1:k] += (sbar[k] * h) * Me
        f[k-1:k] += (fbar[k] * h) * fe
    end

    # Solve system for the interior values.
    u = (K + M) \ f
    u = [0; u; 0]      # put the boundary values into the result
    return x, u
end

Examples

10.1 Two-point BVP

Example 10.1.3

As a system, the MEMS problem from Example 10.1.2 uses y1=wy_1=w, y2=wy_2=w' to obtain

y1=y2,y2=λy12y2r.\begin{split} y_1' &= y_2, \\ y_2' &= \frac{\lambda}{y_1^2} - \frac{y_2}{r}. \end{split}

We will code an in-place form of this ODE, in which the first argument is used to return the computed values of y1y_1' and y2y_2'.

function ode!(f, y, λ, r)
    f[1] = y[2]
    f[2] = λ / y[1]^2 - y[2] / r
    return nothing
end;

Notice that the return value is irrelevant with the in-place style. We use the same style for the boundary conditions y2(0)=0y_2(0)=0, y1(1)=1y_1(1)=1.

function bc!(g, y, λ, r)
    g[1] = y[1][2]          # first node, second component = 0
    g[2] = y[end][1] - 1    # last node, first component = 1
    return nothing
end;

In the bc! function, the y argument is just like an IVP solution from Basics of IVPs. Thus, y(0) is the value of the solution at x=0x=0, and the second component of that value is what we wish to make zero. Similarly, y(1)[1] is the notation for y1(1)y_1(1), which is supposed to equal 1.

The domain of the mathematical problem is r[0,1]r\in [0,1]. However, there is a division by rr in the ODE, so we want to avoid r=0r=0 by truncating the domain a bit.

domain = (eps(), 1.0)
(2.220446049250313e-16, 1.0)

We need one last ingredient that is not part of the mathematical setup: an initial estimate for the solution. As we will see, this plays the same role as initialization in Newton’s method for rootfinding. Here, we try a constant value for each component.

est = [1, 0]
2-element Vector{Int64}: 1 0

Now we set up and solve a BVProblem with the parameter value λ=0.6\lambda=0.6.

using BoundaryValueDiffEq, Plots
bvp = BVProblem(ode!, bc!, est, domain, 0.6)
y = solve(bvp, Shooting(Tsit5()))
plot(y;
    label = [L"w" L"w'"],
    legend = :right,
    xlabel = L"r",
    ylabel = "solution",
    title = "Solution of MEMS problem for λ=0.6",
)
Loading...

To visual accuracy, the boundary conditions have been enforced. We can check them numerically.

@show y(0)[2];    # y_2(0)
@show y(1)[1];    # y_1(1)
(y(0))[2] = -2.1468729870818984e-16

(y(1))[1] = 0.9999999999998375

10.2 Shooting

Example 10.2.1

Let’s first examine the shooting approach for the TPBVP from Example 10.1.2 with λ=0.6\lambda=0.6.

λ = 0.6
ϕ = (r, w, dwdr) -> λ / w^2 - dwdr / r;

We convert the ODE to a first-order system in order to apply a numerical method. We also have to truncate the domain to avoid division by zero.

f = (y, p, r) -> [y[2]; ϕ(r, y[1], y[2])]
a, b = eps(), 1.0;

The BVP specifies w(0)=y2(0)=0w'(0)=y_2(0)=0. We can try multiple values for the unknown w(0)=y1(0)w(0)=y_1(0) and plot the solutions.

using OrdinaryDiffEq, Plots
plt = plot(
    xaxis = (L"x"),  yaxis = (L"w(x)"),
    title = "Different initial values",  legend = :bottomright)

for w0 in 0.4:0.1:0.9
    IVP = ODEProblem(f, [w0, 0], (a, b))
    y = solve(IVP, Tsit5())
    plot!(y, idxs = [1], label = "w(0) = $w0")
end
plt
Loading...

On the graph, it’s the curve starting at w(0)=0.8w(0)=0.8 that comes closest to the required condition w(1)=1w(1)=1, but it’s a bit too large.

Example 10.2.2

We revisit Demo 10.2.1 but let Function 10.2.1 do the heavy lifting.

λ = 0.6
ϕ = (r, w, dwdr) -> λ / w^2 - dwdr / r;
a, b = eps(), 1.0;

We specify the given and unknown endpoint values.

g₁(w, dw) = dw       # w' = 0 at left
g₂(w, dw) = w - 1    # w = 1 at right
r, w, dw_dx = FNC.shoot(ϕ, (a, b), g₁, g₂, [0.8, 0])
plot(r, w, title = "Shooting solution", xaxis = (L"x"), yaxis = (L"w(x)"))
Loading...

The value of ww at r=1r=1, meant to be exactly one, was computed to be

@show w[end];
w[end] = 0.9999999997952056

The accuracy is consistent with the error tolerance used for the IVP solution. The initial value w(0)w(0) that gave this solution is

@show w[1];
w[1] = 0.787757639919671
Example 10.2.3
plt = plot(
    xaxis = (L"x"),
    yaxis = ([-1.2, 0.5], L"u(x)"),
    title = "Shooting instability",
    leg = :topleft,
)
for λ in 6:4:18
    g₁(u, du) = u + 1
    g₂(u, du) = u
    ϕ = (x, u, du_dx) -> λ^2 * (u + 1)
    x, u = FNC.shoot(ϕ, (0.0, 1.0), g₁, g₂, [-1, 0])
    plot!(x, u, label = "λ=$λ")
end
plt
Loading...

The numerical solutions evidently don’t satisfy the right boundary condition as λ increases, which makes them invalid.

10.3 Differentiation matrices

Example 10.3.1

We test first-order and second-order differentiation matrices for the function x+exp(sin4x)x + \exp(\sin 4x) over [1,1][-1,1].

f = x -> x + exp(sin(4 * x));

For reference, here are the exact first and second derivatives.

df_dx = x -> 1 + 4 * exp(sin(4x)) * cos(4x);
d2f_dx2 = x -> 4 * exp(sin(4x)) * (4 * cos(4x)^2 - 4 * sin(4x));

We discretize on equally spaced nodes and evaluate ff at the nodes.

t, Dₓ, Dₓₓ = FNC.diffmat2(18, [-1, 1])
y = f.(t);

Then the first two derivatives of ff each require one matrix-vector multiplication.

yₓ = Dₓ * y
yₓₓ = Dₓₓ * y;

The results show poor accuracy for this small value of nn.

using Plots
plot(df_dx, -1, 1, layout = 2, xaxis = (L"x"), yaxis = (L"f'(x)"))
scatter!(t, yₓ, subplot = 1)
plot!(d2f_dx2, -1, 1, subplot = 2, xaxis = (L"x"), yaxis = (L"f''(x)"))
scatter!(t, yₓₓ, subplot = 2)
Loading...

A convergence experiment confirms the order of accuracy. Because we expect an algebraic convergence rate, we use a log-log plot of the errors.

n = @. round(Int, 2^(4:0.5:11))
err = zeros(length(n), 2)
for (k, n) in enumerate(n)
    t, Dₓ, Dₓₓ = FNC.diffmat2(n, [-1, 1])
    y = f.(t)
    err[k, 1] = norm(df_dx.(t) - Dₓ * y, Inf)
    err[k, 2] = norm(d2f_dx2.(t) - Dₓₓ * y, Inf)
end
plot(n, err, m = :o, label = [L"f'" L"f''"])
plot!(n, 10 * 10 * n .^ (-2);
    l = (:dash, :black),
    label = "2nd order",
    xaxis = (:log10, "n"),
    yaxis = (:log10, "max error"),
    title = "Convergence of finite differences")
Loading...
Example 10.3.2

Here is a 4×44\times 4 Chebyshev differentiation matrix.

t, Dₓ = FNC.diffcheb(3, [-1, 1])
Dₓ
4×4 Matrix{Float64}: -3.16667 4.0 -1.33333 0.5 -1.0 0.333333 1.0 -0.333333 0.333333 -1.0 -0.333333 1.0 -0.5 1.33333 -4.0 3.16667

We again test the convergence rate.

f = x -> x + exp(sin(4 * x));
df_dx = x -> 1 + 4 * exp(sin(4 * x)) * cos(4 * x);
d2f_dx2 = x -> 4 * exp(sin(4 * x)) * (4 * cos(4 * x)^2 - 4 * sin(4 * x));
n = 5:5:70
err1 = zeros(size(n))
err2 = zeros(size(n))
for (k, n) in enumerate(n)
    t, Dₓ, Dₓₓ = FNC.diffcheb(n, [-1, 1])
    y = f.(t)
    err1[k] = norm(df_dx.(t) - Dₓ * y, Inf)
    err2[k] = norm(d2f_dx2.(t) - Dₓₓ * y, Inf)
end

Since we expect a spectral convergence rate, we use a semi-log plot for the error.

plot(n, [err1 err2]; m = :o,
    label=[L"f'" L"f''"],
    xaxis=(L"n"),  yaxis = (:log10, "max error"),
    title="Convergence of Chebyshev derivatives")
Loading...

10.4 Collocation for linear problems

Example 10.4.1
exact = x -> exp(sin(x));

The problem is presented above in our standard form, so we can identify the coefficient functions in the ODE. Each should be coded as a function.

p = x -> -cos(x);
q = sin;
r = x -> 0;      # function, not value

We solve the BVP and compare the result to the exact solution.

x, u = FNC.bvplin(p, q, r, [0, 3π / 2], 1, exp(-1), 30);
using Plots
plot(exact, 0, 3π / 2, layout = (2, 1), label = "exact")
scatter!(x, u, m = :o,
    subplot=1,  label="numerical",
    yaxis=("solution"),
    title="Solution of a linear BVP")
plot!(x, exact.(x) - u, subplot = 2, xaxis = L"x", yaxis = ("error"))
Loading...
Example 10.4.2
λ = 10
exact = x -> sinh(λ * x) / sinh(λ) - 1;

The following functions define the ODE.

p = x -> 0
q = x -> -λ^2
r = x -> λ^2;

We compare the computed solution to the exact one for increasing nn.

n = 5 * [round(Int, 10^d) for d in 0:0.25:3]
err = zeros(length(n))
for (k, n) in enumerate(n)
    x, u = FNC.bvplin(p, q, r, [0, 1], -1, 0, n)
    err[k] = norm(exact.(x) - u, Inf)
end
data = (n = n[1:4:end], err = err[1:4:end])
@pt :header = ["n", "inf-norm error"] data
Loading...

Each factor of 10 in nn reduces error by a factor of 100, which is indicative of second-order convergence.

plot(n, err, m = :o,
    label = "observed",
    xaxis = (:log10, L"n"),
    yaxis = (:log10, "inf-norm error"),
    title = "Convergence for a linear BVP")
plot!(n, 0.25 * n .^ (-2), l = (:dash, :gray), label = "2nd order")
Loading...

10.5 Nonlinearity and boundary conditions

Example 10.5.2

The first step is to define the function ϕ that equals θ\theta''.

ϕ = (t, θ, ω) -> -0.05 * ω - sin(θ);

Next, we define the boundary conditions.

g₁(u, du) = u - 2.5
g₂(u, du) = u + 2;

The last ingredient is an initial estimate of the solution. Here we choose n=100n=100 and a linear function between the endpoint values.

init = collect(range(2.5, -2, length = 101));

We find a solution with negative initial slope, i.e., the pendulum is initially pushed back toward equilibrium.

using Plots
t, θ = FNC.bvp(ϕ, [0, 5], g₁, g₂, init)
plot(t, θ;
    xaxis=(L"t"),  yaxis=(L"\theta(t)"),
    title="Pendulum over [0,5]" )
Loading...

If we extend the time interval longer for the same boundary values, then the initial slope must adjust.

t, θ = FNC.bvp(ϕ, [0, 8], g₁, g₂, init)
plot(t, θ;
    xaxis=(L"t"),  yaxis=(L"\theta(t)"),
    title="Pendulum over [0,8]" )
Loading...

This time, the pendulum is initially pushed toward the unstable equilibrium in the upright vertical position before gravity pulls it back down.

Example 10.5.3

Here is the problem definition. We use a truncated domain to avoid division by zero at r=0r=0.

domain = [eps(), 1]
λ = 0.5
ϕ = (r, w, dwdr) -> λ / w^2 - dwdr / r
g₁(w, dw) = dw
g₂(w, dw) = w - 1;

First we try a constant function as the initialization.

init = ones(301)
r, w₁ = FNC.bvp(ϕ, domain, g₁, g₂, init)

plot(r, w₁;
    xaxis = (L"r"),  yaxis = (L"w(r)"), 
    title = "Solution of the MEMS problem")
Loading...

It’s not necessary that the initialization satisfy the boundary conditions. In fact, by choosing a different constant function as the initial guess, we arrive at another valid solution.

init = 0.5 * ones(301)
r, w₂ = FNC.bvp(ϕ, domain, g₁, g₂, init)
plot!(r, w₂, title = "Two solutions of the MEMS problem")
Loading...
Example 10.5.4
ϕ = (x, u, dudx) -> (u^3 - u) / ϵ;
g₁(u, du) = du
g₂(u, du) = u - 1;

Finding a solution is easy at larger values of ε.

ϵ = 0.05
init = collect(range(-1, 1, length = 141))
x, u₁ = FNC.bvp(ϕ, [0, 1], g₁, g₂, init)

plot(x, u₁;
    label=L"\epsilon = 0.05",  legend=:bottomright,
    xaxis=(L"x"),  yaxis=(L"u(x)"),
    title = "Allen–Cahn solution")
Loading...

However, finding a good initialization is not trivial for smaller values of ε. Note below that the iteration stops without converging to a solution.

ϵ = 0.002;
x, z = FNC.bvp(ϕ, [0, 1], g₁, g₂, init);
Warning: Maximum number of iterations reached.
@ FNCFunctions ~/Documents/GitHub/FNCFunctions.jl/src/chapter04.jl:178

The iteration succeeds if we use the first solution instead as the initialization here.

x, u₂ = FNC.bvp(ϕ, [0, 1], g₁, g₂, u₁)
plot!(x, u₂; label = L"\epsilon = 0.002")
Loading...

In this case we can continue further.

ϵ = 0.0005
x, u₃ = FNC.bvp(ϕ, [0, 1], g₁, g₂, u₂)
plot!(x, u₃, label = L"\epsilon = 0.0005")
Loading...

10.6 The Galerkin method

Example 10.6.2

Here are the coefficient function definitions. Even though ss is a constant, it has to be defined as a function for Function 10.6.1 to use it.

c = x -> x^2;
q = x -> 4;
f = x -> sin(π * x);
using Plots
x, u = FNC.fem(c, q, f, 0, 1, 50)
plot(x, u;
    xaxis=(L"x"),  yaxis = (L"u"),
    title = "Solution by finite elements", legend=:none)
Loading...