Solving Differential Equations in Java
Solving Differential Equations in Java
Equations
D
ifferential equations, those that define how the value of one variable changes
with respect to another, are used to model a wide range of physical processes.
You will use differential equations in chemistry, dynamics, fluid dynamics,
thermodynamics, and almost every other scientific or engineering endeavor. A
differential equation that has one independent variable is called an ordinary
differential equation or ODE. Examples of ODEs include the equations to
model the motion of a spring or the boundary layer equations from fluid dy-
namics. A partial differential equation (PDE) has more than one independent
variable. The Navier-Stokes equations are an example of a set of coupled par-
tial differential equations used in fluid dynamic analysis to represent the con-
servation of mass, momentum, and energy.
This chapter will focus primarily on how to solve ordinary differential
equations and will touch upon the more difficult to solve partial differential
equations only briefly at the end of the chapter. We will discuss the difference
between initial value and two-point boundary problems. We will write a class
that represents a generic ODE and write two subclasses that represent the mo-
tion of a damped spring and a compressible boundary layer over a flat plate.
We will develop a class named ODESolver that will define a number of
methods used to solve ODEs and compare results generated by these methods
with results from other sources.
The specific topics covered in this chapter are
271
20
5568ch20.qxd_jd 3/24/03 9:09 AM Page 271
Ordinary differential equations
The ODE class
Initial value problems
Runge-Kutta schemes
Example problem: damped spring motion
Embedded Runge-Kutta solvers
Other ODE solution techniques
Two-point boundary problems
Shooting methods
Example problem: compressible boundary layer
Other two-point boundary solution techniques
Partial differential equations
Ordinary Differential Equations
An ODE is used to express the rate of change of one quantity with respect to
another. You have probably been working with ODEs since you began your
scientific or engineering course work. One defining characteristic of an ODE is
that its derivatives are a function of one independent variable. A general form
of a first-order ODE is shown in Eq. (20.1).
(20.1)
The order of a differential is defined as the order of the highest derivative
appearing in the equation. Ordinary differential equations can be of any order.
A general form of a second-order ODE is shown in Eq. (20.2).
(20.2)
Any higher-order ODE can be expressed as a coupled set of first-order
differential equations. For example, the second-order ODE shown in Eq. (20.2)
can be reduced to a coupled set of two first-order differential equations.
(20.3)
The second expression in Eq. (20.3) looks trivial in that the left-hand side
is the same as the right-hand side, but the ODE solvers we will discuss later in
d
dx
1y2 =
dy
dx
d
dx
a
dy
dx
b = -a1x2
dy
dx
- b1x2y - c1x2 - d
d
2
y
dx
2
+ a1x2
dy
dx
+ b1x2y + c1x2 + d = 0
dy
dx
+ a1x2y + b1x2 + c = 0
272 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 272
this chapter use the coupled first-order form of the ODE in their solution
process. The ODE solvers would integrate the first-order equations shown in
Eq. (20.3) to obtain values for the dependent variables y and dy/dx as a function
of the independent variable x.
The ODE Class
As you certainly know by this time, everything in Java is defined within a
class. If we are working with ODEs we need to define a class that will encapsu-
late an ODE. We will write the ODE class to represent a generic ODE. It will be
the superclass for specific ODE subclasses. The ODE class will declare fields
and methods used by all ODE classes. Since an ODE is a mathematical entity,
we will place the ODE class in the TechJava.MathLib package.
When writing a class you must always consider the state and behavior of
the item you are modeling. Let us first consider the fields that will define the
state of an ODE. The ODE class will represent its associated ODE by one or
more first-order differential equations. The ODE class will declare a field to
store the number of first-order equations. Another field is needed to store the
number of free variables in the ODE. Free variables are those that are not spec-
ified by boundary conditions at the beginning of the integration range. For ini-
tial value problems, the number of free variables will be zero. Two-point
boundary problems will have one or more free variables.
The coupled set of first-order ODEs is solved by integrating each of the
ODEs step-wise over a certain range. The values of the independent and depen-
dent variables will have to be stored at every step in the integration. To facili-
tate this, the ODE class declares two arrays x[]which stores the values of
the independent variable at each step of the integration domain and y[][]
which stores the dependent variable or variables. The y[][] array is 2-D be-
cause an ODE might represent a system of first-order differential equations and
therefore have more than one dependent variable.
The ODE class constructor will take two input arguments that specify the
number of first-order differential equations and number of free variables used
by the ODE. Because the required number of steps along the integration path is
not a fixed value, the x[] and y[][] arrays are allocated to a maximum num-
ber of steps. This approach may waste a little memory but is the simplest way
to do things.
Now lets turn to the behavior of an ODE class. What does an ODE class
have to do? It must declare a method to return the right-hand sides of the first-
The ODE Class 273
5568ch20.qxd_jd 3/24/03 9:09 AM Page 273
order differential equations that describe the ODE. The ODE class will declare
methods to return the number or first-order equations and free variables as well
as methods to return the values of the x[] and y[][] arrays. There will be
one method to return the entire array and another to return a single element of
the array.
The ODE class also declares methods to set the conditions at the start of
the integration range and to compute the error at the end. These methods and
the right-hand side method are ODE-specific. Since the ODE class represents a
generic ODE, they are implemented as stubs. ODE subclasses will override
these methods according to their needs.
The ODE class code listing is shown next.
package TechJava.MathLib;
public class ODE
{
// This is used to allocate memory to the
// x[] and y[][] arrays
public static int MAX_STEPS = 999;
// numEqns = number of 1st order ODEs to be solved
// numFreeVariables = number of free variables
// at domain boundaries
// x[] = array of independent variables
// y[][] = array of dependent variables
private int numEqns, numFreeVariables;
private double x[];
private double y[][];
public ODE(int numEqns, int numFreeVariables) {
this.numEqns = numEqns;
this.numFreeVariables = numFreeVariables;
x = new double[MAX_STEPS];
y = new double[MAX_STEPS][numEqns];
}
// These methods return the values of some of
// the fields.
public int getNumEqns() {
return numEqns;
}
public int getNumFreeVariables() {
return numFreeVariables;
}
274 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 274
public double[] getX() {
return x;
}
public double[][] getY() {
return y;
}
public double getOneX(int step) {
return x[step];
}
public double getOneY(int step, int equation) {
return y[step][equation];
}
// This method lets you change one of the
// dependent or independent variables
public void setOneX(int step, double value) {
x[step] = value;
}
public void setOneY(int step, int equation,
double value) {
y[step][equation] = value;
}
// These methods are implemented as stubs.
// Subclasses of ODE will override them.
public void getFunction(double x, double dy[],
double ytmp[]) {}
public void getError(double E[], double endY[]) {}
public void setInitialConditions(double V[]) {}
}
Initial Value Problems
Before we discuss how to solve them, lets explore a little bit about the nature
of ODEs themselves. There are two basic types of boundary condition cate-
gories for ODEsinitial value problems and two-point boundary value prob-
lems. With an initial value problem, values for all of the dependent variables
are specified at the beginning of the range of integration. The initial boundary
serves as the anchor for the solution. The solution is marched outward from
Initial Value Problems 275
5568ch20.qxd_jd 3/24/03 9:09 AM Page 275
the initial boundary by integrating the ODE at discrete steps of the independent
variable. The dependent variables are computed at every step.
Initial value problems are simpler to solve because you only have to inte-
grate the ODE one time. The solution of a two-point boundary value problem
usually involves iterating between the values at the beginning and end of the
range of integration. The most commonly used techniques to solve initial value
problem ODEs are called Runge-Kutta schemes and will be discussed in the
next section.
Runge-Kutta Schemes
One of the oldest and still most widely used groups of ODE integration algo-
rithms is the Runge-Kutta family of methods. These are step-wise integration
algorithms. Starting from an initial condition, the ODE is solved at discrete
steps over the desired integration range. Runge-Kutta techniques are robust and
will give good results as long as very high accuracy is not required. Runge-
Kutta methods are not the fastest ODE solver techniques but their efficiency
can be markedly improved if adaptive step sizing is used.
Runge-Kutta methods are designed to solve first-order differential equa-
tions. They can be used on a single first-order ODE or on a coupled system of
first-order ODEs. If a higher-order ODE can be expressed as a coupled system
of first-order ODEs, Runge-Kutta methods can be used to solve it. To under-
stand how Runge-Kutta methods work, consider a simple first-order differen-
tial equation.
(20.4)
To solve for the dependent variable y, Eq. (20.4) can be integrated in a
step-wise manner. The derivative is replaced by its delta-form and the x term
is moved to the right-hand side.
(20.5)
Eq. (20.5) is the general form of the equation that is solved. Starting at an
independent variable location x
n
where the value of y
n
is known, the value of
the dependent variable at the next location, y
n1
, is equal to its value at the cur-
rent location, y
n
, added to the independent variable step size, x, times the
right-hand side function.
y = y
n+1
- y
n
= xf1x,y2
dy
dx
= f1x,y2
276 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 276
There is one question left to be resolvedwhere should we evaluate the
right-hand side function? With the Euler method, as shown in Eq. (20.6), the
function is evaluated at the current location, x
n
.
(20.6)
The value of y at the next step is computed using the slope of the f(x,y)
function at the current step. If you perform a Taylor series expansion on
Eulers method you will find that it is first-order accurate in x. The Euler
method is really only useful for linear or nearly linear functions. What hap-
pens, for instance, if the slope of the f(x,y) curve changes between x
n
and x
n1
?
The Euler method will compute an incorrect value for y
n1
.
This is where the Runge-Kutta methods come into play. The Runge-Kutta
methods perform a successive approximation of y
n1
by evaluating the f(x,y) func-
tion at different locations between x
n
and x
n1
. The final computation of y
n1
is a
linear combination of the successive approximations. For example, the second-
order Runge-Kutta method evaluates f(x,y) at two locations, shown in Eq. (20.7).
(20.7)
The first step of the second-order Runge-Kutta algorithm is the Euler
method. A value for y is computed by evaluating f(x,y) at x
n
. The second step
calculates the value of y
n1
by evaluating f(x,y) midway between x
n
and x
n1
using a y value halfway between y
n
and y
n
y
1
. The result is a second-order
accurate approximation to y
n1
. The two-step Runge-Kutta scheme is more ac-
curate than Eulers method because it does a better job of handling potential
changes in slope of f(x,y) between x
n
and x
n1
.
There are numerous Runge-Kutta schemes of various orders of accuracy.
The most commonly used scheme and the one we will implement in this chap-
ter is the fourth-order Runge-Kutta algorithm. As the name implies, it is fourth-
order accurate in x. The algorithm consists of five steps, four successive
approximations of y and a fifth step that computes y
n1
based on a linear
combination of the successive approximations. The fourth-order Runge-Kutta
solution process is;
1. Find y
1
using Eulers method.
2. Compute y
2
by evaluating f(x,y) at .
3. Calculate y
3
by evaluating f(x,y) at . ax
n
+
1
2
x,y
n
+
1
2
y
2
b
ax
n
+
1
2
x,y
n
+
1
2
y
1
b
y
n+1
= y
n
+ xfax
n
+
1
2
x,y
n
+
1
2
y
1
b
y
1
= xf1x
n
,y
n
2
y
n+1
= y
n
+ xf1x
n
,y
n
2
Runge-Kutta Schemes 277
5568ch20.qxd_jd 3/24/03 9:09 AM Page 277
4. Evaluate y
4
by evaluating f(x,y) at (x
n
x,y
n
y
3
).
5. Compute y
n1
using a linear combination of y
1
through y
4
.
The mathematical equations for the five steps are shown in Eq. 20.8.
(20.8)
Now that we have gone over the derivation of the fourth-order Runge-
Kutta method, lets write a method to implement it. The method will be named
rungeKutta4(). As Runge-Kutta solvers are used by a wide variety of ap-
plications, we will define the rungeKutta4() method to be public and
static, so it can be universally accessed. We will define rungeKutta4()
in a class named ODESolver and place the ODESolver class in the Tech-
Java.MathLib package.
The rungeKutta4() method takes three arguments. The first argu-
ment is an ODE object (or an ODE subclass object). If you recall, the ODE class
will define the number of coupled first-order equations that characterize the
ODE and will provide arrays to store the dependent and independent variables.
The ODE class also defines the getFunction() method that returns the
f(x,y) function for a given x and y.
The other two input arguments are the range over which the integration
will take place and the increment to the independent variable. This increment
will be held constant throughout the entire integration. The number of steps
that will be performed is not a user-specified value but is computed based on
the range and dx arguments. The integration will stop if the step number
reaches the MAX_STEPS parameter defined in the ODE class.
The integration follows the steps shown in Eq. (20.8). When the integra-
tion is complete, the x[] and y[][] fields of the ODE object will contain the
values of the integrated independent and dependent variables. The return value
of the rungeKutta4() method is the number of steps computed. The
rungeKutta4() code is shown next.
y
n+1
= y
n
+
y
1
6
+
y
2
3
+
y
3
3
+
y
4
6
y
4
= xf1x
n
+ x,y
n
+ y
3
2
y
3
= xfax
n
+
1
2
x,y
n
+
1
2
y
2
b
y
2
= xfax
n
+
1
2
x,y
n
+
1
2
y
1
b
y
1
= xf1x
n
,y
n
2
278 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 278
package TechJava.MathLib;
public class ODESolver
{
public static int rungeKutta4(ODE ode,
double range, double dx) {
// Define some convenience variables to make the
// code more readable
int numEqns = ode.getNumEqns();
double x[] = ode.getX();
double y[][] = ode.getY();
// Define some local variables and arrays
int i,j,k;
double scale[] = {1.0, 0.5, 0.5, 1.0};
double dy[][] = new double[4][numEqns];
double ytmp[] = new double[numEqns];
// Integrate the ODE over the desired range.
// Stop if you are going to overflow the matrices
i=1;
while( x[i-1] < range && i < ODE.MAX_STEPS-1) {
// Increment independent variable. Make sure it
// doesn't exceed the range.
x[i] = x[i-1] + dx;
if (x[i] > range) {
x[i] = range;
dx = x[i] - x[i-1];
}
// First Runge-Kutta step
ode.getFunction(x[i-1],dy[0],y[i-1]);
// Runge-Kutta steps 2-4
for(k=1; k<4; ++k) {
for(j=0; j<numEqns; ++j) {
ytmp[j] = y[i-1][j] + scale[k]*dx*dy[k-1][j];
}
ode.getFunction(x[i-1]+scale[k]*dx,dy[k],ytmp);
}
// Update the dependent variables
for(j=0; j<numEqns; ++j) {
y[i][j] = y[i-1][j] + dx*(dy[0][j] +
Runge-Kutta Schemes 279
5568ch20.qxd_jd 3/24/03 9:09 AM Page 279
2.0*dy[1][j] + 2.0*dy[2][j] +
dy[3][j])/6.0;
}
// Increment i
++i;
} // end of while loop
// Return the number of steps computed
return i;
}
}
Example Problem: Damped Spring Motion
To demonstrate how the rungeKutta4() method can be used to solve an
initial value ODE problem, we will use as an example the motion of a damped
spring. Consider the spring shown in Figure 20.1. The upper end of the spring
is fastened to a solid object such that it cant move. The lower end of the spring
is attached to a body of mass m.
When the spring is stretched a distance x from its equilibrium position,
the force exerted on the mass by the spring is given by Hookes law.
280 Chapter 20 Solving Differential Equations
Mass = m
x
FIGURE 20.1 Spring configuration
5568ch20.qxd_jd 3/24/03 9:09 AM Page 280
(20.9)
The k parameter is the spring constant. The force on the body attached to
the string can also be characterized by Newtons second law
(20.10)
Putting Eq. (20.9) and Eq. (20.10) together we obtain the general equa-
tion for the motion of an undamped spring.
(20.11)
Eq. (20.11) assumes that there are no other forces acting on the spring. In
reality damping forces such as friction and air resistance will slow the springs
motion. Damping forces are a function of the velocity of the spring and a
damping constant, m. When damping forces are added to Eq. (20.11), we ob-
tain the general equation of motion for a spring.
(20.12)
You can see that the spring equation is a second-order ODE with time as
its independent variable. What makes the spring motion example a good test
case for our ODE solver development is that there is an exact solution to Eq.
(20.12). If m
2
4mk, the spring system is overdamped. When the mass is
moved from its equilibrium position and released it will simply return asymp-
totically to its equilibrium position. If m
2
4mk, the system is underdamped
and the result will be damped harmonic motion. The mass oscillates about its
equilibrium position with asymptotically declining minimum and maximum
values. The general solution for the position of a mass connected to an under-
damped spring is shown in Eq. (20.13).
(20.13)
The A and B constants in Eq. (20.13) are defined by the expressions in
Eq. (20.14).
(20.14)
The constants C
1
and C
2
are derived from the initial conditions. Assume
that initially the spring mass is extended a distance x
0
from its equilibrium po-
sition and the spring velocity is zero. Under these conditions the constants take
the values in Eq. (20.15).
A =
m
2m
B =
24mk - m
2
2m
x1t2 = e
-At
[C
1
cos1Bt2 + C
2
sin1Bt2]
m
d
2
x
dt
2
+ m
dx
dt
+ kx = 0
m
d
2
x
dt
2
+ kx = 0
F = ma = m
d
2
x
dt
2
F = -kx
Example Problem: Damped Spring Motion 281
5568ch20.qxd_jd 3/24/03 9:09 AM Page 281
(20.15)
Before we can use the rungeKutta4() method to numerically solve
for underdamped spring motion, Eq. (20.12) is recast in terms of two first-order
ODEs.
(20.16)
Equation (20.16) is integrated with respect to time to solve for x and dx/dt.
SpringODE class
To solve the damped spring initial value problem, we will write a SpringODE
class that is a subclass of the ODE class. The SpringODE class will represent
the equations of motion for a damped spring. In addition to the members it in-
herits from the ODE class, the SpringODE class defines three new fields rep-
resenting the spring constant, damping constant, and mass. While reading
through this section focus on the process. Even if you dont need to solve for
the motion of a damped spring, you can apply the solution process described in
this section to your own ODE problems.
The SpringODE class declares one constructor. The equations of mo-
tion for a damped spring consist of two first-order differential equations and
zero free variables. The first thing the SpringODE constructor does is to call
the ODE class constructor passing it values of 2 and 0 respectively. The
SpringODE constructor then initializes the k, mu, and mass fields.
The SpringODE class overrides the getFunction() and setIni-
tialConditions() methods of the ODE class. Since SpringODE repre-
sents an initial value problem there is no need to override the getError()
method. The getFunction() method is overridden to return the right-hand
side of Eq. (20.16). This evaluation will occur at various points along the range
of integration. The ytmp[] array holds the value of the dependent variables at
the point currently being evaluated. The setInitialConditions()
method is overridden to provide the initial conditions for the spring motion. At
time t 0 the spring is at rest so dx/dt 0. The V[] array holds the initial dis-
placement of the spring from its equilibrium position.
The SpringODE class source code is shown next.
d
dt
1x2 =
dx
dt
d
dt
a
dx
dt
b = -
m
m
dx
dt
-
k
m
x
C
1
= x
0
C
2
=
C
1
A
B
282 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/27/03 11:38 AM Page 282
package TechJava.MathLib;
public class SpringODE extends ODE
{
double k, mu, mass;
// The SpringODE constructor calls the ODE
// class constructor passing it data for
// a damped spring. There are two first
// order ODEs and no free variables.
public SpringODE(double k, double mu, double mass) {
super(2,0);
this.k = k;
this.mu = mu;
this.mass = mass;
}
// The getFunction() method returns the right-hand
// sides of the two first-order damped spring ODEs
// y[0] = delta(dxdt) = delta(t)*(-k*x - mu*dxdt)/mass
// y[1] = delta(x) = delta(t)*(dxdt)
public void getFunction(double x, double dy[],
double ytmp[]) {
dy[0] = -k*ytmp[1]/mass - mu*ytmp[0]/mass;
dy[1] = ytmp[0];
}
// This method initializes the dependent variables
// at the start of the integration range.
public void setInitialConditions(double V[]) {
setOneY(0, 0, 0.0);
setOneY(0, 1, V[0]);
setOneX(0, 0.0);
}
}
Solving the Spring Motion ODE
Now let us apply the fourth-order Runge-Kutta solver to compute the motion
of a damped spring. We will write a driver program named RK4Spring
.java that will create a SpringODE object and call the rungeKutta4()
method on that object. The mass, mu, and k parameters are given values rep-
resenting an underdamped spring. The initial conditions are set such that the
spring is extended 0.2 meters from its equilibrium position. The ODE is inte-
grated from t = 0 to t = 5.0 seconds using a step size of 0.1 seconds. The
RK4Spring class source code is shown next.
Example Problem: Damped Spring Motion 283
5568ch20.qxd_jd 3/24/03 9:09 AM Page 283
import TechJava.MathLib.*;
public class RK4Spring
{
public static void main(String args[]) {
// Create a SpringODE object
double mass = 1.0;
double mu = 1.5;
double k = 20.0;
SpringODE ode = new SpringODE(k, mu, mass);
// load initial conditions. The spring is
// initially stretched 0.2 meters from its
// equilibrium position.
double V[] = {-0.2};
ode.setInitialConditions(V);
// Solve the ODE over the desired range using
// a constant step size.
double dx = 0.1;
double range = 5.0;
int numSteps = ODESolver.rungeKutta4(ode, range, dx);
// Print out the results
System.out.println("i t dxdt x");
for(int i=0; i<numSteps; ++i) {
System.out.println(
""+i+" " + ode.getOneX(i) +
" " + ode.getOneY(i,0) +" " + ode.getOneY(i,1));
}
}
}
Output
Rather than list a long table of output values, a plot was created that dis-
plays the spring position as a function of time as computed by the
RK4Spring.java program. Also shown on the plot is the exact solution of
the ODE. You can see from Figure 20.2 that the rungeKutta4() method
did an excellent job of reproducing the exact solution of the spring equation.
The spring oscillates around the position x = 0 with asymptotically diminishing
maximum and minimum values.
284 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 284
Embedded Runge-Kutta Solvers
One problem with the fourth-order Runge-Kutta method we developed in the
previous section is that it uses a constant independent variable increment, x,
over the entire integration range. In certain situations, regions of high gradients
may require smaller step sizes while regions of lesser gradients can maintain
solution accuracy with larger step sizes. Using the high-gradient step size over
the entire integration range results in an inefficient algorithm. Using a low-
gradient step size over the entire range may cause the solution to be inaccurate
in certain regions of the domain.
A solution to this problem is to use what is known as an embedded Runge-
Kutta technique with adaptive step size control. There are certain types of Runge-
Kutta schemes that have embedded within them a lower-order Runge-Kutta
scheme. These are called embedded Runge-Kutta algorithms. Why is this signif-
icant? Because the difference in solution between the higher- and lower-order
schemes can provide an estimate of the truncation error of the solution.
Embedded Runge-Kutta Solvers 285
-0.2
-0.1
0.0
0.1
0.2
x
,
m
5 4 3 2 1 0
t, seconds
RK4 Solver
Exact solution
FIGURE 20.2 Spring position as a function of time
5568ch20.qxd_jd 3/24/03 9:09 AM Page 285
The ability to estimate truncation error is what makes adaptive step size
control possible. You can automatically adjust the local step size either up or
down so the computed truncation error is within a certain range. There is no
longer a need to estimate an appropriate step size for the entire domain. The
step size can be quite large in smoothly varying regions of the ODE solution
and can be reduced to smaller values in regions of strong gradients.
The embedded Runge-Kutta solver we will use implements the fifth-
order Runge-Kutta algorithm in Eq. (20.17).
(20.17)
In Eq. (20.17), the a, b, c, and d values are constant coefficients. Using
different coefficients, you can also write a fourth-order Runge-Kutta solver,
shown in Eq. (20.18), using the same computed y values.
(20.18)
The truncation error at any step in the integration process can be esti-
mated by Eq. (20.19).
(20.19)
The a, b, c, and d coefficients we will use were derived by Cash and
Karp
1
and are shown in Table 20.1.
The only thing that remains to be done in the development of our embed-
ded Runge-Kutta solver is to come up with a scheme to compute the value of
x for a given step that will keep the truncation error below a specified maxi-
mum value. There are several ways to specify the maximum allowable error.
You can use a constant value or evaluate the maximum allowable error as a
function of the derivatives of the dependent variables. Were going to keep
things simple in this example. Since the Runge-Kutta scheme is fifth-order ac-
curate, the error should scale with x
5
. The optimum step size can be deter-
mined from Eq. (20.20).
E = y
n+1
- y
n+1
*
=
a
6
i =1
1c
i
- d
i
2y
i
y
n+1
*
= y
n
+ d
1
y
1
+ d
2
y
2
+ d
3
y
3
+ d
4
y
4
+ d
5
y
5
+ d
6
y
6
y
n+1
= y
n
+ c
1
y
1
+ c
2
y
2
+ c
3
y
3
+ c
4
y
4
+ c
5
y
5
+ c
6
y
6
y
6
= xf1x
n
+ a
6
x,y
n
+ b
61
y
1
+ b
62
y
2
+ b
63
y
3
+ b
64
y
4
+ b
65
y
5
2
y
5
= xf1x
n
+ a
5
x,y
n
+ b
51
y
1
+ b
52
y
2
+ b
53
y
3
+ b
54
y
4
2
y
4
= xf1x
n
+ a
4
x,y
n
+ b
41
y
1
+ b
42
y
2
+ b
43
y
3
2
y
3
= xf1x
n
+ a
3
x,y
n
+ b
31
y
1
+ b
32
y
2
2
y
2
= xf1x
n
+ a
2
x,y
n
+ b
21
y
1
2
y
1
= xf1x
n
,y
n
2
286 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 286
(20.20)
The E
???
parameter is the maximum error or tolerance that you are willing
to accept in your calculation. For a situation with more than one dependent vari-
able, E
current
would be the maximum current error among the dependent variables.
We are now ready to write a method that implements an embedded
Runge-Kutta solver. The method is called embeddedRK5() and is placed in-
side the ODESolver class previously described in this chapter. In many re-
spects it is similar to the rungeKutta4() method. The embeddedRK5()
method takes four arguments, an ODE object and three variables of type dou-
ble defining the range, initial x, and error tolerance for the computation.
One principal difference between the embeddedRK5() and
rungeKutta4() methods is that the embeddedRK5() method will esti-
mate the maximum truncation error at each step in the integration. If the maxi-
mum error is greater than the tolerance, then x is decreased and the current
step is integrated again. If the maximum error is less than the tolerance, the de-
pendent variables are updated and x is increased to its optimum value.
The embeddedRK5() method source code follows.
public static int embeddedRK5(ODE ode, double range,
double dx, double tolerance) {
double maxError;
int i,j,k,m;
// Define some convenience variables to make
// the code more readable.
x
optimum
x
current
= B
E
max
E
current
R
0.2
Embedded Runge-Kutta Solvers 287
i a
i
b
i1
b
i2
b
i3
b
i4
b
i5
c
i
d
i
1 0 0 0 0 0 0
2 0 0 0 0 0 0
3 0 0 0
4 0 0
5 1 0 0
6
1
4
512
1771
253
4096
44275
110592
575
13824
175
512
1631
55296
7
8
277
14336
35
27
-
70
27
5
2
-
11
54
13525
55296
125
594
6
5
-
9
10
3
10
3
5
18575
48384
250
621
9
40
3
40
3
10
1
5
1
5
2825
27648
37
378
Table 20.1 Cash-Karp Coefficients
5568ch20.qxd_jd 3/24/03 9:09 AM Page 287
int numEqns = ode.getNumEqns();
double x[] = ode.getX();
double y[][] = ode.getY();
// Create some local arrays
double dy[][] = new double[6][numEqns];
double dyTotal[] = new double[numEqns];
double ytmp[] = new double[numEqns];
double error[][] =
new double[ODE.MAX_STEPS][numEqns];
// load the Cash-Karp parameters
double a[] = {0.0, 0.2, 0.3, 0.6, 1.0, 0.875};
double c[] = {37.0/378.0, 0.0, 250.0/621.0,
125.0/594.0, 0.0, 512.0/1771.0};
double d[] = new double[6];
d[0] = c[0] - 2825.0/27648.0;
d[1] = 0.0;
d[2] = c[2] - 18575.0/48384.0;
d[3] = c[3] - 13525.0/55296.0;
d[4] = c[4] - 277.0/14336.0;
d[5] = c[5] - 0.25;
double b[][] = { {0.0, 0.0, 0.0, 0.0, 0.0},
{0.2, 0.0, 0.0, 0.0, 0.0},
{0.075, 0.225, 0.0, 0.0, 0.0},
{0.3, -0.9, 1.2, 0.0, 0.0},
{-11.0/54.0, 2.5, -70.0/27.0, 35.0/27.0, 0.0},
{1631.0/55296.0, 175.0/512.0, 575.0/13824.0,
44275.0/110592.0, 253.0/4096.0} };
// Integrate the ODE over the desired range.
// Stop if you are going to overflow the matrices
i=1;
while( x[i-1] < range && i < ODE.MAX_STEPS-1) {
// Set up an iteration loop to optimize dx
while (true) {
// First Runge-Kutta step
ode.getFunction(x[i-1],dy[0],y[i-1]);
for(j=0; j<numEqns; ++j) {
dy[0][j] *= dx;
dyTotal[j] = c[0]*dy[0][j];
error[i][j] = d[0]*dy[0][j];
}
// Runge-Kutta steps 2-6
288 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 288
for(k=1; k<6; ++k) {
for(j=0; j<numEqns; ++j) {
ytmp[j] = y[i-1][j];
for(m=0; m<k; ++m) {
ytmp[j] += b[k][m]*dy[m][j];
}
}
ode.getFunction(x[i-1]+a[k]*dx,dy[k],ytmp);
for(j=0; j<numEqns; ++j) {
dy[k][j] *= dx;
dyTotal[j] += c[k]*dy[k][j];
error[i][j] += d[k]*dy[k][j];
}
}
// Compute maximum error
maxError = 0.0;
for(j=0; j<numEqns; ++j) {
maxError =
Math.max(maxError, Math.abs(error[i][j]));
}
// If the maximum error is greater than the
// tolerance, decrease delta-x and try again.
// Otherwise, update the variables and move on to
// the next point.
if ( maxError > tolerance ) {
dx *= Math.pow(tolerance/maxError,0.2);
} else {
break;
}
}
// Update the dependent variables
for(j=0; j<numEqns; ++j) {
y[i][j] = y[i-1][j] + dyTotal[j];
}
// Increment independent variable, reset dx, and
// move on to the next point. Make sure you don't
// go past the specified range.
x[i] = x[i-1] + dx;
dx *= Math.pow(tolerance/maxError,0.2);
if ( x[i]+dx > range ) {
dx = range - x[i];
}
// Go to the next dependent variable location
Embedded Runge-Kutta Solvers 289
5568ch20.qxd_jd 3/24/03 9:09 AM Page 289
++i;
} // end of outer while loop
// Return the number of steps computed
return i;
}
Lets use the embeddedRK5() method to solve the same spring prob-
lem that was solved by the rungeKutta4() method in the earlier example.
The EmbedSpring.java program is a driver program that creates a
SpringODE object with the same initial values as the one from the
rungeKutta4() method example. The ODE is solved by calling the em-
beddedRK5() method. The truncation error tolerance is set to be 1.0e-6.
import TechJava.MathLib.*;
public class EmbedSpring
{
public static void main(String args[]) {
// Create a SpringODE object
double mass = 1.0;
double mu = 1.5;
double k = 20.0;
SpringODE ode = new SpringODE(k, mu, mass);
// load initial conditions. The spring is
// initially stretched 0.1 meters from its
// equilibrium position.
double V[] = {-0.2};
ode.setInitialConditions(V);
// Solve the ODE over the desired range with the
// specified tolerance and initial step size
double dx = 0.1;
double range = 5.0;
double tolerance = 1.0e-6;
int numSteps =
ODESolver.embeddedRK5(ode, range, dx, tolerance);
// Print out the results
System.out.println("i t dxdt x");
for(int i=0; i<numSteps; ++i) {
System.out.println(
""+i+" "+ode.getOneX(i)+
290 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 290
Other ODE Solution Techniques
There are other techniques for solving initial value ODEs including Richardson
extrapolation and predictor-corrector methods. We wont go into any more de-
tail on these methods in this chapter, nor will we implement them. If you want
to implement another ODE solver, the process will be the same as was used in
Other ODE Solution Techniques 291
-0.2
-0.1
0.0
0.1
0.2
x
,
m
5 4 3 2 1 0
t, seconds
Embedded RK
Exact solution
FIGURE 20.3 Spring position as a function of time
" "+ode.getOneY(i,0)+ " "+ode.getOneY(i,1));
}
}
}
The output of the EmbedSpring program is shown in Figure 20.3. At
first glance it seems quite similar to the results generated by the
rungeKutta4() method. The output from the embeddedRK5() method
tracks the exact solution very closely. If you look at the distribution of points
you will notice that the embedded Runge-Kutta algorithm placed more points
in regions of high gradients (around the maximum and minimum amplitudes)
and fewer points in the smoother regions of the curve.
5568ch20.qxd_jd 3/24/03 9:09 AM Page 291
this chapter. You would define your solver as a public, static method.
The method would take as input arguments an ODE object and whatever addi-
tional input arguments were required. You would then write the body of the
method to perform whatever solution technique you were implementing.
Two-Point Boundary Problems
Initial value problems are relatively simple to solve. All of the dependent vari-
ables are assigned values at the start of the range of integration. The solution
then marches out from the starting point to whatever independent variable
value is desired. The only decision is which integration algorithm to use.
With two-point boundary problems, boundary conditions are specified at
both ends of the integration range. Some of the variables at each end of the inte-
gration range will be unspecified by boundary conditions. These are called free
variables. Not only do you have to integrate the ODEs, but you also must assign
values to the free variables at the beginning of the integration range such that the
boundary conditions at the end of the range of integration are satisfied. Unless
you make a very good initial guess, it is likely that the solution will have to be it-
erated. In addition to selecting an integration algorithm you must also develop an
iteration scheme that will efficiently converge to the proper solution.
As you probably guessed, there are several techniques to solve two-point
boundary problems. The method that we will implement in this chapter is
called shooting.
Shooting Methods
Consider a two-point boundary problem that is to be solved from an initial inde-
pendent variable value x x
0
to a far-field location x x
e
. Not all of the depen-
dent variables will be known at x x
0
and some boundary conditions will need to
be maintained at x x
e
. One way to solve this type of problem is by using a tech-
nique known as shooting. An initial guess is made for the free variables at x x
0
.
The ODE is then integrated out to x x
e
. If the far-field boundary conditions are
not met, the x x
0
free variables are updated. The iteration continues until the
x x
e
boundary conditions are met to within a specified tolerance.
The trick with shooting methods is to develop a rapidly converging algo-
rithm to obtain updates to the x x
0
free variables. The method we will use is
called multi-dimensional, globally convergent Newton-Raphson. Consider two
292 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 292
arrays, V[]that contains the values of the free variables at the x x
0
boundary
and E[]that contains the difference between the computed value of the bound-
ary condition variables at the x x
e
boundary and their true boundary condi-
tion values. The size of the V[] and E[] arrays must be the same. To find an
array d V[] that will zero the elements of the E[] array requires the solution
of the system of equations shown in Eq. (20.21).
(20.21)
The updated values of the free variables at the x x
0
boundary can then
be obtained, Eq. (20.22).
(20.22)
Unfortunately, there is no general analytic expression for the dE/dV ma-
trix. The matrix elements can be estimated in a finite-difference manner, as in
Eq. (20.23).
(20.23)
The process to calculate the E/V matrix takes a number of steps. With
an initial guess for the free variables at the x x
0
boundary, integrate the ODE
to get an initial value for the error vector at x x
e
. You then increment the first
free variable at x x
0
by a small amount and reintegrate the ODE to see how
the E[] array values change. Subtracting the updated E[] array from the orig-
inal gives you the first column of the E/V matrix. You then continue the
process by incrementing the other free variables at x x
0
and reintegrating the
ODE until the entire E/V matrix is filled. You then invert the matrix and ob-
tain the updates to the V[] array.
It may take more than one update to the V[] array until the far-field
boundary conditions are satisfied. This sounds like a lot of work and it is when
compared to the solution process for initial value problems. However, with a
reasonably good initial guess for the free variables at x x
0
, the solution
should converge within three to four iterations.
One final conceptual note about shooting is that sometimes if the initial
guess for V[] is not very good the computed updates dV may overshoot physi-
cally allowable values. Depending on the ODE, you might get things like a
square root of a negative number when you evaluate the ODE right-hand side.
One way to enhance the stability of the solution process is to scale the dV up-
dates by a number between 0 and 1. This procedure is called under-relaxation.
dE
dV
=
E
V
V
n+1
= V
n
+ dV
dE
dV
dV = -E
Shooting Methods 293
5568ch20.qxd_jd 3/24/03 9:09 AM Page 293
We will write an ODEshooter() method that will implement the ODE
shooting technique we have just described. This method, like the others that
preceded it in this chapter, will be placed inside the ODESolver class and
will be a public, static method. The ODEshooter() method makes use
of the matrix inversion method EqnSolver.invertMatrix() from
Chapter 19 so an appropriate import declaration is placed at the top of the
ODESolver class code listing.
After initializing the dependent variables at x x
0
, the ODEshooter()
method solves the ODE by calling the embeddedRK5() method. A first cal-
culation of the error at the x x
e
boundary is performed by having the ODE ob-
ject call its getError() method. Any ODE subclass that represents a
two-point boundary problem will override the getError() method from the
ODE class to compute error in the proper manner for that ODE.
The ODEshooter() method then enters a while() loop that updates
the free variables at x x
0
until the error in the far-field boundary conditions is
below a specified tolerance. The V[] array updates are under-relaxed by a fac-
tor of 0.5. When convergence is achieved, the method exits and returns the
number of dependent variable steps used to integrate the ODE. The
ODEshooter() method source code is shown next.
public static int ODEshooter(ODE ode, double V[],
double range, double dx, double tolerance) {
// Define some convenience variables to make
// the code more readable.
int numEqns = ode.getNumEqns();
int numVar = ode.getNumFreeVariables();
double x[] = ode.getX();
double y[][] = ode.getY();
// define some local variables. The E[] array
// holds the error at the end of the range of
// integration.
double E[] = new double[numVar];
double dxInit = dx;
double maxE, dVtotal;
double deltaV = 0.0001;
double underRelax = 0.5;
int i, j, numSteps;
double dV[][] = new double[numVar][numVar];
double dEdV[][] = new double[numVar][numVar];
double Etmp[] = new double[numVar];
294 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 294
// load initial conditions
ode.setInitialConditions(V);
// Solve the ODE over the desired range and compute
// the initial error at the end of the range
numSteps =
ODESolver.embeddedRK5(ode, range, dx, tolerance);
ode.getError(E, y[numSteps-1]);
// If the E[] array doesn't meet the desired
// tolerance try again with new initial conditions.
maxE = 0.0;
for(i=0; i<numVar; ++i) {
maxE = Math.max(maxE,Math.abs(E[i]));
}
while (maxE > tolerance) {
// Fill the dV array. Each row of the array
// is the original V array with one of its
// elements perturbed.
for(i=0; i<numVar; ++i) {
for(j=0; j<numVar; ++j) {
dV[i][j] = V[j];
}
dV[i][i] += deltaV;
}
// Fill the dEdV matrix by determining how the E[]
// elements change when one of the V[] elements is
// incremented.
for(j=0; j<numVar; ++j) {
// Set initial conditions for a given row
ode.setInitialConditions(dV[j]);
dx = dxInit;
// Solve ODE again with V+dVj
numSteps =
ODESolver.embeddedRK5(ode, range, dx, tolerance);
// Recompute error for V+dVj.
ode.getError(Etmp, y[numSteps-1]);
// Compute dEdV
Shooting Methods 295
5568ch20.qxd_jd 3/24/03 9:09 AM Page 295
for(i=0; i<numVar; ++i ) {
dEdV[i][j] = ( Etmp[i] - E[i] )/deltaV;
}
}
// Invert dEdV matrix
EqnSolver.invertMatrix(dEdV);
// Update V[] matrix. The updates to V[] are
// under-relaxed to enhance stability
for(i=0; i<numVar; ++i) {
dVtotal = 0.0;
for(j=0; j<numVar; ++j) {
dVtotal += -dEdV[i][j]*E[j];
}
V[i] += underRelax*dVtotal;
}
// update initial conditions of y[][] using V[]
ode.setInitialConditions(V);
// Integrate ODE with new initial conditions
numSteps =
ODESolver.embeddedRK5(ode, range, dx, tolerance);
// Compute new E[] array and determine maximum
// error
ode.getError(E, y[numSteps-1]);
maxE = 0.0;
for(i=0; i<numVar; ++i) {
maxE = Math.max(maxE,Math.abs(E[i]));
}
} // end of while loop
return numSteps;
}
Example Problem: Compressible
Boundary Layer
For an example of a two-point boundary problem we will look at the equations
that characterize a steady gas flow over a flat plate. Every gas is subject to vis-
cous effects, the ability of one molecule of the gas to transfer momentum or en-
296 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 296
ergy to another molecule. The magnitude of the momentum or energy transfer
is a function of the gradients of velocity or temperature in the flow. There is
also a mass transfer mechanism called diffusion, but we wont concern our-
selves with that here.
Consider a uniform flow of air over a flat plate. The flow velocity can
have a component normal to the plate that we will call v and a component par-
allel to the plate that we will call u. The freestream conditions, the conditions
far away from the plate, are that the flow has a constant u velocity and no v ve-
locity. At the surface of the plate, except under very low-density conditions,
both velocity components will be zero.
This sets up a velocity gradient and viscous effects come into play. The
plate surface slows down the air molecules close to it. Molecules traveling at
the freestream velocity try to speed up any slower molecules they encounter.
The result is a velocity profile called the momentum boundary layer shown in
Figure 20.4. Boundary layers have a finite thickness. At some distance above
the flat plate the velocity will return to freestream conditions. The transition
Exmaple Problem: Compressible Boundary Layer 297
y
1.0 0.8 0.6 0.4 0.2 0.0
u/u
e
Flat plate
Boundary layer edge
FIGURE 20.4 Boundary layer velocity profile
5568ch20.qxd_jd 3/24/03 9:09 AM Page 297
line between freestream and boundary layer conditions is known as the bound-
ary layer edge.
There can be a thermal boundary layer as well. When the molecules slow
down close to the flat plate, they lose kinetic energy. If the flat plate is insu-
lated, adiabatic conditions exist and the lost kinetic energy is recovered in the
form of increased temperature at the wall. If the flat plate is conducting, the ki-
netic energy will be transferred into the flat plate material. In either case, a
temperature profile known as a thermal boundary layer is created.
The equations used to describe boundary layer flow start with the Navier-
Stokes equations that represent the conservation of mass, momentum, and en-
ergy within a gas mixture. If the flow is steady and laminar, and if the
boundary layer thickness is assumed to be small compared with the length
scale of the flat plate, the original Navier-Stokes equations can be simplified
into the following partial differential equations
(20.24)
The r term in Eq. (20.24) is the density of the gas and p is the pressure.
The m and k terms are the coefficient of viscosity and thermal conductivity.
The h term is the enthalpy and T is the temperature.
You could solve the system of equations given by Eq. (20.24) if you like,
but you would most likely need to use a finite-difference or finite-element
technique. This can be a difficult and computationally intensive process. Fortu-
nately, the compressible boundary layer equations can be converted to a system
of two ODEs with a single independent variable. For a flat plate, the indepen-
dent variable is defined by Eq. (20.25).
(20.25)
The e subscript in Eq. (20.25) denotes conditions at the boundary layer
edge, equal to the freestream conditions for a flat plate. Two other variables,
h =
A
u
e
2r
e
m
e
x
L
y2
y1
rdy
ru
0h
0x
+ rv
0h
0y
= u
0p
0x
+
0
0y
ak
0T
0y
b + ma
0u
0y
b
2
0p
0y
= 0
ru
0u
0x
+ rv
0u
0y
+
0p
0x
=
0
0y
am
0u
0y
b
0ru
0x
+
0rv
0y
= 0
298 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 298
Eq. (20.26) and Eq. (20.28) are introduced. The first is a nondimensional
stream function.
(20.26)
It turns out that the derivative of f with respect to h is equal to the ratio of
the local streamwise velocity to the boundary layer edge velocity.
(20.27)
The second variable used in the transformation is the enthalpy ratio.
(20.28)
Without going through all the details of the derivation, using Eq. (20.25),
(20.26), and (20.28), the conservation of mass, momentum, and energy expres-
sions shown in Eq. (20.24) can be transformed into two ordinary differential
equations.
(20.29)
The Pr parameter in Eq. (20.29) is the Prandtl number, and C is given by
the expression in Eq. (20.30).
(20.30)
The boundary conditions for flat plate boundary layer flow with an adia-
batic surface are listed in Eq. (20.31). The physical condition corresponding to
each boundary condition is shown in parentheses.
At (flat plate surface)
( )
( )
(20.31)
(adiabatic wall)
dg
dh
= 0
u = 0
df
dh
= 0
v = 0 f = 0
h = 0
C =
rm
r
e
m
e
d
dh
a
C
Pr
dg
dh
b + f
dg
dh
+ C
u
2
e
h
e
a
d
2
f
dh
2
b
2
= 0
d
dh
aC
d
2
f
dh
2
b + f
d
2
f
dh
2
= 0
g =
h
h
e
df
dh
=
u
u
e
f =
22r
e
u
e
m
e
x
Exmaple Problem: Compressible Boundary Layer 299
5568ch20.qxd_jd 3/24/03 9:09 AM Page 299
At (boundary layer edge)
( )
( )
( )
(20.32)
( )
To solve the compressible flat-plate boundary layer equations, the two
ODEs shown in Eq. (20.29) are expressed as a system of five first-order differ-
ential equations.
(20.33)
You can see from Eq. (20.33) that the first-order form of the compress-
ible boundary layer equations has five dependent variables. From Eq. (20.31),
the h 0 boundary specifies three boundary conditions. This problem is there-
fore a two-point boundary problem with two free variables, one that we will
solve using the shooting technique. Once we have computed the profiles
of df /dh and g, we can determine the velocity and enthalpy profiles from
Eq. (20.27) and Eq. (20.28).
The CompressODE Class
The first step in the solution process is to write a class that represents the com-
pressible boundary layer ODEs. We will name the class CompressODE. In
addition to the members it inherits from the ODE class, the CompressODE
d
dh
1g2 =
dg
dh
d
dh
a
C
Pr
dg
dh
b = -f
dg
dh
- C
u
2
e
h
e
a
d
2
f
dh
2
b
2
d
dh
1f2 =
df
dh
d
dh
a
df
dh
b =
d
2
f
dh
2
d
dh
aC
d
2
f
dh
2
b = -f
d
2
f
dh
2
dh
dy
= 0
dg
dh
= 0
h = h
e
g = 1
du
dy
= 0
d
2
f
dh
2
= 0
u = u
e
df
dh
= 1
h = h
e
300 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/27/03 11:38 AM Page 300
class declares a number of other fields specific to the compressible boundary
layer equations. These include the boundary layer edge enthalpy, velocity, and
temperature, the Mach number of the freestream flow, the Prandtl number, and
the C ratio defined in Eq. (20.30).
The CompressODE class defines one constructor. The constructor first
calls the ODE class constructor passing it the numbers 5 (number of first-order
ODEs) and 2 (number of free variables). The constructor then initializes the
fields declared in the CompressODE class. Strictly speaking, the Prandtl
number is a function of temperature. At low to moderate temperatures it is
more or less a constant value. We use the constant value 0.75 in the Com-
pressODE constructor. This was the same value used by Van Driest,
2
whose
results we will compare against.
The CompressODE class then overrides the three sub methods declared
in the ODE class. The getFunction() method is overridden to return the
right-hand sides of Eq. (20.33). The setInitialConditions() method
enforces the boundary conditions shown in Eq. (20.31). Before we override the
getError() method we must decide how we will implement it. There are
two free variables at the h 0 boundary. There are four specified boundary
conditions at the far-field boundary. Since the V[] and E[] arrays must be the
same size, we must choose two of the far-field boundary conditions with which
to compute the errors. The getError() method is written to compute the
error vector based on the df /dh 1 and g 1 boundary conditions.
The CompressODE class source code is shown here.
package TechJava.MathLib;
import TechJava.Gas.*;
public class CompressODE extends ODE
{
double he, ue, Te, mach, Pr, C;
double ratio1, ratio2;
// The CompressODE constructor calls the ODE
// constructor passing it some compressible
// boundary layer specific values. There are five
// first order ODEs and two free variables.
public CompressODE(double Te, double mach) {
super(5,2);
this.Te = Te;
this.mach = mach;
he = 3.5*AbstractGas.R*Te/0.02885;
ue = mach*Math.sqrt(1.4*AbstractGas.R*Te/0.02885);
Exmaple Problem: Compressible Boundary Layer 301
5568ch20.qxd_jd 3/24/03 9:09 AM Page 301
ratio1 = ue*ue/he;
ratio2 = 110.4/Te;
Pr = 0.75;
}
// The getFunction() method returns the right-hand
// sides of the five first-order compressible
// boundary layer ODEs
// y[0] = delta(C*f'') = delta(n)*(-f*f'')
// y[1] = delta(f') = delta(n)*(f'')
// y[2] = delta(f) = delta(n)*(f')
// y[3] = delta(Cg') = delta(n)*(-Pr*f*g'
// Pr*C*(ue*ue/he)*f''*f'')
// y[4] = delta(g) = delta(n)*(g')
public void getFunction(double x, double dy[],
double ytmp[]) {
C = Math.sqrt(ytmp[4])*(1.0+ratio2)/
(ytmp[4]+ratio2);
dy[0] = -ytmp[2]*ytmp[0]/C;
dy[1] = ytmp[0]/C;
dy[2] = ytmp[1];
dy[3] = -Pr*(ytmp[2]*ytmp[3]/C +
ratio1*ytmp[0]*ytmp[0]/C);
dy[4] = ytmp[3]/C;
}
// The getE() method returns the error, E[], in
// the free variables at the end of the range
// that was integrated.
public void getError(double E[], double endY[]) {
E[0] = endY[1] - 1.0;
E[1] = endY[4] - 1.0;
}
// This method initializes the dependent variables
// at the start of the integration range. The V[]
// contains the current guess of the free variable
// values
public void setInitialConditions(double V[]) {
setOneY(0, 0, V[0]);
setOneY(0, 1, 0.0);
setOneY(0, 2, 0.0);
setOneY(0, 3, 0.0);
setOneY(0, 4, V[1]);
setOneX(0, 0.0);
}
}
302 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 302
Solving the Compressible
Boundary Layer Equations
Solving the compressible boundary layer equations for a given set of condi-
tions is quite simple now. All we need to do is to create a CompressODE ob-
ject and send the object to the ODEshooter() method. In this sample
problem we are going to compute a Mach 8 boundary layer with a boundary
layer edge temperature of 218.6 K. The dx parameter holds the initial incre-
ment value for the dependent variable (h in this case). The V[] array contains
the initial guesses for C
2
f /h
2
and g at h 0.
The class that does all this is named ShootingCompress. Its source
code is
import TechJava.MathLib.*;
public class ShootingCompress
{
public static void main(String args[]) {
// Create a CompressODE object
CompressODE ode = new CompressODE(218.6, 8.0);
// Solve the ODE over the desired range with the
// specified tolerance and initial step size.
// The V[] array holds the initial conditions of
// the free variables.
double dx = 0.1;
double range = 5.0;
double tolerance = 1.0e-6;
double V[] = {0.0826, 25.8};
// Solve the ODE over the desired range
int numSteps =
ODESolver.ODEshooter(ode, V, range, dx, tolerance);
// Print out the results
System.out.println("i eta Cf'' f' f");
for(int i=0; i<numSteps; ++i) {
System.out.println(
""+i+" "+ode.getOneX(i)+" "+ode.getOneY(i,0)+
" "+ode.getOneY(i,1)+" "+ode.getOneY(i,2));
}
System.out.println();
System.out.println("i eta Cg' g");
for(int i=0; i<numSteps; ++i) {
System.out.println(
Exmaple Problem: Compressible Boundary Layer 303
5568ch20.qxd_jd 3/24/03 9:09 AM Page 303
As with the damped spring problem, it is more meaningful to show the
compressible boundary layer results in plot form rather than as columns of
data. Figure 20.5 shows the velocity profile computed by the shooting method
as well as data from Van Driest
2
obtained by an analytical method known as
Croccos method. The two methods produce very similar velocity profile re-
sults. The independent variable, h, in Figure 20.5 has been converted into the
nondimensional length y/x , where Re
x
is the Reynolds number per unit
length.
The nondimensional temperature profile results are shown in Figure 20.6.
There are slight discrepancies between the shooting and Van Driest data but
overall the two methods produce very similar results. You can see by Figure
20.6 the effects of using an adiabatic wall boundary condition. Since energy
cannot be conducted into the flat plate surface, the loss of kinetic energy near
2Re
x
304 Chapter 20 Solving Differential Equations
25
20
15
10
5
0
y
/
x
*
s
q
r
t
(
R
e
x
)
1.0 0.8 0.6 0.4 0.2 0.0
u/u
e
Shooting RK
Van Driest
FIGURE 20.5 Velocity profile, Mach 8 boundary layer
""+i+" "+ode.getOneX(i)+
" "+ode.getOneY(i,3)+" "+ode.getOneY(i,4));
}
}
}
5568ch20.qxd_jd 3/24/03 9:09 AM Page 304
the flat plate surface shows up as increased thermal energy. The temperature of
the gas at the surface is 12 times that at the boundary layer edge.
Other Two-Point Boundary
Solution Techniques
As with initial value problems, there are techniques other than shooting for
solving two-point boundary problems. The most commonly used alternative
technique is called relaxation. Relaxation methods divide the integration range
into a 1-D grid of points. The ODE is represented by finite-difference equa-
tions that are solved at each point over the integration domain. The solution is
iterated on until the required boundary conditions are met.
We wont implement a relaxation method in this chapter, but if you
wanted to you probably know how to do it by now. You would define a pub-
lic, static method that would take an ODE object as one of its input argu-
Other Two-Point Boundary Solution Techniques 305
25
20
15
10
5
0
y
/
x
*
s
q
r
t
(
R
e
x
)
12 10 8 6 4 2 0
T/T
e
Shooting RK
Van Driest
FIGURE 20.6 Temperature profile, Mach 8 boundary layer
5568ch20.qxd_jd 3/24/03 9:09 AM Page 305
ments. The body of the method would then implement whatever relaxation
technique was desired.
Partial Differential Equations
So far in this chapter we have been exploring ways to solve ordinary differen-
tial equations. These are equations with one independent variable. A PDE is
one that has more than one independent variable. An example of a PDE is the
conservation of mass equation used in fluid dynamics. shown in Eq. (20.34).
(20.34)
As you probably guessed, solving the typical PDE is more complicated
and more treacherous than solving an ODE. Another complicating factor is that
many physical models involve coupled sets of PDEs. The most commonly used
ways to solve PDEs are by using finite-difference or finite-element techniques.
The computational domain is subdivided into smaller domains called cells. The
cells will characterize a 1-, 2-, or 3-D space. The collection of cells that model
the computational domain is called a grid. The PDEs are then discretized and
solved at each cell. A cumulative record of the solution error is computed. The
solution is iterated on until the error falls below a certain convergence criteria.
A detailed discussion of methods to solve partial differential equations is
beyond the scope and intent of this book. There are entire books devoted to the
subject of solving PDEs. The Java language is well suited to developing meth-
ods to solve PDEs. Classes would be defined to represent the PDEs, the com-
putational grid, and each cell within the computational grid. The PDE solvers
themselves could be written as public static methods and stored in a
package that could be readily accessed by other programs.
References
1. Shampine, L. F., and M. K., Gordon, Computer Solution of Ordinary Differ-
ential Equations: The Initial Value Problem, W.H. Freeman Press, San
Francisco, 1975.
2. Van Driest, E. R., Investigation of Laminar Boundary Layer in Compress-
ible Fluids Using the Crocco Method, NACA TN-2597, Jan. 1952.
0r
0t
+
0ru
0x
+
0rv
0y
= 0
306 Chapter 20 Solving Differential Equations
5568ch20.qxd_jd 3/24/03 9:09 AM Page 306