In [1]:
using Plots,LaTeXStrings
default(markersize=3,linewidth=1.5)
using LinearAlgebra,DifferentialEquations
using SparseArrays
include("FNC.jl");

Example 11.1.2

Here are the parameters for a particular simulation of the Black--Scholes equation.


In [2]:
Smax = 8;  T = 6;
K = 3;  sigma = 0.06;  r = 0.08;

We discretize space and time.


In [3]:
m = 200;  h = Smax / m;
x = h*(0:m)
n = 1000;  tau = T / n;
t = tau*(0:n)
lambda = tau / h^2;  mu = tau / h;

Set the initial condition, then march forward in time.


In [4]:
V = zeros(m+1,n+1)
V[:,1] = @. max( 0, x-K )
for j = 1:n
    # Fictitious value from Neumann condition.
    Vfict = 2*h + V[m,j]
    Vj = [ V[:,j]; Vfict ]
    # First row is zero by the Dirichlet condition.
    for i = 2:m+1 
        diff1 = (Vj[i+1] - Vj[i-1])
        diff2 = (Vj[i+1] - 2*Vj[i] + Vj[i-1])
        V[i,j+1] = Vj[i] +
            (lambda*sigma^2*x[i]^2/2)*diff2 +
            (r*x[i]*mu)/2*diff1 - r*tau*Vj[i]
    end   
end

We plot at a few times.


In [5]:
select_times = @. 1 + 250*(0:4)
show_times = t[select_times]

plot(x,V[:,select_times],label=["t=$t" for t in show_times], 
    title="Black-Scholes solution",leg=:topleft,  
    xaxis=("stock price"),yaxis=("option value") )


Out[5]:
0 2 4 6 8 0 1 2 3 4 5 6 Black-Scholes solution stock price option value t=0.0 t=1.5 t=3.0 t=4.5 t=6.0

The lowest curve is the initial condition, and the highest curve is the last time. The results are easy to interpret, recalling that the time variable really means "time before strike." Say you are close to the option's strike time. If the current stock price is, say, $S=2$, then it's not likely that the stock will end up over the strike price $K=3$ and therefore the option has little value. On the other hand, if presently $S=3$, then there are good odds that the option will be exercised at the strike time, and you will need to pay a substantial portion of the stock price in order to take advantage.

Let's try to extend the simulation time to $T=8$, keeping everything else the same.


In [6]:
T = 8;
n = 1000;  tau = T / n;
t = tau*(0:n)
lambda = tau / h^2;  mu = tau / h;

for j = 1:n
    # Fictitious value from Neumann condition.
    Vfict = 2*h + V[m,j]
    Vj = [ V[:,j]; Vfict ]
    # First row is zero by the Dirichlet condition.
    for i = 2:m+1 
        diff1 = (Vj[i+1] - Vj[i-1])
        diff2 = (Vj[i+1] - 2*Vj[i] + Vj[i-1])
        V[i,j+1] = Vj[i] +
            (lambda*sigma^2*x[i]^2/2)*diff2 +
            (r*x[i]*mu)/2*diff1 - r*tau*Vj[i]
    end   
end


plot(x,V[:,select_times],label=["t=$t" for t in show_times], 
    title="Black-Scholes solution",leg=:topleft,  
    xaxis=("stock price"),yaxis=("option value",[0,1e20]) )


Out[6]:
0 2 4 6 8 0 2.5×10 19 5.0×10 19 7.5×10 19 1.0×10 20 Black-Scholes solution stock price option value t=0.0 t=1.5 t=3.0 t=4.5 t=6.0

This "solution" is nonsensical. Look at the scale of the ordinate!

Example 11.2.1

Let's try out the Euler and backward Euler time stepping methods using the second-order semidiscretization:


In [7]:
m = 100
x,Dx,Dxx = FNC.diffper(m,[0,1]);

First we apply the Euler discretization.


In [8]:
tfinal = 0.05;  n = 500;  
tau = tfinal/n;  t = tau*(0:n);
U = zeros(m,n+1);

This is where we set the initial condition. It isn't mathematically periodic, but the end values and derivatives are so small that for numerical purposes it may as well be.


In [9]:
U[:,1] = @. exp( -60*(x-0.5)^2 );

The Euler time stepping simply multiplies by a constant matrix for each time step.


In [10]:
A = I + tau*Dxx;
for j = 1:n
    U[:,j+1] = A*U[:,j]
end

Things seem to start well.


In [11]:
plot(x,U[:,1:3:7],label=["t=$t" for t in t[1:3:7]],
    xaxis=(L"x"),yaxis=(L"u(x,t)"),
    title="Heat equation by forward Euler")


Out[11]:
0.00 0.25 0.50 0.75 1.00 0.00 0.25 0.50 0.75 1.00 Heat equation by forward Euler t=0.0 t=0.0003 t=0.0006

Shortly thereafter, though, there is nonphysical growth.


In [12]:
plot(x,U[:,13:3:19],label=["t=$t" for t in t[13:3:19]],
    xaxis=(L"x"),yaxis=(L"u(x,t)"),
    title="Heat equation by forward Euler")


Out[12]:
0.00 0.25 0.50 0.75 1.00 -4 -2 0 2 4 Heat equation by forward Euler t=0.0012 t=0.0015 t=0.0018

The growth is exponential in time.


In [13]:
M = vec(maximum( abs.(U), dims=1 ))     # max in each column
plot(t,M,xaxis=(L"t"), yaxis=(:log10,L"\max_x |u(x,t)|"),
    title="Nonphysical growth",leg=:none)


Out[13]:
0.00 0.01 0.02 0.03 0.04 0.05 10 0 10 50 10 100 10 150 10 200 Nonphysical growth

Now we try backward Euler. In this case there is a tridiagonal linear system to solve at each time step. We will use a sparse matrix to get sparse LU factorization, although the time savings at this size are negligible.


In [14]:
B = sparse(I - tau*Dxx)
for j = 1:n
    U[:,j+1] = B\U[:,j]
end

plot(x,U[:,1:100:501],label=["t=$t" for t in t[1:100:501]],
    xaxis=(L"x"),yaxis=(L"u(x,t)"),
    title="Heat equation by backward Euler")


Out[14]:
0.00 0.25 0.50 0.75 1.00 0.00 0.25 0.50 0.75 1.00 Heat equation by backward Euler t=0.0 t=0.01 t=0.02 t=0.03 t=0.04 t=0.05

This solution looks physically realistic, as the large concentration in the center diffuses outward. Observe that the solution remains periodic in space.

Example 11.2.2

We set up the semidiscretization and initial condition in $x$ just as before.


In [15]:
m = 100
x,Dx,Dxx = FNC.diffper(m,[0,1])
u0 = @. exp( -60*(x-0.5)^2 );

Now, though, we apply a DifferentialEquations solver to the initial-value problem $\mathbf{u}'=\mathbf{D}_{xx}\mathbf{u}$.


In [16]:
tfinal = 0.05
ODE = (u,p,t) -> Dxx*u;  
IVP = ODEProblem(ODE,u0,(0,tfinal))

u = solve(IVP,DP5())  # select DP5 solver

plot(x,u[:,1:50:end],label=["t=$t" for t in round.(u.t[1:50:end],digits=4)], 
    xaxis=(L"x"), yaxis=(L"u(x,t)"),
    title="Heat equation by ode45")


Out[16]:
0.00 0.25 0.50 0.75 1.00 0.00 0.25 0.50 0.75 1.00 Heat equation by ode45 t=0.0 t=0.0043 t=0.0084 t=0.0126 t=0.0167 t=0.0209 t=0.025 t=0.0291 t=0.0333 t=0.0374 t=0.0415 t=0.0457 t=0.0498

The solution is at least plausible. But the number of time steps that were selected automatically is surprisingly large, considering how smoothly the solution changes.


In [17]:
num_steps_1 = length(u.t)-1


Out[17]:
603

Now we apply another solver.


In [18]:
u = solve(IVP,Rodas4P());

The number of steps selected is reduced by a factor of more than 40!


In [19]:
num_steps_2 = length(u.t)-1


Out[19]:
15

Example 11.3.3

Both time stepping methods solved $\mathbf{u}'=\mathbf{D}_{xx}\mathbf{u}$, for the matrix


In [20]:
m = 40;  Dxx = FNC.diffper(m,[0,1])[3];

The eigenvalues of this martrix are real and negative.


In [21]:
lambda = eigvals(Dxx)
scatter(real(lambda),imag(lambda),
    xaxis=("Re \\lambda"),  yaxis=("Im \\lambda"),
    title="Eigenvalues",leg=:none)


Out[21]:
-6000 -5000 -4000 -3000 -2000 -1000 0 0.00 0.25 0.50 0.75 1.00 Eigenvalues Re λ Im λ

The Euler method is absolutely stable in the region $|\zeta+1| \le 1$ in the complex plane:


In [22]:
phi = 2pi*(0:360)/360
z = @.exp(1im*phi) - 1;   # unit circle shifted to the left by 1

plot(Shape(real(z),imag(z)),color=RGB(.8,.8,1),
    xaxis=("Re \\lambda"), yaxis=("Im \\lambda"), aspect_ratio=1,
    title="Stability region",leg=:none)


Out[22]:
-2.0 -1.5 -1.0 -0.5 0.0 -1.0 -0.5 0.0 0.5 1.0 Stability region Re λ Im λ

In order to get inside this region, we have to find $\tau$ such that $\lambda \tau > -2$ for all eigenvalues $\lambda$. This is an upper bound on $\tau$.


In [23]:
lambda_min = minimum(lambda)
@show max_tau = -2 / lambda_min;


max_tau = -2 / lambda_min = 0.0003124999999999999

Here we plot the resulting values of $\zeta=\lambda \tau$.


In [24]:
zeta = lambda*max_tau
scatter!(real(zeta),imag(zeta),
    title="Stability region and \\zeta  values")


Out[24]:
-2.0 -1.5 -1.0 -0.5 0.0 -1.0 -0.5 0.0 0.5 1.0 Stability region and ζ values Re λ Im λ

In backward Euler, the region is $|\zeta-1|\ge 1$. Because they are all on the negative real axis, all of the $\zeta$ values will fit no matter what $\tau$ is chosen.


In [25]:
plot(Shape([-6,6,6,-6],[-6,-6,6,6]),color=RGB(.8,.8,1))
z = @.exp(1im*phi) + 1;   # unit circle shifted right by 1
plot!(Shape(real(z),imag(z)),color=:white)
scatter!(real(zeta),imag(zeta),
    xaxis=([-4,2],"Re \\lambda"),  yaxis=([-3,3],"Im \\lambda"),aspect_ratio=1,
    title="Stability region and \\zeta values",leg=:none)


Out[25]:
-4 -3 -2 -1 0 1 2 -3 -2 -1 0 1 2 3 Stability region and ζ values Re λ Im λ

Example 11.4.2


In [26]:
f = function (u,p,t)
    q,s,w = p
    [ s*(u[2]-u[1]*u[2]+u[1]-q*u[1]^2);
    (-u[2]-u[1]*u[2]+u[3])/s; 
    w*(u[1]-u[3]) ]
end


Out[26]:
#17 (generic function with 1 method)

One of the stiff solvers, called Rodas4P, is fast.


In [27]:
IVP = ODEProblem(f,[1.,1.,4.],(0.,6.),(8.375e-6,77.27,0.161))
@elapsed u = solve(IVP,Rodas4P())  # select Rodas4P solver


Out[27]:
1.761251899

In [28]:
plot(u)
scatter!(u.t,u[:,:]',m=2,xaxis=(L"t"),yaxis=(L"u(t)"),
    title="Oregonator",leg=:none)


Out[28]:
0 1 2 3 4 5 6 0 2.5×10 4 5.0×10 4 7.5×10 4 1.0×10 5 Oregonator

In [29]:
@show num_steps_stiff = length(u.t);


num_steps_stiff = length(u.t) = 107

You can see that the stiff solver takes small time steps only when some part of the solution is changing rapidly. However, a nonstiff solver, based on an explicit method, is much slower and takes a lot more steps.


In [30]:
@elapsed u = solve(IVP,DP5())  # select DP5 solver


Out[30]:
1.594145059

In [31]:
@show num_steps_nonstiff = length(u.t);


num_steps_nonstiff = length(u.t) = 28512

Here is the Jacobian matrix at any value of $\mathbf{u}$.


In [32]:
q,s,w = (8.375e-6,77.27,0.161)
J = u -> [ -s*(u[2]+1-2*q*u[1]) s*(1-u[1]) 0; 
    -u[2]/s (-1-u[1])/s 1/s; 
    w 0 -w];

During the early phase, the time steps seem fairly large. The eigenvalues around $t=1/2$ are


In [33]:
t = u.t
i1 = findfirst(@. t>0.5) 
lambda1 = eigvals( J(u[:,i1]) )


Out[33]:
3-element Array{Float64,1}:
 -131.13742993372898  
   -3.3910437818666472
   -0.3394987956128886

These are real and negative. Checking the stability region of RK4 along the negative real axis, we see that stability requires a maximum time step


In [34]:
maxstep1 = 2.8 / maximum(abs.(lambda1))


Out[34]:
0.021351646142638264

The actual step size chosen by the solver was comparable:


In [35]:
step1 = t[i1+1] - t[i1]


Out[35]:
0.031366886495779234

Later in the simulation, the steps seem quite small compared to the apparent rate of activity. We look near $t=4$:


In [36]:
i2 = findfirst(@. t>4)   
lambda2 = eigvals( J(u[:,i2]) )


Out[36]:
3-element Array{Float64,1}:
 -18238.851056969022      
     -0.025883880781886193
     -0.16099966593810738 

These are also real and negative. We compare the maximum and observed step sizes again:


In [37]:
@show maxstep2 = 2.8 / maximum(abs.(lambda2));
@show step2 = t[i2+1] - t[i2];


maxstep2 = 2.8 / maximum(abs.(lambda2)) = 0.00015351844210220283
step2 = t[i2 + 1] - t[i2] = 0.00018281251555229971

Again, the eigenvalues give a good indication of how the steps are being chosen, at least to order of magnitude.

Example 11.5.3

We solve $u_t=u_{xx}$ on $[-1,1]$ subject to the Dirichlet conditions $u(-1,t)=0$, $u(1,t)=2$.


In [38]:
m = 100
x,Dx,Dxx = FNC.diffcheb(m,[-1,1]);

Our next step is to write a function that defines $\mathbf{f}$. Since the boundary values are given explicitly, there is no need to "solve" for them---we just append them to each end of the vector.


In [39]:
extend(v) = [0;v;2];

We can also define the inverse operation of chopping off the boundary values from a full vector. Note that the indexing starts at 1 and ends at $m+1$ for the extended vector.


In [40]:
chop(u) = u[2:m];

All the pieces are now in place to define and solve the IVP.


In [41]:
f = function (v,p,t)
    u = extend(v)
    phi = Dxx*u
    return chop(phi)
end
u0 = @. 1 + sin(pi/2*x) + 3*(1-x^2)*exp(-4*x^2);
V = solve(ODEProblem(f,chop(u0),(0.,0.15)));

We extend the solution to the boundaries at each time, then plot.


In [42]:
t = range(0,stop=0.15,length=11) 
U = hcat( (extend(V(t)) for t in t)... )
plot(x,U,label=["t=$t" for t in t],
    xaxis=(L"x"), yaxis=(L"u(x,t)"), title="Heat equation",leg=:topleft)


Out[42]:
-1.0 -0.5 0.0 0.5 1.0 0 1 2 3 4 Heat equation t=0.0 t=0.015 t=0.03 t=0.045 t=0.06 t=0.075 t=0.09 t=0.105 t=0.12 t=0.135 t=0.15

Example 11.5.4

We solve a diffusion equation with source term: $u_t=u^2+u_{xx}$, on $[-1,1]$ subject to homogeneous Dirichlet conditions.


In [43]:
m = 100
x,Dx,Dxx = FNC.diffcheb(m,[-1,1]);

In [44]:
extend(v) = [0;v;0];  # extend to boundary
chop(u) = u[2:m];     # discard boundary
ODE = function (v,p,t) 
    u = extend(v)
    uxx = Dxx*u
    f = @.u^2 + uxx
    return chop(f)
end


Out[44]:
#27 (generic function with 1 method)

All the pieces are now in place to define and solve the IVP.


In [45]:
u0 = @. 6*(1-x^2)*exp(-4*(x-.5)^2)
V = solve(ODEProblem(ODE,chop(u0),(0.,1.5)));

Extend the solution to the boundaries at each time, then plot.


In [46]:
t = range(0,stop=1.5,length=7) 
U = hcat( (extend(V(t)) for t in t)... )
plot(x,U,label=["t=$t" for t in t],
    xaxis=(L"x"), yaxis=(L"u(x,t)"), title="Heat equation with source",leg=:topleft)


Out[46]:
-1.0 -0.5 0.0 0.5 1.0 0 2 4 6 8 Heat equation with source t=0.0 t=0.25 t=0.5 t=0.75 t=1.0 t=1.25 t=1.5

An animation better captures how the source term takes over gradually but at an accelerating rate.


In [47]:
an = @animate for t = range(0,stop=1.5,length=100)
    plot(x,extend(V(t)),label="t=$(round(t,digits=3))",
        xaxis=(L"x"), yaxis=([0,10],L"u(x,t)"), title="Heat equation with source",leg=:topleft)
end
gif(an,"molbratu.gif")


┌ Info: Saved animation to 
│   fn = /Users/driscoll/Dropbox/books/fnc-extras/julia/molbratu.gif
└ @ Plots /Users/driscoll/.julia/packages/Plots/oiirH/src/animation.jl:90
Out[47]:

Example 11.5.5


In [48]:
Smax = 8;  T = 8;
K = 3;  sigma = 0.06;  r = 0.08;
m = 200;  x,Dx,Dxx = FNC.diffmat2(m,[0,Smax]);
h = x[2]-x[1];

Using the boundary conditions and defining the ODE follow next.


In [49]:
extend(v) = [ 0; v; 2/3*(h-0.5*v[m-2]+2*v[m-1]) ];
chop(u) = u[2:m];
ODE = function (v,p,t) 
    sigma,r = p
    u = extend(v)
    ux = Dx*u;  uxx = Dxx*u;
    f = @.sigma^2/2*x^2*uxx + r*x*ux - r*u
    return chop(f)
end;

Now we define the initial conditions and solve the IVP.


In [50]:
v0 = @.max( 0, x-K )
V = solve(ODEProblem(ODE,chop(v0),(0.,T),(sigma,r)));

Extend the solution to the boundaries at each time, then plot.


In [51]:
t = range(0,stop=T,length=7) 
U = hcat( (extend(V(t)) for t in t)... )
plot(x,U,label=["t=$(round(t,digits=2))" for t in t],
    xaxis=(L"x"), yaxis=(L"u(x,t)"), title="Black-Scholes equation",leg=:topleft)


Out[51]:
0 2 4 6 8 0 1 2 3 4 5 6 Black-Scholes equation t=0.0 t=1.33 t=2.67 t=4.0 t=5.33 t=6.67 t=8.0

In [52]:
an = @animate for t = range(0,stop=T,length=100)
    plot(x,extend(V(t)),label="t=$(round(t,digits=3))",
        xaxis=(L"x"), yaxis=([0,10],L"u(x,t)"), title="Black-Scholes equation",leg=:topleft)
end
gif(an,"molbsbc.gif")


┌ Info: Saved animation to 
│   fn = /Users/driscoll/Dropbox/books/fnc-extras/julia/molbsbc.gif
└ @ Plots /Users/driscoll/.julia/packages/Plots/oiirH/src/animation.jl:90
Out[52]: