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
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')
# ############################################################################## # _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 ----------
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.