![]() | Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license. (c) Kyle T. Mandli |
Note: The presentation below largely follows part I in “Finite Difference Methods for Ordinary and Partial Differential Equations” by LeVeque (SIAM, 2007).
from __future__ import print_function
%matplotlib inline
import matplotlib.pyplot as plt
import numpySolving Boundary Value Problems¶
The Problem¶
We want to solve an ODE (PDE) that instead of having initial conditions is contained to an interval and has values at the edges of the interval. This naturally comes about when we consider spatial problems. One of the simplest cases for this is the Poisson problem in one-dimension
where we will use the short-hand
Note that due to the order of the derivative we require two conditions to solve this. The simplest case where we are on the domain is to have conditions such that we require and and are commonly termed boundary value problems (BVP). If these conditions are both at one end of the domain then we can actually phrase the ODE (PDE) again as an initial value problem (IVP). So what do we need to do to solve these types of problems? We will consider two approaches to this problem:
Rephrase the BVP to an IVP and use our standard methods for ODEs.
Use finite differences to represent the unknowns as a linear system and solve the resulting system.
The Shooting Method¶
The shooting method takes the approach that we want to use our ability to solve IVP problems and so tries to term the problem as a root finding problem for the higher order initial condition that we are not given. This is best illustrated in an example.
We can rewrite this problem as a system of two ODEs as
We know that we want but what do we use for ? Making an initial guess at and solving the associated IVP ODE we can then find out what these initial conditions produces on the right boundary of the problem. Using a root-finding approach (or minimization routine) we can write this procedure as
where the parameter we vary is .
# Basic Shooting Method solving u_xx = -sin(u)
import scipy.integrate as integrate
# Algorithm parameters
TOLERANCE = 1e-8
MAX_ITERATIONS = 100
# Problem Statement
a = 0.0
b = 2.0
N = 100
x = numpy.linspace(a, b, N)
u_a = 0.0
u_b = numpy.pi / 2.0
# RHS function
def f(x, u):
return numpy.array([u[1], -numpy.sin(u[0])])
# Initial guess
# Slope at RHS
u_prime_rhs = 1.0
# Initial step size
du_prime = 0.5
# Plotting
fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2)
axes = fig.add_subplot(1, 2, 1)
# Main loop
success = False
u = numpy.empty((2, N))
convergence = numpy.zeros(MAX_ITERATIONS)
for n in range(MAX_ITERATIONS):
# Initial condition
u[0, 0] = u_a
u[1, 0] = u_prime_rhs
# Construct integrator
integrator = integrate.ode(f)
integrator.set_integrator("dopri5")
integrator.set_initial_value(u[:, 0])
# Compute solution - note that we are only producing the intermediate values
# for demonstration purposes
for i, x_output in enumerate(x[1:]):
integrator.integrate(x_output)
if not integrator.successful():
raise Exception("Integration Failed!")
u[:, i + 1] = integrator.y
# Stopping Criteria
convergence[n] = numpy.abs(u[0, -1] - u_b)
if numpy.abs(u[0, -1] - u_b) < TOLERANCE:
success = True
break
else:
if u[0, -1] < u_b:
u_prime_rhs += du_prime
else:
u_prime_rhs -= du_prime
du_prime *= 0.5
axes.plot(x, u[0, :], "b")
axes.plot(b, u_b, "ro")
axes.set_title("Shooting Method Iterations")
axes.set_xlabel("$x$")
axes.set_ylabel("$u(x)$")
axes = fig.add_subplot(1, 2, 2)
n_range = numpy.arange(n)
axes.semilogy(n_range, convergence[:n])
axes.set_title("Convergence of Shooting Method")
axes.set_xlabel("step")
axes.set_ylabel("$|u(b) - U(b)|$")
plt.show()The tricky part of this procedure is coming up with the search criteria, i.e. coming up with the decision of how to change with respect to the position of compared to what we want .
In general any minimization routine can be used in a shooting method. These approaches are generally very effective at approaching non-linear BVPs where the next method we will discuss is too expensive to perform.
Linear System Approach¶
Formulation¶
The second approach we will consider involves the formation of a system of equations to solve based on finite difference approximations. Again let’s consider an example problem where
with the initial conditions and .
We know from our finite difference discussion that the second order centered difference approximation for the second derivative for a function is
If we descretize the domain of the original BVP into points (not including the boundaries) such that
we can then write the finite difference approximation as a system of linear equations!
Using these approximations to the derivatives we can then write the ODE as
Note that our previous example used for the shooting method is difficult in the current context as the unknown function is in the function so that we would need to actual solve a non-linear system of equations. This is still possible in this context using an approach such as a Newton solver and has similar properties as the shooting method (although not as simple to implement).
Boundary Conditions¶
This does not include the boundary conditions though. We can add these values easily for Dirichlet boundary conditions by sending the values we know to the vector:
so that final system looks like
# Problem setup
a = 0.0
b = 1.0
u_a = 0.0
u_b = 3.0
f = lambda x: numpy.exp(x)
u_true = lambda x: (4.0 - numpy.exp(1.0)) * x - 1.0 + numpy.exp(x)
# Descretization
N = 10
x_bc = numpy.linspace(a, b, N + 2)
x = x_bc[1:-1]
delta_x = (b - a) / (N + 1)
# Construct matrix A
A = numpy.zeros((N, N))
diagonal = numpy.ones(N) / delta_x**2
A += numpy.diag(diagonal * -2.0, 0)
A += numpy.diag(diagonal[:-1], 1)
A += numpy.diag(diagonal[:-1], -1)
# Construct RHS
b = f(x)
b[0] -= u_a / delta_x**2
b[-1] -= u_b / delta_x**2
# Solve system
U = numpy.empty(N + 2)
U[0] = u_a
U[-1] = u_b
U[1:-1] = numpy.linalg.solve(A, b)
# Plot result
fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x_bc, U, "o", label="Computed")
axes.plot(x_bc, u_true(x_bc), "k", label="True")
axes.set_title("Solution to $u_{xx} = e^x$")
axes.set_xlabel("x")
axes.set_ylabel("u(x)")
plt.show()If we instead have Neumann boundary conditions it is no longer clear how to handle the boundary conditions using the above approach. Instead a ghost cell approach is often used. These ghost cells are added unknowns that represent the boundary values that we actually know.
For instance, if we had the BVP
then we could keep the boundary values in the vector of unknowns so that now
where here and are actually the boundary points.
The matrix is then modified to have the appropriate relationships. In this case the left boundary condition leads to
which multiplied out simply gives
For the right boundary condition we can use the second order backward finite difference approximation for the first derivative
which can be incorporated into the matrix and vector as
All together the new system looks like
Example¶
Want to solve the BVP
via the construction of a linear system of equations.
First find the true solution and then compute the solution.
# Problem setup
a = -1.0
b = 1.0
u_a = 3.0
u_x_b = -5.0
f = lambda x: numpy.exp(x)
u_true = (
lambda x: -(5.0 + numpy.exp(1.0)) * x
- (2.0 + numpy.exp(1.0) + numpy.exp(-1.0))
+ numpy.exp(x)
)
# Descretization
N = 10
x_bc = numpy.linspace(a, b, N + 2)
x = x_bc[1:-1]
delta_x = (b - a) / (N + 1)
# Construct matrix A
A = numpy.zeros((N + 2, N + 2))
diagonal = numpy.ones(N + 2) / delta_x**2
A += numpy.diag(diagonal * -2.0, 0)
A += numpy.diag(diagonal[:-1], 1)
A += numpy.diag(diagonal[:-1], -1)
# Construct RHS
b = f(x_bc)
# Boundary conditions
A[0, 0] = 1.0
A[0, 1] = 0.0
A[-1, -1] = 3.0 / (2.0 * delta_x)
A[-1, -2] = -4.0 / (2.0 * delta_x)
A[-1, -3] = 1.0 / (2.0 * delta_x)
b[0] = u_a
b[-1] = u_x_b
# Solve system
U = numpy.empty(N + 2)
U = numpy.linalg.solve(A, b)
# Plot result
fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x_bc, U, "o", label="Computed")
axes.plot(x_bc, u_true(x_bc), "k", label="True")
axes.set_title("Solution to $u_{xx} = e^x$")
axes.set_xlabel("x")
axes.set_ylabel("u(x)")
plt.show()Ways to Solve ¶
We have proposed solving the linear system which we have implemented naively above with the numpy.linalg.solve command but perhaps given the special structure of here that we can do better.
Direct Methods (Gaussian Elimination)¶
We could use Gaussian elimination to solve the system (or some factorization) which leads to a solution in a finite number of steps. For large, sparse methods however these direct solvers are much more expensive in general over iterative solvers. As was discussed for eigenproblems, iterative solvers start with an initial guess and try to improve on that guess.
Consider a 3D Poisson Problem:
Discretize using unknowns
Gaussian elimination requires operations
Solving this system would take 1018 floating point operations to complete
How long?
Today’s computer is gigaflops (floating point operations per second) - 1011 flops / second. We would be waiting 115 days for the solve to finish!
Memory?
Matrix requires memory locations ( here). Single precision floating point storage (4-bytes per number) would require bytes of 4 terabytes of memory.
The situation really is not as bad as we are making it out to be as long as we take advantage of the sparse nature of the matrices. In fact for 1 dimensional problems direct methods can be reduced to in the case for a tridiagonal system. The situation is not so great for higher-dimensional problems however unless most structure can be leveraged. Examples of these types of solvers include fast Fourier methods such as fast Poisson solvers.
Iterative Methods¶
Iterative methods take another tact that direct methods. If we have the system we form an iterative procedure that applies a function, say , such that
where we want errot between the real solution and goes to zero as . We will explore these methods in the next lecture.
