Repository: yt-project/unyt (language: python)

Issue:
unyt processes units for FFTs incorrectly
* unyt version: 3.0.3
* Python version: 3.12
* Operating System: MacOS 

### Description

When using a fast Fourier transform, I expect the units that are returned to be the same as the original function. Dimensionally, the Fourier transform ($`F(k)`$) is a decomposition of the form:

$$f(x) ~= \int F(k) e^{-ikx} \cdot dk$$

However, when using the `np.fft` functions on `unyt` arrays, they return units that are the inverse of the original array.

For example, the minimal working example below:

```python
import matplotlib.pyplot as plt
import unyt
import numpy as np

# Dirac Delta
x = unyt.unyt_array(np.linspace(-5, 5, 128), "s", name="Time")
y = unyt.unyt_array(np.zeros_like(x.v), "K", name="Temperature")
y[len(y) // 2] = 1

# FFT that thing
fft = np.fft.fft(y)
amps = np.sqrt((fft * fft.conj()).real)
dk = 2.0 * np.pi / (10.0 * unyt.s)
k = np.fft.fftfreq(len(x), d=1.0 / dk) * len(x)

k.name = "Wavenumber $k$"
amps.name = "FFT of Temperature"

# Plot it up
with unyt.matplotlib_support:
    fig, (axreal, axfft) = plt.subplots(1, 2, figsize=(6, 2.5))

    axreal.plot(x, y)
    axfft.plot(k, amps)

fig.tight_layout()

plt.show()
```

This produces the following figure:

<img width="879" alt="Screenshot 2024-10-25 at 8 20 19 AM" src="https://github.com/user-attachments/assets/f424a07f-2a50-41b7-996e-164781532950">

edit by @neutrinoceros: fixed latex formatting 


Discussion / hints:
oof. Thanks for catching this. That's a two y.o. mistake I made in https://github.com/yt-project/unyt/pull/313
It should be pretty straight forward to fix though, once you know where to look (and the affected code is pretty much self-contained in #313). Are you interested in giving it a go yourself ?
Yes, I can take a look. For sanity's sake, I just checked all this with astropy, which gives the correct units:

```python
import matplotlib.pyplot as plt
from astropy.visualization import quantity_support
import astropy.units as u
import numpy as np

quantity_support()

# Dirac Delta
x = np.linspace(-5, 5, 128) * u.s
y = np.zeros_like(x.value) * u.K
y[len(y) // 2] = 1 * u.K

# FFT that thing
fft = np.fft.fft(y)
amps = np.sqrt((fft * fft.conj()).real)
dk = 2.0 * np.pi / (10.0 * u.s)
k = np.fft.fftfreq(len(x), d=1.0 / dk) * len(x)

# Plot it up
fig, (axreal, axfft) = plt.subplots(1, 2, figsize=(6, 2.5))

axreal.plot(x, y)
axfft.plot(k, amps)

fig.tight_layout()

plt.show()
```

<img width="594" alt="Screenshot 2024-10-25 at 9 10 42 AM" src="https://github.com/user-attachments/assets/3ede0992-b7eb-4736-8239-4c2a77a7cda9">

(yes, I also had a look at how astropy does it before I applied the "bug" label, to make sure I didn't just copied a bug from there and made my *own* mistakes :) )

Fix patch:
diff --git a/unyt/_array_functions.py b/unyt/_array_functions.py
index 4b22dd20..646629e2 100644
--- a/unyt/_array_functions.py
+++ b/unyt/_array_functions.py
@@ -358,72 +358,72 @@ def block(arrays):
 
 @implements(np.fft.fft)
 def ftt_fft(a, *args, **kwargs):
-    return np.fft.fft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.fft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.fft2)
 def ftt_fft2(a, *args, **kwargs):
-    return np.fft.fft2._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.fft2._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.fftn)
 def ftt_fftn(a, *args, **kwargs):
-    return np.fft.fftn._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.fftn._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.hfft)
 def ftt_hfft(a, *args, **kwargs):
-    return np.fft.hfft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.hfft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.rfft)
 def ftt_rfft(a, *args, **kwargs):
-    return np.fft.rfft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.rfft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.rfft2)
 def ftt_rfft2(a, *args, **kwargs):
-    return np.fft.rfft2._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.rfft2._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.rfftn)
 def ftt_rfftn(a, *args, **kwargs):
-    return np.fft.rfftn._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.rfftn._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.ifft)
 def ftt_ifft(a, *args, **kwargs):
-    return np.fft.ifft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.ifft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.ifft2)
 def ftt_ifft2(a, *args, **kwargs):
-    return np.fft.ifft2._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.ifft2._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.ifftn)
 def ftt_ifftn(a, *args, **kwargs):
-    return np.fft.ifftn._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.ifftn._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.ihfft)
 def ftt_ihfft(a, *args, **kwargs):
-    return np.fft.ihfft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.ihfft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.irfft)
 def ftt_irfft(a, *args, **kwargs):
-    return np.fft.irfft._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.irfft._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.irfft2)
 def ftt_irfft2(a, *args, **kwargs):
-    return np.fft.irfft2._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.irfft2._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.irfftn)
 def ftt_irfftn(a, *args, **kwargs):
-    return np.fft.irfftn._implementation(np.asarray(a), *args, **kwargs) / a.units
+    return np.fft.irfftn._implementation(np.asarray(a), *args, **kwargs) * a.units
 
 
 @implements(np.fft.fftshift)


Test patch:
diff --git a/unyt/tests/test_array_functions.py b/unyt/tests/test_array_functions.py
index 4903e93c..bfd900ab 100644
--- a/unyt/tests/test_array_functions.py
+++ b/unyt/tests/test_array_functions.py
@@ -923,7 +923,7 @@ def test_fft_1D(func):
     x1 = [0, 1, 2] * cm
     res = func(x1)
     assert type(res) is unyt_array
-    assert res.units == (1 / cm).units
+    assert res.units == (1 * cm).units
 
 
 @pytest.mark.parametrize(
@@ -943,7 +943,7 @@ def test_fft_ND(func):
     x1 = [[0, 1, 2], [0, 1, 2], [0, 1, 2]] * cm
     res = func(x1)
     assert type(res) is unyt_array
-    assert res.units == (1 / cm).units
+    assert res.units == (1 * cm).units
 
 
 @pytest.mark.parametrize("func", [np.fft.fftshift, np.fft.ifftshift])