30

I'm trying to understand scipy.signal.deconvolve.

From the mathematical point of view a convolution is just the multiplication in fourier space so I would expect that for two functions f and g:
Deconvolve(Convolve(f,g) , g) == f

In numpy/scipy this is either not the case or I'm missing an important point. Although there are some questions related to deconvolve on SO already (like here and here) they do not address this point, others remain unclear (this) or unanswered (here). There are also two questions on SignalProcessing SE (this and this) the answers to which are not helpful in understanding how scipy's deconvolve function works.

The question would be:

  • How do you reconstruct the original signal f from a convoluted signal, assuming you know the convolving function g.?
  • Or in other words: How does this pseudocode Deconvolve(Convolve(f,g) , g) == f translate into numpy / scipy?

Edit: Note that this question is not targeted at preventing numerical inaccuracies (although this is also an open question) but at understanding how convolve/deconvolve work together in scipy.

The following code tries to do that with a Heaviside function and a gaussian filter. As can be seen in the image, the result of the deconvolution of the convolution is not at all the original Heaviside function. I would be glad if someone could shed some light into this issue.

import numpy as np
import scipy.signal
import matplotlib.pyplot as plt

# Define heaviside function
H = lambda x: 0.5 * (np.sign(x) + 1.)
#define gaussian
gauss = lambda x, sig: np.exp(-( x/float(sig))**2 )

X = np.linspace(-5, 30, num=3501)
X2 = np.linspace(-5,5, num=1001)

# convolute a heaviside with a gaussian
H_c = np.convolve( H(X),  gauss(X2, 1),  mode="same"  )
# deconvolute a the result
H_dc, er = scipy.signal.deconvolve(H_c, gauss(X2, 1) )


#### Plot #### 
fig , ax = plt.subplots(nrows=4, figsize=(6,7))
ax[0].plot( H(X),          color="#907700", label="Heaviside",    lw=3 ) 
ax[1].plot( gauss(X2, 1),  color="#907700", label="Gauss filter", lw=3 )
ax[2].plot( H_c/H_c.max(), color="#325cab", label="convoluted" ,  lw=3 ) 
ax[3].plot( H_dc,          color="#ab4232", label="deconvoluted", lw=3 ) 
for i in range(len(ax)):
    ax[i].set_xlim([0, len(X)])
    ax[i].set_ylim([-0.07, 1.2])
    ax[i].legend(loc=4)
plt.show()

enter image description here

Edit: Note that there is a matlab example, showing how to convolve/deconvolve a rectangular signal using

yc=conv(y,c,'full')./sum(c); 
ydc=deconv(yc,c).*sum(c); 

In the spirit of this question it would also help if someone was able to translate this example into python.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Running it with `mode=full` gives a reasonable good result (until around index 1000, then boundary effects(?) are seen); don't know enough about the theory, unfortunately. – Cleb Nov 15 '16 at 18:07
  • @Cleb Nice. But running it with `mode= "full"` first of all shifts the convoluted signal, such that the edge sits at 1000 instead of 500 in this case. Any idea about the reason? How can I interprete the result of the convoluted array compared to the original one? – ImportanceOfBeingErnest Nov 15 '16 at 18:22
  • I don't know yet. In the [documentation](https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.signal.deconvolve.html) is a small toy example given where it works perfectly; but I have no clue why your result looks like it looks like, unfortunately. – Cleb Nov 15 '16 at 18:49
  • The operation `np.convolve(f,g)` may be (a) non-invertible or (b) its inversion may be numerically unstable, depending on `g`. `deconvolve` will succeed in finding `f` only for a limited class of "well-behaved" functions `g`. Your question is more appropriate for the [signal processing SE](http://dsp.stackexchange.com/) – Stelios Nov 15 '16 at 21:55
  • @Stelios If you think the gaussian is not well-behaved enough, you are free to suggest a different function g. The question is really **how** `scipy.signal.deconvolve` works and how to use it and not in which cases it may fail due to numerical issues. – ImportanceOfBeingErnest Nov 16 '16 at 08:26
  • 3
    @ImportanceOfBeingErnest You are explicitly asking in your post "How do you reconstruct the original signal f from a convoluted signal, assuming you know the convolving function g?", based on the a-priori assumption that `Deconvolve(Convolve(f,g) , g) == f` holds. I was simply pointing out that the latter is not true in general. – Stelios Nov 16 '16 at 08:56
  • You can get a perfect reconstruction with two changes: (i) use the option `full` instead `same` and (ii) use more data points for the gauss signal (e.g. 10500). That at least shows that you can reconstruct it the way you wanted it to reconstruct but I am not sure about the meaning of the convoluted signal... – Cleb Nov 16 '16 at 10:58
  • @Cleb Using more Datapoints for the gaussian actually stretches the filterfunction, which is probably undesired in a real usage case. – ImportanceOfBeingErnest Nov 16 '16 at 17:08
  • Yes, it was just the only way I could reconstruct it; maybe you should ask this question on the signal processing SE as suggested by Stelios. I guess I won't be able to help any further :( – Cleb Nov 16 '16 at 17:56

2 Answers2

20

After some trial and error I found out how to interprete the results of scipy.signal.deconvolve() and I post my findings as an answer.

Let's start with a working example code

import numpy as np
import scipy.signal
import matplotlib.pyplot as plt

# let the signal be box-like
signal = np.repeat([0., 1., 0.], 100)
# and use a gaussian filter
# the filter should be shorter than the signal
# the filter should be such that it's much bigger then zero everywhere
gauss = np.exp(-( (np.linspace(0,50)-25.)/float(12))**2 )
print gauss.min()  # = 0.013 >> 0

# calculate the convolution (np.convolve and scipy.signal.convolve identical)
# the keywordargument mode="same" ensures that the convolution spans the same
#   shape as the input array.
#filtered = scipy.signal.convolve(signal, gauss, mode='same') 
filtered = np.convolve(signal, gauss, mode='same') 

deconv,  _ = scipy.signal.deconvolve( filtered, gauss )
#the deconvolution has n = len(signal) - len(gauss) + 1 points
n = len(signal)-len(gauss)+1
# so we need to expand it by 
s = (len(signal)-n)/2
#on both sides.
deconv_res = np.zeros(len(signal))
deconv_res[s:len(signal)-s-1] = deconv
deconv = deconv_res
# now deconv contains the deconvolution 
# expanded to the original shape (filled with zeros) 


#### Plot #### 
fig , ax = plt.subplots(nrows=4, figsize=(6,7))

ax[0].plot(signal,            color="#907700", label="original",     lw=3 ) 
ax[1].plot(gauss,          color="#68934e", label="gauss filter", lw=3 )
# we need to divide by the sum of the filter window to get the convolution normalized to 1
ax[2].plot(filtered/np.sum(gauss), color="#325cab", label="convoluted" ,  lw=3 )
ax[3].plot(deconv,         color="#ab4232", label="deconvoluted", lw=3 ) 

for i in range(len(ax)):
    ax[i].set_xlim([0, len(signal)])
    ax[i].set_ylim([-0.07, 1.2])
    ax[i].legend(loc=1, fontsize=11)
    if i != len(ax)-1 :
        ax[i].set_xticklabels([])

plt.savefig(__file__ + ".png")
plt.show()    

This code produces the following image, showing exactly what we want (Deconvolve(Convolve(signal,gauss) , gauss) == signal)

enter image description here

Some important findings are:

  • The filter should be shorter than the signal
  • The filter should be much bigger than zero everywhere (here > 0.013 is good enough)
  • Using the keyword argument mode = 'same' to the convolution ensures that it lives on the same array shape as the signal.
  • The deconvolution has n = len(signal) - len(gauss) + 1 points. So in order to let it also reside on the same original array shape we need to expand it by s = (len(signal)-n)/2 on both sides.

Of course, further findings, comments and suggestion to this question are still welcome.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Did you figure out the optimal lengths and minimal filter values by yourself or did you find a source for that somewhere? – Cleb Nov 17 '16 at 16:05
  • @Cleb I did not find any source, this is all based on trial and error. Comparing to your translation of the matlab code, it seems that even if the filter has the same shape as the signal, the whole thing can work. I think the reason is, that the first point of the filter needs to be much larger than zero (if it is exactly equal to zero, there will even be an error thrown). – ImportanceOfBeingErnest Nov 17 '16 at 16:56
  • Ok. Well, maybe this Matlab translation is of any help for you... I will check this question from time to time to see if any better answers show up. Good luck with your project! – Cleb Nov 17 '16 at 17:00
  • 1
    Yes, it is. And probably also for others seeking advice on this topic. Thanks for that. – ImportanceOfBeingErnest Nov 17 '16 at 21:35
  • 1
    Sirs, have you learned anything new on the topic in the last two years maybe? I find your examples helpful, except that the padding seems excessive. I can't believe that one can only deconvolve the points where the signal and the filter intersect fully. Naively speaking, there is always as many equations as unknowns, so it should be possible to reconstruct the whole signal, not just the middle. Perhaps the values towards the end will be less accurate than in the middle, but still. Any ideas? – Aleksejs Fomins Jan 09 '19 at 17:01
7

As written in the comments, I cannot help with the example you posted originally. As @Stelios has pointed out, the deconvolution might not work out due to numerical issues.

I can, however, reproduce the example you posted in your Edit:

enter image description here

That is the code which is a direct translation from the matlab source code:

import numpy as np
import scipy.signal
import matplotlib.pyplot as plt

x = np.arange(0., 20.01, 0.01)
y = np.zeros(len(x))
y[900:1100] = 1.
y += 0.01 * np.random.randn(len(y))
c = np.exp(-(np.arange(len(y))) / 30.)

yc = scipy.signal.convolve(y, c, mode='full') / c.sum()
ydc, remainder = scipy.signal.deconvolve(yc, c)
ydc *= c.sum()

fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(4, 4))
ax[0][0].plot(x, y, label="original y", lw=3)
ax[0][1].plot(x, c, label="c", lw=3)
ax[1][0].plot(x[0:2000], yc[0:2000], label="yc", lw=3)
ax[1][1].plot(x, ydc, label="recovered y", lw=3)

plt.show()
Cleb
  • 25,102
  • 20
  • 116
  • 151