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.
Related
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.
I am trying to implement masked-array fitting using lmfit.
Prior I was using curve_fit using the following code snippet. I have omitted the def spec due to its line length. Essentially def spec is function that performs 50 interpolations of templates.
Both the observation data and templates have the file structure x=wavelength, y=flux from x=500 to x=550nm.
The observation data is a mixtures of two spectra with each spectrum have different parameters.
The file lambda_input is a list of wavelengths (example: list of wavelengths[1]) to be used in the fitting process. The fit is only to be carried out in these wavelength ranges (+/- line-widths (lw))
Using sigma=weight works but now I need to use lmfit I am struggling to find a method that works.
How can I mask both the observation and templates wavelengths to input_lambda in order to use lmfit?
Thank you
import pandas as pd
import numpy as np
from scipy import interpolate
from scipy.optimize import curve_fit
import lmfit
i_run = pd.read_csv("run.txt",delimiter='\t')
i_run.columns = ["wave","flux"]
run_w = np.array(i_run['wave'])
run_f = np.array(i_run['flux']) # observation data
lambda_input = pd.read_excel("../lambda/weights.xlsx") #fitting wavelengths
v1 = 5 # conversion factors
v2 = 6
lw = 0.01 # linewidths (+/- fitting wavelength)
weight = np.zeros(len(run_w))
for w in range (0,187):
n = lambda_input.iat[w,1]
weight += np.select([np.logical_and(run_w >= ((n *(1 + (v1)))-lw),run_w <= ((n *(1 + (v1)))+lw)),run_w>0],[1,0])
for w in range (0,187):
n = lambda_input.iat[w,1]
weight += np.select([np.logical_and(run_w >= ((n *(1 + (v2)))-lw),run_w <= ((n *(1 + (v2)))+lw)),run_w>0],[1,0])
weight = np.select([weight>0,weight==0],[1,1e6])
p1 = 750
p2 = 800
p3 = 1.0
p4 = 20.0
p5 = 30.0
p6 = 0.5
p7 = 0.5
p8 = 100
p9 = -100
p10 = 4.0
p11 = 4.0
p12 = 3.0
p13 = 3.0
def spec(run_w,t1,t2,r,v1,v2,m1,m2,r1,r2,l1,l2,mi1,mi2):
return spectrum
popt, pcov = curve_fit(spec, xdata=run_w,sigma=weight,ydata=run_f, p0=[p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13])
model = lmfit.Model(spec) #fitting: lmfit
p = model.make_params(t1=p1,t2=p2,r=p3,v1=p4,v2=p5,m1=p6,m2=p7,r1=p8,r2=p9,l1=p10,l2=p11,mi1=p12,mi2=p13)
fit = model.fit(data=run_f, params=p, run_w=run_w, method='nelder', nan_policy='omit')
lmfit.report_fit(fit)
fit.plot()
[1]: https://i.stack.imgur.com/9wAb2.png
With lmfit.Model, weighting the elements of the data array is done with the weights to the lmfit.Model.fit() method, to modify the quantity to be minimized in the least-squares sense from data-model to weights*(data-model). See https://lmfit.github.io/lmfit-py/model.html#lmfit.model.ModelResult.fit
Ironically, you have a weight array, but use this as sigma in curve_fit. To use with lmfit.Model.fit(), you would want to use 1.0/weight, or modify your weight` to something like:
weight = np.select([weight>0,weight==0],[1,1.e6])
and then use with
fit = model.fit(data=run_f, params=p, run_w=run_w,
weights=weight, method='nelder', nan_policy='omit')
lmfit.report_fit(fit)
I would make a couple more suggestions:
don't use method='nelder' as the fitting method unless you know
you need that (and if, then ask yourself why you didn't use that with
curve_fit). The default fitting method is highly recommended, especially to start.
If your data has NaNs or Infs, it is way better to remove them before doing the fit than relying on nan_policy. If your model generates NaNs or Infs, nan_policy won't help at all - those would have to be checked for and resolved.
a really key feature of lmfit.Model is that parameters are named so as to be meaningful to the person running and interpreting the results. Having 13 parameters with names like t1, t2, v1, v2, m1, m2`, etc is going to be hard to interpret in the future. Names like "time1", "theta1", "velo1", "mass1", "mean1", etc will be much clearer.
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.
I'm trying to implement Bayesian PCA using PyMC library for python. But, I'm stuck where I define lower dimensional coordinates...
Model is
x = Wz + e
where x is observation vector, W is the transformation matrix, and z is lower dimensional coordinate vector.
First I define a distribution for the transformation matrix W. Each column is drawn from a normal distribution (zero mean, and identity covariance for simplicity)
def W_logp(value):
logLikes = np.array([multivariate_normal.logpdf(value[:,i], mean=np.zeros(dimX), cov=1) for i in range(0, dimZ)])
return logLikes.sum()
def W_random():
W = np.zeros([dimX, dimZ])
for i in range(0, dimZ):
W[:,i] = multivariate_normal.rvs(mean=np.zeros(dimX), cov=1)
return W
w0 = np.random.randn(dimX, dimZ)
W = pymc.Stochastic(
logp = W_logp,
doc = 'Transformation',
name = 'W',
parents = {},
random = W_random,
trace = True,
value = w0,
dtype = float,
rseed = 116.,
observed = False,
cache_depth = 2,
plot = False,
verbose = 0)
Then, I want to define distribution for z that is again a multivariate normal (zero mean, and identity covariance). However, I need to draw a z for each observation separately while W is common for all of them. So, I tried
z = pymc.MvNormal('z', np.zeros(dimZ), np.eye(dimZ), size=N)
However, pymc.MvNormal does not have a size parameter. So it raises an error. Next step would be
m = Data.mean(axis=0) + np.dot(W, z)
obs = pymc.MvNormal('Obs', m, C, value=Data, observed=True)
I did not give the specification for C above since it is irrelevant for now. Any ideas how to implement?
Thanks
EDIT
After Chris Fonnesbeck's answer I changed my code as follows
numD, dimX = Data.shape
dimZ = 3
mm = Data.mean(axis=0)
tau = pymc.Gamma('tau', alpha=10, beta=2)
tauW = pymc.Gamma('tauW', alpha=20, beta=2, size=dimZ)
#pymc.deterministic(dtype=float)
def C(tau=tau):
return (tau)*np.eye(dimX)
#pymc.deterministic(dtype=float)
def CW(tau=tauW):
return np.diag(tau)
W = [pymc.MvNormal('W%i'%i, np.zeros(dimZ), CW) for i in range(dimX)]
z = [pymc.MvNormal('z%i'%i, np.zeros(dimZ), np.eye(dimZ)) for i in range(numD)]
mu = [pymc.Lambda('mu%i'%i, lambda W=W, z=z: mm + np.dot(np.array(W), np.array(z[i]))) for i in range(numD)]
obs = [pymc.MvNormal('Obs%i'%i, mu[i], C, value=Data[i,:], observed=True) for i in range(numD)]
model = pymc.Model([tau, tauW] + obs + W + z)
mcmc = pymc.MCMC(model)
But this time, it tries to allocate a huge amount of memory (more than 8GB) when running pymc.MCMC(model), with numD=45 and dimX=504. Even when I try it with only numD=1 (so creating only 1 z, mu, and obs), it does the same. Any idea why?
Unfortunately, PyMC does not easily let you define vectors of multivariate stochastics. Hopefully we can make this happen in PyMC 3. For now, you would have to specify this using a container. For example:
z = [pymc.MvNormal('z_%i' % i, np.zeros(dimZ), np.eye(dimZ)) for i in range(N)]
Regarding the memory issue, try using a different backend for the traces. The default ("ram") keeps everything in RAM. You can try something like "pickle" or "sqlite" instead.
Regarding the plate notation, it might be something we could pursue for PyMC 3. Feel free to create an issue suggesting this in our issue tracker.
Short summary: How do I quickly calculate the finite convolution of two arrays?
Problem description
I am trying to obtain the finite convolution of two functions f(x), g(x) defined by
To achieve this, I have taken discrete samples of the functions and turned them into arrays of length steps:
xarray = [x * i / steps for i in range(steps)]
farray = [f(x) for x in xarray]
garray = [g(x) for x in xarray]
I then tried to calculate the convolution using the scipy.signal.convolve function. This function gives the same results as the algorithm conv suggested here. However, the results differ considerably from analytical solutions. Modifying the algorithm conv to use the trapezoidal rule gives the desired results.
To illustrate this, I let
f(x) = exp(-x)
g(x) = 2 * exp(-2 * x)
the results are:
Here Riemann represents a simple Riemann sum, trapezoidal is a modified version of the Riemann algorithm to use the trapezoidal rule, scipy.signal.convolve is the scipy function and analytical is the analytical convolution.
Now let g(x) = x^2 * exp(-x) and the results become:
Here 'ratio' is the ratio of the values obtained from scipy to the analytical values. The above demonstrates that the problem cannot be solved by renormalising the integral.
The question
Is it possible to use the speed of scipy but retain the better results of a trapezoidal rule or do I have to write a C extension to achieve the desired results?
An example
Just copy and paste the code below to see the problem I am encountering. The two results can be brought to closer agreement by increasing the steps variable. I believe that the problem is due to artefacts from right hand Riemann sums because the integral is overestimated when it is increasing and approaches the analytical solution again as it is decreasing.
EDIT: I have now included the original algorithm 2 as a comparison which gives the same results as the scipy.signal.convolve function.
import numpy as np
import scipy.signal as signal
import matplotlib.pyplot as plt
import math
def convolveoriginal(x, y):
'''
The original algorithm from http://www.physics.rutgers.edu/~masud/computing/WPark_recipes_in_python.html.
'''
P, Q, N = len(x), len(y), len(x) + len(y) - 1
z = []
for k in range(N):
t, lower, upper = 0, max(0, k - (Q - 1)), min(P - 1, k)
for i in range(lower, upper + 1):
t = t + x[i] * y[k - i]
z.append(t)
return np.array(z) #Modified to include conversion to numpy array
def convolve(y1, y2, dx = None):
'''
Compute the finite convolution of two signals of equal length.
#param y1: First signal.
#param y2: Second signal.
#param dx: [optional] Integration step width.
#note: Based on the algorithm at http://www.physics.rutgers.edu/~masud/computing/WPark_recipes_in_python.html.
'''
P = len(y1) #Determine the length of the signal
z = [] #Create a list of convolution values
for k in range(P):
t = 0
lower = max(0, k - (P - 1))
upper = min(P - 1, k)
for i in range(lower, upper):
t += (y1[i] * y2[k - i] + y1[i + 1] * y2[k - (i + 1)]) / 2
z.append(t)
z = np.array(z) #Convert to a numpy array
if dx != None: #Is a step width specified?
z *= dx
return z
steps = 50 #Number of integration steps
maxtime = 5 #Maximum time
dt = float(maxtime) / steps #Obtain the width of a time step
time = [dt * i for i in range (steps)] #Create an array of times
exp1 = [math.exp(-t) for t in time] #Create an array of function values
exp2 = [2 * math.exp(-2 * t) for t in time]
#Calculate the analytical expression
analytical = [2 * math.exp(-2 * t) * (-1 + math.exp(t)) for t in time]
#Calculate the trapezoidal convolution
trapezoidal = convolve(exp1, exp2, dt)
#Calculate the scipy convolution
sci = signal.convolve(exp1, exp2, mode = 'full')
#Slice the first half to obtain the causal convolution and multiply by dt
#to account for the step width
sci = sci[0:steps] * dt
#Calculate the convolution using the original Riemann sum algorithm
riemann = convolveoriginal(exp1, exp2)
riemann = riemann[0:steps] * dt
#Plot
plt.plot(time, analytical, label = 'analytical')
plt.plot(time, trapezoidal, 'o', label = 'trapezoidal')
plt.plot(time, riemann, 'o', label = 'Riemann')
plt.plot(time, sci, '.', label = 'scipy.signal.convolve')
plt.legend()
plt.show()
Thank you for your time!
or, for those who prefer numpy to C. It will be slower than the C implementation, but it's just a few lines.
>>> t = np.linspace(0, maxtime-dt, 50)
>>> fx = np.exp(-np.array(t))
>>> gx = 2*np.exp(-2*np.array(t))
>>> analytical = 2 * np.exp(-2 * t) * (-1 + np.exp(t))
this looks like trapezoidal in this case (but I didn't check the math)
>>> s2a = signal.convolve(fx[1:], gx, 'full')*dt
>>> s2b = signal.convolve(fx, gx[1:], 'full')*dt
>>> s = (s2a+s2b)/2
>>> s[:10]
array([ 0.17235682, 0.29706872, 0.38433313, 0.44235042, 0.47770012,
0.49564748, 0.50039326, 0.49527721, 0.48294359, 0.46547582])
>>> analytical[:10]
array([ 0. , 0.17221333, 0.29682141, 0.38401317, 0.44198216,
0.47730244, 0.49523485, 0.49997668, 0.49486489, 0.48254154])
largest absolute error:
>>> np.max(np.abs(s[:len(analytical)-1] - analytical[1:]))
0.00041657780840698155
>>> np.argmax(np.abs(s[:len(analytical)-1] - analytical[1:]))
6
Short answer: Write it in C!
Long answer
Using the cookbook about numpy arrays I rewrote the trapezoidal convolution method in C. In order to use the C code one requires three files (https://gist.github.com/1626919)
The C code (performancemodule.c).
The setup file to build the code and make it callable from python (performancemodulesetup.py).
The python file that makes use of the C extension (performancetest.py)
The code should run upon downloading by doing the following
Adjust the include path in performancemodule.c.
Run the following
python performancemodulesetup.py build
python performancetest.py
You may have to copy the library file performancemodule.so or performancemodule.dll into the same directory as performancetest.py.
Results and performance
The results agree neatly with one another as shown below:
The performance of the C method is even better than scipy's convolve method. Running 10k convolutions with array length 50 requires
convolve (seconds, microseconds) 81 349969
scipy.signal.convolve (seconds, microseconds) 1 962599
convolve in C (seconds, microseconds) 0 87024
Thus, the C implementation is about 1000 times faster than the python implementation and a bit more than 20 times as fast as the scipy implementation (admittedly, the scipy implementation is more versatile).
EDIT: This does not solve the original question exactly but is sufficient for my purposes.