How to implement 'solve_ivp' with vectorized='True' in python - python

I've been trying to solve a set of differential equations using solve_ivp. The Jacobian matrix of the system is the A as you can see below. I wanted to enable the option vectorized='True' but unfortunately i do not know how to modify the present code to vectorize the Jacobian matrix A. Does anyone know how this can be done?
# imports
import numpy as np
import scipy.sparse as sp
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
# grid sizing
R=0.05 #sphere radius
N=1000#number of points
D=0.00002 #diffusion coefficient
k=10 # Arrhenius
Cs=1.0 # Boundary concentration
C0=0.0 # Initial concentration
time_constant=R**2.0/D
dr=R/(N-1)
# Algebra simplification
a=D/dr**2
Init_conc=np.linspace(0,0,N)
B=np.zeros(N)
B[N-1]=Cs*(a+a/(N-1))
#
e1 = np.ones(N)
e2 = np.ones(N)
e3 = np.ones(N)
#
#
#
e1[0]=-k-6*a
e1[1:]=-k-2*a
#
#
e2[1]=6*a
for i in range(2,N) :
e2[i]=a+a/(i-1)
#
#
#
for i in range (0,N-1) :
e3[i]=a-a/(i+1)
A = sp.spdiags([e3,e1,e2],[-1,0,1],N,N,format="csc")
def dc_dt(t,C) :
dc=A.dot(C)+B
return dc
# Solving the system, I want to implement the same thing with vectorized='True'
OutputTimes=np.linspace(0,0.2*time_constant,100)
ans=solve_ivp(dc_dt,(0,0.2*time_constant),Init_conc,method='RK45',t_eval=OutputTimes,vectorized='False')
print (ans)

Please have a look at this answer, the explanation is thorough. For your code in particular, please see below for updated snippet and figure. It is not obvious that vectorize is providing any speed-up. However, providing A for the keyword jac makes a difference. But I guess it is only valid if A is constant?
# imports
import numpy as np
import scipy.sparse as sp
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt # noqa
def dc_dt(t, C):
print(C.shape)
if len(C.shape) == 1:
return np.squeeze(A.dot(C)) + B
else:
return A.dot(C) + np.transpose(np.tile(B, (C.shape[1], 1)))
# return np.squeeze(A.dot(C)) + B
# grid sizing
R = 0.05 # sphere radius
N = 1000 # number of points
D = 0.00002 # diffusion coefficient
k = 10 # Arrhenius
Cs = 1.0 # Boundary concentration
C0 = 0.0 # Initial concentration
time_constant = R**2.0 / D
dr = R / (N - 1)
# Algebra simplification
a = D / dr**2
Init_conc = np.repeat(0, N)
B = np.zeros(N)
B[-1] = Cs * (a + a / (N - 1))
e1 = np.ones(N)
e2 = np.ones(N)
e3 = np.ones(N)
e1[0] = -k - 6 * a
e1[1:] = -k - 2 * a
e2[1] = 6 * a
for i in range(2, N):
e2[i] = a + a / (i - 1)
for i in range(0, N - 1):
e3[i] = a - a / (i + 1)
A = sp.spdiags([e3, e1, e2], [-1, 0, 1], N, N, format="csc")
# Solving the system, I want to implement the same thing with vectorized='True'
OutputTimes = np.linspace(0, 0.2 * time_constant, 10000)
ans = solve_ivp(dc_dt, (0, 0.2 * time_constant), Init_conc,
method='BDF', t_eval=OutputTimes, jac=A, vectorized=True)
plt.plot(np.arange(N), ans.y[:, 0])
plt.plot(np.arange(N), ans.y[:, 1])
plt.plot(np.arange(N), ans.y[:, 10])
plt.plot(np.arange(N), ans.y[:, 20])
plt.plot(np.arange(N), ans.y[:, 50])
plt.plot(np.arange(N), ans.y[:, -1])
plt.show()

Related

CFD simulation (with multiple for loops and matrix operations) is very slow to run. Looking to replace with faster numpy functions (or alternative)

As mentioned above, the function below works, however its very slow. I am very interested in using faster/optimised numpy (or other) vectorized alternatives. I have not posted the entire script here due to it being too large.
My specific question is - are there suitable numpy (or other) functions that I can use to 1) reduce run time and 2) reduce code volume of this function, specifically the for loop?
Edit: mass, temp, U and dpdh are functions that carry out simple algebraic calculations and return constants
def my_system(t, y, n, hIn, min, mAlumina, cpAlumina, sa, V):
dydt = np.zeros(3 * n) #setting up zeros array for solution (solving for [H0,Ts0,m0,H1,Ts1,m1,H2,Ts2,m2,..Hn,Tsn,mn])
# y = [h_0, Ts_0, m_0, ... h_n, Ts_n, m_n]
# y[0] = hin
# y[1] = Ts0
# y[2] = minL
i=0
## Using thermo
T = temp(y[i],P) #initial T
m = mass(y[i],P) #initial m
#initial values
dydt[i] = (min * (hIn - y[i]) + (U(hIn,P,min) * sa * (y[i + 1] - T))) / m # dH/dt (eq. 2)
dydt[i + 1] = -(U(hIn,P,min) * sa * (y[i + 1] - T)) / (mAlumina * cpAlumina) # dTs/dt from eq.3
dmdt = dydt[i] * dpdh(y[i], P) * V # dm/dt (holdup variation) eq. 4b
dydt[i + 2] = min - dmdt # mass flow out (eq.4a)
for i in range(3, 3 * n, 3): #starting at index 3, and incrementing by 3 because we are solving for 'triplets' [h,Ts,m] in each loop
## Using thermo
T = temp(y[i],P)
m = mass(y[i],P)
# [h, TS, mdot]
dydt[i] = (dydt[i-1] * (y[i - 3] - y[i]) + (U(y[i-3], P, dydt[i-1]) * sa * (y[i + 1] - T))) / m # dH/dt (eq.2), dydt[i-1] is the mass of the previous tank
dydt[i + 1] = -(U(y[i-3], P, dydt[i-1]) * sa * (y[i + 1] - T)) / (mAlumina * cpAlumina) # dTs/dt eq. (3)
dmdt = dydt[i] * dpdh(y[i], P) * V # Equation 4b
dydt[i + 2] = dydt[i-1] - dmdt # Equation 4a
return dydt
The functions mass, temp, U, and dpdh used inside the my_system function all take numbers as input, perform some simple algebraic operation and return a number (no need to optimise these I am just providing them for further context)
def temp(H,P):
# returns temperature given enthalpy (after processing function)
T = flasher.flash(H=H, P=P, zs=zs, retry=True).T
return T
def mass(H, P):
# returns mass holdup in mol
m = flasher.flash(H=H, P=P, zs=zs, retry=True).rho()*V
return m
def dpdh(H, P):
res = flasher.flash(H=H, P=P, zs=zs, retry=True)
if res.phase_count == 1:
if res.phase == 'L':
drho_dTf = res.liquid0.drho_dT()
else:
drho_dTf = res.gas.drho_dT()
else:
drho_dTf = res.bulk._equilibrium_derivative(of='rho', wrt='T', const='P')
dpdh = drho_dTf/res.dH_dT_P()
return dpdh
def U(H,P,m):
# Given T, P, m
air = Mixture(['nitrogen', 'oxygen'], Vfgs=[0.79, 0.21], H=H, P=P)
mu = air.mu*1000/mWAir #mol/m.s
cp = air.Cpm #J/mol.K
kg = air.k #W/m.K
g0 = m/areaBed #mol/m2.s
a = sa*n/vTotal #m^2/m^3 #QUESTIONABLE
psi = 1
beta = 10
pr = (mu*cp)/kg
re = (6*g0)/(a*mu*psi)
hfs = ((2.19*(re**1/3)) + (0.78*(re**0.619)))*(pr**1/3)*(kg)/diameterParticle
h = 1/((1/hfs) + ((diameterParticle/beta)/kAlumina))
return h
Reference Image:
enter image description here
For improving the speed, you can see Numba, which is useable if you use NumPy a lot but not every code can be used with Numba. Apart from that, the formulation of the equation system is confusing. You are solving 3 equations and adding the result to a single dydt list by 3 elements each. You can simply create three lists, solve each equation and add them to their respective list. For this, you need to re-write my_system as:
import numpy as np
def my_system(t, RHS, hIn, Ts0, minL, mAlumina, cpAlumina, sa, V):
# get initial boundary condition values
y1 = RHS[0]
y2 = RHS[1]
y3 = RHS[2]
## Using thermo
T = # calculate T
m = # calculate m
# [h, TS, mdot] solve dy1dt for h, dy2dt for TS and dy3dt for mdot
dy1dt = # dH/dt (eq.2), y1 corresponds to initial or previous value of dy1dt
dy2dt = # dTs/dt eq. (3), y2 corresponds to initial or previous value of dy2dt
dmdt = # Equation 4b
dy3dt = # Equation 4a, y3 corresponds to initial or previous value of dy3dt
# Left-hand side of ODE
LHS = np.zeros([3,])
LHS[0] = dy1dt
LHS[1] = dy2dt
LHS[2] = dy3dt
return LHS
In this function, you can pass RHS as a list with initial values ([dy1dt, dy2dt, dy3dt]) which will be unpacked as y1, y2, and y3 respectively and use them for respective differential equations. The solved equations (next values) will be saved to dy1dt, dy2dt, and dy3dt which will be returned as a list LHS.
Now you can solve this using scipy.integrate.odeint. Therefore, you can leave the for loop structure and solve the equations by using this method as follows:
hIn = #some val
Ts0 = #some val
minL = #some val
mAlumina = #some vaL
cpAlumina = #some val
sa = #some val
V = #some val
P = #some val
## Using thermo
T = temp(hIn,P) #initial T
m = mass(hIn,P) #initial m
#initial values
y01 = # calculate dH/dt (eq. 2)
y02 = # calculate dTs/dt from eq.3
dmdt = # calculate dm/dt (holdup variation) eq. 4b
y03 = # calculatemass flow out (eq.4a)
n = # time till where you want to solve the equation system
y0 = [y01, y02, y03]
step_size = 1
t = np.linspace(0, n, int(n/step_size)) # use that start time to which initial values corresponds
res = odeint(my_sytem, y0, t, args=(hIn, Ts0, minL, mAlumina, cpAlumina, sa, V,), tfirst=True)
print(res[:,0]) # print results for dH/dt
print(res[:,1]) # print results for dTs/dt
print(res[:,2]) # print results for Equation 4a
Here, I have passed all the initial values as y0 and chosen a step size of 1 which you can change as per your need.

Generating phase plane/portrait using scipy

This code allows me to plot the the mutualistic relationship of two species. As the code gives, the graph is logistical. Given this, I should be able to see how its phase portrait looks like. My question is: how and where should I start if I need to show its phase plane/portrait using scipy?
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import pylab as p
# parameters
r1 = 1.0
r2 = 0.5
e1 = 1
e2 = 0.75
a12 = 0.25
a21 = 0.25
# initial population
N10 = 1
N20 = 1
# store initial values in an array
X0 = [N10, N20]
# model/equation
def mutualism(X,t):
N1, N2 = X
dX = np.zeros(2) # initialize dX as array containing three zeroes
dX[0] = N1 * (r1 - (e1 * N1) + (a12 * N2)) # equation for dN10dt
dX[1] = N2 * (r2 - (e2 * N2) + (a21 * N1)) # equation for dN20dt
return dX
# set time length
t = np.linspace(0, 100,100*10)
# odeint returns an array containing values for each value of t
X = odeint(mutualism,X0,t)
N1 = X[:,0]; N2 = X[:,1]
#plot
f1 = p.figure()
p.plot(t, N1, 'r-', label='Species 1')
p.plot(t, N2 , 'b-', label='Species 2')
p.grid()
p.legend(loc='best')
p.xlabel('Time')
p.ylabel('Population')
p.title('Mutualism')
So far I've tried using matplotlib in generating the phase portrait but to no avail, it did not work.

Improve precision in solution for Sylvester equation with python (scipy)

I am trying to solve numerically the following Sylvester equation (eq. (11) from arXiv:1906.08808)
where W+ is defined as exp(K*t). I have defined the following function to compute V(t) at a given time t.
Edited (after comment from Warren Weckesser)
import numpy as np
from scipy.linalg import expm, solve_sylvester
def covariance_matrix(K, V0, D, t, precision=precision):
"""
It computes the covariance matrix at time t.
See eq. (11) in arXiv:1906.08808
"""
Wp = expm(K*t)
WpT = Wp.transpose()
KT = K.transpose()
C = -D + K # Wp # V0 # WpT + Wp # V0 # WpT # KT + Wp # D # WpT
return solve_sylvester(K, KT, C)
However, it is not working as expected. For example, if I call the function with t=0, I don't recover the initial V0 as it should (see screenshot below)
This looks like some kind of numerical error. I have tried different precisions (np.float64 and np.float128) but I don't see any difference. My guess is that the precision is lost when using expm from scipy.linalg.
Any ideas how to change this code to get better precision?
Expanded (also after edition)
The problem with these small errors (order of 1e-13 at time 0) is that later I am using this matrix to compute some quantity that is very sensible to this little differences.
I leave here the exact code that I am using later with this matrix.
def entanglement(V):
"""
It gives the logarithmic negativity computed from the covariance matrix.
See: Phys. Rev. A 65, 032314 (2002) for details.
"""
Ia = V[:2, :2]
Ib = V[2:, 2:]
L = V[:2, 2:]
s = det(Ia) + det(Ib) - 2 * det(L)
v = np.sqrt((s - np.sqrt(s**2 - 4 * det(V))) / 2)
ent = -np.log2(2*v)
if ent > 0:
return ent
else:
return 0
Minimal, reproducible example
#!/usr/bin/python3
import numpy as np
from scipy.linalg import expm, det, solve_sylvester
precision = np.float128
def drift_matrix(w, eta, Q):
return np.array([[0, w, 0, 0],
[-w*(1-eta), -w/Q, -w*eta, 0],
[0, 0, 0, w],
[-w*eta, 0, -w*(1-eta), -w/Q]], dtype=precision)
def init_covariant_matrix(n, s=0):
return np.diag(np.ones(4, dtype=precision) * (n + 1/2) * np.cosh(2*s))
def diagonal_matrix(n, w, Q):
return np.diag(np.array([0,1,0,1], dtype=precision) * w/Q * (1 + 2*n))
def covariance_matrix(K, V0, D, t):
Wp = expm(K*t)
WpT = Wp.transpose()
KT = K.transpose()
C = -D + K # Wp # V0 # WpT + Wp # V0 # WpT # KT + Wp # D # WpT
return solve_sylvester(K, KT, C)
w = precision(0.1)
eta = precision(1.36e-6) / w**2
n = precision(0)
Q = precision(1e4)
K = drift_matrix(w, eta, Q)
V0 = init_covariant_matrix(n, 1.73)
D = diagonal_matrix(n, w, Q)
V0_1 = covariance_matrix(K, V0, D, 0)
# What I want (ideally)
np.all(np.abs(V0 - V0_1) < np.finfo(np.float128).resolution)
# I would still be happy if...
np.all(np.abs(V0 - V0_1) < np.finfo(np.float64).resolution)

Splitting the state vector when solving a series of odes using python odeint scipy

I'm converting my code from Matlab to Python and stuck on how to split the state vector such that the result returns the two solution. I have a vector and a single value for the two initial conditions and I expect as the final result a matrix and a vector.
I tried joining the initial conditions (y0 = [c_pt_0, x_0]) in the same manner as the solution (soln = [dfdt,dcdt]) (which is shown below in the code). I also tried a similar approach that is used in matlab, which is concatenating the the initial conditions to one single array and unpacking the results but I think the problem is in the dimensions.
#Basic imports
import numpy as np
import pylab
import matplotlib. pyplot as plt
import scipy
from scipy.integrate import odeint
import matplotlib.pyplot as plt
# define parameters
pi = 3.14159265
V_m = 9.09
m_V__M_Pt = 1e6/195.084
rho = 21.45
R0 = 10**(-8.19)
k_d = 10**(-13)
k_r = 10**(-5)
S = 0.314 #distribution parameter
M = 0.944 #distribution parameter
## geometry
# Finite Volume Method with equidistant elements
r_max = 30.1e-9 #maximum value
n = 301 #number of elements of FVM
dr = r_max/n #length of elements, equidistant
ini_r = np.linspace(5e-10,r_max,n+1) #boundaries of elements
mid_r = ini_r[0:n]+dr/2 #center of elements
## initial conditions
#initial distribution
x0 = 1/(S*np.sqrt(2*pi)*mid_r*1e9)*np.exp((-(np.log(mid_r*1e9)-M)**2)/(2*S**2))
c_pt_0 = 0
y0 = [x0, c_pt_0]
MN_0 = scipy.trapz(np.power(mid_r, 3)*x0,
x=mid_r) # initial mass
M_0 = 4/3*pi*rho*MN_0
def f(y, t):
r = y[0]
c_pt = y[1]
#materials balance
drdt = V_m * k_r * c_pt * np.exp(-R0/ mid_r) - V_m * k_d * np.exp(R0/ mid_r)
dmdt = 4*pi*rho*mid_r**2*drdt
dMdt = np.trapz(r*dmdt, x=mid_r)
dcdt = m_V__M_Pt*(-dMdt)/M_0
dfdt = -(np.gradient(r*drdt, dr))
soln = [dfdt, dcdt]
return soln
#------------------------------------------------------
#define timespace
time = np.linspace(0, 30, 500)
#solve ode system
sln_1 = odeint (f , y0 , time,
rtol = 1e-3, atol = 1e-5)
pylab.plot(mid_r, sln_1[1,:], color = 'r', marker = 'o')
pylab.plot(mid_r, sln_1[-1,:], color = 'b', marker = 'o')
plt.show()
Traceback:
ValueError: setting an array element with a sequence.
Any help is much appreciated.
EDIT: ADDED MATLAB CODE
Here is the MATLAB code that works that I want to convert to python where the state vector is split. I have three files (one main, the f function, and the parameters). Please excuse any face palm coding errors but I do appreciate any suggestions even for this.
modified_model.m:
function modified_model
% import parameters
p = cycling_parameters;
% initial conditions
c_pt_0 = 0;
y0 = [p.x0; c_pt_0];
% call integrator
options_ODE=odeset('Stats','on', 'RelTol',1e-3,'AbsTol',1e-5);
[~, y] = ode15s(#(t,y) f(t, y, p), p.time, y0, options_ODE);
%% Post processing
% split state vector
r = y(:,1:p.n);
c_Pt = y(:,p.n+1);
%% Plot results
figure
hold on;
plot(p.r_m, r(1,:));
plot(p.r_m, r(end,:));
xlabel({'size'},'FontSize',15)
ylabel({'counts'},'FontSize',15)
f.m
function soln = f(~, y, p)
%split state vector
r = y(1:p.n);
c_pt = y(p.n+1);
% materials balance
drdt = p.Vm_Pt.*p.k_rdp.*c_pt.*exp(-p.R0./p.r_m) - p.Vm_Pt.*p.k_dis.*exp(p.R0./p.r_m);
dmdt = 4*pi*p.rho*p.r_m.^2.*drdt;
dMdt = trapz(p.r_m, r.*dmdt);
dcdt = p.I_V*p.m_V__M_Pt*(-dMdt)/p.M_0;
dfdt = - gradient(r.*drdt,p.dr);
soln = [dfdt; dcdt];
and the parameters file: cycling_parameters.m
function p=cycling_parameters
p.M = 195.084;
p.rho = 21.45;
p.time = linspace(0, 30, 500);
p.m_V__M_Pt = 1e6/p.M;
p.Vm_Pt = 9.09;
p.R0_log = -8.1963;
p.k_dis_log = -13;
p.k_rdp_log = -11;
p.R0 = 10^(p.R0_log);
p.k_dis = 10^(p.k_dis_log);
p.k_rdp = 10^(p.k_rdp_log);
%%% geometry %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Finite Volume Method with equidistant elements
p.r_max = 10.1e-9; % [m] maximum radius of PRD
p.n = 301; % number of elements of FVM
p.dr = p.r_max/p.n; % [m] length of elements, equidistant
p.r = linspace(5e-10,p.r_max,p.n+1)'; % [m] boundaries of elements
p.r_m = p.r(1:p.n)+p.dr/2; % [m] center of elements
%log normal initial distribution
S = 0.314;
M = 0.944;
p.x0 = 1./(S.*sqrt(2.*pi).*p.r_m*1e9).*exp((-(log(p.r_m*1e9)-M).^2)./(2.*S.^2));
p.r_squared = p.r_m.^2; % [m^2] squares of the radius (center of elements)
p.r_cubed = p.r_m.^3; % [m^3] cubes of the radius (center of elements)
p.MN_0 = trapz(p.r_m, p.r_cubed.*p.x0); % Eq. 2.11 denominator
p.M_0 = 4/3*pi*p.rho*p.MN_0;
p.I_V = 1; %ionomer volume fraction in the catalyst layer
After looking at both codes, the issue is that the odeint solver only takes 1D array inputs and your y0 is [int, array(300,)] and odeint can't work with that. However, you can merge the y0 into a 1D array and then split it up in the function you are integrating over to do the calculation then recombine as the output. Here's a working code of that:
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
class P:
def __init__(self, S, M):
self.M = 195.084
self.rho = 21.45
self.m_V__M_Pt = (1*10**6)/self.M
self.Vm_Pt = 9.09
self.R0_log = -8.1963
self.k_dis_log = -13
self.k_rdp_log = -11
self.R0 = 10**(self.R0_log)
self.k_dis = 10**(self.k_dis_log)
self.k_rdp = 10**(self.k_rdp_log)
self.r_max = 10.1*10**(-9)
self.n = 301
self.dr = self.r_max / self.n
self.r = np.linspace(5*10**(-10), self.r_max, self.n)
self.r_m = self.r[0:self.n+1]+self.dr/2
self.x0 = self.compute_x0(S, M)
self.r_squared = np.power(self.r_m, 2)
self.r_cubed = np.power(self.r_m, 3)
self.MN_0 = np.trapz(self.r_m, np.multiply(self.r_cubed, self.x0))
self.M_0 = (4 / 3)* np.pi * self.rho * self.MN_0
self.I_V = 1
def compute_x0(self, S, M):
p1 = np.multiply(2, np.power(S, 2))
p2 = np.multiply(S, np.sqrt(np.multiply(2, np.pi)))
p3 = np.log(self.r_m*1*10**(9)) - M
p4 = np.multiply(p2, self.r_m*10**(9))
p5 = np.power(-p3, 2)
p6 = np.multiply(p4, np.exp(np.divide(p5,p1)))
p7 = np.divide(1, p6)
return p7
def cycling_parameters():
S = 0.314
M = 0.944
p = P(S, M)
return p
def f(y, t):
p = cycling_parameters()
c_pt = y[0]
r = np.delete(y, 0)
p1 = np.multiply(p.Vm_Pt, p.k_rdp)
p2 = np.multiply(p1, c_pt)
p3 = np.multiply(p.Vm_Pt, p.k_dis)
drdt = np.multiply(p2, np.exp(np.divide(-p.R0, p.r_m))) - np.multiply(p3, np.exp(np.divide(p.R0, p.r_m)))
dmdt = np.multiply(4*np.pi*p.rho*np.power(p.r_m, 2), drdt)
p4 = np.multiply(r, dmdt)
dMdt = np.trapz(p.r_m, p4)
dcdt = p.I_V*p.m_V__M_Pt*(-dMdt)/p.M_0
p5 = np.multiply(r, drdt)
dfdt = - np.gradient(p5,p.dr)
ans = np.insert(dfdt, 0, dcdt)
return ans
def modified_model():
p = cycling_parameters()
c_pt_0 = 0
y0 = np.insert(p.x0, 0, c_pt_0)
t = np.linspace(0, 30, 500)
ans = odeint(f, y0, t, rtol = 1e-3, atol = 1e-5)
r = ans[:, 1:p.n+1]
c_Pt = ans[:, 0]
print(r)
print(c_Pt)
plt.plot(p.r_m, r[0, :], color='r', linewidth=0.5)
plt.plot(p.r_m, r[r.shape[0]-1, :], color='b', linewidth=0.5)
plt.show()
if __name__ == '__main__':
modified_model()
Python plot (what this script outputs):
Original Matlab Plot:

How to use if statement in a differential equation (SciPy)?

I am trying to solve a differential equation with Python.
In this two system differential equation if the value of first variable (v) is more than a threshold (30) it should be reset to another value (-65). Below I put my code. The problem is that the value of first variable after reaching 30 remains constant and won't reset to -65. These equations describe the dynamics of a single neuron. The equations are taken from this website and this PDF file.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter
from scipy.integrate import odeint
plt.close('all')
a = 0.02
b = 0.2
c = -65
d = 8
i = 0
p = [a,b,c,d,i]
def fun(u,tspan,*p):
du = [0,0]
if u[0] < 30: #Checking if the threshold has been reached
du[0] = (0.04*u[0] + 5)*u[0] + 150 - u[1] - p[4]
du[1] = p[0]*(p[1]*u[0]-u[1])
else:
u[0] = p[2] #reset to -65
u[1] = u[1] + p[3]
return du
p = tuple(p)
y0 = [0,0]
tspan = np.linspace(0,100,1000)
sol = odeint(fun, y0, tspan, args=p)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
plt.plot(tspan,sol[:,0],'k',linewidth = 5)
plt.plot(tspan,sol[:,1],'r',linewidth = 5)
myleg = plt.legend(['v','u'],\
loc='upper right',prop = {'size':28,'weight':'bold'}, bbox_to_anchor=(1,0.9))
The solution looks like:
Here is the correct solution by Julia, here u1 represent v:
This is the Julia code:
using DifferentialEquations
using Plots
a = 0.02
b = 0.2
c = -65
d = 8
i = 0
p = [a,b,c,d,i]
function fun(du,u,p,t)
if u[1] <30
du[1] = (0.04*u[1] + 5)*u[1] + 150 - u[2] - p[5]
du[2] = p[1]*(p[2]*u[1]-u[2])
else
u[1] = p[3]
u[2] = u[2] + p[4]
end
end
u0 = [0.0;0.0]
tspan = (0.0,100)
prob = ODEProblem(fun,u0,tspan,p)
tic()
sol = solve(prob,reltol = 1e-8)
toc()
plot(sol)
Recommended solution
This uses events and integrates separately after each discontinuity.
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_ivp
a = 0.02
b = 0.2
c = -65
d = 8
i = 0
p = [a,b,c,d,i]
# Define event function and make it a terminal event
def event(t, u):
return u[0] - 30
event.terminal = True
# Define differential equation
def fun(t, u):
du = [(0.04*u[0] + 5)*u[0] + 150 - u[1] - p[4],
p[0]*(p[1]*u[0]-u[1])]
return du
u = [0,0]
ts = []
ys = []
t = 0
tend = 100
while True:
sol = solve_ivp(fun, (t, tend), u, events=event)
ts.append(sol.t)
ys.append(sol.y)
if sol.status == 1: # Event was hit
# New start time for integration
t = sol.t[-1]
# Reset initial state
u = sol.y[:, -1].copy()
u[0] = p[2] #reset to -65
u[1] = u[1] + p[3]
else:
break
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
# We have to stitch together the separate simulation results for plotting
ax.plot(np.concatenate(ts), np.concatenate(ys, axis=1).T)
myleg = plt.legend(['v','u'])
Minimum change "solution"
It appears as though your approach works just fine with solve_ivp.
Warning I think in both Julia and solve_ivp, the correct way to handle this kind of thing is to use events. I believe the approach below relies on an implementation detail, which is that the state vector passed to the function is the same object as the internal state vector, which allows us to modify it in place. If it were a copy, this approach wouldn't work. In addition, there is no guarantee in this approach that the solver is taking small enough steps that the correct point where the limit is reached will be stepped on. Using events will make this more correct and generalisable to other differential equations which perhaps have lower gradients before the discontinuity.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FormatStrFormatter
from scipy.integrate import solve_ivp
plt.close('all')
a = 0.02
b = 0.2
c = -65
d = 8
i = 0
p = [a,b,c,d,i]
def fun(t, u):
du = [0,0]
if u[0] < 30: #Checking if the threshold has been reached
du[0] = (0.04*u[0] + 5)*u[0] + 150 - u[1] - p[4]
du[1] = p[0]*(p[1]*u[0]-u[1])
else:
u[0] = p[2] #reset to -65
u[1] = u[1] + p[3]
return du
y0 = [0,0]
tspan = (0,100)
sol = solve_ivp(fun, tspan, y0)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
plt.plot(sol.t,sol.y[0, :],'k',linewidth = 5)
plt.plot(sol.t,sol.y[1, :],'r',linewidth = 5)
myleg = plt.legend(['v','u'],loc='upper right',prop = {'size':28,'weight':'bold'}, bbox_to_anchor=(1,0.9))
Result

Categories

Resources