Example #1
0
    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])
Example #2
0
 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
Example #3
0
    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])
Example #4
0
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
Example #5
0
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
Example #6
0
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
Example #7
0
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
Example #8
0
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
Example #9
0
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