Inverse FFT returns negative values when it should not

旧城冷巷雨未停 提交于 2019-12-24 00:09:10

问题


I have several points (x,y,z coordinates) in a 3D box with associated masses. I want to draw an histogram of the mass-density that is found in spheres of a given radius R.

I have written a code that, providing I did not make any errors which I think I may have, works in the following way:

  • My "real" data is something huge thus I wrote a little code to generate non overlapping points randomly with arbitrary mass in a box.

  • I compute a 3D histogram (weighted by mass) with a binning about 10 times smaller than the radius of my spheres.

  • I take the FFT of my histogram, compute the wave-modes (kx, ky and kz) and use them to multiply my histogram in Fourier space by the analytic expression of the 3D top-hat window (sphere filtering) function in Fourier space.

  • I inverse FFT my newly computed grid.

Thus drawing a 1D-histogram of the values on each bin would give me what I want.

My issue is the following: given what I do there should not be any negative values in my inverted FFT grid (step 4), but I get some, and with values much higher that the numerical error.

If I run my code on a small box (300x300x300 cm3 and the points of separated by at least 1 cm) I do not get the issue. I do get it for 600x600x600 cm3 though.

If I set all the masses to 0, thus working on an empty grid, I do get back my 0 without any noted issues.

I here give my code in a full block so that it is easily copied.

import numpy as np
import matplotlib.pyplot as plt
import random
from numba import njit

# 1. Generate a bunch of points with masses from 1 to 3 separated by a radius of 1 cm

radius = 1
rangeX = (0, 100)
rangeY = (0, 100)
rangeZ = (0, 100)
rangem = (1,3)
qty = 20000  # or however many points you want

# Generate a set of all points within 1 of the origin, to be used as offsets later
deltas = set()
for x in range(-radius, radius+1):
    for y in range(-radius, radius+1):
        for z in range(-radius, radius+1):
            if x*x + y*y + z*z<= radius*radius:
                deltas.add((x,y,z))

X = []
Y = []
Z = []
M = []
excluded = set()
for i in range(qty):
    x = random.randrange(*rangeX)
    y = random.randrange(*rangeY)
    z = random.randrange(*rangeZ)
    m = random.uniform(*rangem)
    if (x,y,z) in excluded: continue
    X.append(x)
    Y.append(y)
    Z.append(z)
    M.append(m)
    excluded.update((x+dx, y+dy, z+dz) for (dx,dy,dz) in deltas)

print("There is ",len(X)," points in the box")

# Compute the 3D histogram
a = np.vstack((X, Y, Z)).T
b = 200

H, edges = np.histogramdd(a, weights=M, bins = b)      

# Compute the FFT of the grid
Fh = np.fft.fftn(H, axes=(-3,-2, -1))

# Compute the different wave-modes
kx = 2*np.pi*np.fft.fftfreq(len(edges[0][:-1]))*len(edges[0][:-1])/(np.amax(X)-np.amin(X))
ky = 2*np.pi*np.fft.fftfreq(len(edges[1][:-1]))*len(edges[1][:-1])/(np.amax(Y)-np.amin(Y))
kz = 2*np.pi*np.fft.fftfreq(len(edges[2][:-1]))*len(edges[2][:-1])/(np.amax(Z)-np.amin(Z))

# I create a matrix containing the values of the filter in each point of the grid in Fourier space

R = 5                                                                                               
Kh = np.empty((len(kx),len(ky),len(kz)))

@njit(parallel=True)
def func_njit(kx, ky, kz, Kh):
    for i in range(len(kx)):
        for j in range(len(ky)):
            for k in range(len(kz)):
                if np.sqrt(kx[i]**2+ky[j]**2+kz[k]**2) != 0:
                    Kh[i][j][k] = (np.sin((np.sqrt(kx[i]**2+ky[j]**2+kz[k]**2))*R)-(np.sqrt(kx[i]**2+ky[j]**2+kz[k]**2))*R*np.cos((np.sqrt(kx[i]**2+ky[j]**2+kz[k]**2))*R))*3/((np.sqrt(kx[i]**2+ky[j]**2+kz[k]**2))*R)**3
                else:
                    Kh[i][j][k] = 1
    return Kh

Kh = func_njit(kx, ky, kz, Kh)

# I multiply each point of my grid by the associated value of the filter (multiplication in Fourier space = convolution in real space)

Gh = np.multiply(Fh, Kh)

# I take the inverse FFT of my filtered grid. I take the real part to get back floats but there should only be zeros for the imaginary part.

Density = np.real(np.fft.ifftn(Gh,axes=(-3,-2, -1)))

# Here it shows if there are negative values the magnitude of the error

print(np.min(Density))

D = Density.flatten()
N = np.mean(D)

# I then compute the histogram I want

hist, bins = np.histogram(D/N, bins='auto', density=True)
bin_centers = (bins[1:]+bins[:-1])*0.5
plt.plot(bin_centers, hist)
plt.xlabel('rho/rhom')
plt.ylabel('P(rho)')

plt.show()

Do you know why I'm getting these negative values? Do you think there is a simpler way to proceed?

Sorry if this is a very long post, I tried to make it very clear and will edit it with your comments, thanks a lot!

-EDIT-

A follow-up question on the issue can be found [here].1


回答1:


The filter you create in the frequency domain is only an approximation to the filter you want to create. The problem is that we are dealing with the DFT here, not the continuous-domain FT (with its infinite frequencies). The Fourier transform of a ball is indeed the function you describe, however this function is infinitely large -- it is not band-limited!

By sampling this function only within a window, you are effectively multiplying it with an ideal low-pass filter (the rectangle of the domain). This low-pass filter, in the spatial domain, has negative values. Therefore, the filter you create also has negative values in the spatial domain.

This is a slice through the origin of the inverse transform of Kh (after I applied fftshift to move the origin to the middle of the image, for better display):

As you can tell here, there is some ringing that leads to negative values.

One way to overcome this ringing is to apply a windowing function in the frequency domain. Another option is to generate a ball in the spatial domain, and compute its Fourier transform. This second option would be the simplest to achieve. Do remember that the kernel in the spatial domain must also have the origin at the top-left pixel to obtain a correct FFT.

A windowing function is typically applied in the spatial domain to avoid issues with the image border when computing the FFT. Here, I propose to apply such a window in the frequency domain to avoid similar issues when computing the IFFT. Note, however, that this will always further reduce the bandwidth of the kernel (the windowing function would work as a low-pass filter after all), and therefore yield a smoother transition of foreground to background in the spatial domain (i.e. the spatial domain kernel will not have as sharp a transition as you might like). The best known windowing functions are Hamming and Hann windows, but there are many others worth trying out.


Unsolicited advice:

I simplified your code to compute Kh to the following:

kr = np.sqrt(kx[:,None,None]**2 + ky[None,:,None]**2 + kz[None,None,:]**2)
kr *= R
Kh = (np.sin(kr)-kr*np.cos(kr))*3/(kr)**3
Kh[0,0,0] = 1

I find this easier to read than the nested loops. It should also be significantly faster, and avoid the need for njit. Note that you were computing the same distance (what I call kr here) 5 times. Factoring out such computation is not only faster, but yields more readable code.




回答2:


Just a guess:

Where do you get the idea that the imaginary part MUST be zero? Have you ever tried to take the absolute values (sqrt(re^2 + im^2)) and forget about the phase instead of just taking the real part? Just something that came to my mind.



来源:https://stackoverflow.com/questions/54022376/inverse-fft-returns-negative-values-when-it-should-not

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!