Skip to content

Commit

Permalink
Merge pull request #190 from PrincetonUniversity/spec_fft
Browse files Browse the repository at this point in the history
Spec fft class added and first unittests thereof included.
  • Loading branch information
smiet authored Oct 16, 2023
2 parents fb6ec0b + 95ea0bc commit c30cff4
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 14 deletions.
2 changes: 2 additions & 0 deletions Utilities/pythontools/py_spec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
from .ci import test
from .input.spec_namelist import SPECNamelist
from .output.spec import SPECout
from .math.spec_fft import spec_fft
from .math.spec_invfft import spec_invfft
192 changes: 192 additions & 0 deletions Utilities/pythontools/py_spec/math/spec_fft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import numpy as np
import copy
import warnings

def spec_fft(tarr: np.array,
zarr: np.array,
freal: np.array,
Mpol: int=None,
Ntor: int=None,
output: str='1D'):
"""
Fourier transform as in SPEC fortran source
Args:
- tarr: 1D numpy array of size nt. Poloidal angle coordinate, each point
should be equidistant
- zarr: 1D numpy array of size nz. Toroidal angle coordinate, each point
should be equidistant
- freal: 2D numpy array of size (nz,nt). Function, in real space, for which
Fourier harmonics should be evaluated
- Mpol: integer. Poloidal resolution. Default is nt/2.
- Ntor: integer. Toroidal resolution. Default is nz/2.
- output: either '1D' or '2D'. Determine the form of the output.
Output:
If output=='1D', the output is a 4-tuple 1D numpy array, with modes
organized as in SPEC (first increasing n, then increasing m). First
tuple element are the even modes, second are the odd modes, third
are the poloidal mode number, and fourth the toroidal mode number.
If output=='2D', the output is tuple of 2D numpy array, with modes (m,n)
in element (Ntor+n,m). First tuple elements are the even modes,
seconds are the odd modes
"""

# Check input
# -----------
# tarr
if not isinstance(tarr, np.ndarray):
raise ValueError('tarr should be a numpy array')
if tarr.ndim>1:
raise ValueError('tarr should be one-dimensional')

nt = tarr.size
if nt<3:
raise ValueError('Not enough points in tarr')

# zarr
if not isinstance(zarr, np.ndarray):
raise ValueError('zarr should be a numpy array')
if zarr.ndim>1:
raise ValueError('zarr should be one-dimensional')

nz = zarr.size
if nz<3:
raise ValueError('Not enough points in zarr')

# freal
if not isinstance(freal, np.ndarray):
raise ValueError('freal should be a numpy array')
if freal.ndim!=2:
raise ValueError('freal should be two-dimensional')
if freal.shape!=(nz,nt):
raise ValueError('freal should be of size (nz,nt)')

if tarr[0]+2*np.pi==tarr[-1]:
warnings.warn('tarr[-1] should not be equal to tarr[0]. Removing last element...')
tarr = tarr[:-1]
nt -= 1
freal = freal[:,:-1]

if zarr[0]+2*np.pi==zarr[-1]:
warnings.warn('zarr[-1] should not be equal to zarr[0]. Removing last element...')
zarr = zarr[:-1]
nz -= 1
freal = freal[:-1,:]

if not np.all(np.abs(np.diff(np.diff(tarr)))<1e-14):
raise ValueError('Points should be equidistant in tarr')
if not np.all(np.abs(np.diff(np.diff(zarr)))<1e-14):
raise ValueError('Points should be equidistant in zarr')

# Mpol
M = int(np.floor(nt/2))
if Mpol is None:
Mpol = M
if not isinstance(Mpol, int):
raise ValueError('Mpol should be an integer')
if Mpol<1 or Mpol>nt/2:
raise ValueError('Mpol should be at least 1 and smaller than nt/2')

# Ntor
N = int(np.floor(nz/2))
if Ntor is None:
Ntor = N
if not isinstance(Ntor, int):
raise ValueError('Ntor should be an integer')
if Ntor<1 or Ntor>nz/2:
raise ValueError('Ntor should be at least 1 and smaller than nt=z/2')

# output
if not isinstance(output, str):
raise ValueError('output should be a string')
if not (output=='1D' or output=='2D'):
raise ValueError('output should be 1D or 2D')

# Do the fft
# ----------
# Actual fft
fmn = np.fft.fft2( freal ) / (nt*nz)

# Now construct 2D array. Negative m-modes have to be added to positive
# m-modes, i.e. (m,n)+(-m,-n) for even modes, and (m,n)-(-m,-n) for odd
# modes.
# Difference if the number of elements (either nt or nz) is odd - this is
# due to the internal implementation of numpy.fft.fft.
efmn = copy.deepcopy(fmn)
efmn[1:,1:] = fmn[1:,1:] + np.flip(fmn[1:,1:])

ofmn = copy.deepcopy(fmn)
ofmn[1:,1:] = fmn[1:,1:] - np.flip(fmn[1:,1:])
if np.mod(nt,2)==0:
efmn[0,1:M+1] += np.flip(fmn[0,M:]) #n=0
ofmn[0,1:M+1] -= np.flip(fmn[0,M:])
else:
efmn[0,1:M+1] += np.flip(fmn[0,M+1:]) #n=0
ofmn[0,1:M+1] -= np.flip(fmn[0,M+1:])

if np.mod(nz,2)==0:
efmn[1:N+1,0] += np.flip(fmn[N:,0]) # m=0
ofmn[1:N+1,0] -= np.flip(fmn[N:,0])
else:
efmn[1:N+1,0] += np.flip(fmn[N+1:,0]) # m=0
ofmn[1:N+1,0] -= np.flip(fmn[N+1:,0])

efmn = (np.absolute(efmn) * np.cos(np.angle(efmn)))[:,0:M+1]
ofmn = (np.absolute(ofmn) * np.sin(np.angle(ofmn)))[:,0:M+1]

# Change sign of m>0 odd modes
ofmn[:,1:] = -ofmn[:,1:]

# Shift result to be centered around n=0 mode
efmn = np.fft.fftshift(efmn, axes=0)
ofmn = np.fft.fftshift(ofmn, axes=0)

# Invert n -> -n
if np.mod(nz,2)==0:
efmn[1:,1:] = np.flip(efmn[1:,1:],axis=0)
ofmn[1:,1:] = np.flip(ofmn[1:,1:],axis=0)
else:
efmn[:,1:] = np.flip(efmn[:,1:],axis=0)
ofmn[:,1:] = np.flip(ofmn[:,1:],axis=0)


# Prepare output
# --------------
if output=='2D':
# 2D - truncate efmn, ofmn to the required resolution
even_out = efmn[N-Ntor:N+Ntor+1,0:Mpol+1]
odd_out = ofmn[N-Ntor:N+Ntor+1,0:Mpol+1]
return (even_out, odd_out)

elif output=='1D':
# 1D - construct array mode by mode
nmn = Ntor+1 + Mpol*(2*Ntor+1)
even_out = np.zeros((nmn,))
odd_out = np.zeros((nmn,))
_im = np.zeros((nmn,), dtype=int)
_in = np.zeros((nmn,), dtype=int)

ind = -1
for mm in range(0,Mpol+1):
for nn in range(-Ntor,Ntor+1):
if mm==0 and nn<0:
continue

ind += 1

_im[ind]=mm
_in[ind]=nn
even_out[ind] = efmn[N+nn,mm]
odd_out[ind] = ofmn[N+nn,mm]

return (even_out, odd_out, _im, _in)

else:
raise ValueError('Invalid output')






89 changes: 89 additions & 0 deletions Utilities/pythontools/py_spec/math/spec_invfft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import numpy as np

def spec_invfft(tarr:np.ndarray, zarr:np.ndarray,
efmn:np.ndarray, ofmn:np.ndarray,
_im:np.ndarray, _in:np.ndarray):
"""
Inverse Fourier transform as in SPEC Fortran source.
Args:
- tarr: 1D numpy array of size nt. Poloidal angle coordinate.
- zarr: 1D numpy array of size nz. Toroidal angle coordinate.
- efmn: 1D numpy array of size nmn. Even mode numbers.
- ofmn: 1D numpy array of size nmn. Odd mode numbers.
- _im: 1D numpy array of size nmn. Poloidal mode numbers
- _in: 1D numpy array of size nmn. Toroidal mode numbers (multiples of Nfp)
Output:
- freal: 2D numpy array of size nz x nt. Function evaluation in real space.
"""

# Check input
# -----------
# tarr
if not isinstance(tarr, np.ndarray):
raise ValueError('tarr should be a numpy array')
if tarr.ndim>1:
raise ValueError('tarr should be one-dimensional')

nt = tarr.size

# zarr
if not isinstance(zarr, np.ndarray):
raise ValueError('zarr should be a numpy array')
if zarr.ndim>1:
raise ValueError('zarr should be one-dimensional')

nz = zarr.size

# efmn
if not isinstance(efmn, np.ndarray):
raise ValueError('efmn should be a numpy array')
if efmn.ndim>1:
raise ValueError('efmn should be one-dimensional')

nmn = efmn.size

# ofmn
if not isinstance(ofmn, np.ndarray):
raise ValueError('ofmn should be a numpy array')
if ofmn.ndim>1:
raise ValueError('ofmn should be one-dimensional')
if ofmn.size!=nmn:
raise ValueError('ofmn should have the same size as efmn')

# _im
if not isinstance(_im, np.ndarray):
raise ValueError('_im should be a numpy array')
if _im.ndim>1:
raise ValueError('_im should be one-dimensional')
if _im.size!=nmn:
raise ValueError('_im should have the same size as efmn')
if not np.all(_im>=0):
raise ValueError('_im should not have any negative values')

# _in
if not isinstance(_in, np.ndarray):
raise ValueError('_in should be a numpy array')
if _in.ndim>1:
raise ValueError('_in should be one-dimensional')
if _in.size!=nmn:
raise ValueError('_in should have the same size as efmn')

# Inverse Fourier transform
# -------------------------

# Generate grid:
tgrid, zgrid = np.meshgrid(tarr, zarr)

# Evaluate function:
freal = np.zeros((nz,nt))
for mm, nn, emode, omode in zip(_im, _in, efmn, ofmn):
freal += emode * np.cos(mm*tgrid - nn*zgrid) \
+ omode * np.sin(mm*tgrid - nn*zgrid)

# Output
return freal



14 changes: 0 additions & 14 deletions Utilities/pythontools/py_spec/output/_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,6 @@ def get_RZ_derivatives(

return Rarr0, Rarr1, Rarr2, Rarr3, Zarr0, Zarr1, Zarr2, Zarr3






def get_grid_and_jacobian_and_metric(
self,
lvol=0,
Expand Down Expand Up @@ -362,7 +357,6 @@ def get_grid_and_jacobian_and_metric(
else:
return Rarr0, Zarr0, jacobian, g


def grid(
self,
lvol=0,
Expand All @@ -377,7 +371,6 @@ def grid(
)
return Rarr0, Zarr0


def jacobian(
self,
lvol=0,
Expand All @@ -392,7 +385,6 @@ def jacobian(
)
return jacobian


def metric(
self,
lvol=0,
Expand All @@ -407,7 +399,6 @@ def metric(
)
return g


def get_B(
self,
lvol=0,
Expand Down Expand Up @@ -484,7 +475,6 @@ def get_modB(self, Bcontrav, g, derivative=False, dBcontrav=None, dg=None):
) + np.einsum("...i,...kji,...j->...k", Bcontrav, dg, Bcontrav)
return modB, dmodB2


def get_B_covariant(self, Bcontrav, g, derivative=False):
"""Get covariant component of B"""
Bco = np.einsum("...i,...ji->...j", Bcontrav, g)
Expand Down Expand Up @@ -568,7 +558,6 @@ def get_average_beta(self, ns=64, nt=64, nz=64):

return betavol.sum() / vols.sum()


def get_peak_beta(self, ns=64, nt=64, nz=64):
if self.input.physics.pressure.size>1:
press = self.input.physics.pressure[0] * self.input.physics.pscale
Expand All @@ -594,7 +583,6 @@ def get_peak_beta(self, ns=64, nt=64, nz=64):
y=integrate.simpson(
y=sg / modB**2, x=zarr ), x=tarr ), x=sarr ) / vol


def test_derivatives(self, lvol=0, s=0.3, t=0.4, z=0.5, delta=1e-6, tol=1e-6):
ds = delta
R, Z, j, g = self.get_grid_and_jacobian_and_metric(lvol, np.array([s-ds, s+ds]), np.array([t-ds, t+ds]), np.array([z-ds, z+ds]))
Expand All @@ -618,7 +606,6 @@ def test_derivatives(self, lvol=0, s=0.3, t=0.4, z=0.5, delta=1e-6, tol=1e-6):
print((g[0,1,0,:,:] - g[0,0,0,:,:])/ds/2-dg[0,0,0,1,:,:])
print((g[0,0,1,:,:] - g[0,0,0,:,:])/ds/2-dg[0,0,0,2,:,:])


def get_surface_current_density(self, lsurf:int=None, nt:int=64, nz:int=64):
"""Compute j_surf.B on each side of the provided interfaces
Expand Down Expand Up @@ -696,7 +683,6 @@ def get_surface_current_density(self, lsurf:int=None, nt:int=64, nz:int=64):

return j_dot_B, tarr, zarr


def get_surface(self, lsurf:int=None, nt:int=64, nz:int=64):
"""Compute the surface area of a volume interface
Expand Down
4 changes: 4 additions & 0 deletions Utilities/pythontools/py_spec/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
To run tests, do
```
python -m unittest -v test_math.test_spec_fft
```
Loading

0 comments on commit c30cff4

Please sign in to comment.