logo

Systems Represented by Differential Equations and Their Lyapunov Spectrum with Numerical Calculation Methods 📂Dynamics

Systems Represented by Differential Equations and Their Lyapunov Spectrum with Numerical Calculation Methods

Algorithm

Assume that a vector field $X = \mathbb{R}^{n}$ for space and function $f : X \to X$ is given by a differential equation $$ \dot{x} = f(x) $$. Define a variational equation for the Jacobian matrix $J$ of $f$ as follows $$ \dot{Y} = J Y $$. Here, the initial condition for the matrix function $Y = Y(t) \in \mathbb{R}^{n \times n}$ is set as the identity matrix $Y(0) = I_{n}$. The Lyapunov spectrum $\lambda_{1} , \cdots , \lambda_{n}$ of the system $\dot{x} = f(x)$ is approximated by repeatedly solving the variational equation for the numerical solution (e.g., calculated by methods like RK4) $\left\{ x_{k} \right\}_{k=0}^{K}$ of $\dot{x} = f(x)$. $J(x)$ is dependent on the trajectory $x(t)$ of the original system.

  • Solving the variational equation $\dot{Y} = J Y$ implies using a method like RK4, which was used to obtain the numerical solution of matrices or vectors, known as perturbations $Y$. If the sufficiently small time interval used to obtain $x_{k}$ is $h \approx 0$, the same time interval $h$ should be used for the variational equation.
  • The Runge-Kutta method $RK4(J, U, h)$ is defined for matrices $J$ and $U$ as follows $$ \begin{align*} U(t + h) \approx RK4(J, U, h) =& {\frac{ h }{ 6 }} \left( V_{1} + 2 V_{2} + 2 V_{3} + V_{4} \right)\\ V_{1} =& J U \\ V_{2} =& J \left( U + {\frac{ h }{ 2 }} V_{1} \right) \\ V_{3} =& J \left( U + {\frac{ h }{ 2 }} V_{2} \right) \\ V_{4} =& J \left( U + h V_{3} \right) \end{align*} $$.

Now, introduce the traditional method using Gram-Schmidt orthogonalization and a method using QR decomposition based on geometric definitions.

Based on Gram-Schmidt Orthogonalization 1

  • Represent the $j$-th column vector of the matrix $U$ as $U_{:j}$.
  • Represent the transpose matrix of the matrix $U$ as $U^{T}$.
  • Represent the norm of the matrix $U$ as $\left\| U \right\|$.
Algorithm: Gram-Schmidt Orthogonalization $GS(A)$
InMatrix $A \in \mathbb{R}^{n \times n}$
1.$U, V \gets A, A$
2.$U_{:1} \gets {\frac{ V_{i1} }{ \left\| V_{i1} \right\| }} $
3.for $j = 2, \cdots, n$ do
4.   for $j_{p} = 1, \cdots, (j-1)$ do
5.     $V_{:j} \gets V_{:j} - \left( U_{:j}^{T} U_{:j_{p}} \right) U_{:j_{p}} $
6.   end for
7.   $U_{:j} \gets {\frac{ V_{:j} }{ \left\| V_{:j} \right\| }}$
8.end for
OutObtain orthonormal matrix $U$ and orthogonal matrix $V$

Define a function $GS$ to obtain $U$ with normalized axis length and a matrix $V$ that is merely orthogonal without normalization, unlike the widely known Gram-Schmidt orthogonalization in linear algebra. Here, each column of $V$ in the variational equation $\dot{Y} = J Y$ represents how much the perturbation $Y$ stretches or compresses the axis length at specific points $x(t)$ of the original equation $\dot{x} = f(x)$ via the linear transformation $J \left( x(t) \right)$.

Algorithm: Calculating Lyapunov Spectrum
InTrajectory $\left\{ x_{k} \right\}_{k=0}^{K}$
Jacobian matrix $J(x)$
1.$\lambda \gets \mathbf{0}$# Zero vector of length $n$
2.$U \gets I_{n}$# Initialization
3.for $k = 1, \cdots, K$ do
4.   $U, V \gets GS(U)$
5.   $\lambda \gets \lambda + \left( \log \left\| V_{:1} \right\| , \cdots , \log \left\| V_{:n} \right\| \right)$
6.   $U \gets RK4 \left( J \left( x_{k} \right) , U , h \right)$
7.end for
8.$\lambda \gets \lambda / (hK)$
OutLyapunov spectrum $\lambda$

Based on QR Decomposition 2

Assume that a function $QR(A)$ for the matrix $A$ returns the results of the QR decomposition, $Q$ and $R$, of $A$.

Algorithm: Calculating Lyapunov Spectrum
InTrajectory $\left\{ x_{k} \right\}_{k=0}^{K}$
Jacobian matrix $J(x)$
1.$\lambda \gets \mathbf{0}$# Zero vector of length $n$
2.$J \gets J\left( x_{0} \right)$
2.$Q, R \gets QR(J)$# Initialization
3.for $k = 1, \cdots, K$ do
4.   $Q, R \gets QR(J)$
5.   $\lambda \gets \lambda + \left( \log \left| R_{11} \right| , \cdots , \log \left| R_{nn} \right| \right)$# Diagonal elements of $R$
6.   $Q \gets RK4 \left( J \left( x_{k} \right) , Q , h \right)$
7.end for
8.$\lambda \gets \lambda / (hK)$
OutLyapunov spectrum $\lambda$

Explanation

Calculating the Lyapunov spectrum for a dynamical system expressed by ODEs is much more challenging than calculating a bifurcation diagram. Since the concept of chaos itself is defined by the Lyapunov spectrum, it’s an extremely important skill for studying dynamics, and it’s also a part where one can feel a significant computational barrier. Naturally, there is a big difference between having a vague notion of Lyapunov exponents and actually being able to calculate them.

Although the two introduced methods may seem unrelated at first glance, a detailed look at the proof of the existence of QR decomposition reveals that they are fundamentally very similar. The differences can be summarized as follows:

  • GS-based: Geometrically intuitive and easy to understand. Reading through the algorithm reveals that it’s essentially measuring the length of axes of an ellipsoid, transformed by $J$ from a matrix $Y$ having $n$ axes of a unit ball as column vectors.
  • QR-based: Relatively easier to implement. After QR decomposition, just gather the diagonal elements of $R$, and since you’re likely using a package for QR decomposition from the start, the reliability of the program is high.

Comparison of Results

The results of calculating the Lyapunov spectrum of the Lorenz attractor at $\rho \in [0, 100]$ using two methods are as follows:

  • Based on Gram-Schmidt:

alt text

  • Based on QR decomposition:

alt text

  • Reference 3

alt text

As seen, it’s hard to distinguish visually, obtaining similar results, and it’s also confirmed to be reliable when compared to the reference. Now, let’s compare as concrete data. Blue (left) is calculated based on Gram-Schmidt, while orange (right) is based on QR decomposition.

alt text

Performance Comparison

alt text

  • Based on Gram-Schmidt (top): 2:39:31
  • Based on QR decomposition (bottom): 9:50:20

However, the two techniques show considerable differences in speed. The result itself can be achieved by either method, but benchmarking to calculate these numerical values reveals a speed difference of about 3 to 4 times.

The following is a measure of the time taken to obtain the Lorenz attractor for a more accurate comparison. It took about 9 minutes, confirming that calculating the Lyapunov spectrum itself did not significantly affect it.

alt text

Code 4 5

Full Code

Julia code calculating the Lyapunov spectrum from $\rho \in [0, 100]$ of the Lorenz attractor.

using CSV, DataFrames, LinearAlgebra, ProgressMeter, Plots, Dates; @info now()
using Base.Threads: @threads # Base.Threads.nthreads()

function RK4(f::Function, v::AbstractVector, h=1e-3)
    V1 = f(v)
    V2 = f(v + (h/2)*V1)
    V3 = f(v + (h/2)*V2)
    V4 = f(v + h*V3)
    return v + (h/6)*(V1 + 2V2 + 2V3 + V4), V1
end
function RK4(J::AbstractMatrix, U::AbstractMatrix, dt=1e-3)
    V1 = J*U
    V2 = J*(U + (dt/2)*V1)
    V3 = J*(U + (dt/2)*V2)
    V4 = J*(U + dt*V3)
    return U + (dt/6)*(V1 + 2V2 + 2V3 + V4)
end
function factory_lorenz(ρ::Number; ic = [10.,10.,10.], tspan = [0., 1000.], dt = 1e-3)
    σ = 10
    β = 8/3
    function sys(v::AbstractVector)
        x, y, z = v
        dx = σ*(y - x)
        dy = x*(ρ - z) - y
        dz = x*y - β*z
        return [dx, dy, dz]
    end
        
    t_ = first(tspan):dt:last(tspan)
    len_t_ = length(t_)
    
    t, tk = .0, 0
    v = ic; DIM = length(v)
    traj = zeros(len_t_+2, 2DIM)
    while tk ≤ len_t_
        v, dv = RK4(sys, v, dt)

        if t ≥ first(t_)
            tk += 1
            traj[tk+1,         1:DIM ] =  v
            traj[tk  , DIM .+ (1:DIM)] = dv
        end
    end
    return traj[2:(end-2), :]
end
factory_lorenz(T::Type, args...; kargs...) =
DataFrame(factory_lorenz(args...; kargs...), ["x", "y", "z", "dx", "dy", "dz"])

function gram_schmidt(J)
    N = size(J, 1)
    U, V = deepcopy(J), deepcopy(J)
    U[:,1] = V[:,1] / norm(V[:,1])
    for j in 2:N
        for jp in 1:(j-1)
            V[:,j] -= (J[:,j]'U[:,jp])*U[:,jp]
        end
        U[:,j] = V[:,j] / norm(V[:,j])
    end
    return U, V
end
J_lorenz(x,y,z,ρ) = [
     -10   10  0
      ρ-z -1  -x
        y  x  -8/3
]

const dt = 1e-3
ρ_range = 0:.1:100
cd("G:/DDM")

function lyapunov_lorenz_GS()
    schedules = DataFrame(idx = eachindex(ρ_range), ρ = ρ_range)
    schedules[!, :λ1] .= .0; schedules[!, :λ2] .= .0; schedules[!, :λ3] .= .0; 
    @showprogress @threads for dr = eachrow(schedules)
        data = factory_lorenz(DataFrame, dr.ρ; dt)
        
        λ = zeros(3);
        J = J_lorenz(collect(data[1, 1:3])..., dr.ρ)
        U = I(3)
        for i in 2:nrow(data)
            U, V = gram_schmidt(U)
            λ += V |> eachcol .|> norm .|> log          

            U = RK4(J, U, dt)
            J = J_lorenz(collect(data[i, 1:3])..., dr.ρ)
        end

        λ ./= dt*nrow(data)
        dr[[:λ1, :λ2, :λ3]] .= sort(λ, rev = true)
    end
    sort!(schedules, :ρ)
    CSV.write("lyapunov/lorenzGS.csv", schedules, bom = true)
    plot(xticks = 0:20:100, legend = :none, xlabel = "ρ", ylabel = "λ", size = [600, 300])
    plot!(schedules.ρ, schedules.λ1, lw = 2, color = 1)
    plot!(schedules.ρ, schedules.λ2, lw = 2, color = 2)
    plot!(schedules.ρ, schedules.λ3, lw = 2, color = 3)
    png("lyapunov/lorenzGS.png")
end

function lyapunov_lorenz_QR()
    schedules = DataFrame(idx = eachindex(ρ_range), ρ = ρ_range)
    schedules[!, :λ1] .= .0; schedules[!, :λ2] .= .0; schedules[!, :λ3] .= .0; 
    @showprogress @threads for dr = eachrow(schedules)
        data = factory_lorenz(DataFrame, dr.ρ; dt)
        
        λ = zeros(3);
        J = J_lorenz(collect(data[1, 1:3])..., dr.ρ)
        Q, _ = qr(J)
        for i in 2:nrow(data)
            Q, R = qr(Q)
            λ += R .|> abs |> diag .|> log   

            Q = RK4(J, Matrix(Q), dt)
            J = J_lorenz(collect(data[i, 1:3])..., dr.ρ)
        end

        λ ./= dt*nrow(data)
        dr[[:λ1, :λ2, :λ3]] .= sort(λ, rev = true)
    end
    sort!(schedules, :ρ)
    CSV.write("lyapunov/lorenzQR.csv", schedules, bom = true)
    plot(xticks = 0:20:100, legend = :none, xlabel = "ρ", ylabel = "λ", size = [600, 300])
    plot!(schedules.ρ, schedules.λ1, lw = 2, color = 1)
    plot!(schedules.ρ, schedules.λ2, lw = 2, color = 2)
    plot!(schedules.ρ, schedules.λ3, lw = 2, color = 3)
    png("lyapunov/lorenzQR.png")
end

lyapunov_lorenz_GS()
lyapunov_lorenz_QR()

See Also

  • Lyapunov Exponents of Multidimensional Maps and Their Numerical Calculation

  1. Parker, Thomas S., and Leon Chua. Practical numerical algorithms for chaotic systems. Springer Science & Business Media, 2012. ↩︎

  2. von Bremen, Hubertus F., Firdaus E. Udwadia, and Wlodek Proskurowski. “An efficient QR based method for the computation of Lyapunov exponents.” Physica D: Nonlinear Phenomena 101.1-2 (1997): 1-16. https://doi.org/10.1016/S0167-2789(96)00216-3 ↩︎

  3. Balcerzak, M., Pikunov, D. & Dabrowski, A. The fastest, simplified method of Lyapunov exponents spectrum estimation for continuous-time dynamical systems. Nonlinear Dyn 94, 3053–3065 (2018). https://doi.org/10.1007/s11071-018-4544-z ↩︎

  4. Lutz Lehmann, Numerical computation of Lyapunov exponent, URL (version: 2023-12-07): https://scicomp.stackexchange.com/q/36023 ↩︎

  5. https://github.com/JuliaDynamics/ChaosTools.jl/blob/4053d184aa91b62b550ecd722193225ea2b198cd/src/chaosdetection/lyapunovs/lyapunovspectrum.jl#L5-L51 ↩︎