class LofarBlock(EarthBoundInstrumentGeometryBlock):
    """
    `LOw-Frequency ARray (LOFAR) <http://www.lofar.org/>`_ located in Europe.

    This LOFAR model consists of 62 stations, each containing between 17 to 24 HBA dipole antennas.
    """
    @chk.check('N_station', chk.allow_None(chk.is_integer))
    def __init__(self, N_station=None):
        """
        Parameters
        ----------
        N_station : int
            Number of stations to use. (Default = all)

            Sometimes only a subset of an instrument’s stations are desired.
            Setting `N_station` limits the number of stations to those that appear first in `XYZ` when sorted by STATION_ID.
        """
        XYZ = self._get_geometry()
        super().__init__(XYZ, N_station)

    def _get_geometry(self):
        """
        Load instrument geometry.

        Returns
        -------
        :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            ITRS instrument geometry.
        """
        rel_path = pathlib.Path('data', 'phased_array', 'instrument',
                                'LOFAR.csv')
        abs_path = pkg.resource_filename('pypeline', str(rel_path))

        itrs_geom = (pd.read_csv(abs_path).set_index(
            ['STATION_ID', 'ANTENNA_ID']))

        XYZ = _as_InstrumentGeometry(itrs_geom)
        return XYZ
class MwaBlock(EarthBoundInstrumentGeometryBlock):
    """
    `Murchison Widefield Array (MWA) <http://www.mwatelescope.org/>`_ located in Australia.

    MWA consists of 128 stations, each containing 16 dipole antennas.
    """
    @chk.check(
        dict(N_station=chk.allow_None(chk.is_integer),
             station_only=chk.is_boolean))
    def __init__(self, N_station=None, station_only=False):
        """
        Parameters
        ----------
        N_station : int
            Number of stations to use. (Default = all)

            Sometimes only a subset of an instrument’s stations are desired.
            Setting `N_station` limits the number of stations to those that appear first in `XYZ` when sorted by STATION_ID.

        station_only : bool
            If :py:obj:`True`, model MWA stations as single-element antennas. (Default = False)
        """
        XYZ = self._get_geometry(station_only)
        super().__init__(XYZ, N_station)

    def _get_geometry(self, station_only):
        """
        Load instrument geometry.

        Parameters
        ----------
        station_only : bool
            If :py:obj:`True`, model stations as single-element antennas.

        Returns
        -------
        :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            ITRS instrument geometry.
        """
        rel_path = pathlib.Path('data', 'phased_array', 'instrument',
                                'MWA.csv')
        abs_path = pkg.resource_filename('pypeline', str(rel_path))

        itrs_geom = (pd.read_csv(abs_path).set_index('STATION_ID'))

        station_id = itrs_geom.index.get_level_values('STATION_ID')
        if station_only:
            itrs_geom.index = (pd.MultiIndex.from_product(
                [station_id, [0]], names=['STATION_ID', 'ANTENNA_ID']))
        else:
            # Generate flat 4x4 antenna grid pointing towards the Noth pole.
            x_lim = y_lim = 1.65
            lY, lX = np.meshgrid(np.linspace(-y_lim, y_lim, 4),
                                 np.linspace(-x_lim, x_lim, 4),
                                 indexing='ij')
            l = np.stack((lX, lY, np.zeros((4, 4))), axis=0)

            # For each station: rotate 4x4 array to lie on the sphere's surface.
            xyz_station = itrs_geom.loc[:, ['X', 'Y', 'Z']].values
            df_stations = []
            for st_id, st_cog in zip(station_id, xyz_station):
                _, st_colat, st_lon = sph.cart2pol(*st_cog)
                st_cog_unit = np.array(sph.pol2cart(1, st_colat, st_lon))

                R_1 = pylinalg.rot([0, 0, 1], st_lon)
                R_2 = pylinalg.rot(axis=np.cross([0, 0, 1], st_cog_unit),
                                   angle=st_colat)
                R = R_2 @ R_1

                st_layout = np.reshape(
                    st_cog.reshape(3, 1, 1) + np.tensordot(R, l, axes=1),
                    (3, -1))
                idx = (pd.MultiIndex.from_product(
                    [[st_id], range(16)], names=['STATION_ID', 'ANTENNA_ID']))
                df_stations += [
                    pd.DataFrame(data=st_layout.T,
                                 index=idx,
                                 columns=['X', 'Y', 'Z'])
                ]
            itrs_geom = pd.concat(df_stations)

        XYZ = _as_InstrumentGeometry(itrs_geom)
        return XYZ
class EarthBoundInstrumentGeometryBlock(InstrumentGeometryBlock):
    """
    Sub-class specialized in instruments that move with the Earth, such as radio telescopes.
    """
    @chk.check(
        dict(XYZ=chk.is_instance(InstrumentGeometry),
             N_station=chk.allow_None(chk.is_integer)))
    def __init__(self, XYZ, N_station=None):
        """
        Parameters
        ----------
        XYZ : :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            ITRS instrument geometry.
        N_station : int
            Number of stations to use. (Default = all)

            Sometimes only a subset of an instrument’s stations are desired.
            Setting `N_station` limits the number of stations to those that appear first in `XYZ` when sorted by STATION_ID.
        """
        super().__init__(XYZ, N_station)

    @chk.check('time', chk.is_instance(time.Time))
    def __call__(self, time):
        """
        Determine instrument antenna positions in ICRS.

        Parameters
        ----------
        time : :py:class:`~astropy.time.Time`
            Moment at which the coordinates are wanted.

        Returns
        -------
        :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            (N_antenna, 3) ICRS instrument geometry.

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

           from pypeline.phased_array.instrument import LofarBlock
           import astropy.time as atime
           import astropy.units as u

        .. doctest::

           >>> instr = LofarBlock()

           >>> time = atime.Time('J2000')
           >>> xyz = instr(time)
           >>> np.around(xyz.data[:5], 2)
           array([[ 1148711.63, -3679538.21,  5064569.  ],
                  [ 1148716.62, -3679538.41,  5064567.74],
                  [ 1148705.76, -3679542.23,  5064567.43],
                  [ 1148710.75, -3679542.42,  5064566.16],
                  [ 1148715.74, -3679542.61,  5064564.9 ]])

           >>> xyz = instr(time + 30 * u.min)
           >>> np.around(xyz.data[:5], 2)
           array([[ 1620400.85, -3497579.41,  5064547.21],
                  [ 1620405.82, -3497578.94,  5064545.95],
                  [ 1620395.56, -3497584.15,  5064545.63],
                  [ 1620400.53, -3497583.69,  5064544.37],
                  [ 1620405.5 , -3497583.23,  5064543.11]])
        """
        layout = self._layout.loc[:, ['X', 'Y', 'Z']].values.T
        r = linalg.norm(layout, axis=0)

        itrs_layout = coord.CartesianRepresentation(layout)
        itrs_position = coord.SkyCoord(itrs_layout, obstime=time, frame='itrs')
        icrs_position = r * (itrs_position.transform_to('icrs').cartesian.xyz)
        icrs_layout = pd.DataFrame(data=icrs_position.T,
                                   index=self._layout.index,
                                   columns=('X', 'Y', 'Z'))
        return _as_InstrumentGeometry(icrs_layout)

    @chk.check(
        dict(obs_start=chk.is_instance(time.Time),
             obs_end=chk.is_instance(time.Time)))
    def icrs2bfsf_rot(self, obs_start, obs_end):
        """
        Rotation matrix from ICRS to the local *Bluebild FastSynthesis Frame* (BFSF).

        Parameters
        ----------
        obs_start : :py:class:`~astropy.time.Time`
            Start of the observation period.
        obs_end : :py:class:`~astropy.time.Time`
            End of the observation period.

        Returns
        -------
        R : :py:class:`~numpy.ndarray`
            (3, 3) ICRS -> BFSF rotation matrix.

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

           from pypeline.phased_array.instrument import LofarBlock
           import astropy.time as atime
           import astropy.units as u

        .. doctest::

           >>> instr = LofarBlock()

           >>> obs_start = atime.Time('J2000')
           >>> obs_end = obs_start + 4 * u.h
           >>> R = instr.icrs2bfsf_rot(obs_start, obs_end)
           >>> np.around(R, 3)
           array([[ 0.963, -0.27 , -0.   ],
                  [ 0.27 ,  0.963,  0.   ],
                  [-0.   ,  0.   , -1.   ]])
        """
        if obs_start > obs_end:
            raise ValueError('Parameter[obs_start] must precede '
                             'Parameter[obs_end].')

        # Find the position of the antennas at several time-instants
        # during `period`.
        N_interval = 20
        sampling_times = (obs_start +
                          ((obs_end - obs_start) /
                           (N_interval - 1)) * np.arange(N_interval))
        icrs_layouts = [self.__call__(t).data for t in sampling_times]
        icrs_layouts = np.stack(icrs_layouts, axis=1)  # (N_antenna, N_time, 3)

        # For each antenna `i`, find a normal vector `n_{i}` to the
        # rotation plane.
        N_antenna = len(icrs_layouts)
        abc = np.zeros((N_antenna, 3))
        for i, xyz in enumerate(icrs_layouts):
            a = np.concatenate([xyz[:, :2], np.ones((len(xyz), 1))], axis=-1)
            b = xyz[:, 2]
            coeffs, *_ = linalg.lstsq(a, b)
            abc[i] = coeffs

        # Average vectors `n_{i}` to obtain the global normal vector `n`.
        # Construct rotation matrix R using 3 basis vectors Ex, Ey, Ez.
        #    Ez must be co-linear to the plane's normal vector.
        a, b, c = np.mean(abc, axis=0)
        z_ax = np.r_[a, b, -1]
        y_ax = np.r_[b, -a, 0]
        x_ax = np.cross(z_ax, y_ax)
        R = np.stack([x_ax, y_ax, z_ax], axis=0)
        R /= linalg.norm(R, axis=1, keepdims=True)
        return R

    @chk.check(
        dict(wl=chk.is_real,
             obs_start=chk.is_instance(time.Time),
             obs_end=chk.is_instance(time.Time)))
    def bfsf_kernel_bandwidth(self, wl, obs_start, obs_end):
        """
        Bandwidth of :math:`2 \pi`-periodic complex plane-wave kernel in BFSF coordinates.

        Parameters
        ----------
        wl : float
            Wave-length [m] of observations.
        obs_start : :py:class:`~astropy.time.Time`
            Start of the observation period.
        obs_end : :py:class:`~astropy.time.Time`
            End of the observation period.

        Returns
        -------
        N_FS : int
            Kernel bandwidth when evaluated in the BFSF reference frame.

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

           from pypeline.phased_array.instrument import LofarBlock
           import astropy.time as atime
           import astropy.units as u
           from scipy.constants import speed_of_light

        .. doctest::

           >>> instr = LofarBlock(N_station=48)

           >>> freq = 145e6
           >>> wl = speed_of_light / freq
           >>> obs_start = atime.Time('J2000')
           >>> obs_end = obs_start + 4 * u.h
           >>> instr.bfsf_kernel_bandwidth(wl, obs_start, obs_end)
           21783
        """
        if wl <= 0:
            raise ValueError('Parameter[wl] must be positive.')

        R = self.icrs2bfsf_rot(obs_start, obs_end)
        obs_mid = obs_start + (obs_end - obs_start) / 2

        icrs_XYZ = self.__call__(obs_mid).data
        bfsf_XYZ = icrs_XYZ @ R.T
        bfsf_XYZ -= np.mean(bfsf_XYZ, axis=0)
        bfsf_XY = bfsf_XYZ[:, :2]
        XY_baseline = linalg.norm(bfsf_XY[:, np.newaxis, :] -
                                  bfsf_XY[np.newaxis, :, :],
                                  axis=-1)

        N = sp.jv_series_threshold((2 * np.pi / wl) * XY_baseline.max())
        return 2 * N + 1
class StationaryInstrumentGeometryBlock(InstrumentGeometryBlock):
    """
    Sub-class specialized in instruments that are stationary, i.e. that do not move with time.
    """
    @chk.check(
        dict(XYZ=chk.is_instance(InstrumentGeometry),
             N_station=chk.allow_None(chk.is_integer)))
    def __init__(self, XYZ, N_station=None):
        """
        Parameters
        ----------
        XYZ : :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            Instrument geometry.
        N_station : int
            Number of stations to use. (Default = all)

            Sometimes only a subset of an instrument's stations are desired.
            Setting `N_station` limits the number of stations to those that appear first in `XYZ` when sorted by STATION_ID.
        """
        super().__init__(XYZ, N_station)

    def __call__(self):
        """
        Determine instrument antenna positions.

        As the instrument's geometry does not change through time, :py:func:`~pypeline.phased_array.instrument.StationaryInstrumentGeometryBlock.__call__` always returns the same object.

        Returns
        -------
        :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            (N_antenna, 3) instrument geometry.

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

           from pypeline.phased_array.instrument import PyramicBlock

        .. doctest::

           >>> instr = PyramicBlock()
           >>> xyz = instr().data[:5]
           >>> np.around(xyz, 3)
           array([[-0.014, -0.025,  0.01 ],
                  [-0.026, -0.045,  0.043],
                  [-0.038, -0.065,  0.075],
                  [-0.042, -0.073,  0.088],
                  [-0.044, -0.077,  0.095]])
        """
        return _as_InstrumentGeometry(self._layout)

    @chk.check('wl', chk.is_real)
    def bfsf_kernel_bandwidth(self, wl):
        """
        Bandwidth of :math:`2 \pi`-periodic complex plane-wave kernel in BFSF coordinates.

        Parameters
        ----------
        wl : float
            Wave-length [m] of observations.

        Returns
        -------
        N_FS : int
            Kernel bandwidth when evaluated in the BFSF reference frame.

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

           from pypeline.phased_array.instrument import PyramicBlock
           from scipy.constants import speed_of_sound

        .. doctest::

           >>> instr = PyramicBlock()

           >>> freq = 3500
           >>> wl = speed_of_sound / freq
           >>> instr.bfsf_kernel_bandwidth(wl)
           67
        """
        if wl <= 0:
            raise ValueError('Parameter[wl] must be positive.')

        R = np.eye(
            3)  # Stationary instruments have (BFSF basis) = (Canonical basis)

        icrs_XYZ = self.__call__().data
        bfsf_XYZ = icrs_XYZ @ R.T
        bfsf_XYZ -= np.mean(bfsf_XYZ, axis=0)
        bfsf_XY = bfsf_XYZ[:, :2]
        XY_baseline = linalg.norm(bfsf_XY[:, np.newaxis, :] -
                                  bfsf_XY[np.newaxis, :, :],
                                  axis=-1)

        N = sp.jv_series_threshold((2 * np.pi / wl) * XY_baseline.max())
        return 2 * N + 1
class InstrumentGeometryBlock(core.Block):
    """
    Compute antenna positions.
    """
    @chk.check(
        dict(XYZ=chk.is_instance(InstrumentGeometry),
             N_station=chk.allow_None(chk.is_integer)))
    def __init__(self, XYZ, N_station=None):
        """
        Parameters
        ----------
        XYZ : :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            Instrument geometry.
        N_station : int
            Number of stations to use. (Default = all)

            Sometimes only a subset of an instrument's stations are desired.
            Setting `N_station` limits the number of stations to those that appear first in `XYZ` when sorted by STATION_ID.
        """
        super().__init__()
        if N_station is not None:
            if N_station < 1:
                raise ValueError('Parameter[N_station] must be positive.')

        self._layout = XYZ.as_frame()

        if N_station is not None:
            stations = np.unique(
                XYZ.index[0].get_level_values('STATION_ID'))[:N_station]
            self._layout = self._layout.loc[stations]

    def __call__(self, *args, **kwargs):
        """
        Determine instrument antenna positions.

        Parameters
        ----------
        *args
            Positional arguments.
        **kwargs
            Keyword arguments.
        Returns
        -------
        :py:class:`~pypeline.phased_array.instrument.InstrumentGeometry`
            (N_antenna, 3) instrument geometry.
        """
        raise NotImplementedError

    @chk.check('wl', chk.is_real)
    def nyquist_rate(self, wl):
        """
        Order of imageable complex plane-waves.

        Parameters
        ----------
        wl : float
            Wave-length [m] of observations.

        Returns
        -------
        N : int
            Maximum order of complex plane waves that can be imaged by the instrument.

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

           from pypeline.phased_array.instrument import MwaBlock
           from scipy.constants import speed_of_light

        .. doctest::

           >>> instr = MwaBlock()
           >>> freq = 145e6
           >>> wl = speed_of_light / freq
           >>> instr.nyquist_rate(wl)
           8753
        """
        if wl <= 0:
            raise ValueError('Parameter[wl] must be positive.')

        XYZ = self._layout.values
        baseline = linalg.norm(XYZ[:, np.newaxis, :] - XYZ[np.newaxis, :, :],
                               axis=-1)

        N = sp.spherical_jn_series_threshold((2 * np.pi / wl) * baseline.max())
        return N
Beispiel #6
0
class SphericalImage:
    """
    Wrapper around :py:class:`SphericalImageContainer_float32` and
    :py:class:`SphericalImageContainer_float64` to enable advanced functionality.

    Main features:

    * import images from FITS format;
    * export images to FITS format;
    * advanced 2D plotting based on `Matplotlib <https://matplotlib.org/>`_;
    * view exported images with `DS9 <http://ds9.si.edu/site/Home.html>`_.

    Examples
    --------
    .. doctest::

       import numpy as np
       import pypeline.phased_array.util.grid as grid
       import pypeline.phased_array.util.io.image as image
       import pypeline.util.math.sphere as sph
       import pypeline.util.math.stat as stat

       # grid settings =======================
       direction = np.array(sph.eq2cart(1,
                                        lat=np.radians(30),
                                        lon=np.radians(20)))
       FoV = np.radians(60)
       N_height, N_width = 256, 384
       px_grid = grid.uniform_grid(direction, FoV, size=[N_height, N_width])

       # data settings =======================
       beta0, a0 = 0.7, [1, 1, 1]
       beta1, a1 = 0.9, [0, 0, 1]
       kent0 = stat.Kent(k=stat.Kent.min_scale(FoV, beta0) * 2,
                         beta=beta0,
                         g1=direction,
                         a=a0)
       kent1 = stat.Kent(k=stat.Kent.min_scale(FoV, beta1) * 2,
                         beta=beta1,
                         g1=direction,
                         a=a1)

       data0 = (kent0
                .pdf(px_grid.reshape(3, N_height * N_width).T)
                .reshape(N_height, N_width))
       data1 = (kent1
                .pdf(px_grid.reshape(3, N_height * N_width).T)
                .reshape(N_height, N_width))
       data = np.stack([data0, data1], axis=0)

       # Image creation ======================
       I_container = image.SphericalImageContainer_float64(data, px_grid)
       I = image.SphericalImage(I_container)

    Data IO:

    .. doctest::

       I.to_fits('test.fits')  # save to FITS
       I2 = image.from_fits('test.fits')  # load from FITS

    Interactive plotting:

    .. doctest::

       I.draw()  # AEQD projection by default, all layers.

    .. image:: _img/sphericalimage_aeqd_example.png

    .. doctest::

       I.draw(index=0, projection='GNOM')  # Only show first data slice.

    .. image:: _img/sphericalimage_gnom_example.png

    .. doctest::

       I.draw(index=1, projection='LCC', data_kwargs=dict(cmap='jet'))

    .. image:: _img/sphericalimage_lcc_example.png
    """
    @chk.check('container',
               chk.is_instance(im_cpp.SphericalImageContainer_float32,
                               im_cpp.SphericalImageContainer_float64))
    def __init__(self, container):
        """
        Parameters
        ----------
        container: :py:class:`~pypeline_phased_array_util_io_image_pybind11.SphericalImageContainer_float32` or :py:class:`~pypeline_phased_array_util_io_image_pybind11.SphericalImageContainer_float64`
            Bare container holding spherical image data.
        """
        self._container = container

    @property
    def image(self):
        """
        Returns
        -------
        :py:class:`~numpy.ndarray`
            (N_image, ...) data cube.
        """
        return self._container.image

    @property
    def grid(self):
        """
        Returns
        -------
        :py:class:`~numpy.ndarray`
            (3, ...) Cartesian coordinates of the sky on which the data points are defined.
        """
        return self._container.grid

    @chk.check('file_name', chk.is_instance(str))
    def to_fits(self, file_name):
        """
        Save image to FITS file.

        Parameters
        ----------
        file_name : str
            Name of file.

        Notes
        -----
        * :py:class:`~pypeline.phased_array.util.io.image.SphericalImage` subclasses that write WCS information assume the grid is specified in ICRS.
          If this is not the case, rotate the grid accordingly before calling :py:meth:`~pypeline.phased_array.util.io.image.SphericalImage.to_fits`.

        * Data cubes are stored in a secondary IMAGE frame and can be viewed with DS9 using::

              $ ds9 <FITS_file>.fits[IMAGE]

          WCS information is only available in external FITS viewers if using :py:class:`~pypeline.phased_array.util.io.image.EqualAngleImage`.
        """
        primary_hdu = self._PrimaryHDU()
        image_hdu = self._ImageHDU()

        hdulist = fits.HDUList([primary_hdu, image_hdu])
        hdulist.writeto(file_name, overwrite=True)

    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__,
                                  'SphericalImage subclass'), )

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

        hdu = fits.PrimaryHDU(data=coordinates)
        for k, v in metadata.items():
            hdu.header[k] = v
        return hdu

    def _ImageHDU(self):
        """
        Generate image Header Descriptor Unit (HDU) for FITS export.

        Returns
        -------
        hdu : :py:class:`~astropy.io.fits.ImageHDU`
        """
        hdu = fits.ImageHDU(data=self.image, name='IMAGE')
        return hdu

    @classmethod
    @chk.check(
        dict(primary_hdu=chk.is_instance(fits.PrimaryHDU),
             image_hdu=chk.is_instance(fits.ImageHDU)))
    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:`~pypeline.phased_array.util.io.image.SphericalImage`
        """
        # PrimaryHDU: grid specification.
        colat, lon = primary_hdu.data
        x, y, z = sph.pol2cart(1, np.radians(colat), np.radians(lon))
        grid = np.stack([x, y, z], axis=0)

        # ImageHDU: extract data cube.
        image = image_hdu.data
        # Make sure (image, grid) have the same dtype to work with SphericalImageContainer_floatxx().
        image = image.astype(grid.dtype)

        if grid.dtype == np.dtype(np.float32):
            I_container = im_cpp.SphericalImageContainer_float32(image, grid)
        else:  # float64 mode
            I_container = im_cpp.SphericalImageContainer_float64(image, grid)
        I = cls(I_container)
        return I

    @property
    def shape(self):
        """
        Returns
        -------
        tuple
            Shape of data cube.
        """
        return self.image.shape

    @chk.check(
        dict(index=chk.accept_any(chk.is_integer, chk.has_integers,
                                  chk.is_instance(slice)),
             projection=chk.is_instance(str),
             catalog=chk.allow_None(chk.is_instance(sky.SkyEmission)),
             show_gridlines=chk.is_boolean,
             show_colorbar=chk.is_boolean,
             ax=chk.allow_None(chk.is_instance(axes.Axes)),
             data_kwargs=chk.allow_None(chk.is_instance(dict)),
             grid_kwargs=chk.allow_None(chk.is_instance(dict)),
             catalog_kwargs=chk.allow_None(chk.is_instance(dict))))
    def draw(self,
             index=slice(None),
             projection='AEQD',
             catalog=None,
             show_gridlines=True,
             show_colorbar=True,
             ax=None,
             data_kwargs=None,
             grid_kwargs=None,
             catalog_kwargs=None):
        """
        Plot spherical image using a 2D projection.

        Parameters
        ----------
        index : int, array-like(int), slice
            Slices of the data-cube to show.

            If multiple layers are provided, they are summed together.
        projection : str
            Plot projection.

            Must be one of (case-insensitive):

            * AEQD: `Azimuthal Equi-Distant <https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection>`_; (default)
            * LAEA: `Lambert Equal-Area <https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>`_;
            * LCC: `Lambert Conformal Conic <https://en.wikipedia.org/wiki/Lambert_conformal_conic_projection>`_;
            * ROBIN: `Robinson <https://en.wikipedia.org/wiki/Robinson_projection>`_;
            * GNOM: `Gnomonic <https://en.wikipedia.org/wiki/Gnomonic_projection>`_;
            * HEALPIX: `Hierarchical Equal-Area Pixelisation <https://en.wikipedia.org/wiki/HEALPix>`_.

            Notes
            -----
            * (AEQD, LAEA, LCC, GNOM) are recommended for mapping portions of the sphere.

                * LCC breaks down when mapping polar regions.
                * GNOM breaks down when mapping large FoVs.

            * (ROBIN, HEALPIX) are recommended for mapping the entire sphere.
        catalog : :py:class:`~pypeline.phased_array.util.data_gen.sky.SkyEmission`
            Source catalog to overlay on top of images. (Default: no overlay)
        show_gridlines : bool
            Show RA/DEC gridlines. (Default: True)

            It is possible for the gridlines to be plotted on the wrong range
            when the grid crosses the 180W/E meridian.
        show_colorbar : bool
            Show colorbar. (Default: True)
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to draw on.

            If :py:obj:`None`, a new axes is used.
        data_kwargs : dict
            Keyword arguments related to data-cube visualization.

            Accepted keys are:

            * :py:meth:`~matplotlib.axes.Axes.contourf` options.
            * :py:meth:`~matplotlib.axes.Axes.tricontourf` options.
        grid_kwargs : dict
            Keyword arguments related to grid visualization.

            Accepted keys are:

            * N_parallel : int
                Number declination lines to show in viewable region. (Default: 3)
            * N_meridian : int
                Number of right-ascension lines to show in viewable region. (Default: 3)
            * polar_plot : bool
                Correct RA/DEC gridlines when mapping polar regions. (Default: False)

                When mapping polar regions, meridian lines may be doubled at 180W/E, making it seem like a meridian line is missing.
                Setting `polar_plot` to :py:obj:`True` redistributes the meridians differently to correct the issue.

                This option only makes sense when mapping polar regions, and will produce incorrect gridlines otherwise.
            * ticks : bool
                Add RA/DEC labels next to gridlines. (Default: False)
                TODO: change to True once implemented
        catalog_kwargs : dict
            Keyword arguments related to catalog visualization.

            Accepted keys are:

            * :py:meth:`~matplotlib.axes.Axes.scatter` options.

        Returns
        -------
        ax : :py:class:`~matplotlib.axes.Axes`
        """
        if ax is None:
            fig, ax = plt.subplots()

        proj = self._draw_projection(projection)
        scm = self._draw_data(index, data_kwargs, proj, ax)
        cbar = self._draw_colorbar(show_colorbar, scm, ax)  # noqa: F841
        self._draw_gridlines(show_gridlines, grid_kwargs, proj, ax)
        self._draw_catalog(catalog, catalog_kwargs, proj, ax)
        self._draw_beautify(proj, ax)

        return ax

    @chk.check('projection', chk.is_instance(str))
    def _draw_projection(self, projection):
        """
        Setup :py:class:`pyproj.Proj` object to do (lon,lat) <-> (x,y) transforms.

        Parameters
        ----------
        projection : str
            `projection` parameter given to :py:meth:`draw`.

        Returns
        -------
        proj : :py:class:`pyproj.Proj`
        """
        # Most projections can be provided a point in space around which distortions are minimized.
        # We choose this point to approximately map to the center of the grid when appropriate.
        # (approximate since it is not always a spherical cap.)
        if self._container.is_gridded:  # (3, N_height, N_width) grid
            grid_dir = np.mean(self.grid, axis=(1, 2))
        else:  # (3, N_points) grid
            grid_dir = np.mean(self.grid, axis=1)
        _, grid_lat, grid_lon = sph.cart2eq(*grid_dir)
        grid_lat = coord.Angle(grid_lat * u.rad).to_value(u.deg)
        grid_lon = coord.Angle(grid_lon * u.rad).wrap_at(180 * u.deg).to_value(
            u.deg)

        p_name = projection.lower()
        if p_name == 'lcc':
            # Lambert Conformal Conic
            proj = pyproj.Proj(proj='lcc', lon_0=grid_lon, lat_0=grid_lat, R=1)
        elif p_name == 'aeqd':
            # Azimuthal Equi-Distant
            proj = pyproj.Proj(proj='aeqd',
                               lon_0=grid_lon,
                               lat_0=grid_lat,
                               R=1)
        elif p_name == 'laea':
            # Lambert Equal-Area
            proj = pyproj.Proj(proj='laea',
                               lon_0=grid_lon,
                               lat_0=grid_lat,
                               R=1)
        elif p_name == 'robin':
            # Robinson
            proj = pyproj.Proj(proj='robin', lon_0=grid_lon, R=1)
        elif p_name == 'gnom':
            # Gnomonic
            proj = pyproj.Proj(proj='gnom',
                               lon_0=grid_lon,
                               lat_0=grid_lat,
                               R=1)
        elif p_name == 'healpix':
            # Hierarchical Equal-Area Pixelisation
            proj = pyproj.Proj(proj='healpix',
                               lon_0=grid_lon,
                               lat_0=grid_lat,
                               R=1)
        else:
            raise ValueError('Parameter[projection] is not a valid projection '
                             'specifier.')

        return proj

    @chk.check(
        dict(index=chk.accept_any(chk.is_integer, chk.has_integers,
                                  chk.is_instance(slice)),
             data_kwargs=chk.allow_None(chk.is_instance(dict)),
             projection=chk.is_instance(pyproj.Proj),
             ax=chk.is_instance(axes.Axes)))
    def _draw_data(self, index, data_kwargs, projection, ax):
        """
        Contour plot of data.

        Parameters
        ----------
        index : int, array-like(int), slice
            `index` parameter given to :py:meth:`draw`.
        data_kwargs : dict
            `data_kwargs` parameter given to :py:meth:`draw`.
        projection : :py:class:`~pyproj.Proj`
            PyProj projection object.
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to plot on.

        Returns
        -------
        scm : :py:class:`~matplotlib.cm.ScalarMappable`
        """
        if data_kwargs is None:
            data_kwargs = dict()

        N_image = self.shape[0]
        if chk.is_integer(index):
            index = np.array([index], dtype=int)
        elif chk.has_integers(index):
            index = np.array(index, dtype=int)
        else:  # slice()
            index = np.arange(N_image, dtype=int)[index]
            if index.size == 0:
                raise ValueError('No data-cube slice chosen.')
        if not np.all((0 <= index) & (index < N_image)):
            raise ValueError('Parameter[index] is out of bounds.')
        data = np.sum(self.image[index], axis=0)

        # Transform (lon,lat) to (x,y).
        # Some projections have unmappable regions or exhibit singularities at certain points.
        # These regions are colored white in contour plots by replacing their incorrect value (1e30) with NaN.
        _, grid_lat, grid_lon = sph.cart2eq(*self.grid)
        grid_lat = coord.Angle(grid_lat * u.rad).to_value(u.deg)
        grid_lon = coord.Angle(grid_lon * u.rad).wrap_at(180 * u.deg).to_value(
            u.deg)

        grid_x, grid_y = projection(grid_lon, grid_lat, errcheck=False)
        grid_x[np.isclose(grid_x, 1e30)] = np.nan
        grid_y[np.isclose(grid_y, 1e30)] = np.nan

        # Colormap choice
        if 'cmap' in data_kwargs:
            obj = data_kwargs.pop('cmap')
            if chk.is_instance(str)(obj):
                cmap = cm.get_cmap(obj)
            else:
                cmap = obj
        else:
            cmap = plot.cmap('matthieu-custom-sky', N=38)

        if self._container.is_gridded:
            scm = ax.contourf(grid_x,
                              grid_y,
                              data,
                              cmap.N,
                              cmap=cmap,
                              **data_kwargs)
        else:
            triangulation = tri.Triangulation(grid_x, grid_y)
            scm = ax.tricontourf(triangulation,
                                 data,
                                 cmap.N,
                                 cmap=cmap,
                                 **data_kwargs)

        # Show coordinates in status bar
        def sexagesimal_coords(x, y):
            lon, lat = projection(x, y, errcheck=False, inverse=True)
            lon = (coord.Angle(lon * u.deg).wrap_at(180 * u.deg).to_string(
                unit=u.hourangle, sep='hms'))
            lat = (coord.Angle(lat * u.deg).to_string(unit=u.degree,
                                                      sep='dms'))

            msg = f'RA: {lon}, DEC: {lat}'
            return msg

        ax.format_coord = sexagesimal_coords

        return scm

    @chk.check(
        dict(show_colorbar=chk.is_boolean,
             scm=chk.is_instance(cm.ScalarMappable),
             ax=chk.is_instance(axes.Axes)))
    def _draw_colorbar(self, show_colorbar, scm, ax):
        """
        Attach colorbar.

        Parameters
        ----------
        show_colorbar : bool
            `show_colorbar` parameter given to :py:meth:`draw`.
        scm : :py:class:`~matplotlib.cm.ScalarMappable`
            Intensity scale.
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to plot on.

        Returns
        -------
        cbar : :py:class:`~matplotlib.colorbar.Colorbar`
        """
        if show_colorbar:
            cbar = plot.colorbar(scm, ax)
        else:
            cbar = None

        return cbar

    @chk.check(
        dict(show_gridlines=chk.is_boolean,
             grid_kwargs=chk.allow_None(chk.is_instance(dict)),
             projection=chk.is_instance(pyproj.Proj),
             ax=chk.is_instance(axes.Axes)))
    def _draw_gridlines(self, show_gridlines, grid_kwargs, projection, ax):
        """
        Plot Right-Ascension / Declination lines.

        Parameters
        ----------
        show_gridlines : bool
            `show_gridlines` parameter given to :py:meth:`draw`.
        grid_kwargs : dict
            `grid_kwargs` parameter given to :py:meth:`draw`.
        projection : :py:class:`pyproj.Proj`
            PyProj projection object.
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to plot on.
        """
        if grid_kwargs is None:
            grid_kwargs = dict()

        if 'N_parallel' in grid_kwargs:
            N_parallel = grid_kwargs.pop('N_parallel')
            if not (chk.is_integer(N_parallel) and (N_parallel >= 3)):
                raise ValueError('Value[N_parallel] must be at least 3.')
        else:
            N_parallel = 3

        if 'N_meridian' in grid_kwargs:
            N_meridian = grid_kwargs.pop('N_meridian')
            if not (chk.is_integer(N_meridian) and (N_meridian >= 3)):
                raise ValueError('Value[N_meridian] must be at least 3.')
        else:
            N_meridian = 3

        if 'polar_plot' in grid_kwargs:
            polar_plot = grid_kwargs.pop('polar_plot')
            if not chk.is_boolean(polar_plot):
                raise ValueError('Value[polar_plot] must be boolean.')
        else:
            polar_plot = False

        if 'ticks' in grid_kwargs:
            show_ticks = grid_kwargs.pop('ticks')
            if not chk.is_boolean(show_ticks):
                raise ValueError('Value[ticks] must be boolean.')
        else:
            # TODO: change to True once implemented.
            show_ticks = False

        plot_style = dict(alpha=0.5, color='k', linewidth=1, linestyle='solid')
        plot_style.update(grid_kwargs)

        _, grid_lat, grid_lon = sph.cart2eq(*self.grid)
        grid_lat = coord.Angle(grid_lat * u.rad).to_value(u.deg)
        grid_lon = coord.Angle(grid_lon * u.rad).wrap_at(180 * u.deg).to_value(
            u.deg)

        # RA curves
        meridian = dict()
        dec_span = np.linspace(grid_lat.min(), grid_lat.max(), 200)
        if polar_plot:
            ra = np.linspace(-180, 180, N_meridian, endpoint=False)
        else:
            ra = np.linspace(grid_lon.min(), grid_lon.max(), N_meridian)
        for _ in ra:
            ra_span = _ * np.ones_like(dec_span)

            # Transform (lon,lat) to (x,y).
            # Some projections have unmappable regions or exhibit singularities at certain points.
            # These regions are colored white in contour plots by replacing their incorrect value (1e30) with NaN.
            grid_x, grid_y = projection(ra_span, dec_span, errcheck=False)
            grid_x[np.isclose(grid_x, 1e30)] = np.nan
            grid_y[np.isclose(grid_y, 1e30)] = np.nan

            if show_gridlines:
                mer = ax.plot(grid_x, grid_y, **plot_style)[0]
                meridian[_] = mer

        # DEC curves
        parallel = dict()
        ra_span = np.linspace(grid_lon.min(), grid_lon.max(), 200)
        if polar_plot:
            dec = np.linspace(grid_lat.min(), grid_lat.max(), N_parallel + 1)
        else:
            dec = np.linspace(grid_lat.min(), grid_lat.max(), N_parallel)
        for _ in dec:
            dec_span = _ * np.ones_like(ra_span)

            # Transform (lon,lat) to (x,y).
            # Some projections have unmappable regions or exhibit singularities at certain points.
            # These regions are colored white in contour plots by replacing their incorrect value (1e30) with NaN.
            grid_x, grid_y = projection(ra_span, dec_span, errcheck=False)
            grid_x[np.isclose(grid_x, 1e30)] = np.nan
            grid_y[np.isclose(grid_y, 1e30)] = np.nan

            if show_gridlines:
                par = ax.plot(grid_x, grid_y, **plot_style)[0]
                parallel[_] = par

        # LAT/LON ticks
        if show_gridlines and show_ticks:
            raise NotImplementedError('Not yet implemented.')

    @chk.check(
        dict(catalog=chk.allow_None(chk.is_instance(sky.SkyEmission)),
             projection=chk.is_instance(pyproj.Proj),
             ax=chk.is_instance(axes.Axes)))
    def _draw_catalog(self, catalog, catalog_kwargs, projection, ax):
        """
        Overlay catalog on top of map.

        Parameters
        ----------
        catalog : :py:class:`~pypeline.phased_array.util.data_gen.sky.SkyEmission`
            `catalog` parameter given to :py:meth:`draw`.
        catalog_kwargs : dict
            `catalog_kwargs` parameter given to :py:meth:`draw`.
        projection : :py:class:`pyproj.Proj`
            PyProj projection object.
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to plot on.
        """
        if catalog is not None:
            _, c_lat, c_lon = sph.cart2eq(*catalog.xyz.T)
            c_lat = coord.Angle(c_lat * u.rad).to_value(u.deg)
            c_lon = coord.Angle(c_lon * u.rad).wrap_at(180 * u.deg).to_value(
                u.deg)

            c_x, c_y = projection(c_lon, c_lat, errcheck=False)
            c_x[np.isclose(c_x, 1e30)] = np.nan
            c_y[np.isclose(c_y, 1e30)] = np.nan

            if catalog_kwargs is None:
                catalog_kwargs = dict()

            plot_style = dict(s=400, facecolors='none', edgecolors='w')
            plot_style.update(catalog_kwargs)

            ax.scatter(c_x, c_y, **plot_style)

    @chk.check(
        dict(projection=chk.is_instance(pyproj.Proj),
             ax=chk.is_instance(axes.Axes)))
    def _draw_beautify(self, projection, ax):
        """
        Format plot.

        Parameters
        ----------
        projection : :py:class:`pyproj.Proj`
            PyProj projection object.
        ax : :py:class:`~matplotlib.axes.Axes`
            Axes to draw on.
        """
        ax.axis('off')
        ax.axis('equal')
Beispiel #7
0
# ##############################################################################
# _linalg.py
# ==========
# Author : Sepand KASHANI [[email protected]]
# ##############################################################################

import numpy as np
import scipy.linalg as linalg

import pypeline.util.argcheck as chk


@chk.check(
    dict(A=chk.accept_any(chk.has_reals, chk.has_complex),
         B=chk.allow_None(chk.accept_any(chk.has_reals, chk.has_complex)),
         tau=chk.is_real,
         N=chk.allow_None(chk.is_integer)))
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
    ----------
Beispiel #8
0
       cb = colorbar(im, ax)

       fig.show()

    .. image:: _img/colorbar_example.png
    """
    fig = ax.get_figure()
    divider = ax_grid.make_axes_locatable(ax)
    ax_colorbar = divider.append_axes('right', size='5%', pad=0.05,
                                      axes_class=axes.Axes)
    colorbar = fig.colorbar(scm, cax=ax_colorbar)
    return colorbar


@chk.check(dict(name=chk.is_instance(str),
                N=chk.allow_None(chk.is_integer)))
def cmap(name, N=None):
    """
    Load one of Pypeline's custom colormaps.

    All maps are defined under ``<pypeline_dir>/data/colormap/``.

    Parameters
    ----------
    name : str
        colormap name.
    N : int, optional
        Number of color levels. (Default: all).

        If `N` is smaller than the number of levels available in the colormap, then the last `N` colors will be used.