Different result between ODE and signal.lsim resolution - python

For training purpose, I try to modeling and analyze a dynamic system using python. I follow the example given on this site.
In this example, they are using ODEint to resolve the differential equation and simulate a step response. This method gives the following result:
enter image description here
Then, I'm using the state space representation to simulate the same system. This gives the following code:
import scipy.signal as signal
import numpy as np
import matplotlib.pyplot as plt
m1 = 1.2
m2 = 1.2
k1 = 0.9
k2 = 0.4
b1 = 0.8
b2 = 0.4
A = [[0, 0, 1, 0],[0, 0, 0, 1],[(-k1-k2)/m1, k2/m1, -b1/m1, 0],[k2/m2, -k2/m2, 0, -b2/m2]]
B = [[0],[0],[1],[0]]
C = [[1, 0, 0, 0]]
D = [0]
sys = signal.StateSpace(A, B, C, D)
x0 = [0,0,0,0]
start = 0
stop = 40
step = 0.01
t = np.arange(start, stop, step)
u = np.ones(len(t))
t, y, x = signal.lsim(sys, u, t, x0)
plt.plot(t, x)
plt.legend(['m1_position', 'm2_position', 'm1 speed', 'm2_speed'])
plt.grid(True)
And the result of the state space simulation:
enter image description here
We see that the results are different between the two methods. The shapes are identical, but the value are different. I expect that the both methods give the same values. I have tried to understand if the both methods use different resolution algorithm, but lsim documentation do not give any details for that.
I would like to understand the reason for this difference? Is one method more accurate than the other? In this particular case or in general? How can we improve the accuracy?

The equilibrium position in your coded system is at x2 = x1 = m1/k1 = 1.2/0.9=4/3=1.3333, which is where the solution goes to.
To get the same behavior as the explicitly coded ODE function, you need to set B = [[0],[0],[1/m1],[0]] to correctly translate force into acceleration, to then get the equilibrium at x2 = x1 = 1/k1 = 1/0.9=1.111.
As the force input is the only non-homogeneity, the solutions differ, as observed, only by a scale factor.

Related

smoothed state disturbances in statsmodels' state space model

I am building a custom state-space model with statsmodels as follows
from statsmodels.tsa.statespace import mlemodel
mod = mlemodel.MLEModel(YY, k_states=n_st, k_posdef=n_sh)
mod['design'] = Z
mod['transition'] = T
mod['selection'] = R
mod['state_cov'] = np.eye(n_sh)
mod['obs_intercept'] = d
mod.initialize_stationary()
I am interested in the smoothed states and smoothed state disturbances, which I get with
results = mod.smooth([])
The smoothed states in results.smoothed_state are correct (I have the true values against which I am comparing), but the smoothed state disturbances in results.smoothed_state_disturbance are shifted forward one period - the first column contains the (correct) smoothed disturbances for the second period, etc, while the last column contains zeros, which are the correct smoothed disturbances for one period after the end of the sample.
My understanding is that this has to do with the timing of the state equation, which according to statsmodels docs here is
alpha(t+1) = T alpha(t) + R eta(t) (1)
and therefore implies that the first observation y_{1} is related to the state alpha_{1} which in turn depends on the disturbance eta_{0}, and that the smoothed value of that disturbance is not returned by the smoother. On the other hand, in this statmodels docs, the timing of the state equation is
alpha(t) = T alpha(t-1) + R eta(t) (2)
and implies that the state alpha_{1} depends on eta_{1}, not on eta_{0}. Since both (future (1) and contemporaneous (2)) timing conventions appear in the statsmodels docs, I thought that it would be possible to choose which one to use. Unfortunately, haven't been able to find out how. I tried changing the smoother timing with results = mod.smooth([], filter_timing=1) which according to the docs uses Kim and Nelson (1999) (contemporaneous) timing, rather than the default Durbin and Koopman (2012) (future) timing. But then I get totally different (and wrong, because I know what the true values are) results not only from the smoother but also for the value of the loglikelihood. I also looked for examples in the unit tests for smoothing but there are only tests against MATLAB and R libraries that also use the future timing, and there are no tests (for disturbance smoothing) against STATA, which uses the alternative contemporaneous timing.
My question is, is there a way to either write the state equation with the contemporaneous timing ((2) above) or to recover the smoothed state disturbances associated with the observed data in the first period.
Here is some code for the following AR(1) model with a measurement error, using contemporaneous timing for the state equation, initialized with the stationary distribution.
alpha(0) ~ N(0, 1/(1-.5**2))
alpha(t) = .5 alpha(t-1) + eta(t), eta(t) ~ N(0, 1)
y(t) = alpha(t) + e(t), e(t) ~ N(0, 1)
from statsmodels.tsa.statespace import mlemodel
import numpy as np
import sys
from scipy.stats import multivariate_normal
from numpy.random import default_rng
gen = default_rng(42)
T = np.array([.5])
Z = np.array([1.])
Q = np.array([1.])
H = np.array([1.])
R = np.array([1.])
P0 = 1/(1-T**2)
# Simulate data for 2 periods
alpha0 = gen.normal(0, np.sqrt(P0))
eta1 = gen.normal(0, 1)
e1 = gen.normal(0, 1)
eta2 = gen.normal(0, 1)
e2 = gen.normal(0, 1)
alpha1 = .5*alpha0 + eta1
y1 = alpha1 + e1
alpha2 = .5*alpha1 + eta2
y2 = alpha2 + e2
First, use statsmodels.statespace for to compute smoothed state, smoothed state disturbance and log-likelihood given just the first data point
mod1 = mlemodel.MLEModel(y1, k_states=1, k_posdef=1)
mod1['design'] = Z
mod1['transition'] = T
mod1['selection'] = R
mod1['state_cov'] = Q
mod1['obs_cov'] = H
mod1.initialize_stationary()
results1 = mod1.smooth([])
results1.smoothed_state, results1.smoothed_state_disturbance, results1.llf
gives
(array([[-0.06491681]]), array([[0.]]), -1.3453530272821392)
Note that observing y(1) we can compute the conditional expectations of eta(1), however, what is returned here is only the conditional expectations of eta(2). Since the model is stationary and Gaussian, the conditional expectations of alpha(1) and eta(1) given y(1) can be computed from their joint distribution (see here for the relevant formulae), as shown in the following code
# Define a matrix L1 which maps [alpha(0), eta(1), e(1)] into [alpha0, eta1, e1, alpha1, y1]
L1 = np.vstack((np.eye(3), # alpha(0), eta(1), e(1)
np.r_[T, 1, 0], # alpha(1)
np.r_[T, 1, 1], # y(1)
))
# check
np.testing.assert_array_equal(np.r_[alpha0, eta1, e1, alpha1, y1],
L1 # np.r_[alpha0, eta1, e1])
# Compute Sigma1 as the covariance matrix of [alpha0, eta1, e1, alpha1, y1]
D1 = np.eye(3)
D1[0, 0] = P0
Sigma1 = L1 # D1 # L1.T
# [alpha0, eta1, e1, alpha1, y1] has a multivariate Normal distribution, and we can apply well-known formulae to compute conditional expectations and the log-likelihood
ind_e1 = 1
ind_eta1 = 2
ind_alpha1 = 3
ind_y1 = 4
smooth_eta1 = (Sigma1[ind_eta1, ind_y1]/Sigma1[ind_y1, ind_y1])*y1
smooth_alpha1 = (Sigma1[ind_alpha1, ind_y1]/Sigma1[ind_y1, ind_y1])*y1
loglik1 = multivariate_normal.logpdf(y1, cov=Sigma1[ind_y1, ind_y1])
smooth_alpha1, smooth_eta1, loglik1
which gives
(array([-0.06491681]), array([-0.04868761]), -1.3453530272821392)
Extending to the first 2 periods, with statsmodels
y = np.array([y1, y2])
mod2 = mlemodel.MLEModel(y, k_states=1, k_posdef=1)
mod2.ssm.timing_init_filtered = True
mod2['design'] = Z
mod2['transition'] = T
mod2['selection'] = R
mod2['state_cov'] = Q
mod2['obs_cov'] = H
mod2.initialize_stationary()
results2 = mod2.smooth([])
results2.smoothed_state, results2.smoothed_state_disturbance, results2.llf
gives
(array([[-0.25292213, -0.78447967]]),
array([[-0.65801861, 0. ]]),
-3.1092778246103645)
And computing the conditional expectations from the joint distribution
# L2 maps [alpha(0), eta(1), e(1), eta(2), e(2)] into [alpha0, eta1, e1, eta2, e2, alpha1, alpha2, y1, y2]
L2 = np.vstack((np.eye(5), # alpha(0), eta(1), e(1), eta(2), e(2)
np.r_[T, 1, 0, 0, 0], # alpha(1)
np.r_[T**2, T, 0, 1, 0], # alpha(2)
np.r_[T, 1, 1, 0, 0], # y(1)
np.r_[T**2, T, 0, 1, 1], # y(2)
))
np.testing.assert_array_equal(np.r_[alpha0, eta1, e1, eta2, e2, alpha1, alpha2, y1, y2],
L2 # np.r_[alpha0, eta1, e1, eta2, e2,])
# Sigma2 is the covariance of [alpha0, eta1, e1, eta2, e2, alpha1, alpha2, y1, y2]
D2 = np.eye(5)
D2[0, 0] = P0
Sigma2 = L2 # D2 # L2.T
ind_e = [2, 4]
ind_eta = [1, 3]
ind_alpha = [5, 6]
ind_y = [7, 8]
# compute smoothed disturbances and states, and loglikelihood
smooth_eta = Sigma2[ind_eta, :][:, ind_y] # np.linalg.solve(Sigma2[ind_y, :][:, ind_y], y)
smooth_alpha = Sigma2[ind_alpha, :][:, ind_y] # np.linalg.solve(Sigma2[ind_y, :][:, ind_y], y)
loglik2 = multivariate_normal.logpdf(y.flatten(), cov=Sigma2[ind_y, :][:, ind_y])
smooth_alpha.flatten(), smooth_eta.flatten(), loglik2
gives
(array([-0.25292213, -0.78447967]),
array([-0.1896916 , -0.65801861]),
-3.1092778246103636)
The smoothed states alpha(t), and the loglikelihood values are the same. The smoothed disturbances returned by statsmodels.statespace.mlemodel are for eta(2) and eta(3).
Shortest answer
The short answer to your question is that you can do the following to recover the smoothed estimate that you're looking for:
r_0 = results1.smoother_results.scaled_smoothed_estimator_presample
R = mod1['selection']
Q = mod1['state_cov']
eta_hat_0 = Q # R.T # r_0
print(eta_hat_0)
This gives [-0.04868761] as you wanted. This comes from the usual disturbance smoother equation (e.g. Durbin and Koopman's 2012 book, equation 4.69) for the period 0. In Statsmodels, we store the smoothed disturbance estimates for the sample period, 1 - nobs, but we do store the r_0, as given above, and so you can compute the value you want directly.
Alternative, maybe easier, method
This is an alternative method to get this output, which we can derive by writing your problem in a different way. Under the timing assumption of Statsmodels, as you point out, the distribution of the first state alpha(1) ~ N(a1, P1) is specified as a prior. We can view your desired model as specifying the 0th state, alpha(0) ~ N(a0, P0), as a prior and then consider the first state as being produced by the transition equation: alpha(1) = T alpha(0) + eta(0).
This is now written using the error convention of Statsmodels, and we can use Statsmodels to compute the smoothed results. The only trick is that there is no observation y(0) associated with the first state alpha(0), because we are only including the alpha(0) -> alpha(1) transition step so that we can specify the prior as you wanted. But that is no problem, we can simply include a missing value.
So if we just modify your original model by putting an nan presample value into the input data, we get the result we want:
mod1 = mlemodel.MLEModel(np.r_[np.nan, y1], k_states=1, k_posdef=1)
mod1['design'] = Z
mod1['transition'] = T
mod1['selection'] = R
mod1['state_cov'] = Q
mod1['obs_cov'] = H
mod1.initialize_stationary()
results1 = mod1.smooth([])
print(results1.smoothed_state, results1.smoothed_state_disturbance, results1.llf)
yields:
[[-0.03245841 -0.06491681]] [[-0.04868761 0. ]] -1.3453530272821392
Note on the filter_timing flag:
The filter_timing flag also allows you to change the timing convention for the prior, but not for the states. If you set the prior alpha(0|0) ~ N(a_00, P_00) in the alternative timing (filter_timing=1), then setting alpha(1) ~ N(a_1, P_1) with a_1 = T a_00 and P_1 = T P_00 T' + R Q R' in the default timing (filter_timing=0) will give you exactly the same results.

Improving execution time of this Python "angle-of-arrival" estimation algorithm

Background
I've written a python implementation of this answer to my recent question over on Math SE. In short, the problem is as follows:
I have an experimental setup consisting of three receivers, with known rectangular coordinates [xi, yi, zi], and a transmitter with unknown coordinates [x,y,z] emitting a signal at unknown time t with velocity c and arriving at receiver i at known time ti.
This information is insufficient to uniquely determine the rectangular coordinates of the transmitter. We can, however, estimate the angle to the transmitter (i.e. the transmitter's phi and theta in spherical coordinates). This may be done by solving the system of equations given in the linked post.
My goal is to solve these equations for real, experimental data.
Problem
I have been able to write an effective python implementation of the approach described in the linked post. For both simulated and experimental data, this gives satisfactory estimates of the angle to the transmitter.
I now wish to use this in practice. Unfortunately, however, my code runs too slowly to be useful in my desired application.
Ideally, we'd like to be able to solve for on the order of 1 million datapoints per hour in
a near-real-time application. Currently, this takes several hours. While I recognize that, particularly with Python, dramatic performance improvements are not to be expected, any improvement would be helpful.
In short, I'd like to reduce the execution time of this algorithm. Due to the minimal Linux install (and my lack of control over it) being used on the host machine, I'd like to do so by improving my code, and without use of additional modules/external libraries/etc if possible.
Code
import math
import numpy as np
import ROOT
from scipy.optimize import root
from dataclasses import dataclass
from ROOT import TFile, TNtuple
c = 299792
#dataclass
class Vertexer:
roc: list
def fun(self, var, dat):
f0 = var.dot(self.roc[0] - self.roc[1]) - c * (dat[1] - dat[0])
f1 = var.dot(self.roc[1] - self.roc[2]) - c * (dat[2] - dat[1])
n = np.linalg.norm(var) - 1
return [f0, f1, n]
def find(self, dat):
result = root(
self.fun,
(0, 0, 0),
method="lm",
args=dat,
options={
"col_deriv": 1,
"xtol": 1.49012e-08,
"ftol": 1.49012e-08,
"gtol": 0.0,
"maxiter": 0,
"eps": 0.0,
"factor": 100,
"diag": None,
},
)
if result.success:
return result.x
def main():
myVertexer = Vertexer(
[
np.array([3.0085470085470085, 3.7642857142857116, -0.06]),
np.array([2.0034188034188034, 2.0142857142857133, -0.19]),
np.array([1.0324786324786326, 0.27142857142857135, -0.19]),
]
)
data = ROOT.RDataFrame("D", "2018_filtered.root")
colns = data.AsNumpy(columns=["ai", "aj", "ak"])
f = TFile("theta_phi_2.root", "RECREATE")
ntuple = TNtuple("N", "N", "i:j:k:ai:aj:ak")
for (ai, aj, ak) in zip(colns["ai"], colns["aj"], colns["ak"]):
v = myVertexer.find([ai, aj, ak])
if v.any() != None:
ntuple.Fill(v[0], v[1], v[2], ai, aj, ak)
ntuple.Write()
f.Write()
main()
Step-Through
The Vertexer class contains a fairly standard and straightforward SciPy based system of equations solution "algorithm". The function func() contains the system of equations described in the linked post, and find() solves the system given the times of arrival (dat) and the receiver coordinates (roc, provided upon instantiation). I'm using a Levenberg Marquardt solver with coln_deriv set to True in order to improve solution speed. I see similar solution accuracy across all solvers. All other settings are set to default.
The main() function reads time-of-arrival data from a .root file (see) into a dict of NumPy arrays, which I loop over, feed to the algorithm, and record the result in another .root file.
If you like, you may replace the main() function with the following crude "source simulation" code I've written, which simply produces a random point, computes the arrival time of a signal from that point to three randomly-placed "receivers", and feeds those times to the algorithm:
x0 = random.randrange(0,1000); y0 = random.randrange(0,1000); z0 = random.randrange(0,1000)
x1 = random.randrange(0,50); x2 = random.randrange(0,50); x3 = random.randrange(0,50);
y1 = random.randrange(0,50); y2 = random.randrange(0,50); y3 = random.randrange(0,50);
z1 = random.randrange(0,50); z2 = random.randrange(0,50); z3 = random.randrange(0,50);
t1 = math.sqrt((x0-x1)**2 + (y0-y1)**2 + (z0-z1)**2)/c
t2 = math.sqrt((x0-x2)**2 + (y0-y2)**2 + (z0-z2)**2)/c
t3 = math.sqrt((x0-x3)**2 + (y0-y3)**2 + (z0-z3)**2)/c
myVertexer = Vertexer([[x1,y1,z1], [x2,y2,z2], [x3,y3,z3]])
result = myVertexer.find([t1,t2,t3])
You can solve your equations analytically rather than numerically (probably someone will say that this is not the place for this).
Let's analyze your function
def fun(self, var, dat):
f0 = var.dot(self.roc[0] - self.roc[1]) - c * (dat[1] - dat[0])
f1 = var.dot(self.roc[1] - self.roc[2]) - c * (dat[2] - dat[1])
n = np.linalg.norm(var) - 1
return [f0, f1, n]
The solutions of f0 = 0 lie in a plane normal to self.roc[0] - self.roc[1].
The solutions of f1 = 0 lie in a plane normal to self.roc[1] - self.roc[2].
The solutions of n = 0 lies in a sphere.
The intersection of two planes is a line going on the direction give by the cross product between the normal of the two planes.
The intersection between a line and a sphere will either be (a) one tangent point; (b) two points, one entering, one leaving the sphere; (c) no solution if the line passes is far from the sphere.

Numerically Solving A Multivariate Differential Equation Without odeint

I am curious. I know this can be solved by using odeint, but I'm trying to do it from scratch, and I've encountered an interesting behaviour.
Assume a simple oscillator, of equation m * x_ddot + k * x = 0. Wikipedia
Initial conditions are x0 != 0;
Theoretically, the solution is a sine function.
But, on python, the solution is a sine that keeps growing in amplitude. Now I'm curious why that's happening, because it shouldn't. Does it have to do with numerical stability, or something similar? Like, why is it diverging? From a physics point of view, there is no reason it should, so why is it behaving as such?
Here's the code.
dt = 0.05
t_start = 0
t_finish = 20
t = 0
x1 = 1
X1 = []
x2 = 0
X2 = []
while t <= t_finish:
X1.append(x1)
X2.append(x2)
# state space representation
x1_dot = x2
x2_dot = -9.81*x1
x1 += dt*x1_dot
x2 += dt*x2_dot
t += dt
# to make sure the vectors are of equal size for plotting
if len(X1) > len(time):
X1 = X1[:len(X1)-1]
elif len(X1) < len(time):
time = time[:len(time)-1]
plt.figure(figsize=(10,10))
plt.plot(time,X1)
plt.grid()
Here's the plot.
I'm thankful for any insight you guys can offer.
I think the issue is in your understanding of Euler scheme. This is a very simple ODE, in fact its a textbook example for the harmonic oscillation system. The Euler scheme is pretty much based off N = T/(dt) where N is the number of steps, T the final time and dt the step size. So, if your step size or final time is not small relative to the N, the solution drifts up.
There is no need to use RK4, Euler will get the job done. The trick though is that you need a small dt. I have rewritten your scheme in a more clear way and used an appropriate dt respectively.
import numpy as np
import matplotlib.pyplot as plt
# Parameters
t_finish = 20.0
dt = 0.00005 # Very small dt (infinitesmal dt)
n = int(t_finish/dt)
tvalue = np.linspace(0, t_finish, n+1)
x1 = np.zeros(n+1)
x2 = np.zeros(n+1)
# create canvas
plt.figure(figsize=(10, 5))
# Initialize
x1[0] = 1.0 # Not at zero
x2[0] = 0.0
# Simulation with Euler scheme
for i in range(n):
t = (i+1)*dt
x1[i+1] = x1[i] + x2[i]*dt
x2[i+1] = x2[i] -9.81*x1[i]*dt
# Plot paths
plt.plot(tvalue, x1, label=r'$x_1$')
plt.plot(tvalue, x2, label=r'$x_2$')
# Add legend and axes labels
plt.legend(loc=0)
plt.xlabel(r'$t$')
plt.ylabel(r'$x_{t}$')
plt.show()

Using dopri5 to plot a system of ODEs in matrix form

The system of equations I'm interested in plotting is the following:
I was able to plot them modifying an example someone posted by doing the following:
import scipy as sp
import pylab as plt
import numpy as np
import scipy.integrate as spi
#Constants
c13 = 4.2
c14 = 4.2
c21 = 4.3
c32 = 4.4
c34 = 4.4
c42 = 4.4
c43 = 4.4
e12 = 1.9
e23 = 2.5
e24 = 2.2
e31 = 2.0
e41 = 2.0
#Time
t_end = 700
t_start = 0
t_step = 1
t_interval = sp.arange(t_start, t_end, t_step)
#Initial Condition
r = [0.2,0.3,0.3,0.5]
def model(t,r):
Eqs= np.zeros((4))
Eqs[0] = (r[0]*(1-r[0]*r[0]-r[1]*r[1]-r[2]*r[2]-r[3]*r[3])-c21*((r[1]*r[1])*r[0])+e31*((r[2]*r[2])*r[0])+e41*((r[3]*r[3])*r[0]))
Eqs[1] = (r[1]*(1-r[0]*r[0]-r[1]*r[1]-r[2]*r[2]-r[3]*r[3])+e12*((r[0]*r[0])*r[1])-c32*((r[2]*r[2])*r[1])-c42*((r[3]*r[3])*r[1]))
Eqs[2] = (r[2]*(1-r[0]*r[0]-r[1]*r[1]-r[2]*r[2]-r[3]*r[3])-c13*((r[0]*r[0])*r[2])+e23*((r[1]*r[1])*r[2])-c43*((r[3]*r[3])*r[2]))
Eqs[3] = (r[3]*(1-r[0]*r[0]-r[1]*r[1]-r[2]*r[2]-r[3]*r[3])-c14*((r[0]*r[0])*r[3])+e24*((r[1]*r[1])*r[3])-c34*((r[2]*r[2])*r[3]))
return Eqs
ode = spi.ode(model)
ode.set_integrator('dopri5')
ode.set_initial_value(r,t_start)
ts = []
ys = []
while ode.successful() and ode.t < t_end:
ode.integrate(ode.t + t_step)
ts.append(ode.t)
ys.append(ode.y)
t = np.vstack(ts)
x1,x2,x3,x4 = np.vstack(ys).T
plt.subplot(1, 1, 1)
plt.plot(t, x1, 'r', label = 'x1')
plt.plot(t, x2, 'b', label = 'x2')
plt.plot(t, x3, 'g', label = 'x3')
plt.plot(t, x4, 'purple', label = 'x4')
plt.xlim([0,t_end])
plt.legend()
plt.ylim([-0.2,1.5])
plt.show()
This certainly appears to give me the plot I want. However, I want to end up doing stochastic analysis with this set of ODEs, and for that reason, it is much easier to model this if the system of ODEs is written in matrix form (that way, I can easily change the dimension of the noise and see how that affects the ODEs). I understand how mathematically to write the equation in matrix form, but I don't understand how to modify my code so that in the "def model(t,r):" part, it's read as an array/matrix. To convert the equations to matrix form, I can define:
b = np.array([1, 1, 1, 1])
A = np.array([[1, 1+c21, 1-e31, 1-e41],
[1-e12, 1, 1+c32, 1+c42],
[c13+1, 1-e23, 1, 1+c43],
[c14+1, 1-e24, 1+c34, 1]])
And then the system of equations would be (where x is the vector (x1,x2,x3,x4)):
x(t) = diag(x)[b^{T}-Adiag(x)x]
So my question is: How do I modify where I defined my ODEs so that I can enter them as a matrix instead of writing out each equation individually? (this would also make it easier if I later look at a system with more than 4 dimensions)
Using the implemented preference for numpy.array operations to act element-wise (in contrast to numpy.matrix operations that operate in matrix fashion) the formula for the system of equations is simply
def model(t,x):
return x*(b-A.dot(x*x))
x*x produces the vector of element-wise squares, x**2 would be another option, A.dot(x2) performs the matrix-vector product for numpy.array objects, and x*(b-...) is again the vector-valued element-wise product of the two operand vectors.
Using u=x*x as variables reduces the system to
dotu = 2*u*(b-A.dot(u))
thus it has one degree less and is quadratic in u which may help in the examination of the stationary points. I suspect that they all are hyperbolic, so that there is no asymptotically stable solution.
Using the substitution u=log(x) and thus
dotu = b-A.dot(exp(2*u))
hides the stationary points at minus infinity, thus the analytical value of this substitution may be limited. However, the positivity of x=exp(u) is built-in, which may allow for more aggressive numerical methods or provide a bit more accuracy using the same cautiousness as before.

Using dopri5 for ODE Integration and Stochastic Simulations

I'm still very much a beginner in python, but had some questions about running simulations (I apologize if this is the wrong area to post my question in which case, if someone would kindly show me where it would be better suited, I will gladly move it). My goal is to simulate the following system of differential equations:
Because the dynamical system was experiencing stiffness, the odeint method of integration I learned kept having problems, so I tried to integrate it another way, namely using the dopri5 method I have heard about. It seems to work but I've noticed some odd behavior in the plots. The code I was using is as follows:
import scipy as sp
import pylab as plt
import numpy as np
import scipy.integrate as spi
#Constants
c13 = 6.2
c14 = 1.0
c21 = 7.3
c32 = 2.4
c34 = 12.7
c42 = 5.7
c43 = 5.0
e12 = 1.5
e23 = 2.5
e24 = 2.0
e31 = 3.0
e41 = 4.8
#Time
t_end = 300.
t_start = 0.
t_step = 1.
t_interval = sp.arange(t_start, t_end, t_step)
#Initial Condition
ic = [0.6,0.3,0.2,0.5]
#Noise
sigma1 = 0.1
sigma2 = 0.1
sigma3 = 0.1
sigma4 = 0.1
def model(t,ic):
Eqs= np.zeros((4))
Eqs[0] = ic[0]+(ic[0]*(1-ic[0]*ic[0]-ic[1]*ic[1]-ic[2]*ic[2]-ic[3]*ic[3])-c21*((ic[1]*ic[1])*ic[0])+e31*((ic[2]*ic[2])*ic[0])+e41*((ic[3]*ic[3])*ic[0]))
Eqs[1] = ic[1]+(ic[1]*(1-ic[0]*ic[0]-ic[1]*ic[1]-ic[2]*ic[2]-ic[3]*ic[3])+e12*((ic[0]*ic[0])*ic[1])-c32*((ic[2]*ic[2])*ic[1])-c42*((ic[3]*ic[3])*ic[1]))
Eqs[2] = ic[2]+(ic[2]*(1-ic[0]*ic[0]-ic[1]*ic[1]-ic[2]*ic[2]-ic[3]*ic[3])-c13*((ic[0]*ic[0])*ic[2])+e23*((ic[1]*ic[1])*ic[2])-c43*((ic[3]*ic[3])*ic[2]))
Eqs[3] = ic[3]+(ic[3]*(1-ic[0]*ic[0]-ic[1]*ic[1]-ic[2]*ic[2]-ic[3]*ic[3])-c14*((ic[0]*ic[0])*ic[3])+e24*((ic[1]*ic[1])*ic[3])-c34*((ic[2]*ic[2])*ic[3]))
return Eqs
ode = spi.ode(model)
ode.set_integrator('dopri5',method='bdf')
ode.set_initial_value(ic,t_start)
ts = []
ys = []
while ode.successful() and ode.t < t_end:
ode.integrate(ode.t + t_step)
ts.append(ode.t)
ys.append(ode.y)
t = np.vstack(ts)
x1,x2,x3,x4 = np.vstack(ys).T
plt.subplot(4, 1, 1)
plt.plot(t, x1, 'b')
plt.xlim([0,t_end])
plt.subplot(4, 1, 2)
plt.plot(t, x2, 'r')
plt.xlim([0,t_end])
plt.subplot(4, 1, 3)
plt.plot(t, x3, 'g')
plt.xlim([0,t_end])
plt.subplot(4, 1, 4)
plt.plot(t, x4, 'purple')
plt.xlim([0,t_end])
plt.xlabel('Time')
plt.show()
However, the plot looks like this:
What I don't understand is why the plots reach a maximum of about 1.4. I did some analytical work and cross-referenced it with another paper that used these equations, and the maximum value it should reach is 1. Is it something in the dopri5 code that makes it go above 1? I also don't understand why there's a bit of spike appearing early on in each of the blue peaks (x1).
My other question is that I would like to do some stochastic simulation with these equations. So for this part, the equations I would like to work with are of the form:
dx_i = ()dt + sigma_i * x_i * dW_t
where dW_t is white noise. However, when I tried to modify the above equation and do this for example:
Eqs[0] = ic[0]+(ic[0]*(1-ic[0]*ic[0]-ic[1]*ic[1]-ic[2]*ic[2]-ic[3]*ic[3])-c21*((ic[1]*ic[1])*ic[0])+e31*((ic[2]*ic[2])*ic[0])+e41*((ic[3]*ic[3])*ic[0]))+sigma1*ic[0]*np.random.normal(0,1)
the code fails to run properly. It seems that what I wrote is not the proper method for making these equations stochastic. What is the best way to put stochastic perturbations into these equations? I've recently heard of something called PyS3DE but I know nothing about it.

Categories