def test_ffsn_sample(self): N_s = [4, 3] for mod in AVAILABLE_MOD: sample_points, idx = ffsn_sample(T=[1, 1], N_FS=[3, 3], T_c=[0, 0], N_s=N_s, mod=mod) # check sample points assert sample_points[0].shape == (N_s[0], 1) assert sample_points[1].shape == (1, N_s[1]) mod.testing.assert_array_equal( sample_points[0][:, 0], mod.array([0.125, 0.375, -0.375, -0.125])) mod.testing.assert_array_equal(sample_points[1][0, :], mod.array([0, 1 / 3, -1 / 3])) # check index values assert idx[0].shape == (N_s[0], 1) assert idx[1].shape == (1, N_s[1]) mod.testing.assert_array_equal(idx[0][:, 0], mod.array([2, 3, 0, 1])) mod.testing.assert_array_equal(idx[1][0, :], mod.array([1, 2, 0])) assert all([get_array_module(s) == mod for s in sample_points]) assert all([get_array_module(i) == mod for i in idx])
def test_ffs_sample(self): for mod in AVAILABLE_MOD: sample_points, idx = ffs_sample(T=1, N_FS=5, T_c=mod.pi, N_s=8, mod=mod) mod.testing.assert_array_equal( mod.around(sample_points, 2), mod.array([3.2, 3.33, 3.45, 3.58, 2.7, 2.83, 2.95, 3.08]), ) mod.testing.assert_array_equal( idx, mod.array([4, 5, 6, 7, 0, 1, 2, 3]), ) assert get_array_module(sample_points) == mod assert get_array_module(idx) == mod
def test_ffsn_sample_shape(self): for mod in AVAILABLE_MOD: D = 5 T = mod.ones(D) N_FS = mod.arange(D) * 2 + 3 T_c = mod.zeros(D) N_s = N_FS sample_points, idx = ffsn_sample(T=T, N_FS=N_FS, T_c=T_c, N_s=N_s, mod=mod) # check shape for d in range(D): sh = [1] * D sh[d] = N_s[d] assert list(sample_points[d].shape) == sh assert list(idx[d].shape) == sh assert all([get_array_module(s) == mod for s in sample_points]) assert all([get_array_module(i) == mod for i in idx])
def dirichlet(x, T, T_c, N_FS): r""" Return samples of a shifted Dirichlet kernel of period :math:`T` and bandwidth :math:`N_{FS} = 2 N + 1`: .. math:: \phi(t) = \sum_{k = -N}^{N} \exp\left( j \frac{2 \pi}{T} k (t - T_{c}) \right) = \frac{\sin\left( N_{FS} \pi [t - T_{c}] / T \right)}{\sin\left( \pi [t - T_{c}] / T \right)}. Parameters ---------- x : :py:class:`~numpy.ndarray` Sampling points. T : float Function period. T_c : float Period mid-point. N_FS : int Function bandwidth. Returns ------- vals : :py:class:`~numpy.ndarray` Function values. See Also -------- :py:func:`~pyffs.func.dirichlet_fs` """ xp = get_array_module(x) y = x - T_c n, d = xp.zeros((2, *x.shape)) nan_mask = xp.isclose(xp.fmod(y, xp.pi), 0) n[~nan_mask] = xp.sin(N_FS * xp.pi * y[~nan_mask] / T) d[~nan_mask] = xp.sin(xp.pi * y[~nan_mask] / T) n[nan_mask] = N_FS * xp.cos(N_FS * xp.pi * y[nan_mask] / T) d[nan_mask] = xp.cos(xp.pi * y[nan_mask] / T) return n / d
def dirichlet_2D(sample_points, T, T_c, N_FS): r""" Return samples of a shifted 2D Dirichlet kernel of period :math:`(T_x, T_y)` and bandwidth :math:`N_{FS, x} = 2 N_x + 1, N_{FS, y} = 2 N_y + 1`: .. math:: \phi(x, y) &= \sum_{k_x = -N_x}^{N_x} \sum_{k_y = -N_y}^{N_y} \exp\left( j \frac{2 \pi}{T_x} k_x (x - T_{c,x}) \right) \exp\left( j \frac{2 \pi}{T_y} k_y (y - T_{c,y}) \right) \\ &= \frac{\sin\left( N_{FS, x} \pi [x - T_{c,x}] / T_x \right)}{\sin\left( \pi [x - T_{c, x}] / T_x \right)} \frac{\sin\left( N_{FS, y} \pi [y - T_{c,y}] / T_y \right)}{\sin\left( \pi [y - T_{c, y}] / T_y \right)}. Parameters ---------- sample_points : list(:py:class:`~numpy.ndarray`) (2,) coordinates at which to sample the function in the x- and y-dimensions respectively. Array dimensions must be compatible after broadcasting. T : list(float) Function period. T_c : list(float) Period mid-point. N_FS : list(int) Function bandwidth. Returns ------- vals : :py:class:`~numpy.ndarray` Function values at `sample_points`. See Also -------- :py:func:`~pyffs.util.ffsn_sample`, :py:func:`~pyffs.func.dirichlet_fs` """ xp = get_array_module(sample_points) x, y = sample_points x_vals = dirichlet(x, T=T[0], T_c=T_c[0], N_FS=N_FS[0]) y_vals = dirichlet(y, T=T[1], T_c=T_c[1], N_FS=N_FS[1]) return x_vals * y_vals
def cztn(x, A, W, M, axes=None): """ Multi-dimensional Chirp Z-transform. Parameters ---------- x : :py:class:`~numpy.ndarray` (..., N_1, N_2, ..., N_D, ...) input values. A : list(float or complex) Circular offset from the positive real-axis, for each dimension. W : list(float or complex) Circular spacing between transform points, for each dimension. M : list(int) Length of transform for each dimension. axes : tuple Dimensions of `x` along which transform should be applied. Returns ------- x_czt : :py:class:`~numpy.ndarray` (..., M_1, M_2, ..., M_D, ...) transformed input along the axes indicated by `axes`. Notes ----- Due to numerical instability when using large `M`, this implementation only supports transforms where each element of `A` and `W` has unit norm. Examples -------- .. testsetup:: import numpy as np from pyffs import cztn Implementation of N-dimensional DFT: .. doctest:: >>> N = M = 10 >>> W = np.exp(-1j * 2 * np.pi / N) >>> x = np.random.randn(N, N, N) + 1j * np.random.randn(N, N, N) # extra dimension >>> dft_x = np.fft.fftn(x, axes=(1, 2)) >>> czt_x = cztn(x, A=[1, 1], W=[W, W], M=[M, M], axes=(1, 2)) >>> np.allclose(dft_x, czt_x) True """ axes, A, W = _verify_cztn_input(x, A, W, M, axes) xp = get_array_module(x) # Initialize variables D = len(axes) N = [x.shape[d] for d in axes] L = [] n = [] for d in range(D): _L = next_fast_len(N[d] + M[d] - 1, mod=xp) L.append(_L) n.append(xp.arange(_L)) # Initialize input sh_U = list(x.shape) for d in range(D): sh_U[axes[d]] = L[d] dtype_u = (xp.complex64 if ((x.dtype == xp.dtype("complex64")) or (x.dtype == xp.dtype("float32"))) else xp.complex128) u = xp.zeros(sh_U, dtype=dtype_u) idx = _index_n(u, axes, [slice(n) for n in N]) u[idx] = x # Modulate along each dimension for d in range(D): sh_N = [1] * x.ndim sh_N[axes[d]] = N[d] u_mod_d = (A[d]**-n[d][:N[d]]) * xp.power(W[d], (n[d][:N[d]]**2) / 2) u[idx] *= u_mod_d.reshape(sh_N) U = fftn(u, axes=axes) # Convolve along each dimension -> multiply in frequency domain for d in range(D): _N = N[d] sh_L = [1] * x.ndim sh_L[axes[d]] = L[d] v = xp.zeros(L[d], dtype=complex) v[:M[d]] = xp.power(W[d], -(n[d][:M[d]]**2) / 2) v[L[d] - _N + 1:] = xp.power(W[d], -((L[d] - n[d][L[d] - _N + 1:])**2) / 2) V = fft(v).reshape(sh_L) U *= V g = ifftn(U, axes=axes) # Final modulation in time time_idx = _index_n(g, axes, [slice(m) for m in M]) for d in range(D): sh_M = [1] * x.ndim sh_M[axes[d]] = M[d] g_mod = xp.power(W[d], (n[d][:M[d]]**2) / 2) g[time_idx] *= g_mod.reshape(sh_M) x_czt = g[time_idx] return x_czt
def iffsn(x_FS, T, T_c, N_FS, axes=None): r""" Signal samples from Fourier Series coefficients of a D-dimension signal. :py:func:`~pyffs.ffs.iffsn` is basically the inverse of :py:func:`~pyffs.ffs.ffsn`. Parameters ---------- x_FS : :py:class:`~numpy.ndarray` (..., N_s1, N_s2, ..., N_sD, ...) FS coefficients in ascending order. T : list(float) Function period along each dimension. T_c : list(float) Period mid-point for each dimension. N_FS : list(int) Function bandwidth along each dimension. axes : tuple Dimensions of `x_FS` along which FS coefficients are stored. Returns ------- x : :py:class:`~numpy.ndarray` (..., N_s1, N_s2, ..., N_sD, ...) array containing original function samples given to :py:func:`~pyffs.ffs.ffsn`. In short: :math:`(\text{iFFS} \circ \text{FFS})\{ x \} = x`. Notes ----- Theory: :ref:`FFS_def`. See Also -------- :py:func:`~pyffs.util.ffsn_sample`, :py:func:`~pyffs.ffs.ffsn` """ axes, N_s = _verify_ffsn_input(x_FS, T, T_c, N_FS, axes) xp = get_array_module(x_FS) # check for input type if (x_FS.dtype == xp.dtype("complex64")) or (x_FS.dtype == xp.dtype("float32")): is_complex64 = True x = x_FS.copy().astype(xp.complex64) else: is_complex64 = False x = x_FS.copy().astype(xp.complex128) C_2 = [] for d, ax in enumerate(axes): A_d, B_d = _create_modulation_vectors(N_s[d], N_FS[d], T[d], T_c[d], xp) sh = [1] * x.ndim sh[ax] = N_s[d] # apply pre-mod C_1 = A_d.reshape(sh) if is_complex64: C_1 = C_1.astype(xp.complex64) x *= C_1 # save post-mod C_2.append(B_d.reshape(sh) * N_s[d]) if is_complex64: C_2[d].astype(xp.complex64) x = ifftn(x, axes=axes) # apply modulation after FFT for _c2 in C_2: x *= _c2 return x
def ffsn(x, T, T_c, N_FS, axes=None): r""" Fourier Series coefficients from signal samples of a D-dimension signal. Parameters ---------- x : :py:class:`~numpy.ndarray` (..., N_s1, N_s2, ..., N_sD, ...) function values at sampling points specified by :py:func:`~pyffs.util.ffsn_sample`. T : list(float) Function period along each dimension. T_c : list(float) Period mid-point for each dimension. N_FS : list(int) Function bandwidth along each dimension. axes : tuple Dimensions of `x` along which function samples are stored. Returns ------- x_FS : :py:class:`~numpy.ndarray` (..., N_s1, N_s2, ..., N_sD, ...) array containing Fourier Series coefficients in ascending order (top-left of matrix). Examples -------- Let :math:`\phi(x, y)` be a shifted Dirichlet kernel of periods :math:`(T_x, T_y)` and bandwidths :math:`N_{FS, x} = 2 N_x + 1, N_{FS, y} = 2 N_y + 1`: .. math:: \phi(x, y) &= \sum_{k_x = -N_x}^{N_x} \sum_{k_y = -N_y}^{N_y} \exp\left( j \frac{2 \pi}{T_x} k_x (x - T_{c,x}) \right) \exp\left( j \frac{2 \pi}{T_y} k_y (y - T_{c,y}) \right) \\ &= \frac{\sin\left( N_{FS, x} \pi [x - T_{c,x}] / T_x \right)}{\sin\left( \pi [x - T_{c, x}] / T_x \right)} \frac{\sin\left( N_{FS, y} \pi [y - T_{c,y}] / T_y \right)}{\sin\left( \pi [y - T_{c, y}] / T_y \right)}. Its Fourier Series (FS) coefficients :math:`\phi_{k_x, k_y}^{FS}` can be analytically evaluated using the shift-modulation theorem: .. math:: \phi_{k_x, k_y}^{FS} = \begin{cases} \exp\left( -j \frac{2 \pi}{T_x} k_x T_{c,x} \right) \exp\left( -j \frac{2 \pi}{T_y} k_y T_{c,y} \right) & -N_x \le k_x \le N_x, -N_y \le k_y \le N_y, \\ 0 & \text{otherwise}. \end{cases} Being bandlimited, we can use :py:func:`~pyffs.ffs.ffsn` to numerically evaluate :math:`\{\phi_{k_x, k_y}^{FS}, k_x = -N_x, \ldots, N_x, k_y = -N_y, \ldots, N_y\}`: .. testsetup:: import math import numpy as np from pyffs import ffsn_sample, ffsn from pyffs.func import dirichlet_2D, dirichlet_fs .. doctest:: >>> T = [1, 1] >>> T_c = [0, 0] >>> N_FS = [3, 3] >>> N_s = [4, 3] # Sample the kernel and do the transform. >>> sample_points, _ = ffsn_sample(T=T, N_FS=N_FS, T_c=T_c, N_s=N_s) >>> diric_samples = dirichlet_2D(sample_points, T, T_c, N_FS) >>> diric_FS = ffsn(x=diric_samples, T=T, N_FS=N_FS, T_c=T_c) # Compare with theoretical result. >>> diric_FS_exact = np.outer( ... dirichlet_fs(N_FS[0], T[0], T_c[0]), dirichlet_fs(N_FS[1], T[1], T_c[1]) ... ) >>> np.allclose(diric_FS[: N_FS[0], : N_FS[1]], diric_FS_exact) True Notes ----- Theory: :ref:`FFS_def`. See Also -------- :py:func:`~pyffs.util.ffsn_sample`, :py:func:`~pyffs.ffs.iffsn` """ axes, N_s = _verify_ffsn_input(x, T, T_c, N_FS, axes) xp = get_array_module(x) # check for input type if (x.dtype == xp.dtype("complex64")) or (x.dtype == xp.dtype("float32")): is_complex64 = True x_FS = x.copy().astype(xp.complex64) else: is_complex64 = False x_FS = x.copy().astype(xp.complex128) C_1 = [] for d, ax in enumerate(axes): A_d, B_d = _create_modulation_vectors(N_s[d], N_FS[d], T[d], T_c[d], xp) sh = [1] * x.ndim sh[ax] = N_s[d] # apply pre-mod C_2 = B_d.conj().reshape(sh) if is_complex64: C_2 = C_2.astype(xp.complex64) x_FS *= C_2 # save post-mod vectors C_1.append(A_d.conj().reshape(sh) / N_s[d]) if is_complex64: C_1[d].astype(xp.complex64) x_FS = fftn(x_FS, axes=axes) # apply modulation after FFT for _c1 in C_1: x_FS *= _c1 return x_FS
def fs_interpn(x_FS, T, a, b, M, axes=None, real_x=False): r""" Interpolate D-dimensional bandlimited periodic signal. Parameters ---------- x_FS : :py:class:`~numpy.ndarray` (..., N_FSx, N_FSy, ...) FS coefficients in ascending order. T : list(float) Function period along each dimension. a : list(float) Interval LHS for each dimension. b : list(float) Interval RHS for each dimension. M : list(int) Number of points to interpolate for each dimension. axes : tuple, optional Dimensions of `x_FS` along which the FS coefficients are stored. real_x : bool, optional If True, assume that `x_FS` is conjugate symmetric in each dimension and use a more efficient algorithm. In this case, the FS coefficients corresponding to negative frequencies are not used. Note that this approach is only available for D < 3, and will raise an error otherwise. Returns ------- x : :py:class:`~numpy.ndarray` (..., M_1, M_2, ..., M_D, ...) interpolated values along the axes indicated by `axes`. If `real_x` is :py:obj:`True`, the output is real-valued, otherwise it is complex-valued. Notes ----- Theory: :ref:`fp_interp_def`. See Also -------- :py:func:`~pyffs.czt.cztn` """ axes = _verify_fs_interp_input(x_FS, T, a, b, M, axes) D = len(axes) xp = get_array_module(x_FS) # precompute modulation terms N_FS = [x_FS.shape[d] for d in axes] N = [(nfs - 1) // 2 for nfs in N_FS] A = [] W = [] sh = [] E = [] for d in range(D): A.append(xp.exp(-1j * 2 * xp.pi / T[d] * a[d])) W.append(xp.exp(1j * (2 * xp.pi / T[d]) * (b[d] - a[d]) / (M[d] - 1))) sh.append([1] * x_FS.ndim) sh[d][axes[d]] = M[d] E.append(xp.arange(M[d])) if real_x: x0_FS = x_FS[_index_n(x_FS, axes, [slice(n, n + 1) for n in N])] if D == 1: x_FS_p = x_FS[_index(x_FS, axes[0], slice(N[0] + 1, N_FS[0]))] C = xp.reshape(W[0]**E[0], sh[0]) / A[0] x = czt(x_FS_p, A[0], W[0], M[0], axis=axes[0]) # exploit conjugate symmetry x = 2 * C * x + x0_FS elif D == 2: # positive / positive x_FS_pp = x_FS[_index_n(x_FS, axes, [slice(N[d], N_FS[d]) for d in range(D)])] x_pp = cztn(x_FS_pp, A, W, M, axes=axes) # negative / positive x_FS_np = x_FS[_index_n( x_FS, axes, [slice(0, N[0]), slice(N[1] + 1, N_FS[1])])] x_np = cztn(x_FS_np, A, W, M, axes=axes) x_np *= xp.reshape(W[0]**(-N[0] * E[0]), sh[0]) * (A[0]**N[0]) x_np *= xp.reshape(W[1]**E[1], sh[1]) / A[1] # exploit conjugate symmetry x = 2 * x_pp + 2 * x_np - x0_FS else: raise NotImplementedError( "[real_x] approach not available for D > 2.") return x.real else: # General complex case. x = cztn(x_FS, A, W, M, axes=axes) # modulate along each dimension for d in range(D): C = xp.reshape(W[d]**(-N[d] * E[d]), sh[d]) * (A[d]**N[d]) x *= C return x