2

All, I am trying to take the laplacian of the following function:

g(x,y) = 1/2cx^2+1/2dy2

The laplacian is c + d, which is a constant. Using FFT I should get the same ( in my FFT example I am padding the function to avoid edge effects).

Here is my code:

Define a 2D function
n = 30 # number of points
Lx = 30 # extension in x
Ly = 30 # extension in x
dx = n/Lx # Step in x 
dy = n/Ly # Step in x 
c=4
d=4
x=np.arange(-Lx/2,Lx/2)
y=np.arange(-Ly/2,Ly/2)
g = np.zeros((Lx,Ly))
lapg = np.zeros((Lx,Ly))

for j in range(Ly):
    for i in range(Lx):
        g[i,j] = (1/2)*c*x[i]**2 + (1/2)*d*y[j]**2
        lapg[i,j] = c + d

kxpad = 2*np.pi*np.fft.fftfreq(2*Lx,d=dx)
#kxpad = (2*np.pi/(2*Lx))*np.arange(-2*Lx/2,2*Lx/2)
#kxpad = np.fft.fftshift(kxpad)

#kypad = (2*np.pi/(2*Ly))*np.arange(-2*Ly/2,2*Ly/2)
#kypad = np.fft.fftshift(kypad)

kypad = 2*np.pi*np.fft.fftfreq(2*Ly,d=dy)
kpad = np.zeros((2*Lx,2*Ly))


for j in range(2*Ly):
    for i in range(2*Lx):
        kpad[i,j] =  math.sqrt(kxpad[i]**2+kypad[j]**2)

kpad = np.fft.fftshift(kpad)

gpad = np.zeros((2*Lx,2*Ly))


gpad[:Lx,:Ly] = g # Filling main part of g in gpad

gpad[:Lx,Ly:] = g[:,-1::-1] # Filling the last 3 columns of gpad with g flipped

gpad[Lx:,:Ly] = g[-1::-1,:]# Filling the last 3 lines of gpad with g flipped

gpad[Lx:,Ly:] = g[-1::-1, -1::-1]# Filling the last 3 lines and last 3 columns of gpad with g flipped in line and column

rdFFT2D = np.zeros((Lx,Ly))

gpadhat = np.fft.fft2(gpad)
dgpadhat = -(kpad**2)*gpadhat #taking the derivative iwFFT(f)
rdpadFFT2D = np.real(np.fft.ifft2(dgpadhat))
rdFFT2D = rdpadFFT2D[:Lx,:Ly]

Plot of the original function g

[Laplacian of g, analytical result][2]

enter image description here

First image is the plot of the original function g(x,y), 2nd image is the analytical laplacian of g and 3rd image is the sugar loaf in Rio de Janeiro( lol ), actually it is the laplacian using FFT. What Am I doing wrong here?

Edit : Commenting on ripple effect. Cris you mean the ripple effect due to the set_zlimit in the image below?Just to remember you that the result should be 8.enter image description here

Edit 2 : Using non-symmetrical x and y values, produce the two images.enter image description here

  • The most likely explanation is that you don't have the origin in the right place for the FFT. Did you look at the imaginary component of the IFFT that you are discarding? If it's not approximately zero, you're doing something wrong with the origin. I'm losing track in your code around `gpad`, I don't understand why you'd need any padding. Also, you define some arrays that you later don't use, it would be good if you could remove those lines from your code for clarity and simplicity, makes it easier to spot issues if we're not reading useless stuff. – Cris Luengo Feb 23 '21 at 20:54
  • Thanks for answering. The pad was necessaring to avoid edge effects. In 1D derivative I did that and saw this clearly. I will share with you the image in case you want. So your test would be take a look at the output of IFFT? What lines do you mean to be removed? – Rafael_Cristo Feb 23 '21 at 21:12
  • Rafael, I see the deleted post, you can comment on an answer if you need further clarification. I don’t know exactly what is wrong about your code, I didn’t try to run it, and it’s too complex for me to try to figure out what it does. As I said, it likely has to do with the location of the origin in your code, you move bits and pieces around a lot. You also use a `dx` in your call to `fftfreq` that doesn’t match the sampled function, but that should just be a constant offset for the output. – Cris Luengo Feb 24 '21 at 15:40
  • As I said, boundary extension is beneficial, but your method is pointless because it doesn’t change the output. You should just sample a larger region of your function and crop out the center of the final result. – Cris Luengo Feb 24 '21 at 15:41
  • Cris, I did the FFT using an extended domain and cut it out as you said, however if I set x=np.arange(-Lx,Lx) y=np.arange(-Ly,Ly), and If x=np.arange(-Lx/2,3Lx/2) y=np.arange(-Ly/2,3Ly/2) not symmetrical, the results look weird. Why is that? Could you explain me? – Rafael_Cristo Feb 25 '21 at 19:53
  • Rafael, I've edited my answer to show boundary extension and cropping. You should be able to define your input range however you like, the origin doesn't need to be in the middle. Just make sure it has the right number of values. – Cris Luengo Feb 25 '21 at 20:41
  • Cris, no words to thank you! I am astonished the way you make this super simple and easy to understand. One question remaining is if I write X and Y like that x = np.arange(-n//2,3*n//2) y = np.arange(-n//2,3*n//2) is it going to work the same? I don't know Cris if we can discuss more about this problem. I know that right now I have the solution but I was desiring to understand deeply the problem. Is there a live chat on stackoverflow? So I can share with you all my doubts. I appreciate, Cris! – Rafael_Cristo Feb 26 '21 at 01:30
  • @CrisLuengo I edited my post to show you a picture with the "supposed" ripple effect you mean? Check it out.just to remember that my expected value should be 8. – Rafael_Cristo Feb 26 '21 at 01:38
  • Yes, that is likely caused by the ripple effect. On average it is 8, but it extends above and below a bit, alternating. If you do `x = np.arange(-n//2,3*n//2)` you don’t have `n` samples, you have `2*n`. But that should be fine if you adjust your arrays elsewhere too. You can also do `x = np.arange(3, n+3)` to get exactly `n` samples starting at 3. It doesn’t matter where the origin is, the filter (a convolution) is translation-invariant. – Cris Luengo Feb 26 '21 at 02:06
  • @CrisLuengo I did the test you suggested changing x and y to x = np.arange(3,n+3) y = np.arange(3,n+3). I will edit my post again to show the results, they are not correct. – Rafael_Cristo Feb 26 '21 at 11:36
  • That is exactly the result I would expect. Note that your signal now has a range of 6000, the ripple of +50 to -50 is very small in comparison, less than 1% of the range. If you want to improve those results you need to increase your padding significantly. If you take the mean value of your `lapg` output you’ll see it’s close to 8. – Cris Luengo Feb 26 '21 at 14:45
  • @CrisLuengo what I did was to take a central value of lapg. For instance lapg[15,:] for n = 30, I should expect everything closes to 8 isn't it?Since is the central value so It is safe for any edge effect. However the values are totally apart from that rray([-1.11237102e+01, 6.62620543e+01, -4.50551585e+00, 6.08004912e+01, 2.72410878e-02, 5.70251735e+01, 3.17560403e+00, 5.44034504e+01, 5.34832910e+00, 5.26194332e+01, 6.79039681e+00, 5.14833273e+01, 7.64775504e+00, 5.08848435e+01, 8.00089866e+00, 5.07691844e+01, – Rafael_Cristo Feb 26 '21 at 15:05
  • The ripples extend all across the image. They get smaller nearer the middle, but they never go away. The ripple is relative to the magnitude of your data, the larger the range of values in the input, the larger the ripple. By averaging many samples in the output you can reduce their influence. You might also reduce the ripples by applying a windowing function to your input. Or, best of all, use spatial-domain filtering, where your kernel has a limited extend and when sufficiently far away from the edge you have 0 edge effect. Basically, Fourier-domain filtering sucks. – Cris Luengo Feb 26 '21 at 15:10
  • OK. Cris, So in the beginning we had a range of 800 and the ripple was small so everything was close to 8. When I decided to use non-symmetrical values the range of my signal was 6000 so the ripple effect was bigger than before. I supposed it is proportinal to the signal range input. You are saying that If I increase the boundary probably I will have like what I expect which is 8, right? Is there a relationship between the number of points in the boundary to the ripple effect in my data? I was thinking to choose an optimum boundary points. – Rafael_Cristo Feb 26 '21 at 15:15
  • The ripple effect is a superimposed sinc function. The envelope of this function decreases as you move away from the origin as 1/x. This is very slow. It basically never goes away. There is no optimum boundary extension size. The larger the boundary, the smaller the ripple, until infinity. – Cris Luengo Feb 26 '21 at 15:20
  • @CrisLuengo Could you please evaluate my result on this post? https://stackoverflow.com/questions/66392198/solving-poisson-equation-fft-domain-vs-finite-difference. I have the Poisson's equation using Finite Difference and FFT. The round shape on FFT result is due to ripple effect? The results are right for FFT? Thank you very much! – Rafael_Cristo Feb 26 '21 at 20:10

1 Answers1

3

The padding will not change the boundary condition: You are padding by replicating the function, mirrored, four times. The function is symmetric, so the mirroring doesn't change it. Thus, your padding simply repeats the function four times. The convolution through the DFT (which you're attempting to implement) uses a periodic boundary condition, and thus already sees the input function as periodic. Replicating the function will not improve the convolution results at the edges.

To improve the result at the edges, you would need to implement a different boundary condition, the most effective one (since the input is analytical anyway) is to simply extend your domain and then crop it after applying the convolution. This introduces a boundary extension where the image is padded by seeing more data outside the original domain. It is an ideal boundary extension suitable for an ideal case where we don't have to deal with real-world data.

This implements the Laplace though the DFT with greatly simplified code, where we ignore any boundary extension, as well as the sample spacing (basically setting dx=1 and dy=1):

import numpy as np
import matplotlib.pyplot as pp

n = 30 # number of points
c = 4
d = 4
x = np.arange(-n//2,n//2)
y = np.arange(-n//2,n//2)
g = (1/2)*c*x[None,:]**2 + (1/2)*d*y[:,None]**2

kx = 2 * np.pi * np.fft.fftfreq(n)
ky = 2 * np.pi * np.fft.fftfreq(n)
lapg = np.real(np.fft.ifft2(np.fft.fft2(g) * (-kx[None, :]**2 - ky[:, None]**2)))

fig = pp.figure()
ax = fig.add_subplot(121, projection='3d')
ax.plot_surface(x[None,:], y[:,None], g)
ax = fig.add_subplot(122, projection='3d')
ax.plot_surface(x[None,:], y[:,None], lapg)
pp.show()

plots produced by code above


Edit: Boundary extension would work as follows:

import numpy as np
import matplotlib.pyplot as pp

n_true = 30 # number of pixels we want to compute
n_boundary = 15 # number of pixels to extend the image in all directions
c = 4
d = 4

# First compute g and lapg including boundary extenstion
n = n_true + n_boundary * 2
x = np.arange(-n//2,n//2)
y = np.arange(-n//2,n//2)
g = (1/2)*c*x[None,:]**2 + (1/2)*d*y[:,None]**2
kx = 2 * np.pi * np.fft.fftfreq(n)
ky = 2 * np.pi * np.fft.fftfreq(n)
lapg = np.real(np.fft.ifft2(np.fft.fft2(g) * (-kx[None, :]**2 - ky[:, None]**2)))

# Now crop the two images to our desired size
x = x[n_boundary:-n_boundary]
y = y[n_boundary:-n_boundary]
g = g[n_boundary:-n_boundary, n_boundary:-n_boundary]
lapg = lapg[n_boundary:-n_boundary, n_boundary:-n_boundary]

# Display
fig = pp.figure()
ax = fig.add_subplot(121, projection='3d')
ax.plot_surface(x[None,:], y[:,None], g)
ax.set_zlim(0, 800)
ax = fig.add_subplot(122, projection='3d')
ax.plot_surface(x[None,:], y[:,None], lapg)
ax.set_zlim(0, 800)
pp.show()

graphical output of code above

Note that I'm scaling the z-axes of the two plots in the same way to not enhance the effects of the boundary too much. Fourier-domain filtering like this is typically much more sensitive to edge effects than spatial-domain (or temporal-domain) filtering because the filter has an infinitely-long impulse response. If you leave out the set_zlim command, you'll see a ripple effect in the otherwise flat lapg image. The ripples are very small, but no matter how small, they'll look huge on a completely flat function because they'll stretch from the bottom to the top of the plot. The equal set_zlim in the two plots just puts this noise in proportion.

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120