def __call__(self, colat_idx, lon_idx, f, r):
        """
        Interpolate function samples at order `N`.

        Parameters
        ----------
        colat_idx : :py:class:`~numpy.ndarray`
            (N_colat,) polar support indices from :py:func:`~imot_tools.math.sphere.grid.equal_angle`.
        lon_idx : :py:class:`~numpy.ndarray`
            (N_lon,) azimuthal support indices from :py:func:`~imot_tools.math.sphere.grid.equal_angle`.
        f : :py:class:`~numpy.ndarray`
            (L, N_colat, N_lon) zonal function values at support points. (float or complex)
        r : :py:class:`~numpy.ndarray`
            (3, N_px) evaluation points.

        Returns
        -------
        f_interp : :py:class:`~numpy.ndarray`
            (L, N_px) function values at specified coordinates.
        """
        N_colat = colat_idx.size
        if not (colat_idx.shape == (N_colat, )):
            raise ValueError(
                "Parameter[colat_idx] must have shape (N_colat,).")

        N_lon = lon_idx.size
        if not (lon_idx.shape == (N_lon, )):
            raise ValueError("Parameter[lon_idx] must have shape (N_lon,).")

        L = len(f)
        if not (f.shape == (L, N_colat, N_lon)):
            raise ValueError(
                "Parameter[f] must have shape (L, N_colat, N_lon).")

        if not ((r.ndim == 2) and (r.shape[0] == 3)):
            raise ValueError("Parameter[r] must have shape (3, N_px).")

        # Apply weights directly onto `f` to avoid memory blow-up.
        _, _, colat, lon = grid.equal_angle(self._N)
        a = np.arange(self._N + 1)
        weight = (np.sum(np.sin((2 * a + 1) * colat[colat_idx]) / (2 * a + 1),
                         axis=1) * np.sin(colat[colat_idx, 0]) *
                  ((2 * np.pi) / ((self._N + 1)**2)))  # (N_colat,)
        fw = f * weight.reshape((1, N_colat, 1))  # (L, N_colat, N_lon)

        f_interp = super().__call__(
            weight=np.broadcast_to([1], (N_colat * N_lon, )),
            support=transform.pol2cart(1, colat[colat_idx, :],
                                       lon[:, lon_idx]).reshape((3, -1)),
            f=fw.reshape((L, -1)),
            r=r,
            sparsity_mask=None,
        )
        return f_interp
Exemple #2
0
def spherical(direction, FoV, size):
    """
    Spherical pixel grid.

    Parameters
    ----------
    direction : :py:class:`~numpy.ndarray`
        (3,) vector around which the grid is centered.
    FoV : float
        Span of the grid [rad] centered at `direction`.
    size : :py:class:`~numpy.ndarray`
        (N_height, N_width)

        The grid will consist of `N_height` concentric circles around `direction`, each containing
        `N_width` pixels.

    Returns
    -------
    XYZ : :py:class:`~numpy.ndarray`
        (3, N_height, N_width) pixel grid.
    """
    direction = np.array(direction, dtype=float)
    direction /= linalg.norm(direction)

    if not (0 < np.rad2deg(FoV) <= 179):
        raise ValueError("Parameter[FoV] must be in (0, 179] degrees.")

    size = np.array(size, copy=False)
    if np.any(size <= 0):
        raise ValueError("Parameter[size] must contain positive entries.")

    N_height, N_width = size
    colat, lon = np.meshgrid(np.linspace(0, FoV / 2, N_height),
                             np.linspace(0, 2 * np.pi, N_width),
                             indexing="ij")
    XYZ = transform.pol2cart(1, colat, lon)

    # Center grid at 'direction'
    _, dir_colat, _ = transform.cart2pol(*direction)
    R_axis = np.cross([0, 0, 1], direction)
    if np.allclose(R_axis, 0):
        # R_axis is in span(E_z), so we must manually set R
        R = np.eye(3)
        if direction[2] < 0:
            R[2, 2] = -1
    else:
        R = ilinalg.rot(axis=R_axis, angle=dir_colat)

    XYZ = np.tensordot(R, XYZ, axes=1)
    return XYZ
def spherical_geometry():
    """
    Spherical 3D microphone array geometry.

    Radius: 0.20[m]

    Returns
    -------
    XYZ : :py:class:`~numpy.ndarray`
        (3, N_antenna) Cartesian coordinates.
    """
    N_antenna = 64

    n = np.arange(N_antenna)
    colat = np.arccos(1 - (2 * n + 1) / N_antenna)
    lon = (4 * np.pi * n) / (1 + np.sqrt(5))
    r = 0.2

    XYZ = np.stack(transform.pol2cart(r, colat, lon), axis=0)
    return XYZ
Exemple #4
0
def from_circular_distribution(direction, FoV, N_src):
    """
    Distribute `N_src` sources on a circle centered at `direction`.

    Parameters
    ----------
    direction : :py:class:`~numpy.ndarray`
        (3,) direction in the sky.
    FoV : float
        Spherical angle [rad] of the sky centered at `direction` from which sources are extracted.
    N_src : int
        Number of dominant sources to extract.

    Returns
    -------
    sky_model : :py:class:`~deepwave.tools.data_gen.source.SkyModel`
        Sky model.
    """
    if not (0 < FoV < 2 * np.pi):
        raise ValueError('Parameter[FoV] must lie in (0, 360) degrees.')

    colat = FoV / 4
    lon = np.linspace(0, 2 * np.pi, N_src, endpoint=False)
    XYZ = np.stack(transform.pol2cart(1, colat, lon), axis=0)

    # Center grid at 'direction'
    _, dir_colat, _ = transform.cart2pol(*direction)
    R_axis = np.cross([0, 0, 1], direction)
    if np.allclose(R_axis, 0):
        # R_axis is in span(E_z), so we must manually set R
        R = np.eye(3)
        if direction[2] < 0:
            R[2, 2] = -1
    else:
        R = pylinalg.rot(axis=R_axis, angle=dir_colat)

    XYZ = np.tensordot(R, XYZ, axes=1)
    I = np.ones((N_src, ))
    sky_model = SkyModel(XYZ, I)
    return sky_model
    def _from_fits(cls, primary_hdu, image_hdu):
        """
        Load image from Header Descriptor Units.

        Parameters
        ----------
        primary_hdu : :py:class:`~astropy.io.fits.PrimaryHDU`
        image_hdu : :py:class:`~astropy.io.fits.ImageHDU`

        Returns
        -------
        I : :py:class:`~imot_tools.io.s2image.Image`
        """
        # PrimaryHDU: grid specification.
        colat, lon = primary_hdu.data
        grid = transform.pol2cart(1, colat, lon)

        # ImageHDU: extract data cube.
        data = image_hdu.data

        I = cls(data=data, grid=grid)
        return I
Exemple #6
0
def sky(f):
    """
    Extract Sky Model from file name.

    Parameters
    ==========
    f : :py:class:`~pathlib.Path`
        .wav file with name of the form "<characters>_spkr[012]_angle[0:360:2].wav"

    Returns
    =======
    sky_model : :py:class:`~deepwave.tools.data_gen.source.SkyModel`
        Container with source information.
        `sky_model.intensity` will contain the speaker number {0, 1, 2}.
    """
    pattern = r'.+_spkr(\d+)_angle(\d+)\.wav'
    match = re.search(pattern, str(f))
    if match:
        idx_speaker, idx_angle = map(int, match.group(1, 2))
        if idx_speaker not in (0, 1, 2):
            raise ValueError(f'speaker number {idx_speaker} is out of bounds.')
        if idx_angle not in np.arange(0, 360, 2):
            raise ValueError(f'angle number {idx_angle} is out of bounds.')

        m_speaker = speaker_map()
        colat = data()['sources'][m_speaker[idx_speaker]]['colatitude'][str(
            idx_angle)]
        lon = data()['sources'][m_speaker[idx_speaker]]['azimuth'][str(
            idx_angle)]

        src_xyz = transform.pol2cart(1, colat, lon)
        src_I = np.r_[idx_speaker]
        sky_model = source.SkyModel(src_xyz, src_I)
        return sky_model
    else:
        raise ValueError('Parameter[f] does not have the right form.')
Exemple #7
0
def simulate_dataset(N_sample, N_src, XYZ, R, wl, src_mask, intensity=None, rate=None):
    """
    Generate APGD dataset.

    Parameters
    ----------
    N_sample : int
        Number of images to generate.
    N_src : int
        Number of sources present per image.
    XYZ : :py:class:`~numpy.ndarray`
        (3, N_antenna) microphone positions.
    R : :py:class:`~numpy.ndarray`
        (3, N_px) pixel grid.
    wl : float
        Wavelength [m] of plane wave.
    src_mask : :py:class:`~numpy.ndarray`
        (N_px,) boolean mask saying near which pixels it is possible to place sources.
    intensity : float
        If present, generate equi-amplitude sources.
    rate : float
        If present, generate rayleigh-amplitude sources.

    Returns
    -------
    D : :py:class:`~acoustic_camera.nn.DataSet`
        (N_sample,) dataset

    Note
    ----
    Either `intensity` or `rate` must be specified, not both.
    """
    if not (N_sample > 0):
        raise ValueError('Paremeter[N_sample] must be positive.')

    if not (N_src > 0):
        raise ValueError('Paremeter[N_src] must be positive.')

    if not ((XYZ.ndim == 2) and (XYZ.shape[0] == 3)):
        raise ValueError('Parameter[XYZ]: expected (3, N_antenna) array.')
    N_antenna = XYZ.shape[1]

    if not ((R.ndim == 2) and (R.shape[0] == 3)):
        raise ValueError('Parameter[R]: expected (3, N_px) array.')
    N_px = R.shape[1]

    if wl < 0:
        raise ValueError('Parameter[wl] is out of bounds.')

    if not ((src_mask.ndim == 1) and (src_mask.size == N_px)):
        raise ValueError('Parameter[src_mask]: expected (N_px,) boolean array.')

    if (((intensity is None) and (rate is None)) or
            ((intensity is not None) and (rate is not None))):
        raise ValueError('One of Parameters[intensity, rate] must be specified.')

    if (intensity is not None) and (intensity <= 0):
        raise ValueError('Parameter[intensity] must be positive.')

    if (rate is not None) and (rate <= 0):
        raise ValueError('Parameter[rate] must be positive.')

    vis_gen = statistics.VisibilityGenerator(T=50e-3, fs=48000, SNR=10)
    A = phased_array.steering_operator(XYZ, R, wl)

    sampler = nn.Sampler(N_antenna, N_px)
    N_data = sampler._N_cell
    data = np.zeros((N_sample, N_data))
    ground_truth = [None] * N_sample
    apgd_gamma = 0.5
    apgd_lambda_ = np.zeros(N_sample)
    apgd_N_iter = np.zeros(N_sample)
    apgd_tts = np.zeros(N_sample)

    for i in range(N_sample):
        logging.info(f'Generate APGD image {i + 1}/{N_sample}.')

        ### Create synthetic sky
        if (intensity is not None):
            sky_I = intensity * np.ones(N_src)
        elif (rate is not None):
            sky_I = stats.rayleigh.rvs(scale=rate, size=N_src)
        sky_XYZ = R[:, src_mask][:, np.random.randint(0, np.sum(src_mask), size=N_src)]
        ## Randomize positions slightly to not fall straight onto grid.
        _, sky_colat, sky_lon = transform.cart2pol(*sky_XYZ)
        px_pitch = np.arccos(np.clip(R[:, 0] @ R[:, 1:], -1, 1)).min()
        colat_noise, lon_noise = 0.1 * px_pitch * np.random.randn(2, N_src)
        sky_XYZ = np.stack(transform.pol2cart(1, sky_colat + colat_noise, sky_lon + lon_noise), axis=0)
        sky_model = source.SkyModel(sky_XYZ, sky_I)

        S = vis_gen(XYZ, wl, sky_model)
        # Normalize `S` spectrum for scale invariance.
        S_D, S_V = linalg.eigh(S)
        if S_D.max() <= 0:
            S_D[:] = 0
        else:
            S_D = np.clip(S_D / S_D.max(), 0, None)
        S = (S_V * S_D) @ S_V.conj().T

        I_apgd = apgd.solve(S, A,
                            lambda_=None,
                            gamma=apgd_gamma,
                            L=None,
                            d=50,
                            x0=None,
                            eps=1e-3,
                            N_iter_max=200,
                            verbosity='NONE',  # 'LOW',
                            momentum=True)

        data[i] = sampler.encode(S=S, I=I_apgd['sol'])
        ground_truth[i] = sky_model
        apgd_lambda_[i] = I_apgd['lambda_']
        apgd_N_iter[i] = I_apgd['niter']
        apgd_tts[i] = I_apgd['time']

    D = nn.DataSet(data, XYZ, R, wl, ground_truth,
                   apgd_lambda_, apgd_gamma, apgd_N_iter, apgd_tts)
    return D
Exemple #8
0
def fibonacci(N, direction=None, FoV=None):
    r"""
    (Region-limited) near-uniform sampling on the sphere.

    Parameters
    ----------
    N : int
        Order of the grid, i.e. there will be :math:`4 (N + 1)^{2}` points on the sphere.
    direction : :py:class:`~numpy.ndarray`
        (3,) vector around which the grid is centered.
        If :py:obj:`None`, then the grid covers the entire sphere.
    FoV : float
        Span of the grid [rad] centered at `direction`.
        This parameter is ignored if `direction` left unspecified.

    Returns
    -------
    XYZ : :py:class:`~numpy.ndarray`
        (3, N_px) sample points.
        `N_px == 4*(N+1)**2` if `direction` left unspecified.

    Examples
    --------
    Sampling a zonal function :math:`f(r): \mathbb{S}^{2} \to \mathbb{C}` of order :math:`N` on the
    sphere:

    .. testsetup::

       import numpy as np

       from imot_tools.math.sphere.grid import fibonacci

    .. doctest::

       >>> N = 2
       >>> XYZ = fibonacci(N)

       >>> np.around(XYZ, 2)
       array([[ 0.23, -0.29,  0.04,  0.36, -0.65,  0.61, -0.2 , -0.37,  0.8 ,
               -0.81,  0.39,  0.28, -0.82,  0.95, -0.56, -0.13,  0.76, -1.  ,
                0.71, -0.05, -0.63,  0.97, -0.79,  0.21,  0.46, -0.87,  0.8 ,
               -0.33, -0.27,  0.68, -0.7 ,  0.36,  0.1 , -0.4 ,  0.4 , -0.16],
              [ 0.  , -0.27,  0.51, -0.47,  0.12,  0.39, -0.74,  0.72, -0.29,
               -0.34,  0.82, -0.89,  0.48,  0.21, -0.8 ,  0.98, -0.64, -0.04,
                0.71, -1.  ,  0.76, -0.13, -0.55,  0.93, -0.81,  0.28,  0.37,
               -0.78,  0.76, -0.36, -0.18,  0.56, -0.58,  0.31,  0.03, -0.17],
              [ 0.97,  0.92,  0.86,  0.81,  0.75,  0.69,  0.64,  0.58,  0.53,
                0.47,  0.42,  0.36,  0.31,  0.25,  0.19,  0.14,  0.08,  0.03,
               -0.03, -0.08, -0.14, -0.19, -0.25, -0.31, -0.36, -0.42, -0.47,
               -0.53, -0.58, -0.64, -0.69, -0.75, -0.81, -0.86, -0.92, -0.97]])

    Sampling a zonal function :math:`f(r): \mathbb{S}^{2} \to \mathbb{C}` of order :math:`N` on
    *part* of the sphere:

    .. doctest::

       >>> N = 2
       >>> direction = np.r_[1, 0, 0]
       >>> FoV = np.deg2rad(90)
       >>> XYZ = fibonacci(N, direction, FoV)

       >>> np.around(XYZ, 2)
       array([[ 0.8 ,  0.95,  0.76,  0.71,  0.97,  0.8 ],
              [-0.29,  0.21, -0.64,  0.71, -0.13,  0.37],
              [ 0.53,  0.25,  0.08, -0.03, -0.19, -0.47]])

    Notes
    -----
    The sample positions on the unit sphere are given (in radians) by [2]_:

    .. math::

       \cos(\theta_{q}) & = 1 - \frac{2 q + 1}{4 (N + 1)^{2}}, \qquad & q \in \{ 0, \ldots, 4 (N + 1)^{2} - 1 \},

       \phi_{q} & = \frac{4 \pi}{1 + \sqrt{5}} q, \qquad & q \in \{ 0, \ldots, 4 (N + 1)^{2} - 1 \}.


    .. [2] B. Rafaely, "Fundamentals of Spherical Array Processing", Springer 2015
    """
    if direction is not None:
        direction = np.array(direction, dtype=float)
        direction /= linalg.norm(direction)

        if FoV is not None:
            if not (0 < np.rad2deg(FoV) < 360):
                raise ValueError("Parameter[FoV] must be in (0, 360) degrees.")
        else:
            raise ValueError(
                "Parameter[FoV] must be specified if Parameter[direction] provided."
            )

    if N < 0:
        raise ValueError("Parameter[N] must be non-negative.")

    N_px = 4 * (N + 1)**2
    n = np.arange(N_px)

    colat = np.arccos(1 - (2 * n + 1) / N_px)
    lon = (4 * np.pi * n) / (1 + np.sqrt(5))
    XYZ = np.stack(transform.pol2cart(1, colat, lon), axis=0)

    if direction is not None:  # region-limited case.
        # TODO: highly inefficient to generate the grid this way!
        min_similarity = np.cos(FoV / 2)
        mask = (direction @ XYZ) >= min_similarity
        XYZ = XYZ[:, mask]

    return XYZ