```"""
Low-level functions to work in frequency domain for n-dim arrays
"""

import numpy as np

def fscale(ns, si=1, one_sided=False):
"""
numpy.fft.fftfreq returns Nyquist as a negative frequency so we propose this instead

:param ns: number of samples
:param si: sampling interval in seconds
:param one_sided: if True, returns only positive frequencies
:return: fscale: numpy vector containing frequencies in Hertz
"""
fsc = np.arange(0, np.floor(ns / 2) + 1) / ns / si  # sample the frequency scale
if one_sided:
return fsc
else:
return np.concatenate((fsc, -fsc[slice(-2 + (ns % 2), 0, -1)]), axis=0)

def freduce(x, axis=None):
"""
Reduces a spectrum to positive frequencies only
Works on the last dimension (contiguous in c-stored array)

:param x: numpy.ndarray
:param axis: axis along which to perform reduction (last axis by default)
:return: numpy.ndarray
"""
if axis is None:
axis = x.ndim - 1
siz = list(x.shape)
siz[axis] = int(np.floor(siz[axis] / 2 + 1))
return np.take(x, np.arange(0, siz[axis]), axis=axis)

def fexpand(x, ns=1, axis=None):
"""
Reconstructs full spectrum from positive frequencies
Works on the last dimension (contiguous in c-stored array)

:param x: numpy.ndarray
:param axis: axis along which to perform reduction (last axis by default)
:return: numpy.ndarray
"""
if axis is None:
axis = x.ndim - 1
# dec = int(ns % 2) * 2 - 1
# xcomp = np.conj(np.flip(x[..., 1:x.shape[-1] + dec], axis=axis))
ilast = int((ns + (ns % 2)) / 2)
xcomp = np.conj(np.flip(np.take(x, np.arange(1, ilast), axis=axis), axis=axis))
return np.concatenate((x, xcomp), axis=axis)

def bp(ts, si, b, axis=None):
"""
Band-pass filter in frequency domain

:param ts: time serie
:param si: sampling interval in seconds
:param b: cutout frequencies: 4 elements vector or list
:param axis: axis along which to perform reduction (last axis by default)
:return: filtered time serie
"""
return _freq_filter(ts, si, b, axis=axis, typ='bp')

def lp(ts, si, b, axis=None):
"""
Low-pass filter in frequency domain

:param ts: time serie
:param si: sampling interval in seconds
:param b: cutout frequencies: 2 elements vector or list
:param axis: axis along which to perform reduction (last axis by default)
:return: filtered time serie
"""
return _freq_filter(ts, si, b, axis=axis, typ='lp')

def hp(ts, si, b, axis=None):
"""
High-pass filter in frequency domain

:param ts: time serie
:param si: sampling interval in seconds
:param b: cutout frequencies: 2 elements vector or list
:param axis: axis along which to perform reduction (last axis by default)
:return: filtered time serie
"""
return _freq_filter(ts, si, b, axis=axis, typ='hp')

def _freq_filter(ts, si, b, axis=None, typ='lp'):
"""
Wrapper for hp/lp/bp filters
"""
if axis is None:
axis = ts.ndim - 1
ns = ts.shape[axis]
f = fscale(ns, si=si, one_sided=True)
if typ == 'bp':
filc = _freq_vector(f, b[0:2], typ='hp') * _freq_vector(f, b[2:4], typ='lp')
else:
filc = _freq_vector(f, b, typ=typ)
if axis < (ts.ndim - 1):
filc = filc[:, np.newaxis]
return np.real(np.fft.ifft(np.fft.fft(ts, axis=axis) * fexpand(filc, ns, axis=0), axis=axis))

def _freq_vector(f, b, typ='lp'):
"""
Returns a frequency modulated vector for filtering

:param f: frequency vector, uniform and monotonic
:param b: 2 bounds array
:return: amplitude modulated frequency vector
"""
filc = ((f <= b[0]).astype(float) +
np.bitwise_and(f > b[0], f < b[1]).astype(float) *
(0.5 * (1 + np.sin(np.pi * (f - ((b[0] + b[1]) / 2)) /
(b[0] - b[1])))))
if typ == 'hp':
return 1 - filc
elif typ == 'lp':
return filc
```