Implementing numerical integration using scipy.integrate.nquad - python

I have this 2-dimensional integral with dependent limits. The function can be defined in Python as
def func(gamma, u2, u3):
return (1-1/(1+gamma-u3-u2))*(1/(1+u2)**2)*(1/(1+u3)**2)
where the limits of u3 is from 0 to gamma (a positive real number), and the limits of u2 is from 0 to gamma-u3.
How can I implement this using scipy.integrate.nquad? I tried to read the documentation, but it was not easy to follow, especially I am relatively new to Python.
Extension: I would like to implement a numerical integration for an arbiraty K, where the integrand in this case is given by (1-1/(1+gamma-uk-....-u2))*(1/(1+uK)**2)*...*(1/(1+u2)**2). I wrote the function that takes a dynamic number of arguments as follows:
def integrand(gamma, *args):
'''
inputs:
- gamma
- *args = (uK, ..., u2)
Output:
- (1-1/(1+gamma-uk-....-u2))*(1/(1+uK)**2)*...*(1/(1+u2)**2)
'''
L = len(args)
for ll in range(0, L):
gamma -= args[ll]
func = 1-1/(1+gamma)
for ll in range(0, L):
func *= 1/((1+args[ll])**2)
return func
However, I am not sure how to do the same for the ranges, where I will have one function for the ranges, where uK ranges from 0 to gamma, u_{K-1} ranges from 0 to gamma-uK, ...., u2 ranges from 0 to gamma-uK-...-u2.

Here is a simpler method using scipy.integrate.dblquad instead of nquad:
Return the double (definite) integral of func(y, x) from x = a..b and
y = gfun(x)..hfun(x).
from scipy.integrate import dblquad
def func(u2, u3, gamma):
return (1-1/(1+gamma-u3-u2))*(1/(1+u2)**2)*(1/(1+u3)**2)
gamma = 10
def gfun(u3):
return 0
def hfun(u3):
return gamma-u3
dblquad(func, 0, gamma, gfun, hfun, args=(gamma,))
It seems that gfun and hfun do not accept the extra arguments, so gamma has to be a global variable.
Using nquad, after many trial and error:
from scipy.integrate import nquad
def func(u2, u3, gamma):
return (1-1/(1+gamma-u3-u2))*(1/(1+u2)**2)*(1/(1+u3)**2)
def range_u3(gamma):
return (0, gamma)
def range_u2(u3, gamma):
return (0, gamma-u3)
gamma = 10
nquad(func, [range_u2, range_u3], args=(gamma,) )
Useful quote from the source code of tplquad:
# nquad will hand (y, x, t0, ...) to ranges0
# nquad will hand (x, t0, ...) to ranges1
And from the nquad documentation, the order of the variables is reversed (same for dblquad):
Integration is carried out in order. That is, integration over x0 is the innermost integral, and xn is the outermost
Generic case with k nested integrations:
from scipy.integrate import nquad
import numpy as np
def func(*args):
gamma = args[-1]
var = np.array(args[:-1])
return (1-1/(1+gamma-np.sum(var)))*np.prod(((1+var)**-2))
def range_func(*args):
gamma = args[-1]
return (0, gamma-sum(args[:-1]))
gamma, k = 10, 2
nquad(func, [range_func]*k, args=(gamma,) )

Related

How do I use scipy curve_fit with a custom objective function?

I wish to do a curve fit to some tabulated data using my own objective function, not the in-built normal least squares.
I can make the normal curve_fit work, but I can't understand how to properly formulate my objective function to feed it into the method.
I am interested in knowing the values of my fitted curve at each tabulated x value.
x = np.array([-5.0,-4.5,-4.0,-3.5,-3.0,-2.5,-2.0,-1.5,-1.0,-0.5,0.0,0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5,7.0,7.5,8.0,8.5,9.0,9.5,10.0])
y = np.array([300,300,1000,350,340,1230,500,360,360,920,365,365,350,1000,375,1050,380,385,385,390,400,395,780,410,420,420,415,435,440,435,455])
e = np.array([math.sqrt(i) for i in y]) #uncertainty in y values
def test_func(x, a0, a1):
""" This is the function I want to fit to my data """
return a0 + a1*x
def norm_residual(test_func, x, y, e, params):
""" This calculates the normalised residuals, given the tabulated data and function parameters"""
yhat = test_func(x,*params)
z = (y-yhat)/e
return z
def f(z):
""" This modifies the normalised residual value, depending on it's sign."""
if z <= 0:
return z**2
else:
return 6*np.log(2*z/(np.sqrt(math.pi) * sp.special.erf(z/np.sqrt(2))))-3*np.log(2)
def objective(test_func, x, y, e, params):
"""This returns the sum of the modified normalised residuals. Smaller is better"""
z = norm_residual(test_func, x, y, e, params)
return np.sum(np.array([f(i) for i in z]))
#normal scipy curve fit
params, params_covariance = sp.optimize.curve_fit(test_func, x, y, p0=[0,0])
plt.scatter(x, y, label='Data')
plt.plot(x, test_func(x, params[0], params[1]), label='Fitted function', color="orange")
plt.legend(loc='best')
plt.show()
#how do I use my objective function to do my curve fit?
This is what I came up with, for my slightly more realistic requirements.
Lesson: vectorise everything! Don't just wrap it in a np.vectorize function call. I got a speed-up of ~100x by doing so.
Some guidance taken from https://hernandis.me/2020/04/05/three-examples-of-nonlinear-least-squares-fitting-in-python-with-scipy.html
import numpy as np
import scipy as sp
from scipy import optimize
import matplotlib.pyplot as plt
import math
x_data = np.array([-5.0,-4.5,-4.0,-3.5,-3.0,-2.5,-2.0,-1.5,-1.0,-0.5,0.0,0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5,7.0,7.5,8.0,8.5,9.0,9.5,10.0])
y_data = np.array([300,300,1000,350,340,1230,500,360,360,920,365,365,350,1000,375,1050,380,385,385,390,400,395,780,410,420,420,415,435,440,435,455])
e_data = np.array([math.sqrt(i) for i in y_data]) #uncertainty in y values
# https://hernandis.me/2020/04/05/three-examples-of-nonlinear-least-squares-fitting-in-python-with-scipy.html
def model(params, x):
"""Calculates the model, given params and x. Is vectorised; can be used with numpy.arrays"""
a0, a1 = params
return a0 + a1 * x
def v_f(z):
"""Modifies the residual. Used when you need to calc your own chi2 value. Is vectorised; can be used with numpy.arrays"""
return np.where(z <= 0, np.square(z), 6*np.log(z/sp.special.erf(z*0.7071067811865475)) - 1.3547481158683645)
def v_f_2(z):
"""Modifies the residual. Used when chi2 is calc'd for you. Is vectorised; can be used with numpy.arrays"""
return np.where(z <= 0, z, np.sqrt(6*np.log(z/sp.special.erf(z*0.7071067811865475)) - 1.3547481158683645))
def objective(params, model_func, data, v_modify_residuals_func = None):
""" Calculates the residuals given a model and data. Is vectorised; can be used with numpy.arrays """
if len(data) == 3:
xd, yd, ed = data
elif len(data) == 2:
xd, yd = data
ed = np.ones(len(xd))
r = (yd - model_func(params, xd)) / ed # r is an array of residuals
if v_modify_residuals_func is not None:
r = v_modify_residuals_func(r)
return r
def objective_sum(params, model_func, data, modify_residuals_func = None):
""" Calculates the sum of the residuals given a model and data. Used when you need to calc your own chi2 value. Is vectorised; can be used with numpy.arrays """
r = objective(params, model_func, data, modify_residuals_func)
return np.sum(r)
def v_cheb(n, x):
""" Calculate a chebyshev polynomial. -1.0 <= x <= 1.0, n >= 0, int. Is vectorised; can be used with numpy.arrays """
return np.cos(n * np.arccos(x))
def bkg_model(params, x):
""" Calculate a bkg curve from a number of chebyshev polynomials. Polynomial degree given by len(params). Is vectorised; can be used with numpy.arrays """
r = 0
for i, p in enumerate(params):
r += p * v_cheb(i, x)
return r
def v_normaliseList(nparray):
""" Given a monotonically increasing x-ordinate array, normalise values in the range -1 <= x <= 1. Is vectorised; can be used with numpy.arrays """
min_ = nparray[0]
max_ = nparray[-1]
r = (2*(nparray - min_)/(max_ - min_)) - 1
return r
initial_params = [0,0]
""" least_squares takes an array of residuals, r, and minimises Sum(r**2) """
results1 = sp.optimize.least_squares(objective,
initial_params,
method = 'lm',
args = [bkg_model,
[v_normaliseList(x_data), y_data, e_data],
v_f_2])
""" minimize takes a scalar, r, and minimises r """
results2 = sp.optimize.minimize(objective_sum,
initial_params,
#method = 'SLSQP',
args = (bkg_model,
(v_normaliseList(x_data), y_data, e_data),
v_f))
print(results1.x)
print(results2.x)

How to change support of multivariate integral to [0,1]^k using scipy.integrate.quad?

The following k-dimension integral has dependent limits (a support dependent on gamma):
from scipy.integrate import nquad
import numpy as np
def func(*args):
gamma = args[-1]
var = np.array(args[:-1])
return (1-1/(1+gamma-np.sum(var)))*np.prod(((1+var)**-2))
def range_func(*args):
gamma = args[-1]
return (0, gamma-sum(args[:-1]))
gamma, k = 10, 2
print(nquad(func, [range_func]*k, args=(gamma,) ))
The limits are defined inside the function range_func above
return (0, gamma-sum(args[:-1]))
How can this support be changed to simply [0,1]^k such that scipy.integrate.nquad still works? That is, each integral sign runs from 0 to 1. Do I just change it to return (0,1)?
(source of code, which is just a toy example of what I plan to do)

Passing a function to odeint that should decrease according to each time step in the solution of odeint

I want one of the parameters i pass to odeint to be a function.
In my case, this is the Hubble constant H which should decrease with H ~ 1/t.
This time "t" is exactly the same time that my odeint uses for solving coupled ODEs.
(I want to solve equations of motion in an expanding universe and want H to be determined outside of odeint so to speak)
How do i tell odeint that i want H to be decreasing with each time step that odeint is calculating?
The relevant lines from my code are (Here, H is a constant (H=1) but i want it to be H=1/t):
def func(X, t, parameters):
theta, alpha, phi, beta = X # unpack current values of X
Lambda, fa, H = parameters # unpack parameters
derivs = [alpha, -2*beta/phi*alpha - 3*H*alpha , beta, -3*H*beta + alpha**2 *phi -Lambda**2 *phi**3 + fa**2 * phi] # list of derivatives
return derivs
# Set initial (angle, angularvelo, radius, radialvelo) & bundle them to pass to odeint
theta0, alpha0, phi0, beta0 = np.pi, 30, 4, 100
X0 = [theta0, alpha0, phi0, beta0]
# Set time & stepsize. T should be time for 1 Period in the future.
T = 5 tmax, dt = 1*T, T/10000
t = np.arange(0, tmax, dt)
# Set Parameters & bundle them to pass to odeint
H = 1
Lambda = 30
fa=1.5
parameters = [Lambda, fa, H]
# Call the ODE solver
(solution, restoffulloutput) = odeint(func, X0, t, args=(parameters,), full_output=True)
I think the cleanest approach would be to make a class for the value that is decreasing with each time step.
class MagicValue:
def __init__(self, initial_value):
self._value = initial_value
#property
def next_value(self):
# modify self._value here
return self._value
#property
def value(self):
return self._value
Now modify your function to take in a MagicValue and use the properties as required.
I have found a solution that works just fine.
It's as simple as just defining the function inside your odeint function since it is "t" that odeint is iterating over.
def func(X, t, parameters):
theta, alpha, phi, beta = X # unpack current values of X
Lambda, fa, H = parameters # unpack parameters
H = 1/t # this line is doing the job
derivs = [alpha, -2*beta/phi*alpha - 3*H*alpha , beta, -3*H*beta + alpha**2 *phi -Lambda**2 *phi**3 + fa**2 * phi] # list of derivatives
return derivs

How can I control odeint to stop integration when the result reach a threshold?

Here is my code.
import numpy as np
from scipy.integrate import odeint
#Constant
R0=1.475
gamma=2.
ScaleMeVfm3toEskm3 = 8.92*np.power(10.,-7.)
def EOSe(p):
return np.power((p/450.785),(1./gamma))
def M(m,r):
return (4./3.)*np.pi*np.power(r,3.)*p
# function that returns dz/dt
def model(z,r):
p, m = z
dpdr = -((R0*EOSe(p)*m)/(np.power(r,2.)))*(1+(p/EOSe(p)))*(1+((4*math.pi*(np.power(r,3))*p)/(m)))*((1-((2*R0)*m)/(r))**(-1.))
dmdr = 4.*math.pi*(r**2.)*EOSe(p)
dzdr = [dpdr,dmdr]
return dzdr
# initial condition
r0=10.**-12.
p0=10**-6.
z0 = [p0, M(r0, p0)]
# radius
r = np.linspace(r0, 15, 100000)
# solve ODE
z = odeint(model,z0,r)
The result of z[:,0] keeps decreasing as I expected. But what I want is only positive values. One may run the code and try print(z[69306]) and it will show [2.89636405e-11 5.46983202e-01]. That is the last point I want the odeint to stop integration.
Of course, the provided code shows
RuntimeWarning: invalid value encountered in power
return np.power((p/450.785),(1./gamma))
because the result of p starts being negative. For any further points, the odeint yields the result [nan nan].
However, I could use np.nanmin() to find the minimum of z[:,0] that is not nan. But I have a set of p0 values for my work. I will need to call odeint in a loop like
P=np.linspace(10**-8.,10**-2.,10000)
for p0 in P:
#the code for solving ode provided above.
which takes more time.
I think it would reduce a time for execution if I can just stop at before z[:,0] going to be negative a value?
Here is the modified code using solve_ivp:
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pylab as plt
# Constants
R0 = 1.475
gamma = 2.
def EOSe(p):
return np.power(np.abs(p)/450.785, 1./gamma)
def M(m, r):
return (4./3.)*np.pi*np.power(r,3.)*p
# function that returns dz/dt
# note: the argument order is reversed compared to `odeint`
def model(r, z):
p, m = z
dpdr = -R0*EOSe(p)*m/r**2*(1 + p/EOSe(p))*(1 + 4*np.pi*r**3*p/m)*(1 - 2*R0*m/r)**(-1)
dmdr = 4*np.pi * r**2 * EOSe(p)
dzdr = [dpdr, dmdr]
return dzdr
# initial condition
r0 = 1e-3
r_max = 50
p0 = 1e-6
z0 = [p0, M(r0, p0)]
# Define the event function
# from the doc: "The solver will find an accurate value
# of t at which event(t, y(t)) = 0 using a root-finding algorithm. "
def stop_condition(r, z):
return z[0]
stop_condition.terminal = True
# solve ODE
r_span = (r0, r_max)
sol = solve_ivp(model, r_span, z0,
events=stop_condition)
print(sol.message)
print('last p, m = ', sol.y[:, -1], 'for r_event=', sol.t_events[0][0])
r_sol = sol.t
p_sol = sol.y[0, :]
m_sol = sol.y[1, :]
# Graph
plt.subplot(2, 1, 1);
plt.plot(r_sol, p_sol, '.-b')
plt.xlabel('r'); plt.ylabel('p');
plt.subplot(2, 1, 2);
plt.plot(r_sol, m_sol, '.-r')
plt.xlabel('r'); plt.ylabel('m');
Actually, using events in this case do not prevent a warning because of negative p. The reason is that the solver is going to evaluate the model for p<O anyway. A solution is to take the absolute value of p in the square root (as in the code above). Using np.sign(p)*np.power(np.abs(p)/450.785, 1./gamma) gives interesting result too.

"'float' is not subscriptable" in odeint

I'm trying to implement coupled differential equations in Python, and as a new user I seem to be stuck at something. I used this tutorial as a guide to how to solve my ODEs, and looked into the documentation to no available
This is where I define the function
def Burnout(t, y, m, nu, S0, V, delta, mu):
S = y[0];
E = [0 for i in range(0,m)]
dEdt = [0 for i in range(0,m)]
for i in range(0,m):
E.append(y[i+1])
P = y[m+1]
dSdt = -nu*S*P*(S/S0)**V
dEdt.append(nu*S*P*(S/S0)**V-m*delta*E[0])
for i in range(1,m):
dEdt.append(m*delta*E[i-1]-m*delta*E[i])
dPdt = m*delta*E[m-1]-mu*P
return [dSdt, *dEdt[0:m], dPdt]
Then, as in the tutorial, I define the initial conditions by
S0 = N
y0.append(S0)
for i in range (0, m):
E.append(0)
y0.append(E[i])
P0 = Z
y0.append(P0)
where N and Z are previously defined things, and E was an empty array. When I finally call odeint(Burnout, y0, t, args = p), I get a 'float' object is not subscriptable pointing to my definition of S in my Burnout function. As I passed a list to odeint I'm kind of confused on why Python says I passed a float. Does anyone see what I did wrong? Thanks in advance!
EDIT: Ok, now here is a minimal, complete and verifiable example that gives me the same error
import numpy as np
from scipy.integrate import odeint
def Burnout(t, y, m, nu, S0, V, delta, mu):
S = y[0]
E = [0 for i in range(0,m)]
dEdt = [0 for i in range(0,m)]
for i in range(0,m):
E.append(y[i+1])
P = y[m+1]
dSdt = -nu*S*P*(S/S0)**V
dEdt.append(nu*S*P*(S/S0)**V-m*delta*E[0])
for i in range(1,m):
dEdt.append(m*delta*E[i-1]-m*delta*E[i])
dPdt = m*delta*E[m-1]-mu*P
return [dSdt, *dEdt[0:m], dPdt]
V = 2.97
m = 26
delta = 1/6
mu = 1
nu = 10
S0 = 5
t = np.linspace(0,56,100)
y = [10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100]
p = (m, nu, V, S0, delta, mu)
print(odeint(Burnout,y,t,args=p))
You ordered the arguments in your ode definition wrong. It is possible to have t before y, but then you must define tfirst = True (see docs).
Swapping the arguments in your definition of Burnout fixes the problem for me.
def Burnout(y, t, m, nu, S0, V, delta, mu):
# ...
# rest of function
# ...
Alternately you can define the additional keyword tfirst in the odeint call:
odeint(Burnout, y, t, args=p, tfirst=True)

Categories