예제 #1
0
def uniform(direction, FoV, size):
    """
    Uniform 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 : array-like(int)
        (N_height, N_width)

    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
    lim = np.sin(FoV / 2)
    Y, X = np.meshgrid(np.linspace(-lim, lim, N_height),
                       np.linspace(-lim, lim, N_width),
                       indexing="ij")
    Z = 1 - X**2 - Y**2
    X[Z < 0], Y[Z < 0], Z[Z < 0] = 0, 0, 0
    Z = np.sqrt(Z)
    XYZ = np.stack([X, Y, Z], axis=0)

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

    XYZ = np.tensordot(R, XYZ, axes=1)
    return XYZ
예제 #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
예제 #3
0
    def _PrimaryHDU(self):
        """
        Generate primary Header Descriptor Unit (HDU) for FITS export.

        Returns
        -------
        hdu : :py:class:`~astropy.io.fits.PrimaryHDU`
        """
        metadata = dict(IMG_TYPE=(self.__class__.__name__, "Image subclass"))

        # grid: stored as angles to reduce file size.
        _, colat, lon = transform.cart2pol(*self._grid)
        coordinates = np.stack([colat, lon], axis=0)

        hdu = fits.PrimaryHDU(data=coordinates)
        for k, v in metadata.items():
            hdu.header[k] = v
        return hdu
예제 #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
예제 #5
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
예제 #6
0
def equal_angle(N, direction=None, FoV=None):
    r"""
    (Region-limited) open grid of Equal-Angle sample-points 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
    -------
    q : :py:class:`~numpy.ndarray`
        (N_height,) polar indices.

    l : :py:class:`~numpy.ndarray`
        (N_width,) azimuthal indices.

    colat : :py:class:`~numpy.ndarray`
        (N_height, 1) polar angles [rad].
        `N_height == 2N+2` in the whole-sphere case.

    lon : :py:class:`~numpy.ndarray`
        (1, N_width) azimuthal angles [rad].
        `N_width == 2N+2` in the whole-sphere case.

    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 equal_angle

    .. doctest::

       >>> N = 3
       >>> _, _, colat, lon = equal_angle(N)

       >>> np.around(colat, 2)
       array([[0.2 ],
              [0.59],
              [0.98],
              [1.37],
              [1.77],
              [2.16],
              [2.55],
              [2.95]])
       >>> np.around(lon, 2)
       array([[0.  , 0.79, 1.57, 2.36, 3.14, 3.93, 4.71, 5.5 ]])

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

    .. doctest::

       >>> N = 3
       >>> direction = np.r_[0, 1, 0]
       >>> FoV = np.deg2rad(90)
       >>> q, l, colat, lon = equal_angle(N, direction, FoV)

       >>> q
       array([2, 3, 4, 5])

       >>> np.around(colat, 2)
       array([[0.98],
              [1.37],
              [1.77],
              [2.16]])

       >>> l
       array([1, 2, 3])

       >>> np.around(lon, 2)
       array([[0.79, 1.57, 2.36]])

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

    .. math::

       \theta_{q} & = \frac{\pi}{2 N + 2} \left( q + \frac{1}{2} \right), \qquad & q \in \{ 0, \ldots, 2 N + 1 \},

       \phi_{l} & = \frac{2 \pi}{2N + 2} l, \qquad & l \in \{ 0, \ldots, 2 N + 1 \}.

    * Longitudinal range may be erroneous if direction too close to [1, 0, 0].

    .. [1] 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 np.allclose(np.cross([0, 0, 1], direction), 0):
            raise ValueError(
                "Generating Equal-Angle grids centered at poles currently not supported."
            )
            # Why? Because the grid layout is spatially incorrect in this degenerate case.

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

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

    def ea_sample(N: int):
        _2N2 = 2 * N + 2
        q, l = np.ogrid[:_2N2, :_2N2]

        colat = (np.pi / _2N2) * (0.5 + q)
        lon = (2 * np.pi / _2N2) * l
        return colat, lon

    colat_full, lon_full = ea_sample(N)
    q_full = np.arange(colat_full.size)
    l_full = np.arange(lon_full.size)

    if direction is None:  # full-sphere case
        return q_full, l_full, colat_full, lon_full
    else:
        _, dir_colat, dir_lon = transform.cart2pol(*direction)
        lim_lon = dir_lon + (FoV / 2) * np.r_[-1, 1]
        lim_lon = coord.Angle(lim_lon * u.rad).wrap_at(360 * u.deg).to_value(
            u.rad)
        lim_colat = dir_colat + (FoV / 2) * np.r_[-1, 1]
        lim_colat = (max(np.deg2rad(0.5),
                         lim_colat[0]), min(lim_colat[1], np.deg2rad(179.5)))

        q_mask = (lim_colat[0] <= colat_full) & (colat_full <= lim_colat[1])
        if lim_lon[0] < lim_lon[1]:
            l_mask = (lim_lon[0] <= lon_full) & (lon_full <= lim_lon[1])
        else:
            l_mask = (lim_lon[0] <= lon_full) | (lon_full <= lim_lon[1])
        q_mask = np.reshape(q_mask, (-1, ))
        l_mask = np.reshape(l_mask, (-1, ))

        q, l = q_full[q_mask], l_full[l_mask]
        colat, lon = colat_full[q_mask, :], lon_full[:, l_mask]
        return q, l, colat, lon