MATHEMATICA IN ACTION for 7-4
no In and Out numbering, please

Check Your Answers . . . 
   But How?

Numerical instabilities can take a variety of forms. How can one recognize cases where such instabilities cause the numerical solution to a differential equation to be invalid?

by Rob Knapp and Stan Wagon

One of the joys of using Mathematica is the ability it gives us to check the results of complicated computations in several different ways. For symbolic work such as derivatives, antiderivatives, or infinite sums, this is pretty easy: just make some numerical approximation to the problem and see if the results agree. But the situation regarding work that is numerical in the first place is very different.

NDSolve, the built-in command for finding approximate solutions to differential equations, has AccuracyGoal and PrecisionGoal options that allow you to control the error. The difficulty arises because the all of the methods NDSolve is based on are iterative, taking many steps to arive at a solution. The error control goals only apply to what is referred to as the local truncation error at each individual step. For most differential equations, this is not a problem since the overall, or global, error accumulated over the steps is typically proportional to the number of steps and the local truncation error. However, there are differential equations with sensitivities that cause the global error to be much greater than this.

When S.W. taught differential equations a couple of years ago, he tried hard to discover appropriate techniques to tell whether the solution to a complicated system, such as that governing the motion of a double pendulum or a forced pendulum, was reliable. Techniques for verifying a numerical solution are, as far as we can tell, not mentioned in textbooks. Of course, the techniques presented here are not foolproof. But they are very simple to use and we should surely be teaching them to our students. If readers know of examples that shed new light on these sort of difficulties, we would be pleased to learn about them.

Method :  Don’t Forget Symbolics

There are many reasons why numerical algorithms are better than symbolic ones. But don’t write off symbolic methods entirely, since they can provide an alternative approach. We begin with an example due to Courtney Coleman (Harvey Mudd College).

soln=x[t]/.First[NDSolve[
				{x'[t]==2 x[t] + Cos[t],x[0] == -2/5},x[t],{t, 0, 7.5}]]

\(\(
\(Plot[soln, 	{t, 0, 7.5}]; \)\)\)

The plotted solution looks plausible, but in fact it is highly inaccurate. The sections that follow will present techniques for discovering that such a solution is wrong. However, it is worth noting that this example is easily handled by DSolve. That gives us the symbolic solution (sin t - 2 cos t)/5, and that can be checked by differentiation, so we know it is correct.

trueSoln=x[t]/.First[DSolve[
				{x'[t]==2 x[t] + Cos[t],x[0] == -2/5},x[t],t]]

Plot[trueSoln, {t, 0, 7.5}];

We can easily plot a family of solutions corresponding to initial conditions that differ from (^2)\(/\_5\) by multiples of (10^(-5)). As can be seen in Figure , the difficulty with this equation comes from the tremendous divergence that occurs for solutions with initial values near (^2)\(/\_5\).

soln=x[t]/.First[DSolve[{x'[t]==2 x[t] + Cos[t],x[0] == -2/5 + i (10^(-5))},x[t],t]];Plot[Evaluate[Table[soln, {i, -10, 10}]], {t, 0, 15}, 
        PlotRange -> {-2, 10}, Frame -> True, 
        Axes -> False];

Method :  Look Critically at the Results

Here is an innocent-looking but ultimately nasty equation found by John Polking (Rice University): (x^″)(t)=(((x^′)(t))^2) - x(t). It will be more convenient to look at this as a system: (x^′) = y, (y^′) = (y^2) - x. Small initial values cause no problem (try them), but when x(0) gets to about 9 something interesting happens. Here we solve the equation and plot the pair (x, (x^′)) in the phase plane.

(Subscript[x, 0]) = 9;tmax = 13;
Polking={x'[t]==y[t],y'[t]==y[t]^2 - x[t]};
soln = {x[t], y[t]} /. 
          First[NDSolve[{Polking, x[0] == (Subscript[x, 0]), y[0] == 0}, {x[t], y[t]}, {t, 
                0, tmax}]];

trajectory = 
      ParametricPlot[Evaluate[soln], {t, 0, tmax}, Frame -> True, 
        AxesOrigin -> {0, 0}, Axes -> True, 
        Epilog -> {AbsolutePointSize[4], Point[{(Subscript[x, 0]), 0}]}, 
        FrameTicks -> {Range[0, 9, 3], Range[-2, 2], None, None}];

We can use this example to illustrate two ways of checking a numerical result. First, if we just look at more of the trajectory we will discover something interesting. The code that follows colors the previously computed trajectory gray.

\(soln1 = {x[t], y[t]} /. 
        First[NDSolve[{Polking, x[0] == x\_0, y[0] == 0}, {x[t], y[t]}, {t, 
              \(-tmax\), tmax}]]; 


    Show[Block[{$DisplayFunction = Identity}, 

        		{ParametricPlot[Evaluate[soln1], {t, 0, tmax}, 
            Frame -> True, AxesOrigin -> {0, 0}, Axes -> True, 
            PlotStyle -> {AbsoluteThickness[4], GrayLevel[0.6]}, 
            Epilog -> {AbsolutePointSize[4], Point[{x\_0, 0}]}, 
            FrameTicks 
              -> {Range[0, 9, 3], Range[\(-2\), 2], None, None}], 
          ParametricPlot[Evaluate[soln1], {t, \(-tmax\), 0}]}]]; \)

It is evident from Figure  that the trajectory crosses itself, an impossibility for an autonomous system. We could examine more orbits to get a sense of what is going on (they circulate around the equilibrium point at (0, 0),

It turns out that some algebraic cleverness due to Polking allows us to see the true trajectories. We omit the details (see [Schwalbe and Wagon 1997, §11.2]), but it is not hard to show that the solution with initial value ((Subscript[x, 0]), 0) must lie on the curve defined by (2(Subscript[x, 0])+1) (e^(2 (x  -  (Subscript[x, 0]))))-2 x-1+2 (y^2)=0. Thus we can get an image of the orbits by combining several contour plots as follows. There is one parabolic solution ((y^2) -(^1)\(/\_2\)=x) that serves as a separatrix, as it separates closed orbits from unbounded ones.

\(PolkingOrbit[x0_] := 
      Block[{$DisplayFunction = Identity}, 
		
        ContourPlot[
          \((2\ x0 + 1)\)\ E\^\(2\ \((x - x0)\)\) - 2  x - 1 + 2  y\^2, {x, 
            \(-3\), 20}, {y, \(-5\), 5}, 
          ContourStyle -> AbsoluteThickness[0.6], \ 
          Contours -> {0}, ContourShading -> False, 
          PlotPoints -> 120]]
\)Show[
      PolkingOrbit/@{-3, -2, -1, -0.7, -0.6, -0.53, 
          -0.52, -0.51, -0.5 - (10^(-3)), -0.5 - (10^(-5)), 
          -0.5 - (10^(-7)), -0.5 - (10^(-9)), 
          -0.5 - (10^(-13)), -0.5, 0.3, 0.5, 1, 2, 3, 4, 6, 8, 10, 
          12, 14, 16, 18, 20},  
      Epilog  ->  {AbsolutePointSize[5],  Point[{14, 0}]}];

Method :  Work Backwards

In some cases, a reasonable approach is to time-reverse the equation and use the final value of a numerical solution as the initial value of the auxiliary system. By "time-reverse" we mean let u(t) = x(-t) (similarly for more variables) and solve as t goes from -(Subscript[t, 1]) to (Subscript[t, 0]). However, for such a technique to be useful it is important that the equation be invariant under time-reversal. Consider, for example, (x^′)(t) = -x(t). Nearby solutions converge exponentially towards each other (and towards zero), so nearly all numerical methods do quite well on this. But time-reversal yields (u^′)(t) = u(t), for which nearby solutions diverge from each other exponentially. So, if you started with a final value that had only a small, insignificant roundoff error, working backwards would magnify that error to the visual scale; thus the technique would have indicated a problem where none existed.

But many equations do have symmetries that can be exploited by reversing time, and the Polking equation is one of them. Introduce u(t), v(t) to start at the place where the first solution ends and be the time-reversed versions of x(t) and y(t). So u(t) = x(-t) and v(t) = y(-t). Then \(TraditionalForm
      \`\(u\^′\)(t) = 
        \(\(-\(\(x\^′\)(\(-t\))\)\) = 
          \(\(-\(y(\(-t\))\)\) = \(-\(v(t)\)\)\)\)\) and \(TraditionalForm
      \`\(v\^′\)(t) = 
        \(\(-\(\(y\^′\)(\(-t\))\)\) = 
          \(\(-\((\(y(\(-t\))\)\^2 - x(\(-t\)))\)\) = 
            u(t) - \(v(t)\)\^2\)\)\). Even though this system is not identical to the Polking equation, a simple substitution gets us one that is: let w(t) be -v (t). Then the system becomes (u^′)(t) = w(t) and (w^′)(t) = ((w(t))^2) - u(t), which is identical to the original x-y system! We now solve this system starting from the last point of soln. Before plotting we flip the second coordinate so that the plot looks like a time-reversed trajectory.

backSol={u[t], w[t]}/.First[NDSolve[
	{u'[t]==w[t],w'[t]==w[t]^2 - u[t],
		u[-tmax] == 
                        soln[[1]] /. 
                      t -> tmax,
		w[-tmax] == 
                        -soln[[2
                            ]] /. t -> tmax},{u[t], w[t]},{t, -tmax, 0}]];

\(\(Show[
      Block[{$DisplayFunction = Identity}, 
        \ {ParametricPlot[Evaluate[backSol {1, \(-1\)}], {t, \(-tmax\), 0}, 

            		PlotStyle -> {GrayLevel[0.7], AbsoluteThickness[4]}], 
          trajectory
		}], Frame -> True, 
      PlotRange -> {{\(-1\), 11}, {\(-4\), 4}}]; \)\)

The reversed solution agrees with the original solution for a while, but then falls away, and does not come anywhere near (9, 0). This is not absolute proof that the original solution is bad, but it should raise a red flag.

Method :  Perturb the Initial Conditions

Although this is the last method we present, it is perhaps the easiest to use and most general. Numerical solvers proceed step by step, but in an adaptive way, attempting to make each step satisfy a certain error tolerance. Mathematica’s NDSolve tries for 6-digit precision at each step. This aspect of the error is called local truncation error (as opposed to roundoff error or propagation error). Now, since the result of the first step is accurate to only 6 digits, this inaccuracy might propagate unacceptably through the perhaps thousands of subsequent steps. An easy way to check for this is to perturb the initial conditions by 10–6.

We illustrate with the Duffing equation. The equation, which models a certain type of forced, damped pendulum, is given below. Note how perfect rationals are used, because we will want to use high-precision later and so must avoid machine-precision reals.

Duffing=x''[t]+15/100 x'[t]-x[t]+x[t]^3==3/10 Cos[t];
lowPrecSoln=NDSolve[{Duffing,x[0] == -1,x'[0]==1},x,{t, 0, 100},MaxSteps -> 2000][[1]];
perturbed=NDSolve[{Duffing,x[0] == -1 + (10^(-6)),x'[0]==1},x,{t, 0, 100},MaxSteps -> 2000][[1]];
x[100] /. {lowPrecSoln, perturbed}

The serious disagreement indicates that a 10–6 difference can lead to a radically different solution. Of course — and this is easy to forget — we are not comparing the perturbed solution to a true solution. We are just observing that small changes of the sort that will occur in the numerical algorithm cause major changes in the computed result. Therefore we can place no confidence in either solution. A view of the graphs (Figure ) provides some evidence that the solution is all right up to t = 70.

Plot[Evaluate[x[t] /. {lowPrecSoln, perturbed}], {t, 0, 100}, 
      PlotStyle 
        -> {{AbsoluteThickness[4], GrayLevel[0.6]}, {
            AbsoluteThickness[1.5]}}, Frame -> True];

Just like the other verification techniques we present, the perturbation method is by no means a certain way of catching problems. For example, this method fails to indicate any problem for the Polking equation example shown in sections  and  because the rapid convergence of nearby initial conditions to the left of the equilibrium point in Figure  eradicates the perturbation.

A perturbation of 10–10 leads to x(100)  ≈ -0.42, and so has only a small effect on the computed result. This is because the perturbation is negligible compared to the local truncation error, so it is effectively lost. For these tests to be meaningful, the perturbation must be of the same order of magnitude or larger than the local truncation error.

To see more of the correct solutions we can bring Mathematica’s variable precision capabilities to bear. We can change the local error of the numerical algorithm by increasing the accuracy goal to, say, 10. (In Mathematica, “precision” refers to relative error and “accuracy” refers to absolute error; we will use the latter here since these Duffing solutions have modest scales.) When we increase the accuracy goal to 10, then a 10-10 perturbation leads to the results 0.23 and –0  .52 at t = 100. Thus, as before, we are still getting inaccurate solutions. But the increased accuracy goal (which causes the numerical algorithm to work harder) has improved things in that the perturbed and unperturbed solutions now agree out to about t = 80 (Figure ).

lowPrecSoln=NDSolve[{Duffing,x[0] == -1,x'[0]==1},x,{t, 0, 110},MaxSteps -> 2000,AccuracyGoal -> 10][[1]];

perturbed10=NDSolve[{Duffing,x[0] == -1 + (10^(-10)),x'[0]==1},x,{t, 0, 110},MaxSteps -> 2000,AccuracyGoal -> 10][[1]];
x[100] /. {lowPrecSoln, perturbed10};

\(\(Plot[Evaluate[x[t] /. {lowPrecSoln, perturbed10}], {t, 0, 100}, 

      PlotStyle 
        -> {{AbsoluteThickness[4], GrayLevel[0.8]}, 

          			{AbsoluteThickness[1]}}, 
Frame -> True, 
      FrameTicks -> {Automatic, Automatic, None, None}]; \)\)

With the more accurate solution, it is clear that the divergence of solutions with initial conditions of order 10–6 is not simply an artifact of the numerical method or the choice of size of the local truncation error. On the contrary, the divergence is a property of the differential equation. However, it is true that the smaller the perturbation (or truncation error for that matter), the longer the time interval for which solutions match before visibly diverging.

One must not yield to the temptation to simply increase the accuracy goal to, say, 20, in the hope that this would yield good results past t = 100. That would be very misleading because the inherent imprecision of hardware arithmetic (typically 16 digits) makes such an accuracy goal unattainable. Increasing the WorkingPrecision option is required in that case.

To see that the perturbation technique does indeed discriminate, consider a different initial condition: (0.6, 1.3). Then, going back to the default accuracy goal, a 10–6 perturbation leads to results that are very close: –0.0147109 and –0.0147116. And a plot of the solutions will show that they are essentially identical and asymptotically periodic.

x[100] /. First[NDSolve[
			{Duffing,x[0] == #[[1]],x'[0]==#[[2]]},
		x,{t, 0, 100},MaxSteps -> ∞]]&/@ {{0.6, 1.3}, {0.6 + (10^(-6)), 1.3}}

Of course, there are other responses to an equation as sensitive as the Duffing equation. It might be that the initial condition is only an approximation, so that it is meaningless to talk about starting at exactly (-1, 1). But the idea of starting exactly there makes sense, and Mathematica can help us see what the actual solution looks like. Several differential equations textbooks (see [Knapp and Wagon 1996]) compute trajectories of the Duffing equation for precise initial values with no indication of the numerical instability that exists.

For many sensitive equations there is a theory called shadowing that states that while a trajectory may not correspond to the initial conditions provided, it is the exact solution for some nearby initial condition. However, such theorems are proved only in some cases, and it is still not proved that shadowing applies to the Duffing equation. It can be important to distinguish between an inaccurate approximate solution that may be used to draw some information about the orbits and an accurate solution that can be used in a numerical proof of properties of the system. In fact, due to this very distinction Guckenheimer and Holmes say [Guckenheimer and Holmes 1993, p. 91], “There is a substantial theoretical question as to whether this ‘strange attractor’ of the Duffing equation is an artifact of the noise and is absent in the ideal deterministic system.”

Method :  A Conservative Approach

Sometimes you may be able to get additional information about the system without actually having a solution. Many systems have quantities that mathematically must stay constant over the entire course of the solutions. A whole class of systems that have at least one conserved quantity are Hamiltonian systems. Conserved quantities can be very useful in checking the quality of a solution since one can simply check to see how close such a quantity stays to its original value.

For example, this is effectively what we did with the Polking system at the end of section . As it was pointed out, the quantity

c[x0_][x_, y_] := 
      (2 x0 + 1) (E^(2 (x - x0))) - 2 x - 1 + 2 (y^2)

should always be zero for a solution starting at the intial condition x(0) = (Subscript[x, 0]), y(0) = 0. Let’s look at how c evolves over the solution we computed.

Plot[c[(Subscript[x, 0])]@@soln, {t, 0, tmax}, Frame -> True, 
      Axes -> None];

It looks like this starts to deviate at about t = 9. It is easier to see when we use logarithms.

Plot[Log[10, Abs[c[(Subscript[x, 0])]@@soln]], {t, 0, tmax}];

Now you can see that even though this grows for a while at the beginning of the solution, it does not grow steadily until after about t = 9, which, not surprisingly, is near the point at which the curve goes wrong on the phase plane plot.

Now you can see that even though this grows for a while at the beginning of the solution, it does not grow steadily until after about t = 9, indicating the real problem starts there. The error is not clearly visible in the phase plane plot of Figure  until t = 12, when the error in this conserved quantity is order one and the growth has had a very large effect.

For an example of a Hamiltonian system we use the simple pendulum equation (θ^′) = v,  
      (v^′) = -g sin θ, for which the conserved quantity is the energy, ((v^2)/2) - g cos θ. One way of checking the energy is to look at it after the solution is computed (as in the preceding example) or you can watch the energy as the solution progresses. The latter involves some tricks, so we illustrate that. The function h just returns its second argument, which will be v, but h is designed to act on numbers only, thus avoiding some preprocessing that NDSolve would do if h were symbolic. Then it is a simple matter to add a side condition to h so that it prints out the energy. We use a counter so that the printing occurs only at every 1500th step.

\(g = 9.8; \ 
energy = v\^2/2\  - 9.8\ Cos[θ]; 
i = 0; 

      h[a_?NumericQ, b_, c_] := 
        \((\(i++\); \ 
		
          If[Mod[i, 1500] == 0, 
            Print[StringForm[
					"`` steps; t = ``, energy = ``", 
                
					i, NumberForm[a, 3], 
                energy /. {v -> b, θ -> c}\ ]]]; 
		b)
          \)\)
NDSolve[{θ'[t]==h[t, v[t], θ[t]],v'[t]==-g Sin[θ[t]],v[0] == 0,θ[0] == 0.123},{θ[t], v[t]},{t, 0, 250},MaxSteps -> ∞];

This shows that the energy is pretty constant, though because the time interval is so large some deterioration, but only a little, is evident.

An interesting example to show where the failure to properly conserve a quantity indicates some real problems with the solution is a problem in partial differential equations: the nonlinear Schrödinger equation, which is uses to model a variety of things from fiber optics to water waves.

NLSE[u_[x_, t_]] := 
      I D[u[x, t], t] == D[u[x, t], x, x] + 2 Abs[u[x, t]]^2 u[x, t]

This equation is completely integrable and has an infinite number of conserved quantities (including a Hamiltonian). For this example, we will track the conservation of the analogue to the mass of a wave packet, M  =  
          (∫_(-
                ∞)^∞)|u[x, t]
                      (|^2) dx. We set parameters as follows.

\(f\_n_[x_] := a\ \((1 + ε\ Cos[μ\_n\ x])\); 

    ε = 1/10; 
L = 4\ \@2\ π; 
a = 0.35; 

    μ\_n_ := 2\ π\ n/L; 
T = 100; \)

We first approximate the solution with NDSolve.

u = 
      ϕ  /. 
        First[NDSolve[{NLSE[ϕ[x, t]], ϕ[x, 0] == (Subscript[f, 1])[x], 
              ϕ[0, t] == ϕ[L, t]}, ϕ, {x, 0, L}, {t, 0, T}, 
            MaxSteps -> 20000]]

Plot3D[Abs[u[x, t]], {x, 0, L}, {t, 0, T},  PlotPoints -> 70];

Since the conserved quantity is itself an integral, we need to approximate the integration to get an approximate value to the quantity. One way is to use NIntegrate.

M[u_][t_]  :=  NIntegrate[Abs[u[x, t]]^2, {x, 0, L}]

but this has the drawback of being quite slow

Timing[M[u][0]]

An alternative method is to use a cruder approximation (essentially the Riemann sum computed based on the spatial grid points), which is still good enough to track the value for verification purposes. (The third part of an InterpolatingFunction object contains the points it is defined at, in each dimension, so the first of the third part gives the points in the first (spatial in this case) dimension.)

\(Clear[M]; 
    \(M[u_]\)[t_] := 
      Module[{grid = 
            Drop[u[[3, 1]], \(-1\)], h}, 
        
		h = 
          grid[[2]] - 
            grid[[1]]; 
		
        Plus@@\(Abs[u[grid, t]]\^2\)\ h]\)

Not surprisingly, this is much faster, and allows us to generate a plot or a table without taking hours.

Table[M[u][t], {t, 0, T,  T/20}]

These values are changing too rapidly, and so indicate that the solution is most likely not correct.

It is worth noting that this computation is done very near an instability in the nonlinear Schrödinger equation, so it is very difficult, if not impossible, to get the solution correctly over a long time interval, even with a specialized method. Still, the result of NDSolve can be improved by specifying a small enough spatial grid step size, and properly balancing the error criteria between spatial and temporal error.

\(\(u\_1 = 
      First[ϕ /. 
          NDSolve[

            				{NLSE[ϕ[x, t]], ϕ[x, 0] == f\_1[x], 
              ϕ[0, t] == ϕ[L, t]}, ϕ, {x, 0, L}, {t, 0, T}, 

            				StartingStepSize -> {L/32, Automatic}, 
            MaxSteps -> 20000, AccuracyGoal -> {4, 6}, 
            PrecisionGoal -> {4, 6}]]; \)\)

Now the plot of the solution is much more regular.

Plot3D[Abs[(Subscript[u, 1])[x, t]], {x, 0, L}, {t, 0, T}, PlotPoints -> 70];

And the mass analogue is much better conserved, though not perfectly.

Table[M[(Subscript[u, 1])][t], {t, 0, T,  T/20}]

Be cautioned that while a poorly conserved quantity is an indicator of a poor solution, a well conserved quantity does not necessarily mean that you have a good solution. In fact, there is a whole class of methods, known as symplectic methods, that are designed to conserve Hamiltonians to within roundoff error, but their solutions are by no means that accurate.

Method : The Residual

Mathematica’s framework for solving differential equations numerically gives us the results as interpolating functions. This is an incredibly useful feature because such functions can be differentiated, integrated, plotted, and so on. So it is an easy matter to substitute the solution back into the equation to see how well it solves the equation. The residual refers to the result that we expect to be zero, that is, the difference between the left and right sides of the differential equation when a suspected solution is substituted. Here is a simple example.

soln=NDSolve[{x'[t]==x[t],x[0] == 1},x,{t, 0, 1}]

residual[t_] := Evaluate[x'[t]-x[t]/.First[soln]]

rplot  =  Plot[residual[t], {t, 0, 1}];

This example is quite typical in that the residual appears to be larger at the beginning (with more frequent oscillations). The reason for this is that NDSolve begins by using small steps with a lower-order method, and tries to work up to larger steps with higher-order methods. Because the order of the method is lower, the interpolation error between the actual points where the derivative was evaluated tends to be greater. Since the interpolation error between the points actually hit by the method is in some sense spurious, you may want to check only the points hit by the method. You can check what these points are by looking at the first part of the third part of the returned InterpolatingFunction object.

grid = 
      (x /. First[soln])[[3, 1]]

Here is a plot of the residual at only those points.

rplot1 = 
      ListPlot[Transpose[{grid, residual/@grid}], 
        PlotStyle -> AbsolutePointSize[4]];

And when you show this with the other plot, you see that most of the error visible in the residual is interpolation error.

Show[rplot, rplot1, Ticks -> {{ .5, 1}, {-1, 1} 0.00005}];

It might seem comforting that the residual is small but, because NDSolve tries to keep the residual down locally, one expects a small residual with just about any equation. The reader should compute the residuals for all the examples in this paper; there are differences, but basically all the residuals are small.
 
However, residuals can be useful in a variety of ways. First, they serve as a check that a move to high precision is really causing the algorithm to do the right thing. The residual should decrease in proportion to the amount of precision used. Second, it is worth noting that a small residual does mean that the computed solution is an exact solution to a nearby equation. More precisely, suppose r(t) is the residual for an approximate solution X(t) to the equation (x^′)(t) = h[x(t), t]. Then it is trivial to verify that X is an exact solution to the modified equation (x^′)(t) = h[x(t), t] + r(t). If | r(t) | ((( < 10))^(-10)), say, and we are in a situation where some of the terms in the equation may not be known to 10 significant digits, then X might be a perfectly valid solution from a physical standpoint. And third, the residual can detect singularities that the numerical method might have missed. Here’s an example where NDSolve misses a variation in the function because it occurs between time steps.

First, define an approximation to a Dirac delta function

ApproximateDiracDelta[t_, Δ_] := 
      Δ (E^(-π (((t Δ))^2)))

Then try to solve the harmonic oscillator equation with an approximate impulse at t = 0.

sol=NDSolve[{(x')'[t]+x[t]==ApproximateDiracDelta[t, 10],x[-π] == 0,x'[-π]==0},x,{t, -π, π}]

Plot[Evaluate[x[t] /. sol], {t, -π, π}, 
      Frame -> True, Axes -> None];

The plot, constantly equal to 0, indicates that the approximate impulse had no effect. Is this correct, or is it a numerical artifact? The residual plot in Figure  shows that we are likely to have a problem here!

Plot[Evaluate[(x')'[t]-x[t]-ApproximateDiracDelta[t, 10]/.sol],{t, -π, π},PlotRange -> All];

When you know there is an approximate singularity or a discontinuity in your problem that NDSolve could miss, you can tell NDSolve to be sure to evaluate the function there by including that with the range of values for the independent variable.

sol1  =  
      NDSolve[{x''[t]  +  x[t]  ==  ApproximateDiracDelta[t, 10], 
          x[-π]  ==  0,  x'[-π]  == 0}, 
        x, {t, -π, π, 0}];

Plot[Evaluate[x[t] /. sol1], {t, -π, π}, 
      Frame -> True, Axes -> None];

This technique yields an approximate solution that is identical to the true solution (which you can find symbolically using DSolve).

Method : Old Faithful: Increase the Precision

A traditional approach to numerical computations of any sort is to move from single precision to double precision and compare the results. If the high-precision results differ from the low-precision results, the chances are excellent that the latter is wrong. But Mathematica allows us to increase the precision in steps hopefully seeing convergence to the true solution. Of course, one cannot just change the PrecisionGoal (or AccuracyGoal) without paying attention to the working precision. For NDSolve, a setting of WorkingPrecision to wp, greater than machine precision, causes the precision goal to be set to wp - 10 (unless it is specifcally set otherwise). One should always allow the working precision several more digits than the precision goal to account for rounding errors. 

We illustrate by revisiting Coleman’s example from section .

tmax = 10;
solnCC[pg_]:=x[t]/.First[NDSolve[{x'[t]==2 x[t] + Cos[t],x[0] == -2/5},x[t],{t, 0, 3 π; tmax},PrecisionGoal -> pg, WorkingPrecision ->  pg + 10, MaxSteps -> 5000]];

\(\(Plot[Evaluate[Table[solnCC[pg], {pg, 6, 16, 10}]], {t, 0, tmax}, 
      PlotRange -> {\(-1\), 10}, Frame -> True, \ 

      PlotStyle\  -> \ {{}, \ AbsoluteThickness[2], \ AbsoluteThickness[3]}]; 
    \)\)

The fact that the machine-precision solution diverges from the others at t = 5 indicates that something is surely going wrong there. In fact, this example illustrates the common phenomenon of divergence of solutions, which occurs in the Polking equation as well.

One drawback of resorting to higher precision is that computations of solutions to high precision can be quite time-consuming. Not only is the arithmetic more expensive, but the tightened local truncation error criterion results in more steps.

As a further warning, we can’t resist presenting the following example, due to S. M. Rump [Rump 1988] and presented by WRI’s Mark Sofroniou in a recent lecture. Let f(x, y) be the innocuous function that follows.

f[x_, y_] := 
      ((1335 (y^6))/4) + (x^2) (11 (x^2) (y^2) - (y^6) - 121 (y^4) - 2) + 
        ((11 (y^8))/2) + (x/(2 y));

The next computation gives us several results, all of which are wrong. Note that the high-precision results agree with each other; in many situations that is taken as evidence of accuracy, but for this example, the agreement means little.

Table[f[N[77617,  wp], N[33096, wp]], {wp, 12, 34, 4}]

Getting the correct answer requires nothing more than machine precision, provided it is used at just the right time: the actual f-value is simply a modest rational number.

{ans = f[77617, 33096],  N[ans]}

Conclusion

Numerically sensitive problems will always pose difficulties, despite increased speed and power of hardware and software. Serious applications demand that any approximate solution should be considered guilty until proven innocent! But Mathematica gives us diverse tools for proving beyond a reasonable doubt that a numerical solution is valid.

REFERENCES

GUCKENHEIMER, J.,  and P. HOLMES. 1993. Nonlinear Oscillations, Dynamical Systems, and Bifurcations of Vector Fields, Springer, New York.

KNAPP, R., and S. WAGON, 1996. Orbits worth betting on!, C•ODE•E Newsletter (Consortium for Ordinary Differential Equations Experiments), Winter, 8–13.

RUMP, S. M. 1988. Algorithm for verified inclusions — theory and practice, in Reliability in Computing, R. E. Moore ed., Academic Press, San Diego, pp. 109–126.

SCHWALBE, D.,  and S. WAGON, 1997. VisualDSolve: Visualizing Differential Equations with Mathematica, Springer/TELOS, New York.

ABOUT THE AUTHORS

Rob Knapp works on the development of numerical technology in Mathematica.

Rob Knapp
Wolfram Research, Inc.
100 Trade Center Drive
Champaign, IL 61820
rknapp@wolfram.com

Stan Wagon uses Mathematica extensively in his research, teaching, and exposition, and is especially appreciative of the new ways Mathematica allows us to look at mathematical objects. He is author or coauthor of several books, including Mathematica in Action, Animating Calculus, VisualDSolve, and Which Way Did the Bicycle Go?. He also teaches a high-altitude course, Rocky Mountain Mathematica, every summer in the mountains of Colorado.

Stan Wagon
Department of Mathematics and Computer Science
Macalester College
St. Paul, MN 55105
wagon@macalester.edu