Functions¶
Euler’s method for an initial-value problem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
""" euler(ivp, n) Apply Euler's method to solve the given IVP using `n` time steps. Returns a vector of times and a vector of solution values. """ function euler(ivp, n) # Time discretization. a, b = ivp.tspan h = (b - a) / n t = [a + i * h for i in 0:n] # Initial condition and output setup. u = fill(float(ivp.u0), n+1) # The time stepping iteration. for i in 1:n u[i+1] = u[i] + h * ivp.f(u[i], ivp.p, t[i]) end return t, u end
About the code
The ivp
input argument is an ODEProblem
, like in Demo 6.1.2. It has fields ivp.f
, ivp.tspan
, ivp.u0
, and ivp.p
that fully define the problem. The outputs are vectors of the nodes and approximate solution values at those nodes.
Improved Euler method for an IVP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
""" ie2(ivp, n) Apply the Improved Euler method to solve the given IVP using `n` time steps. Returns a vector of times and a vector of solution values. """ function ie2(ivp, n) # Time discretization. a, b = ivp.tspan h = (b - a) / n t = [a + i * h for i in 0:n] # Initialize output. u = fill(float(ivp.u0), n+1) # Time stepping. for i in 1:n uhalf = u[i] + h / 2 * ivp.f(u[i], ivp.p, t[i]) u[i+1] = u[i] + h * ivp.f(uhalf, ivp.p, t[i] + h / 2) end return t, u end
Fourth-order Runge-Kutta for an IVP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
""" rk4(ivp, n) Apply the common Runge-Kutta 4th order method to solve the given IVP using `n` time steps. Returns a vector of times and a vector of solution values. """ function rk4(ivp, n) # Time discretization. a, b = ivp.tspan h = (b - a) / n t = [a + i * h for i in 0:n] # Initialize output. u = fill(float(ivp.u0), n+1) # Time stepping. for i in 1:n k₁ = h * ivp.f(u[i], ivp.p, t[i]) k₂ = h * ivp.f(u[i] + k₁ / 2, ivp.p, t[i] + h / 2) k₃ = h * ivp.f(u[i] + k₂ / 2, ivp.p, t[i] + h / 2) k₄ = h * ivp.f(u[i] + k₃, ivp.p, t[i] + h) u[i+1] = u[i] + (k₁ + 2(k₂ + k₃) + k₄) / 6 end return t, u end
Adaptive IVP solver based on embedded RK formulas
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
""" rk23(ivp, tol) Apply an adaptive embedded RK formula pair to solve given IVP with estimated error `tol`. Returns a vector of times and a vector of solution values. """ function rk23(ivp, tol) # Initialize for the first time step. a, b = ivp.tspan t = [a] u = [float(ivp.u0)] i = 1 h = 0.5 * tol^(1 / 3) s₁ = ivp.f(ivp.u0, ivp.p, a) # Time stepping. while t[i] < b # Detect underflow of the step size. if t[i] + h == t[i] @warn "Stepsize too small near t=$(t[i])" break # quit time stepping loop end # New RK stages. s₂ = ivp.f(u[i] + (h / 2) * s₁, ivp.p, t[i] + h / 2) s₃ = ivp.f(u[i] + (3h / 4) * s₂, ivp.p, t[i] + 3h / 4) u_new3 = u[i] + h * (2s₁ + 3s₂ + 4s₃) / 9 # 3rd order solution s₄ = ivp.f(u_new3, ivp.p, t[i] + h) err = h * (-5s₁ / 72 + s₂ / 12 + s₃ / 9 - s₄ / 8) # 2nd/3rd difference E = norm(err, Inf) # error estimate maxerr = tol * (1 + norm(u[i], Inf)) # relative/absolute blend # Accept the proposed step? if E < maxerr # yes push!(t, t[i] + h) push!(u, u_new3) i += 1 s₁ = s₄ # use FSAL property end # Adjust step size. q = 0.8 * (maxerr / E)^(1 / 3) # conservative optimal step factor q = min(q, 4) # limit stepsize growth h = min(q * h, b - t[i]) # don't step past the end end return t, u end
About the code
The check t[i]+h == t[i]
on line 19 is to detect when has become so small that it no longer changes the floating-point value of . This may be a sign that the underlying exact solution has a singularity near , but in any case, the solver must halt by using a break
statement to exit the loop.
On line 30, we use a combination of absolute and relative tolerances to judge the acceptability of a solution value, as in (5.7.6). In lines 41--43 we underestimate the step factor a bit and prevent a huge increase in the step size, since a rejected step is expensive, and then we make sure that our final step doesn’t take us past the end of the domain.
Finally, line 37 exploits a subtle property of the BS23 formula called first same as last (FSAL).
While (6.5.5) calls for four stages to find the paired second- and third-order estimates, the final stage computed in stepping from to is identical to the first stage needed to step from to . By repurposing s₄
as s₁
for the next pass, one of the stage evaluations comes for free, and only three evaluations of are needed per successful step.
4th-order Adams–Bashforth formula for an IVP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
""" ab4(ivp, n) Apply the Adams-Bashforth 4th order method to solve the given IVP using `n` time steps. Returns a vector of times and a vector of solution values. """ function ab4(ivp, n) # Time discretization. a, b = ivp.tspan h = (b - a) / n t = [a + i * h for i in 0:n] # Constants in the AB4 method. k = 4 σ = [55, -59, 37, -9] / 24 # Find starting values by RK4. u = fill(float(ivp.u0), n+1) rkivp = ODEProblem(ivp.f, ivp.u0, (a, a + (k - 1) * h), ivp.p) ts, us = rk4(rkivp, k - 1) u[1:k] .= us # Compute history of u' values, from newest to oldest. f = [ivp.f(u[k-i], ivp.p, t[k-i]) for i in 1:k-1] # Time stepping. for i in k:n f = [ivp.f(u[i], ivp.p, t[i]), f[1:k-1]...] # new value of du/dt u[i+1] = u[i] + h * sum(f[j] * σ[j] for j in 1:k) # advance a step end return t, u end
About the code
Line 15 sets σ
to be the coefficients of the generating polynomial of AB4. Lines 19--21 set up the IVP over the time interval , call rk4
to solve it using the step size , and use the result to fill the first four values of the solution. Then line 24 computes the vector .
Line 28 computes , based on the most recent solution value and time. That goes into the first spot of f
, followed by the three values that were previously most recent. These are the four values that appear in (6.7.1). Each particular value starts at the front of f
, moves through each position in the vector over three iterations, and then is forgotten.
2nd-order Adams–Moulton (trapezoid) formula for an IVP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
""" am2(ivp, n) Apply the Adams-Moulton 2nd order method to solve given IVP using `n` time steps. Returns a vector of times and a vector of solution values. """ function am2(ivp, n) # Time discretization. a, b = ivp.tspan h = (b - a) / n t = [a + i * h for i in 0:n] # Initialize output. u = fill(float(ivp.u0), n+1) # Time stepping. for i in 1:n # Data that does not depend on the new value. known = u[i] + h / 2 * ivp.f(u[i], ivp.p, t[i]) # Find a root for the new value. g = z -> z - h / 2 * ivp.f(z, ivp.p, t[i+1]) - known u_new = levenberg(g, known) u[i+1] = u_new[end] end return t, u end
About the code
Lines 22-23 define the function and call levenberg
to find the new solution value, using an Euler half-step as its starting value. A robust code would have to intercept the case where levenberg
fails to converge, but we have ignored this issue for the sake of brevity.
Examples¶
6.1 Basics of IVPs¶
Example 6.1.2
The OrdinaryDiffEq
package offers solvers for IVPs. Let’s use it to define and solve an initial-value problem for over , such that .
Because many practical problems come with parameters that are fixed within an instance but varied from one instance to another, the syntax for IVPs includes a input argument p
that stays fixed throughout the solution. Here we don’t want to use that argument, but it must be in the definition for the solver to work.
Tip
To create an initial-value problem for , you must supply a function that computes , an initial value for , and the endpoints of the interval for . The interval should be defined as (a,b)
, where at least one of the values is a float.
f(u, p, t) = sin((t + u)^2) # defines du/dt, must include p argument
u₀ = -1.0 # initial value
tspan = (0.0, 4.0) # t interval
(0.0, 4.0)
With the data above we define an IVP problem object and then solve it. Here we tell the solver to use the Tsit5
method, which is a good first choice for most problems.
using OrdinaryDiffEq
ivp = ODEProblem(f, u₀, tspan)
sol = solve(ivp, Tsit5());
The resulting solution object can be shown using plot
.
using Plots
plot(sol;
label="solution", legend=:bottom,
xlabel="t", ylabel=L"u(t)",
title=L"u'=\sin((t+u)^2)")
The solution also acts like any callable function that can be evaluated at different values of .
@show sol(1.0);
sol(1.0) = -0.7903205813665345
Under the hood, the solution object holds some information about how the values and plot are produced:
[sol.t sol.u]
15×2 Matrix{Float64}:
0.0 -1.0
0.0867807 -0.93483
0.241035 -0.856617
0.464665 -0.805668
0.696832 -0.793614
1.00862 -0.789925
1.37461 -0.718601
1.70407 -0.476837
1.93572 -0.29033
2.17184 -0.294994
2.4843 -0.483948
2.69425 -0.654121
3.27049 -1.1783
3.62534 -1.51729
4.0 -1.88086
The solver initially finds approximate values of the solution (second column above) at some automatically chosen times (first column above). To compute the solution at other times, the object performs an interpolation on those values. This chapter is about how the discrete and values are computed. For now, just note how we can extract them from the solution object.
scatter!(sol.t, sol.u, label="discrete values")
Example 6.1.3
The equation gives us some trouble.
f(u, p, t) = (t + u)^2
ivp = ODEProblem(f, 1.0, (0.0, 1.0))
sol = solve(ivp, Tsit5());
┌ Warning: At t=0.7853839417697203, dt was forced below floating point epsilon 1.1102230246251565e-16, and step error estimate = 42.16290621054672. Aborting. There is either an error in your model specification or the true solution is unstable (or the true solution can not be represented in the precision of Float64).
└ @ SciMLBase ~/.julia/packages/SciMLBase/XzPx0/src/integrator_interface.jl:623
The warning message we received can mean that there is a bug in the formulation of the problem. But if everything has been done correctly, it suggests that the solution may not exist past the indicated time. This is a possibility in nonlinear ODEs.
plot(sol, label="";
xlabel=L"t", yaxis=(:log10, L"u(t)"),
title="Finite-time blowup")
Example 6.1.5
Consider the ODEs and . In each case we compute , so the condition number bound from Theorem 6.1.2 is in both problems. However, they behave quite differently. In the case of exponential growth, , the bound is the actual condition number.
But with , solutions actually get closer together with time.
In this case the actual condition number is one, because the initial difference between solutions is the largest over all time. Hence the exponentially growing bound is a gross overestimate.
6.2 Euler’s method¶
Example 6.2.1
We consider the IVP over , with .
using OrdinaryDiffEq
f(u, p, t) = sin((t + u)^2);
tspan = (0.0, 4.0);
u0 = -1.0;
ivp = ODEProblem(f, u0, tspan)
ODEProblem with uType Float64 and tType Float64. In-place: false
timespan: (0.0, 4.0)
u0: -1.0
Here is the call to Function 6.2.2.
using Plots
t, u = FNC.euler(ivp, 20)
plot(t, u;
m=2, label="n=20",
xlabel=L"t", ylabel=L"u(t)",
title="Solution by Euler's method")
We could define a different interpolant to get a smoother picture above, but the derivation of Euler’s method assumed a piecewise linear interpolant. We can instead request more steps to make the interpolant look smoother.
t, u = FNC.euler(ivp, 50)
plot!(t, u, m=2, label="n=50")
Increasing changed the solution noticeably. Since we know that interpolants and finite differences become more accurate as , we should anticipate the same behavior from Euler’s method. We don’t have an exact solution to compare to, so we will use a DifferentialEquations
solver to construct an accurate reference solution.
u_exact = solve(ivp, Tsit5(), reltol=1e-14, abstol=1e-14)
plot!(u_exact, l=(2, :black), label="reference")
Now we can perform a convergence study.
n = [round(Int, 5 * 10^k) for k in 0:0.5:3]
err = []
for n in n
t, u = FNC.euler(ivp, n)
push!(err, norm(u_exact.(t) - u, Inf))
end
@pt :header=["n", "inf-norm error"] [n err]
The error is approximately cut by a factor of 10 for each increase in by the same factor. A log-log plot also confirms first-order convergence. Keep in mind that since , it follows that .
plot(n, err;
m=:o, label="results",
xaxis=(:log10, L"n"), yaxis=(:log10, "inf-norm global error"),
title="Convergence of Euler's method")
# Add line for perfect 1st order.
plot!(n, 0.5 * err[end] * (n / n[end]) .^ (-1), l=:dash, label=L"O(n^{-1})")
6.3 IVP systems¶
Example 6.3.2
We encode the predator–prey equations via a function.
function predprey(u, p, t)
α, β = p # rename parameters for convenience
y, z = u # rename solution components
s = (y * z) / (1 + β * y) # appears in both equations
return [y * (1 - α * y) - s, -z + s]
end;
As before, the ODE function must accept three inputs, u
, p
, and t
, even though in this case there is no explicit dependence on t
. The second input is used to pass parameters that don’t change throughout a single instance of the problem.
To specify the IVP we must also provide the initial condition, which is a 2-vector here, and the interval for the independent variable.
using OrdinaryDiffEq
u₀ = [1, 0.01]
tspan = (0.0, 60.0)
α, β = 0.1, 0.25
ivp = ODEProblem(predprey, u₀, tspan, [α, β])
ODEProblem with uType Vector{Float64} and tType Float64. In-place: false
timespan: (0.0, 60.0)
u0: 2-element Vector{Float64}:
1.0
0.01
You can use any DifferentialEquations
solver on the IVP system.
using Plots
sol = solve(ivp, Tsit5());
plot(sol, label=["prey" "predator"],
title="Predator-prey solution")
We can find the discrete values used to compute the interpolated solution. The sol.u
value is a vector of vectors.
t, u = sol.t, sol.u # extract times and solution values
@show size(u);
@show t[20];
@show u[20];
size(u) = (100,)
t[20] = 8.386698083051241
u[20] = [0.027739191695661334, 0.699418322938034]
We can also use Function 6.2.2 to find the solution.
t, u = FNC.euler(ivp, 1200);
The solution u
is a vector of [prey,predator] 2-vectors for each of the discrete times in t
. Manipulating the vector-of-vectors output can be a little tricky. Here, we convert it to an matrix. Each column is one component, while each row is a single value of .
u = [u[j] for u in u, j in 1:2]
plot!(t[1:3:end], u[1:3:end, :];
l=(1, :black), m=2,
label=["Euler prey" "Euler predator"])
Notice above that the accuracy of the Euler solution deteriorates rapidly.
When there are just two components, it’s common to plot the solution in the phase plane, i.e., with and along the axes and time as a parameterization of the curve.
Tip
You can use idxs
in the plot of a solution produced by solve
to specify the components of the solution that appear on each axis.
plot(sol, idxs=(1, 2),
title="Predator-prey in the phase plane",
xlabel=L"y", ylabel=L"z")
From this plot we can deduce that the solution approaches a periodic one, which in the phase plane is represented by a closed loop.
Example 6.3.5
Let’s implement the coupled pendulums from Example 6.3.4. The pendulums will be pulled in opposite directions and then released together from rest.
Tip
The similar
function creates an array of the same size and type as a given value, without initializing the contents.
function couple(u, p, t)
γ, L, k = p
g = 9.8
udot = similar(u)
udot[1:2] .= u[3:4]
udot[3] = -γ * u[3] - (g / L) * sin(u[1]) + k * (u[2] - u[1])
udot[4] = -γ * u[4] - (g / L) * sin(u[2]) + k * (u[1] - u[2])
return udot
end
u₀ = [1.25, -0.5, 0, 0]
tspan = (0.0, 50.0);
First we check the behavior of the system when the pendulums are uncoupled, i.e., when .
Tip
Here idxs
is used to plot two components as functions of time.
γ, L, k = 0, 0.5, 0
ivp = ODEProblem(couple, u₀, tspan, [γ, L, k])
sol = solve(ivp, Tsit5())
plot(sol, idxs=[1, 2],
label=[L"\theta_1" L"\theta_2"],
xlims=[20, 50],
title="Uncoupled pendulums")
You can see that the pendulums swing independently. Because the model is nonlinear and the initial angles are not small, they have slightly different periods of oscillation, and they go in and out of phase.
With coupling activated, a different behavior is seen.
k = 1
ivp = ODEProblem(couple, u₀, tspan, [γ, L, k])
sol = solve(ivp, Tsit5())
plot(sol, idxs=[1, 2],
label=[L"\theta_1" L"\theta_2"],
xlims=[20, 50],
title="Coupled pendulums")
The coupling makes the pendulums swap energy back and forth.
6.4 Runge–Kutta methods¶
Example 6.4.1
We solve the IVP over , with .
using OrdinaryDiffEq
f(u, p, t) = sin((t + u)^2)
tspan = (0.0, 4.0)
u₀ = -1.0
ivp = ODEProblem(f, u₀, tspan)
ODEProblem with uType Float64 and tType Float64. In-place: false
timespan: (0.0, 4.0)
u0: -1.0
We use a DifferentialEquations
solver to construct an accurate approximation to the exact solution.
u_ref = solve(ivp, Tsit5(), reltol=1e-14, abstol=1e-14);
Now we perform a convergence study of our two Runge–Kutta implementations.
n = [round(Int, 2 * 10^k) for k in 0:0.5:3]
err = zeros(length(n), 2)
for (k, n) in enumerate(n)
t, u = FNC.ie2(ivp, n)
err[k, 1] = norm(u_ref.(t) - u, Inf)
t, u = FNC.rk4(ivp, n)
err[k, 2] = norm(u_ref.(t) - u, Inf)
end
@pt :header=["n", "IE2 error", "RK4 error"] [n err]
The amount of computational work at each time step is assumed to be proportional to the number of stages. Let’s compare on an apples-to-apples basis by using the number of -evaluations on the horizontal axis.
using Plots
plot([2n 4n], err;
m=3, label=["IE2" "RK4"], legend=:bottomleft,
xaxis=(:log10, "f-evaluations"), yaxis=(:log10, "inf-norm error"),
title="Convergence of RK methods")
plot!(2n, 0.1 * err[end,1] * (n / n[end]) .^ (-2), l=:dash, label=L"O(n^{-2})")
plot!(4n, 0.1 * err[end,2] * (n / n[end]) .^ (-4), l=:dash, label=L"O(n^{-4})")
The fourth-order variant is more efficient in this problem over a wide range of accuracy.
6.5 Adaptive Runge–Kutta¶
Example 6.5.1
Let’s run adaptive RK on .
using OrdinaryDiffEq, Plots
f(u, p, t) = exp(t - u * sin(u))
ivp = ODEProblem(f, 0, (0.0, 5.0))
t, u = FNC.rk23(ivp, 1e-5)
plot(t, u, m=2,
xlabel=L"t", ylabel=L"u(t)",
title="Adaptive IVP solution")
The solution makes a very abrupt change near . The resulting time steps vary over three orders of magnitude.
Δt = diff(t)
plot(t[1:end-1], Δt;
xaxis=(L"t", (0, 5)), yaxis=(:log10, "step size"),
title="Adaptive step sizes")
If we had to run with a uniform step size to get this accuracy, it would be
println("minimum step size = $(minimum(Δt))")
minimum step size = 4.6096854609878335e-5
On the other hand, the average step size that was actually taken was
println("average step size = $(sum(Δt)/(length(t)-1))")
average step size = 0.03205128205128205
We took fewer steps by a factor of almost 1000! Even accounting for the extra stage per step and the occasional rejected step, the savings are clear.
Example 6.5.2
In Demo 6.1.3 we saw an IVP that appears to blow up in a finite amount of time. Because the solution increases so rapidly as it approaches the blowup, adaptive stepping is required even to get close.
f(u, p, t) = (t + u)^2
ivp = ODEProblem(f, 1, (0.0, 1.0))
t, u = FNC.rk23(ivp, 1e-5);
┌ Warning: Stepsize too small near t=0.7854087204072808
└ @ FNCFunctions ~/Documents/GitHub/FNCFunctions.jl/src/chapter06.jl:102
In fact, the failure of the adaptivity gives a decent idea of when the singularity occurs.
plot(t, u;
legend=:none,
xlabel=L"t", yaxis=(:log10, L"u(t)"),
title="Finite-time blowup")
tf = t[end]
vline!([tf], l=:dash)
annotate!(tf, 1e5, latexstring(@sprintf("t = %.6f ", tf)), :right)
6.6 Multistep methods¶
Example 6.7.1
We study the convergence of AB4 using the IVP over , with . As usual, solve
is called to give an accurate reference solution.
using OrdinaryDiffEq
ivp = ODEProblem((u, p, t) -> sin((t + u)^2), -1.0, (0.0, 4.0))
u_ref = solve(ivp, Tsit5(), reltol=1e-14, abstol=1e-14);
Now we perform a convergence study of the AB4 code.
n = @. [round(Int, 4 * 10^k) for k in 0:0.5:3]
err = []
for n in n
t, u = FNC.ab4(ivp, n)
push!(err, norm(u_ref.(t) - u, Inf))
end
@pt :header=["n", "inf-norm error"] [n err]
The method should converge as , so a log-log scale is appropriate for the errors.
using Plots
plot(n, err, m=3,
label="AB4", legend=:bottomleft,
xaxis=(:log10, L"n"), yaxis=(:log10, "inf-norm error"),
title="Convergence of AB4")
plot!(n, 0.1 * err[end] * (n / n[end]) .^ (-4), l=:dash, label=L"O(n^{-4})")
Example 6.7.2
The following simple ODE uncovers a surprise.
ivp = ODEProblem((u, p, t) -> u^2 - u^3, 0.005, (0, 400.0))
ODEProblem with uType Float64 and tType Float64. In-place: false
timespan: (0.0, 400.0)
u0: 0.005
We will solve the problem first with the implicit AM2 method using steps.
tI, uI = FNC.am2(ivp, 200)
plot(tI, uI;
label="AM2", legend=:bottomright,
xlabel=L"t", ylabel=L"u(t)")
Now we repeat the process using the explicit AB4 method.
tE, uE = FNC.ab4(ivp, 200)
scatter!(tE, uE, m=3, label="AB4", ylim=[-4, 2])
Once the solution starts to take off, the AB4 result goes catastrophically wrong.
uE[105:111]
7-element Vector{Float64}:
0.7553857798343923
1.4372970308402562
-3.2889768512289934
214.1791132643978
-4.482089146771584e7
4.1268902909420876e23
-3.221441244795439e71
We hope that AB4 will converge in the limit , so let’s try using more steps.
plt = scatter(tI, uI;
m=3, label="AM2, n=200", legend=:bottomright,
xlabel=L"t", ylabel=L"u(t)")
for n in [1000, 1600]
tE, uE = FNC.ab4(ivp, n)
plot!(tE, uE, label="AM4, n=$n")
end
plt
So AB4, which is supposed to be more accurate than AM2, actually needs something like 8 times as many steps to get a reasonable-looking answer!
6.7 Implementation of multistep methods¶
Example 6.8.1
We’ll measure the error at the time .
du_dt(u, t) = u
û = exp
a, b = 0.0, 1.0;
n = [5, 10, 20, 40, 60]
err = []
t, u = [], []
for n in n
h = (b - a) / n
t = [a + i * h for i in 0:n]
u = [1; û(h); zeros(n - 1)]
f_val = [du_dt(u[1], t[1]); zeros(n)]
for i in 2:n
f_val[i] = du_dt(u[i], t[i])
u[i+1] = -4 * u[i] + 5 * u[i-1] + h * (4 * f_val[i] + 2 * f_val[i-1])
end
push!(err, abs(û(b) - u[end]))
end
@pt :header=["n", "h", "error"] [n (b - a) ./ n err]
The error starts out promisingly, but things explode from there. A graph of the last numerical attempt yields a clue.
using Plots
plot(t, abs.(u);
m=3, label="",
xlabel=L"t", yaxis=(:log10, L"|u(t)|"),
title="LIAF solution")
It’s clear that the solution is growing exponentially in time.