Exemplo n.º 1
0
    def pdf(self, x):
        """
        Density of the distribution at sample points.

        Parameters
        ----------
        x : :py:class:`~numpy.ndarray`
            (N, 3) values at which to determine the pdf.

        Returns
        -------
        pdf : :py:class:`~numpy.ndarray`
            (N,) densities.
        """
        x = np.array(x, dtype=float)
        if x.ndim == 1:
            x = x[np.newaxis]
        elif x.ndim == 2:
            pass
        else:
            raise ValueError('Parameter[x] must have shape (N, 3).')

        N = len(x)
        if not chk.has_shape([N, 3])(x):
            raise ValueError('Parameter[x] must have shape (N, 3).')
        x /= linalg.norm(x, axis=1, keepdims=True)

        pdf = np.exp((x @ self._g1) - 1 + 0.5 * self._beta *
                     ((x @ self._g2) ** 2 - (x @ self._g3) ** 2)) ** self._k
        pdf /= self._cst
        return pdf
Exemplo n.º 2
0
    def synthesize(self, stat):
        """
        Compute field values from statistics.

        Parameters
        ----------
        stat : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_width) field statistics.

        Returns
        -------
        field : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_width) field values.
        """
        stat = np.array(stat, copy=False)

        if stat.ndim != 3:
            raise ValueError('Parameter[stat] is incorrectly shaped.')

        N_level = len(stat)
        N_height, N_width = self._grid.shape[1:]

        if not chk.has_shape([N_level, N_height, N_width])(stat):
            raise ValueError("Parameter[stat] does not match "
                             "the grid's dimensions.")

        field = stat
        return field
Exemplo n.º 3
0
    def __init__(self, V, n):
        """
        Parameters
        ----------
        V : :py:class:`~numpy.ndarray`
            (p, p) positive-semidefinite Hermitian scale matrix.
        n : int
            degrees of freedom.
        """
        super().__init__()

        V = np.array(V)
        p = len(V)

        if not (chk.has_shape([p, p])(V) and np.allclose(V, V.conj().T)):
            raise ValueError('Parameter[V] must be hermitian symmetric.')
        if not (n > p):
            raise ValueError(f'Parameter[n] must be greater than {p}.')

        self._V = V
        self._p = p
        self._n = n

        Vq = linalg.sqrtm(V)
        _, R = linalg.qr(Vq)
        self._L = R.conj().T
Exemplo n.º 4
0
    def synthesize(self, stat):
        """
        Compute field values from statistics.

        Parameters
        ----------
        stat : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_FS + Q) field statistics.

        Returns
        -------
        field : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_width) field values.
        """
        stat = np.array(stat, copy=False)

        if stat.ndim != 3:
            raise ValueError('Parameter[stat] is incorrectly shaped.')

        N_level = len(stat)
        N_height, _2N1Q = self._FSk.shape[1:]

        if not chk.has_shape([N_level, N_height, _2N1Q])(stat):
            raise ValueError("Parameter[stat] does not match "
                             "the kernel's dimensions.")

        field_FS = fourier.ffs(stat, self._T, self._Tc, self._NFS, axis=2)
        field = fourier.fs_interp(field_FS[:, :, :self._NFS],
                                  T=self._T,
                                  a=self._grid_lon[0, 0],
                                  b=self._grid_lon[0, -1],
                                  M=self._grid_lon.size,
                                  axis=2,
                                  real_x=True)
        return field
Exemplo n.º 5
0
    def __init__(self, q, l, f, N, approximate_kernel=False):
        r"""
        Parameters
        ----------
        q : :py:class:`~numpy.ndarray`
            (N_s,) polar indices of an order-`N` Equal-Angle grid.
        l : :py:class:`~numpy.ndarray`
            (N_s,) azimuthal indices of an order-`N` Equal-Angle grid.
        f : :py:class:`~numpy.ndarray`
            (N_s,) samples of the zonal function at data-points. (float or complex)

            :math:`L`-dimensional zonal functions are also supported by supplying an (N_s, L) array instead.
        N : int
            Order of the reconstructed zonal function.
        approximate_kernel : bool
            If :py:obj:`True`, pass the `approx` option to :py:class:`~pypeline.util.math.func.SphericalDirichlet`.

        Notes
        -----
        If :math:`f(r)` only takes non-negligeable values when :math:`r \in \mathcal{S} \subset \mathbb{S}^{2}`, then the runtime of :py:meth:`~pypeline.util.math.sphere.EqualAngleInterpolator.__call__` can be significantly reduced by only supplying the triplets (`q`, `l`, `f`) that belong to :math:`\mathcal{S}`.
        """
        super().__init__()

        colat_sph, _ = ea_sample(N)
        _2N2 = colat_sph.size
        q, l = np.array(q), np.array(l)
        if not ((q.shape == l.shape) and chk.has_shape((q.size, ))(q)):
            raise ValueError("Parameter[q, l] must be 1D and of equal length.")
        if not all(np.all(0 <= _) and np.all(_ < _2N2) for _ in [q, l]):
            raise ValueError(f"Parameter[q, l] must contain entries in "
                             f"{{0, ..., 2N + 1}}.")
        self._N = N
        self._q = q
        self._l = l

        N_s = q.size
        f = np.array(f, copy=False)
        if (f.ndim == 1) and (len(f) == N_s):
            self._L = 1
        elif (f.ndim == 2) and (len(f) == N_s):
            self._L = f.shape[1]
        else:
            raise ValueError(
                "Parameter[f] must have shape (N_s,) or (N_s, L).")
        f = f.reshape(N_s, self._L)

        _2m1 = np.reshape(2 * np.r_[:N + 1] + 1, (1, N + 1))
        alpha = (
            np.sin(colat_sph) / _2N2 *
            np.sum(np.sin(_2m1 * colat_sph) / _2m1, axis=1, keepdims=True))
        self._weight = f * alpha[q]

        self._kernel_func = func.SphericalDirichlet(N, approximate_kernel)
Exemplo n.º 6
0
    def pdf(self, x):
        """
        Density of the distribution at sample points.

        Parameters
        ----------
        x : :py:class:`~numpy.ndarray`
            (N, p, p) values at which to determine the pdf.

        Returns
        -------
        pdf : :py:class:`~numpy.ndarray`
            (N,) densities.
        """
        x = np.array(x, copy=False)
        if x.ndim == 2:
            x = x[np.newaxis]
        elif x.ndim == 3:
            pass
        else:
            raise ValueError('Parameter[x] must have shape (N, p, p).')

        N = len(x)
        if not (chk.has_shape([N, self._p, self._p])(x) and
                np.allclose(x, x.conj().transpose(0, 2, 1))):
            raise ValueError('Parameter[x] must be hermitian symmetric.')

        if np.linalg.matrix_rank(self._V) < self._p:
            raise linalg.LinAlgError('Wishart density is not defined when '
                                     'scale matrix V is singular.')

        # Determinants: real-valued since (V,X) are Hermitian.
        Vs, Vl = np.linalg.slogdet(self._V)
        dV = np.real(Vs * np.exp(Vl))
        Xs, Xl = np.linalg.slogdet(x)
        dX = np.real(Xs * np.exp(Xl))

        # Trace term
        A = np.linalg.solve(self._V, x)
        trA = np.trace(A, axis1=1, axis2=2).real

        num = (np.float_power(dX, (self._n - self._p - 1) / 2) *
               np.exp(-trA / 2))
        den = (np.float_power(2, self._n * self._p / 2) *
               np.float_power(dV, self._n / 2) *
               np.exp(special.multigammaln(self._n / 2, self._p)))

        pdf = num / den
        return pdf
Exemplo n.º 7
0
    def __init__(self, data, ant_idx, beam_idx):
        """
        Parameters
        ----------
        data : :py:class:`~numpy.ndarray`
            (N_antenna, N_beam) beamforming weights.
        ant_idx
            (N_antenna,) index.
        beam_idx
            (N_beam,) index.
        """
        N_antenna, N_beam = len(ant_idx), len(beam_idx)
        if not chk.has_shape((N_antenna, N_beam))(data):
            raise ValueError('Parameters[data, ant_idx, beam_idx] are not '
                             'consistent.')

        super().__init__(data=data, row_idx=ant_idx, col_idx=beam_idx)
Exemplo n.º 8
0
    def __init__(self, data, beam_idx):
        """
        Parameters
        ----------
        data : :py:class:`~numpy.ndarray`
            (N_beam, N_beam) Gram coefficients.
        beam_idx
            (N_beam,) index.
        """
        data = np.array(data, copy=False)
        N_beam = len(beam_idx)

        if not chk.has_shape((N_beam, N_beam))(data):
            raise ValueError('Parameters[data, beam_idx] are not consistent.')

        if not np.allclose(data, data.conj().T):
            raise ValueError('Parameter[data] must be hermitian symmetric.')

        super().__init__(data, beam_idx, beam_idx)
Exemplo n.º 9
0
    def __init__(self, xyz, ant_idx):
        """
        Parameters
        ----------
        xyz : :py:class:`~numpy.ndarray`
            (N_antenna, 3) Cartesian coordinates.
        ant_idx : :py:class:`~pandas.MultiIndex`
            (N_antenna,) index.
        """
        xyz = np.array(xyz, copy=False)
        N_antenna = len(xyz)
        if not chk.has_shape((N_antenna, 3))(xyz):
            raise ValueError('Parameter[xyz] must be a (N_antenna, 3) array.')

        N_idx = len(ant_idx)
        if N_idx != N_antenna:
            raise ValueError('Parameter[xyz] and Parameter[ant_idx] are not '
                             'compatible.')

        col_idx = pd.Index(['X', 'Y', 'Z'], name='COORDINATE')
        super().__init__(xyz, ant_idx, col_idx)
Exemplo n.º 10
0
class Kent(Distribution):
    r"""
    `Kent <https://en.wikipedia.org/wiki/Kent_distribution>`_ distribution, also known as :math:`\text{FB}_{5}`, the 5-parameter Fisher-Bingham distribution.

    The density of :math:`\text{FB}_{5}(k, \beta, \gamma_{1}, \gamma_{2}, \gamma_{3})` is given by

    .. math::

       f(x) & = \frac{1}{c(k,\beta)} \exp\left( \gamma_{1}^{\intercal} x + \frac{\beta}{2} \left[ (\gamma_{2}^{\intercal} x)^{2} - (\gamma_{3}^{\intercal} x)^{2} \right] - 1 \right)^{k},

       c(k, \beta) & = \sqrt{\frac{8 \pi}{k}} \sum_{j \ge 0} B\left(j + \frac{1}{2}, \frac{1}{2}\right) \beta^{2 j} I_{2 j + \frac{1}{2}}^{e}(k),

    where :math:`\beta \in [0, 1)` determines the distribution's ellipticity, :math:`B(\cdot, \cdot)` denotes the Beta function, and :math:`I_{v}^{e}(z) = I_{v}(z) e^{-|\Re{\{z\}}|}` is the exponentially-scaled modified Bessel function of the first kind.
    """

    @chk.check(dict(k=chk.is_real,
                    beta=chk.is_real,
                    g1=chk.require_all(chk.has_reals, chk.has_shape([3, ])),
                    a=chk.require_all(chk.has_reals, chk.has_shape([3, ]))))
    def __init__(self, k, beta, g1, a):
        r"""
        Parameters
        ----------
        k : float
            Scale parameter.
        beta : float
            Ellipticity in [0, 1[.
        g1 : :py:class:`~numpy.ndarray`
            (3,) mean direction vector :math:`\gamma_{1}`.
        a : :py:class:`~numpy.ndarray`
            (3,) direction of major axis.

            This is *not* the same thing as :math:`\gamma_{2}`!

        Notes
        -----
        :math:`\gamma_{1}` and `a` are sufficient statistics to determine the directional vectors :math:`\gamma_{2}, \gamma_{3} \in \mathbb{R}^{3}`.
        """
        super().__init__()

        if k <= 0:
            raise ValueError('Parameter[k] must be positive.')
        self._k = k

        if not (0 <= beta < 1):
            raise ValueError('Parameter[beta] must lie in [0, 1).')
        self._beta = beta

        self._g1 = np.array(g1, copy=False) / linalg.norm(g1)
        a = np.array(a, copy=False) / linalg.norm(a)
        if np.allclose(self._g1, a):
            raise ValueError('Parameters[g1, a] must not be colinear.')

        # Find major/minor axes (g2,g3)
        Q, _ = linalg.qr(np.stack([self._g1, a], axis=1))
        self._g2 = Q[:, 1]
        self._g3 = np.cross(self._g1, self._g2)

        # Buffered attributes
        ive_threshold = sp.ive_threshold(k)
        j = np.arange((ive_threshold - 0.5) // 2 + 2)
        self._cst = (np.sqrt(8 * np.pi / k) *
                     np.sum(special.beta(j + 0.5, 0.5) *
                            (beta ** (2 * j)) *
                            special.ive(2 * j + 0.5, k)))

    @chk.check('x', chk.has_reals)
    def pdf(self, x):
        """
        Density of the distribution at sample points.

        Parameters
        ----------
        x : :py:class:`~numpy.ndarray`
            (N, 3) values at which to determine the pdf.

        Returns
        -------
        pdf : :py:class:`~numpy.ndarray`
            (N,) densities.
        """
        x = np.array(x, dtype=float)
        if x.ndim == 1:
            x = x[np.newaxis]
        elif x.ndim == 2:
            pass
        else:
            raise ValueError('Parameter[x] must have shape (N, 3).')

        N = len(x)
        if not chk.has_shape([N, 3])(x):
            raise ValueError('Parameter[x] must have shape (N, 3).')
        x /= linalg.norm(x, axis=1, keepdims=True)

        pdf = np.exp((x @ self._g1) - 1 + 0.5 * self._beta *
                     ((x @ self._g2) ** 2 - (x @ self._g3) ** 2)) ** self._k
        pdf /= self._cst
        return pdf

    @classmethod
    @chk.check(dict(k=chk.is_real,
                    beta=chk.is_real,
                    eps=chk.is_real))
    def angular_support(cls, k, beta, eps=1e-2):
        r"""
        Pdf angular span.

        For a given parameterization :math:`k, \beta, \gamma_{1}, \gamma_{2}, \gamma_{3}` of :math:`\text{FB}_{5}`, :py:meth:`~pypeline.util.math.stat.Kent.angular_support` returns the angular separation between :math:`\gamma_{1}` and a point :math:`r` along :math:`\gamma_{2}` on the sphere where :math:`\epsilon f(\gamma_{1}) = f(r)`.

        The solution is given by :math:`\theta^{\ast} = \arg\min_{\theta > 0} \cos\theta + \frac{\beta}{2}\sin^{2}\theta \le 1 + \frac{1}{k} \ln\epsilon`.

        Parameters
        ----------
        k : float
            Scale parameter.
        beta : float
            Ellipticity in [0, 1[.
        eps : float
            Constant :math:`\epsilon` in ]0, 1[.

        Returns
        -------
        theta : :py:class:`~astropy.units.Quantity`
            Angular separation between :math:`r` and :math:`\gamma_{1}`.
        """
        if k <= 0:
            raise ValueError('Parameter[k] must be positive.')

        if not (0 <= beta < 1):
            raise ValueError('Parameter[beta] must lie in [0, 1).')

        if not (0 < eps < 1):
            raise ValueError('Parameter[eps] must lie in (0, 1).')

        theta = np.linspace(0, np.pi, 1e5)
        lhs = np.cos(theta) + 0.5 * beta * np.sin(theta) ** 2
        rhs = 1 + np.log(eps) / k
        mask = lhs <= rhs

        if np.any(mask):
            support = theta[mask][0] * u.rad
        else:
            support = np.pi * u.rad
        return support

    @classmethod
    @chk.check(dict(alpha=chk.is_real,
                    beta=chk.is_real,
                    eps=chk.is_real))
    def min_scale(cls, alpha, beta, eps=1e-2):
        r"""
        Minimum scale parameter for desired concentration.

        For a given parameterization :math:`k, \beta, \gamma_{1}, \gamma_{2}, \gamma_{3}` of :math:`\text{FB}_{5}`, :py:meth:`~pypeline.util.math.stat.Kent.min_scale` returns the minimum value of :math:`k` such that a spherical cap :math:`S` with opening half-angle :math:`\alpha` centered at :math:`\gamma_{1}` contains the distribution's isoline of amplitude :math:`\epsilon f(\gamma_{1})`.

        The solution is given by :math:`k^{\ast} = \log \epsilon / \left( \cos\alpha + \frac{\beta}{2}\sin^{2}\alpha - 1 \right)`.

        Parameters
        ----------
        alpha : float
            Angular span [rad] of the density between :math:`\gamma_{1}` and a point :math:`r` along :math:`\gamma_{2}` on the sphere where :math:`f(r) = \epsilon f(\gamma_{1})`.
        beta : float
            Ellipticity in [0, 1[.
        eps : float
            Constant :math:`\epsilon` in ]0, 1[.

        Returns
        -------
        k : int
            scale parameter.
        """
        if not (0 < alpha <= np.pi):
            raise ValueError('Parameter[alpha] is out of bounds.')

        if not (0 <= beta < 1):
            raise ValueError('Parameter[beta] must lie in [0, 1).')

        if not (0 < eps < 1):
            raise ValueError('Parameter[eps] must lie in (0, 1).')

        denom = np.cos(alpha) + 0.5 * beta * np.sin(alpha) ** 2 - 1

        if np.isclose(denom, 0):
            k = np.inf
        else:
            k = np.abs(np.log(eps) / denom)
        return k
Exemplo n.º 11
0
class Fourier_IMFS_Block(bim.IntegratingMultiFieldSynthesizerBlock):
    """
    Multi-field synthesizer based on PeriodicSynthesis.

    Examples
    --------
    Assume we are imaging a portion of the Bootes field with LOFAR's 24 core stations.

    The short script below shows how to use :py:class:`~pypeline.phased_array.bluebild.imager.fourier_domain.Fourier_IMFS_Block` to form continuous integrated energy level estimates.

    .. testsetup::

       import numpy as np
       import astropy.units as u
       import astropy.time as atime
       import astropy.coordinates as coord
       from tqdm import tqdm as ProgressBar
       from pypeline.phased_array.bluebild.data_processor import IntensityFieldDataProcessorBlock
       from pypeline.phased_array.bluebild.imager.fourier_domain import Fourier_IMFS_Block
       from pypeline.phased_array.instrument import LofarBlock
       from pypeline.phased_array.beamforming import MatchedBeamformerBlock
       from pypeline.phased_array.util.gram import GramBlock
       from pypeline.phased_array.util.data_gen.sky import from_tgss_catalog
       from pypeline.phased_array.util.data_gen.visibility import VisibilityGeneratorBlock
       from pypeline.phased_array.util.grid import ea_grid
       from pypeline.util.math.sphere import pol2cart
       from scipy.constants import speed_of_light

       np.random.seed(0)

    .. doctest::

       ### Experiment setup ================================================
       # Observation
       >>> obs_start = atime.Time(56879.54171302732, scale='utc', format='mjd')
       >>> field_center = coord.SkyCoord(218 * u.deg, 34.5 * u.deg)
       >>> field_of_view = np.radians(5)
       >>> frequency = 145e6
       >>> wl = speed_of_light / frequency

       # instrument
       >>> N_station = 24
       >>> dev = LofarBlock(N_station)
       >>> mb = MatchedBeamformerBlock([(_, _, field_center) for _ in range(N_station)])
       >>> gram = GramBlock()

       # Visibility generation
       >>> sky_model=from_tgss_catalog(field_center, field_of_view, N_src=10)
       >>> vis = VisibilityGeneratorBlock(sky_model,
       ...                                T=8,
       ...                                fs=196e3,
       ...                                SNR=np.inf)

       ### Energy-level imaging ============================================
       # Kernel parameters
       >>> t_img = obs_start + np.arange(200) * 8 * u.s  # fine-grained snapshots
       >>> obs_start, obs_end = t_img[0], t_img[-1]
       >>> R = dev.icrs2bfsf_rot(obs_start, obs_end)
       >>> N_FS = dev.bfsf_kernel_bandwidth(wl, obs_start, obs_end)
       >>> T_kernel = np.radians(10)

       # Pixel grid: make sure to generate it in BFSF coordinates by applying R.
       >>> px_colat, px_lon = ea_grid(direction=np.dot(R, field_center.transform_to('icrs').cartesian.xyz.value),
       ...                            FoV=field_of_view,
       ...                            size=[256, 386])

       >>> I_dp = IntensityFieldDataProcessorBlock(N_eig=7,  # assumed obtained from IntensityFieldParameterEstimator.infer_parameters()
       ...                                         cluster_centroids=[124.927,  65.09 ,  38.589,  23.256])
       >>> I_mfs = Fourier_IMFS_Block(wl, px_colat, px_lon,
       ...                            N_FS, T_kernel, R, N_level=4)
       >>> for t in ProgressBar(t_img):
       ...     XYZ = dev(t)
       ...     W = mb(XYZ, wl)
       ...     S = vis(XYZ, W, wl)
       ...     G = gram(XYZ, W, wl)
       ...
       ...     D, V, c_idx = I_dp(S, G)
       ...
       ...     # (2, N_eig, N_height, N_FS+Q) energy levels [integrated, clustered) (compact descriptor, not the same thing as [D, V]).
       ...     field_stat = I_mfs(D, V, XYZ.data, W.data, c_idx)

       >>> I_std_c, I_lsq_c = I_mfs.as_image()

    The standardized and least-squares images can then be viewed side-by-side:

    .. doctest::

       from pypeline.phased_array.util.io.image import SphericalImage
       import matplotlib.pyplot as plt

       # Transform grid to ICRS coordinates before plotting.
       px_grid = np.tensordot(R.T, pol2cart(1, px_colat, px_lon), axes=1)

       fig, ax = plt.subplots(ncols=2)
       SphericalImage(I_std_c).draw(index=slice(None),  # Collapse all energy levels
                                    catalog=sky_model,
                                    data_kwargs=dict(cmap='cubehelix'),
                                    catalog_kwargs=dict(s=600),
                                    ax=ax[0])
       ax[0].set_title('Standardized Estimate')
       SphericalImage(I_lsq_c).draw(index=slice(None),  # Collapse all energy levels
                                    catalog=sky_model,
                                    data_kwargs=dict(cmap='cubehelix'),
                                    catalog_kwargs=dict(s=600),
                                    ax=ax[1])
       ax[1].set_title('Least-Squares Estimate')
       fig.show()

    .. image:: _img/bluebild_FourierIMFSBlock_integrate_example.png
    """

    @chk.check(dict(wl=chk.is_real,
                    grid_colat=chk.has_reals,
                    grid_lon=chk.has_reals,
                    N_FS=chk.is_odd,
                    T=chk.is_real,
                    R=chk.require_all(chk.has_shape([3, 3]),
                                      chk.has_reals),
                    N_level=chk.is_integer,
                    precision=chk.is_integer))
    def __init__(self, wl, grid_colat, grid_lon, N_FS, T, R, N_level,
                 precision=64):
        """
        Parameters
        ----------
        wl : float
            Wave-length [m] of observations.
        grid_colat : :py:class:`~numpy.ndarray`
            (N_height, 1) BFSF polar angles [rad].
        grid_lon : :py:class:`~numpy.ndarray`
            (1, N_width) equi-spaced BFSF azimuthal angles [rad].
        N_FS : int
            :math:`2\pi`-periodic kernel bandwidth. (odd-valued)
        T : float
            Kernel periodicity [rad] to use for imaging.
        R : array-like(float)
            (3, 3) ICRS -> BFSF rotation matrix.
        N_level : int
            Number of clustered energy-levels to output.
        precision : int
            Numerical accuracy of floating-point operations.

            Must be 32 or 64.

        Notes
        -----
        * `grid_colat` and `grid_lon` should be generated using :py:func:`~pypeline.phased_array.util.grid.ea_grid` or :py:func:`~pypeline.phased_array.util.grid.ea_harmonic_grid`.
        * `N_FS` can be optimally chosen by calling :py:meth:`~pypeline.phased_array.instrument.EarthBoundInstrumentGeometryBlock.bfsf_kernel_bandwidth`.
        * `R` can be obtained by calling :py:meth:`~pypeline.phased_array.instrument.EarthBoundInstrumentGeometryBlock.icrs2bfsf_rot`.
        """
        super().__init__()

        if precision == 32:
            self._fp = np.float32
            self._cp = np.complex64
        elif precision == 64:
            self._fp = np.float64
            self._cp = np.complex128
        else:
            raise ValueError('Parameter[precision] must be 32 or 64.')

        if N_level <= 0:
            raise ValueError('Parameter[N_level] must be positive.')
        self._N_level = N_level

        self._synthesizer = fsfd.ReferenceFourierFieldSynthesizerBlock(wl, grid_colat, grid_lon, N_FS, T, R, precision)

    @chk.check(dict(D=chk.has_reals,
                    V=chk.has_complex,
                    XYZ=chk.has_reals,
                    W=chk.is_instance(np.ndarray,
                                      sparse.csr_matrix,
                                      sparse.csc_matrix),
                    cluster_idx=chk.has_integers))
    def __call__(self, D, V, XYZ, W, cluster_idx):
        """
        Compute (clustered) integrated field statistics for least-squares and standardized estimates.

        Parameters
        ----------
        D : :py:class:`~numpy.ndarray`
            (N_eig,) positive eigenvalues.
        V : :py:class:`~numpy.ndarray`
            (N_beam, N_eig) complex-valued eigenvectors.
        XYZ : :py:class:`~numpy.ndarary`
            (N_antenna, 3) Cartesian instrument geometry.

            `XYZ` must be given in ICRS.
        W : :py:class:`~numpy.ndarray` or :py:class:`~scipy.sparse.csr_matrix` or :py:class:`~scipy.sparse.csc_matrix`
            (N_antenna, N_beam) synthesis beamweights.
        cluster_idx : :py:class:`~numpy.ndarray`
            (N_eig,) cluster indices of each eigenpair.

        Returns
        -------
        stat : :py:class:`~numpy.ndarray`
            (2, N_level, N_height, N_FS + Q) field statistics.
        """
        D = D.astype(self._fp, copy=False)

        stat_std = self._synthesizer(V, XYZ, W)
        stat_lsq = stat_std * D.reshape(-1, 1, 1)

        stat = np.stack([stat_std, stat_lsq], axis=0)
        stat = array.cluster_layers(stat, cluster_idx,
                                    N=self._N_level, axis=1)

        self._update(stat)
        return stat

    def as_image(self):
        """
        Transform integrated statistics to viewable image.

        The image is stored in a :py:class:`~pypeline.phased_arraay.util.io.image.SphericalImageContainer_floatxx` that
        can then be fed to :py:class:`~pypeline.phased_arraay.util.io.image.SphericalImage` for visualization.

        Returns
        -------
        std_c : :py:class:`~pypeline.phased_array.util.io.image.SphericalImageContainer_floatxx`
            (N_level, N_height, N_width) standardized energy-levels.

        lsq_c : :py:class:`~pypeline.phased_array.util.io.image.SphericalImageContainer_floatxx`
            (N_level, N_height, N_width) least-squares energy-levels.
        """
        if self._fp == np.float32:
            container_type = image.SphericalImageContainer_float32
        else:
            container_type = image.SphericalImageContainer_float64

        bfsf_x, bfsf_y, bfsf_z = sph.pol2cart(1,
                                              self._synthesizer._grid_colat,
                                              self._synthesizer._grid_lon)
        bfsf_grid = np.stack([bfsf_x, bfsf_y, bfsf_z], axis=0)
        icrs_grid = np.tensordot(self._synthesizer._R.T,
                                 bfsf_grid,
                                 axes=1).astype(self._fp)

        stat_std = self._statistics[0]
        field_std = self._synthesizer.synthesize(stat_std).astype(self._fp)
        std = container_type(field_std, icrs_grid)

        stat_lsq = self._statistics[1]
        field_lsq = self._synthesizer.synthesize(stat_lsq).astype(self._fp)
        lsq = container_type(field_lsq, icrs_grid)

        return std, lsq
Exemplo n.º 12
0
class ReferenceFourierFieldSynthesizerBlock(synth.FieldSynthesizerBlock):
    """
    Field synthesizer based on PeriodicSynthesis.

    Examples
    --------
    Assume we are imaging a portion of the Bootes field with LOFAR's 24 core stations.

    The short script below shows how to use :py:class:`~pypeline.phased_array.bluebild.field_synthesizer.fourier_domain.ReferenceFourierFieldSynthesizerBlock` to form continuous energy level estimates.

    .. testsetup::

       import numpy as np
       import astropy.units as u
       import astropy.time as atime
       import astropy.coordinates as coord
       from tqdm import tqdm as ProgressBar
       from pypeline.phased_array.bluebild.data_processor import IntensityFieldDataProcessorBlock
       from pypeline.phased_array.bluebild.field_synthesizer.fourier_domain import ReferenceFourierFieldSynthesizerBlock
       from pypeline.phased_array.instrument import LofarBlock
       from pypeline.phased_array.beamforming import MatchedBeamformerBlock
       from pypeline.phased_array.util.gram import GramBlock
       from pypeline.phased_array.util.data_gen.sky import from_tgss_catalog
       from pypeline.phased_array.util.data_gen.visibility import VisibilityGeneratorBlock
       from pypeline.phased_array.util.grid import ea_grid
       from pypeline.util.math.sphere import pol2cart
       from scipy.constants import speed_of_light

       np.random.seed(0)

    .. doctest::

       ### Experiment setup ================================================
       # Observation
       >>> obs_start = atime.Time(56879.54171302732, scale='utc', format='mjd')
       >>> field_center = coord.SkyCoord(218 * u.deg, 34.5 * u.deg)
       >>> field_of_view = np.radians(5)
       >>> frequency = 145e6
       >>> wl = speed_of_light / frequency

       # instrument
       >>> N_station = 24
       >>> dev = LofarBlock(N_station)
       >>> mb = MatchedBeamformerBlock([(_, _, field_center) for _ in range(N_station)])
       >>> gram = GramBlock()

       # Visibility generation
       >>> sky_model=from_tgss_catalog(field_center, field_of_view, N_src=10)
       >>> vis = VisibilityGeneratorBlock(sky_model,
       ...                                T=8,
       ...                                fs=196e3,
       ...                                SNR=np.inf)

       ### Energy-level imaging ============================================
       # Kernel parameters
       >>> t_img = obs_start + np.arange(200) * 8 * u.s  # fine-grained snapshots
       >>> obs_start, obs_end = t_img[0], t_img[-1]
       >>> R = dev.icrs2bfsf_rot(obs_start, obs_end)
       >>> N_FS = dev.bfsf_kernel_bandwidth(wl, obs_start, obs_end)
       >>> T_kernel = np.radians(10)

       # Pixel grid: make sure to generate it in BFSF coordinates by applying R.
       >>> px_colat, px_lon = ea_grid(direction=np.dot(R, field_center.transform_to('icrs').cartesian.xyz.value),
       ...                            FoV=field_of_view,
       ...                            size=[256, 386])

       >>> I_dp = IntensityFieldDataProcessorBlock(N_eig=7,  # assumed obtained from IntensityFieldParameterEstimator.infer_parameters()
       ...                                         cluster_centroids=[124.927,  65.09 ,  38.589,  23.256])
       >>> I_fs = ReferenceFourierFieldSynthesizerBlock(wl, px_colat, px_lon,
       ...                                     N_FS, T_kernel, R)
       >>> for t in ProgressBar(t_img):
       ...     XYZ = dev(t)
       ...     W = mb(XYZ, wl)
       ...     S = vis(XYZ, W, wl)
       ...     G = gram(XYZ, W, wl)
       ...
       ...     D, V, c_idx = I_dp(S, G)
       ...
       ...     # (N_eig, N_height, N_FS+Q) energy levels (compact descriptor, not the same thing as [D, V]).
       ...     field_stat = I_fs(V, XYZ.data, W.data)

       # (N_eig, N_height, N_width) energy levels
       # Depending on the implementation of FieldSynthesizerBlock, `field_stat` and `field` may differ.
       >>> field = I_fs.synthesize(field_stat)

    In the example above, individual snapshots were not added together, hence the final image is just the last field snapshot and can be quite noisy:

    .. doctest::

       from pypeline.phased_array.util.io.image import SphericalImage, SphericalImageContainer_float64
       # Transform grid to ICRS coordinates before plotting.
       px_grid = np.tensordot(R.T,
                              np.stack(pol2cart(1, px_colat, px_lon), axis=0),
                              axes=1)
       I_container = SphericalImageContainer_float64(image=field, grid=px_grid)
       I_snapshot = SphericalImage(I_container)

       ax = I_snapshot.draw(index=slice(None),  # Collapse all energy levels
                            catalog=sky_model,
                            data_kwargs=dict(cmap='cubehelix'),
                            catalog_kwargs=dict(s=600))
       ax.get_figure().show()

    .. image:: _img/bluebild_FourierFieldSynthesizer_snapshot_example.png
    """
    @chk.check(
        dict(wl=chk.is_real,
             grid_colat=chk.has_reals,
             grid_lon=chk.has_reals,
             N_FS=chk.is_odd,
             T=chk.is_real,
             R=chk.require_all(chk.has_shape([3, 3]), chk.has_reals),
             precision=chk.is_integer))
    def __init__(self, wl, grid_colat, grid_lon, N_FS, T, R, precision=64):
        """
        Parameters
        ----------
        wl : float
            Wave-length [m] of observations.
        grid_colat : :py:class:`~numpy.ndarray`
            (N_height, 1) BFSF polar angles [rad].
        grid_lon : :py:class:`~numpy.ndarray`
            (1, N_width) equi-spaced BFSF azimuthal angles [rad].
        N_FS : int
            :math:`2\pi`-periodic kernel bandwidth. (odd-valued)
        T : float
            Kernel periodicity [rad] to use for imaging.
        R : :py:class:`~numpy.ndarray`
            (3, 3) ICRS -> BFSF rotation matrix.
        precision : int
            Numerical accuracy of floating-point operations.

            Must be 32 or 64.

        Notes
        -----
        * `grid_colat` and `grid_lon` should be generated using :py:func:`~pypeline.phased_array.util.grid.ea_grid` or :py:func:`~pypeline.phased_array.util.grid.ea_harmonic_grid`.
        * `N_FS` can be optimally chosen by calling :py:meth:`~pypeline.phased_array.instrument.EarthBoundInstrumentGeometryBlock.bfsf_kernel_bandwidth`.
        * `R` can be obtained by calling :py:meth:`~pypeline.phased_array.instrument.EarthBoundInstrumentGeometryBlock.icrs2bfsf_rot`.
        """
        super().__init__()

        if precision == 32:
            self._fp = np.float32
            self._cp = np.complex64
        elif precision == 64:
            self._fp = np.float64
            self._cp = np.complex128
        else:
            raise ValueError('Parameter[precision] must be 32 or 64.')

        if wl <= 0:
            raise ValueError('Parameter[wl] must be positive.')
        self._wl = wl

        if N_FS <= 0:
            raise ValueError('Parameter[N_FS] must be positive.')

        if not (0 < T <= 2 * np.pi):
            raise ValueError(f'Parameter[T] is out of bounds.')

        if not np.isclose(T, 2 * np.pi):  # PeriodicSynthesis
            self._alpha_window = 0.1
            T_min = (1 + self._alpha_window) * grid_lon.ptp()
            if T < T_min:
                raise ValueError(f'Parameter[T] must be greater that {T_min}.')
            self._T = T

            aw = self._alpha_window
            lon_start, lon_end = grid_lon[0, [0, -1]]
            T_start, T_end = lon_end + T * np.r_[0.5 * aw - 1, 0.5 * aw]
            self._Tc = (T_start + T_end) / 2
            self._mps = lon_start - (T_start + 0.5 * T * aw)  # max_phase_shift

            N_FS_trunc = N_FS / (2 * np.pi) * T
            N_FS_trunc = int(np.ceil(N_FS_trunc))
            N_FS_trunc += 1 if chk.is_even(N_FS_trunc) else 0
            self._NFS = N_FS_trunc
        else:  # No PeriodicSynthesis, but set params to still work.
            self._alpha_window = 0
            self._T = 2 * np.pi
            self._Tc = np.pi
            self._mps = 2 * np.pi  # max_phase_shift
            self._NFS = N_FS

        self._grid_colat = np.array(grid_colat)
        self._grid_lon = np.array(grid_lon)
        self._R = np.array(R)

        # Buffered state
        self._FSk = None  # (N_antenna, N_height, N_FS+Q) FS coefficients
        self._XYZk = None  # (N_antenna, 3) BFSF coordinates

    @chk.check(
        dict(V=chk.has_complex,
             XYZ=chk.has_reals,
             W=chk.is_instance(np.ndarray, sparse.csr_matrix,
                               sparse.csc_matrix)))
    def __call__(self, V, XYZ, W):
        """
        Compute instantaneous field statistics.

        Parameters
        ----------
        V : :py:class:`~numpy.ndarray`
            (N_beam, N_eig) complex-valued eigenvectors.
        XYZ : :py:class:`~numpy.ndarray`
            (N_antenna, 3) Cartesian instrument geometry.

            `XYZ` must be given in ICRS.
        W : :py:class:`~numpy.ndarray` or :py:class:`~scipy.sparse.csr_matrix` or :py:class:`~scipy.sparse.csc_matrix`
            (N_antenna, N_beam) synthesis beamweights.

        Returns
        -------
        stat : :py:class:`~numpy.ndarray`
            (N_eig, N_height, N_FS + Q) field statistics.
        """
        if not fsd._have_matching_shapes(V, XYZ, W):
            raise ValueError('Parameters[V, XYZ, W] are inconsistent.')
        V = V.astype(self._cp, copy=False)
        XYZ = XYZ.astype(self._fp, copy=False)
        W = W.astype(self._cp, copy=False)

        bfsf_XYZ = XYZ @ self._R.T
        if self._XYZk is None:
            phase_shift = np.inf
        else:
            phase_shift = self._phase_shift(bfsf_XYZ)

        if self._regen_required(phase_shift):
            self._regen_kernel(bfsf_XYZ)
            phase_shift = 0

        N_antenna, N_height, _2N1Q = self._FSk.shape
        N = (self._NFS - 1) // 2
        Q = _2N1Q - self._NFS
        N_beam = W.shape[1]

        PW_FS = W.T @ self._FSk.reshape(N_antenna, N_height * _2N1Q)
        E_FS = np.tensordot(V.T,
                            PW_FS.reshape(N_beam, N_height, _2N1Q),
                            axes=1)

        mod_phase = (-1j * 2 * np.pi * phase_shift / self._T)
        E_FS *= np.exp(mod_phase)**np.r_[-N:N + 1, np.zeros(Q)]

        E_Ny = fourier.iffs(E_FS, self._T, self._Tc, self._NFS, axis=2)
        I_Ny = E_Ny.real**2 + E_Ny.imag**2
        return I_Ny

    @chk.check('stat', chk.has_reals)
    def synthesize(self, stat):
        """
        Compute field values from statistics.

        Parameters
        ----------
        stat : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_FS + Q) field statistics.

        Returns
        -------
        field : :py:class:`~numpy.ndarray`
            (N_level, N_height, N_width) field values.
        """
        stat = np.array(stat, copy=False)

        if stat.ndim != 3:
            raise ValueError('Parameter[stat] is incorrectly shaped.')

        N_level = len(stat)
        N_height, _2N1Q = self._FSk.shape[1:]

        if not chk.has_shape([N_level, N_height, _2N1Q])(stat):
            raise ValueError("Parameter[stat] does not match "
                             "the kernel's dimensions.")

        field_FS = fourier.ffs(stat, self._T, self._Tc, self._NFS, axis=2)
        field = fourier.fs_interp(field_FS[:, :, :self._NFS],
                                  T=self._T,
                                  a=self._grid_lon[0, 0],
                                  b=self._grid_lon[0, -1],
                                  M=self._grid_lon.size,
                                  axis=2,
                                  real_x=True)
        return field

    def _phase_shift(self, XYZ):
        """
        Angular shift w.r.t kernel antenna coordinates.

        Parameters
        ----------
        XYZ : :py:class:`~numpy.ndarray`
            (N_antenna, 3) Cartesian instrument geometry.

            `XYZ` must be given in BFSF.

        Returns
        -------
        theta : float
            Angular shift [rad] such that ``dot(_XYZk, R(theta).T) == XYZ``.
        """
        R_T, *_ = linalg.lstsq(self._XYZk[:, :2], XYZ[:, :2])

        R = np.eye(3)
        R[:2, :2] = R_T.T
        theta = pylinalg.z_rot2angle(R)

        return theta

    def _regen_required(self, shift):
        lhs = np.radians(-0.1)  # Slightly below 0 due to numerical rounding
        if lhs <= shift <= self._mps:
            return False
        else:
            return True

    def _regen_kernel(self, XYZ):
        """
        Compute kernel.

        Parameters
        ----------
        XYZ : :py:class:`~numpy.ndarray`
            (N_antenna, 3) Cartesian instrument geometry.

            `XYZ` must be given in BFSF.
        """
        N_samples = fftpack.next_fast_len(self._NFS)
        lon_smpl = fourier.ffs_sample(self._T, self._NFS, self._Tc, N_samples)
        px_x, px_y, px_z = sph.pol2cart(1, self._grid_colat,
                                        lon_smpl.reshape(1, -1))
        pix_smpl = np.stack([px_x, px_y, px_z], axis=0)

        N_antenna = len(XYZ)
        N_height = len(self._grid_colat)

        # `self._NFS` assumes imaging is performed with `XYZ` centered at the origin.
        XYZ_c = XYZ - XYZ.mean(axis=0)
        window = func.Tukey(self._T, self._Tc, self._alpha_window)
        k_smpl = np.zeros((N_antenna, N_height, N_samples), dtype=self._cp)
        ne.evaluate('exp(A * B) * C',
                    dict(A=1j * 2 * np.pi / self._wl,
                         B=np.tensordot(XYZ_c, pix_smpl, axes=1),
                         C=window(lon_smpl)),
                    out=k_smpl,
                    casting='same_kind')  # Due to limitations of NumExpr2

        self._FSk = fourier.ffs(k_smpl, self._T, self._Tc, self._NFS, axis=2)
        self._XYZk = XYZ
Exemplo n.º 13
0
def eigh(A, B=None, tau=1, N=None):
    """
    Solve a generalized eigenvalue problem.

    Finds :math:`(D, V)`, solution of the generalized eigenvalue problem

    .. math::

       A V = B V D.

    This function is a wrapper around :py:func:`scipy.linalg.eigh` that adds energy truncation and extra output formats.

    Parameters
    ----------
    A : :py:class:`~numpy.ndarray`
        (M, M) hermitian matrix.
        If `A` is not positive-semidefinite (PSD), its negative spectrum is discarded.
    B : :py:class:`~numpy.ndarray`
        (M, M) PSD hermitian matrix.
        If unspecified, `B` is assumed to be the identity matrix.
    tau : float, optional
        Normalized energy ratio in [0, 1].
    N : int, optional
        Number of eigenpairs to output. (Default: K, the minimum number of leading eigenpairs that account for `tau` percent of the total energy.)

        * If `N` is smaller than K, then the trailing eigenpairs are dropped.
        * If `N` is greater that K, then the trailing eigenpairs are set to 0.

    Returns
    -------
        D : :py:class:`~numpy.ndarray`
            (N,) positive real-valued eigenvalues.

        V : :py:class:`~numpy.ndarray`
            (M, N) complex-valued eigenvectors.

            The N eigenpairs are sorted in decreasing eigenvalue order.

    Examples
    --------
    .. testsetup::

       import numpy as np
       from pypeline.util.math.linalg import eigh
       import scipy.linalg as linalg

       np.random.seed(0)

       def hermitian_array(N: int) -> np.ndarray:
           '''
           Construct a (N, N) Hermitian matrix.
           '''
           D = np.arange(N)
           Rmtx = np.random.randn(N,N) + 1j * np.random.randn(N, N)
           Q, _ = linalg.qr(Rmtx)

           A = (Q * D) @ Q.conj().T
           return A

       M = 4
       A = hermitian_array(M)
       B = hermitian_array(M) + 100 * np.eye(M)  # To guarantee PSD

    Let `A` and `B` be defined as below:

    .. doctest::

       M = 4
       A = hermitian_array(M)
       B = hermitian_array(M) + 100 * np.eye(M)  # To guarantee PSD

    Then different calls to :py:func:`~pypeline.util.math.linalg.eigh` produce different results:

    * Get all positive eigenpairs:

    .. doctest::

       >>> D, V = eigh(A, B)
       >>> print(np.around(D, 4))  # The last term is small but positive.
       [0.0296 0.0198 0.0098 0.    ]

       >>> print(np.around(V, 4))
       [[-0.0621+0.0001j -0.0561+0.0005j -0.0262-0.0004j  0.0474+0.0005j]
        [ 0.0285+0.0041j -0.0413-0.0501j  0.0129-0.0209j -0.004 -0.0647j]
        [ 0.0583+0.0055j -0.0443+0.0033j  0.0069+0.0474j  0.0281+0.0371j]
        [ 0.0363+0.0209j  0.0006+0.0235j -0.029 -0.0736j  0.0321+0.0142j]]

    * Drop some trailing eigenpairs:

    .. doctest::

       >>> D, V = eigh(A, B, tau=0.8)
       >>> print(np.around(D, 4))
       [0.0296]

       >>> print(np.around(V, 4))
       [[-0.0621+0.0001j]
        [ 0.0285+0.0041j]
        [ 0.0583+0.0055j]
        [ 0.0363+0.0209j]]

    * Pad output to certain size:

    .. doctest::

       >>> D, V = eigh(A, B, tau=0.8, N=3)
       >>> print(np.around(D, 4))
       [0.0296 0.     0.    ]

       >>> print(np.around(V, 4))
       [[-0.0621+0.0001j  0.    +0.j      0.    +0.j    ]
        [ 0.0285+0.0041j  0.    +0.j      0.    +0.j    ]
        [ 0.0583+0.0055j  0.    +0.j      0.    +0.j    ]
        [ 0.0363+0.0209j  0.    +0.j      0.    +0.j    ]]
    """
    A = np.array(A, copy=False)
    M = len(A)
    if not (chk.has_shape([M, M])(A) and np.allclose(A, A.conj().T)):
        raise ValueError('Parameter[A] must be hermitian symmetric.')

    B = np.eye(M) if (B is None) else np.array(B, copy=False)
    if not (chk.has_shape([M, M])(B) and np.allclose(B, B.conj().T)):
        raise ValueError('Parameter[B] must be hermitian symmetric.')

    if not (0 < tau <= 1):
        raise ValueError('Parameter[tau] must be in [0, 1].')

    if (N is not None) and (N <= 0):
        raise ValueError(f'Parameter[N] must be a non-zero positive integer.')

    # A: drop negative spectrum.
    Ds, Vs = linalg.eigh(A)
    idx = Ds > 0
    Ds, Vs = Ds[idx], Vs[:, idx]
    A = (Vs * Ds) @ Vs.conj().T

    # A, B: generalized eigenvalue-decomposition.
    try:
        D, V = linalg.eigh(A, B)

        # Discard near-zero D due to numerical precision.
        idx = D > 0
        D, V = D[idx], V[:, idx]
        idx = np.argsort(D)[::-1]
        D, V = D[idx], V[:, idx]
    except linalg.LinAlgError:
        raise ValueError('Parameter[B] is not PSD.')

    # Energy selection / padding
    idx = np.clip(np.cumsum(D) / np.sum(D), 0, 1) <= tau
    D, V = D[idx], V[:, idx]
    if N is not None:
        M, K = V.shape
        if N - K <= 0:
            D, V = D[:N], V[:, :N]
        else:
            D = np.concatenate((D, np.zeros(N - K)), axis=0)
            V = np.concatenate((V, np.zeros((M, N - K))), axis=1)

    return D, V
Exemplo n.º 14
0
Pixel-grid generation and utilities for spherical surfaces.
"""

import astropy.coordinates as coord
import astropy.units as u
import numpy as np
import scipy.linalg as linalg

import pypeline.util.argcheck as chk
import pypeline.util.math.linalg as pylinalg
import pypeline.util.math.sphere as sph


@chk.check(
    dict(direction=chk.require_all(chk.has_reals, chk.has_shape([
        3,
    ])),
         FoV=chk.is_real,
         size=chk.require_all(chk.has_integers, chk.has_shape([
             2,
         ]))))
def spherical_grid(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 centered at `direction` [rad].