I have previously successfully implemented the QR decomposition method in MATLAB to calculate Lyapunov exponents for Lorenz equations. See here.
This method integrates the stacked system, i.e. the system equation + the variational equation and use one common integrator to integrate them at the same time steps.
What I am trying to do now is to use any of the integrators built in to Scipy's solve_ivp to do this. In order to do this, I would need to switch out the time step method in scipy.integrate._ivp.rk._step_impl_qr
Here is what I have:
import numpy as np
import scipy
import matplotlib.pyplot as plt
import scipy.integrate._ivp.rk as rk
Define a new function for '_step_impl' this called '_step_impl_qr'
The bits I changed from Scipy's source code are in between
This is what I changed
lyap = []
ndim = 3
def _step_impl_qr(self):
t = self.t
y = self.y
max_step = self.max_step
rtol = self.rtol
atol = self.atol
min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t)
if self.h_abs > max_step:
h_abs = max_step
elif self.h_abs < min_step:
h_abs = min_step
else:
h_abs = self.h_abs
step_accepted = False
step_rejected = False
while not step_accepted:
if h_abs < min_step:
return False, self.TOO_SMALL_STEP
h = h_abs * self.direction
t_new = t + h
if self.direction * (t_new - self.t_bound) > 0:
t_new = self.t_bound
h = t_new - t
h_abs = np.abs(h)
y_new, f_new = rk.rk_step(self.fun, t, y, self.f, h, self.A,
self.B, self.C, self.K)
scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol
error_norm = self._estimate_error_norm(self.K, h, scale)
if error_norm < 1:
if error_norm == 0:
factor = rk.MAX_FACTOR
else:
factor = min(rk.MAX_FACTOR,
rk.SAFETY * error_norm ** self.error_exponent)
if step_rejected:
factor = min(1, factor)
h_abs *= factor
step_accepted = True
###################################
## QR algorithm
## 1. local lyapunov exponents
Mn = np.reshape(y_new[ndim:],(ndim,ndim)).transpose()
Q,R = np.linalg.qr(Mn)
lyap.append(np.log(abs(R.diagonal())))
## 2. change solution for the perturbation vectors
y_new[ndim:] = Q.transpose().reshape(-1)
###################################
else:
h_abs *= max(rk.MIN_FACTOR,
rk.SAFETY * error_norm ** self.error_exponent)
step_rejected = True
self.h_previous = h
self.y_old = y
self.t = t_new
self.y = y_new
self.h_abs = h_abs
self.f = f_new
return True, None
Now I switch out Scipy's original function
rk.RungeKutta._step_impl = _step_impl_qr
Now I define my Lorenz eqs and variational eqs:
Lorenz system by itself
def lorenz_jac(x,y,z):
dxxdt = -sigma
dxydt = sigma
dxzdt = 0
dyxdt = rho-z
dyydt = -1
dyzdt = -x
dzxdt = y
dzydt = x
dzzdt = -beta
return np.array([[dxxdt,dxydt,dxzdt],
[dyxdt,dyydt,dyzdt],
[dzxdt,dzydt,dzzdt]])
Stacked solve
def lorenz_system_stacked(t, s):
x,y,z = s[0:3]
pertb_vecs = s[3:].reshape((3,3)).transpose()
sysdot = np.array([sigma * (y - x),
x * (rho - z) - y,
x * y - beta * z])
Mdot = np.matmul(lorenz_jac(x,y,z),pertb_vecs).transpose().reshape(-1)
return np.concatenate((sysdot,Mdot))
Now I specify the parameters and initial conditions
sigma=10
beta=8/3
rho=28
t = np.arange(0, 50, 0.01) # Time points
initial_state = np.array([0,1,1.05]) # Initial conditions
pertb0= np.eye(3).reshape(-1,1)
pertb0=pertb0[:,0]
s0 = np.concatenate((initial_state,pertb0))
Now I call solve_ivp to integrate
sol = scipy.integrate.solve_ivp(lorenz_system_stacked, (t[0], t[-1]), s0, method='RK23')
After the integration is done, I calculate Lyapunov exponents from the local ones
Calculate Lyapunov exponent
lyap = np.array(lyap).transpose()
le = []
for i in range(np.shape(lyap)[1]):
if i == 0:
le.append(lyap[:,:i+1].transpose())
else:
le.append(np.sum(lyap[:,:i+1],axis=1)/sol.t[i]))
The cool thing here is that I can use any of SciPy's built-in integrators (and their error controls) to calculate Lyapunov exponents. However, I am not getting the correct Lyapunov exponents with this. The system (Lorenz) equations are solved correctly. I am really scratching my head on this right now as the algorithm is exactly the same as the old MATLAB implementation.
If anyone can give this a fresh look and catch my (I believe) very subtle error, it will be greatly appreciated!


Mnlist? In the version you commented out, you got the local increments of the exponents, proportional to the time step. If you cumulatively sum them up and divide by the time (cumulative time steps), you should get the standard LE. – Lutz Lehmann Dec 07 '23 at 11:10np.cumsumand divide byt[:, None]. The result should not change, just the execution should be much faster. // It should not have been necessary to go this deep into the stepper class, just using the stepper class and building a time loop around it should be sufficient. You would of course lose thesolve_ivpinterface. – Lutz Lehmann Dec 07 '23 at 23:13