diff --git a/README.md b/README.md
index 3c8de144..f04b8846 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ Required:
* scipy
* astropy >= 4.0
* h5py
+* pyuvdata
Optional:
diff --git a/ci/min_deps.yaml b/ci/min_deps.yaml
index 3e2c08df..82a84144 100644
--- a/ci/min_deps.yaml
+++ b/ci/min_deps.yaml
@@ -9,4 +9,5 @@ dependencies:
- scipy
- coverage
- pytest-cov
+ - pyuvdata
- setuptools_scm
diff --git a/environment.yaml b/environment.yaml
index 23a62ea7..13af4a95 100644
--- a/environment.yaml
+++ b/environment.yaml
@@ -11,6 +11,7 @@ dependencies:
- h5py
- coverage
- pytest-cov
+ - pyuvdata
- setuptools_scm
- flake8
- pip
diff --git a/pyradiosky/data/mock_hera_text_2458098.27471.txt b/pyradiosky/data/mock_hera_text_2458098.27471.txt
new file mode 100644
index 00000000..f7382302
--- /dev/null
+++ b/pyradiosky/data/mock_hera_text_2458098.27471.txt
@@ -0,0 +1,44 @@
+SOURCE_ID RA_J2000 [deg] Dec_J2000 [deg] Flux [Jy] Frequency [Hz]
+src0 28.103507 -32.541308 1.00 150000000.00
+src1 25.734946 -32.672235 1.00 150000000.00
+src2 23.361369 -32.760516 1.00 150000000.00
+src3 22.172001 -32.787771 1.00 150000000.00
+src4 20.983198 -32.804964 1.00 150000000.00
+src5 18.604394 -32.806366 1.00 150000000.00
+src6 16.226085 -32.764718 1.00 150000000.00
+src7 13.852232 -32.679226 1.00 150000000.00
+src8 11.483261 -32.551071 1.00 150000000.00
+src9 28.013547 -31.546839 1.00 150000000.00
+src10 25.670235 -31.675179 1.00 150000000.00
+src11 23.321217 -31.761294 1.00 150000000.00
+src12 18.616585 -31.806457 1.00 150000000.00
+src13 17.440155 -31.791342 1.00 150000000.00
+src14 13.915314 -31.682104 1.00 150000000.00
+src15 11.571614 -31.556511 1.00 150000000.00
+src16 27.927165 -30.552168 1.00 150000000.00
+src17 26.768313 -30.620199 1.00 150000000.00
+src18 25.607956 -30.677942 1.00 150000000.00
+src19 23.283697 -30.762426 1.00 150000000.00
+src20 22.120290 -30.789115 1.00 150000000.00
+src21 18.627961 -30.806786 1.00 150000000.00
+src22 17.463988 -30.791865 1.00 150000000.00
+src23 16.300503 -30.766550 1.00 150000000.00
+src24 13.975997 -30.684803 1.00 150000000.00
+src25 12.815469 -30.628424 1.00 150000000.00
+src26 11.656418 -30.561752 1.00 150000000.00
+src27 27.844011 -29.557348 1.00 150000000.00
+src28 25.548066 -29.680626 1.00 150000000.00
+src29 23.247039 -29.763546 1.00 150000000.00
+src30 18.639216 -29.807115 1.00 150000000.00
+src31 16.335582 -29.767633 1.00 150000000.00
+src32 14.034319 -29.687427 1.00 150000000.00
+src33 11.738024 -29.566848 1.00 150000000.00
+src34 27.764133 -28.562410 1.00 150000000.00
+src35 25.490401 -28.683338 1.00 150000000.00
+src36 23.212815 -28.764195 1.00 150000000.00
+src37 22.071845 -28.790420 1.00 150000000.00
+src38 20.931473 -28.805845 1.00 150000000.00
+src39 18.649710 -28.807197 1.00 150000000.00
+src40 17.509296 -28.793122 1.00 150000000.00
+src41 16.368255 -28.768247 1.00 150000000.00
+src42 12.952166 -28.635639 1.00 150000000.00
diff --git a/pyradiosky/data/single_source.vot b/pyradiosky/data/single_source.vot
new file mode 100644
index 00000000..7cb03006
--- /dev/null
+++ b/pyradiosky/data/single_source.vot
@@ -0,0 +1,635 @@
+
+
+
+
+
+
+ GLEAM first year observing parameters
+
+
+
+ Observation date
+
+
+
+
+
+ RA range
+
+
+
+
+ RA range
+
+
+
+
+ Declination
+
+
+
+
+ Number of flagged tiles out of the 128 available
+
+
+
+
+ Calibrator (1)
+
+
+
+
+
+ 2013-08-09 |
+ 19.5 |
+ 3.5 |
+ -55 |
+ 10 |
+ 3C444 |
+
+
+ 2013-08-10 |
+ 19.5 |
+ 3.5 |
+ -27 |
+ 4 |
+ 3C444 |
+
+
+ 2013-08-18 |
+ 19.5 |
+ 3.5 |
+ -72 |
+ 7 |
+ Pictor A |
+
+
+ 2013-08-22 |
+ 19.5 |
+ 3.5 |
+ -13 |
+ 3 |
+ 3C444 |
+
+
+ 2013-08-25 |
+ 19.5 |
+ 3.5 |
+ -40 |
+ 5 |
+ 3C444 |
+
+
+ 2013-11-05 |
+ 0.0 |
+ 8.0 |
+ -13 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-06 |
+ 0.0 |
+ 8.0 |
+ -40 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-07 |
+ 0.0 |
+ 8.0 |
+ 2 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-08 |
+ 0.0 |
+ 8.0 |
+ -55 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-11 |
+ 0.0 |
+ 8.0 |
+ 19 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-12 |
+ 0.0 |
+ 8.0 |
+ -72 |
+ 4 |
+ Hydra A |
+
+
+ 2013-11-25 |
+ 0.0 |
+ 8.0 |
+ -27 |
+ 4 |
+ Hydra A |
+
+
+ 2014-03-03 |
+ 6.0 |
+ 16.0 |
+ -27 |
+ 4 |
+ Hydra A |
+
+
+ 2014-03-04 |
+ 6.0 |
+ 16.0 |
+ -13 |
+ 4 |
+ Hydra A |
+
+
+ 2014-03-06 |
+ 6.0 |
+ 16.0 |
+ 2 |
+ 4 |
+ Hydra A |
+
+
+ 2014-03-08 |
+ 6.0 |
+ 16.0 |
+ 19 |
+ 4 |
+ Hydra A |
+
+
+ 2014-03-09 |
+ 6.0 |
+ 16.0 |
+ -72 |
+ 4 |
+ Virgo A |
+
+
+ 2014-03-16 |
+ 6.0 |
+ 16.0 |
+ -40 |
+ 4 |
+ Virgo A |
+
+
+ 2014-03-17 |
+ 6.0 |
+ 16.0 |
+ -55 |
+ 4 |
+ Hydra A |
+
+
+ 2014-06-09 |
+ 12.0 |
+ 22.0 |
+ -27 |
+ 8 |
+ 3C444 |
+
+
+ 2014-06-10 |
+ 12.0 |
+ 22.0 |
+ -40 |
+ 8 |
+ 3C444 |
+
+
+ 2014-06-11 |
+ 12.0 |
+ 22.0 |
+ 2 |
+ 8 |
+ Hercules A |
+
+
+ 2014-06-12 |
+ 12.0 |
+ 18.5 |
+ -55 |
+ 8 |
+ 3C444 |
+
+
+ 2014-06-13 |
+ 12.0 |
+ 19.0 |
+ -13 |
+ 8 |
+ Centaurus A |
+
+
+ 2014-06-14 |
+ 12.0 |
+ 22.0 |
+ -72 |
+ 9 |
+ Hercules A |
+
+
+ 2014-06-15 |
+ 12.0 |
+ 22.0 |
+ 18 |
+ 13 |
+ Virgo A |
+
+
+ 2014-06-16 |
+ 18.5 |
+ 22.0 |
+ -13 |
+ 8 |
+ 3C444 |
+
+
+ 2014-06-18 |
+ 18.5 |
+ 22.0 |
+ -55 |
+ 8 |
+ 3C444 |
+
+
+
+
+
+
+
+ Right ascension (FK5, Equinox=J2000.0) (computed by VizieR, not
+ part of the original data. The format may include more digits
+ than the original data because of internal accuracy requirements
+ in VizieR and across other CDS services)
+
+
+
+
+ Declination (FK5, Equinox=J2000.0) (computed by VizieR, not part
+ of the original data. The format may include more digits than the
+ original data because of internal accuracy requirements in VizieR
+ and across other CDS services)
+
+
+
+
+ GLEAM name (JHHMMSS+DDMMSS) (Name)
+
+
+
+
+ Right Ascension J2000 (RAJ2000)
+
+
+
+
+ Declination J2000 (DEJ2000)
+
+
+
+
+ Peak flux in wide (170-231MHz) image (peak_flux_wide)
+
+
+
+
+ Integrated flux in wide (170-231MHz) image (int_flux_wide)
+
+
+
+
+ Percent error in absolute flux scale - all frequencies
+ (err_abs_flux_pct)
+
+
+
+
+ Percent error on internal flux scale - all frequencies
+ (err_fit_flux_pct)
+
+
+
+
+ ? Peak flux in 072-080MHz image (peak_flux_076)
+
+
+
+
+
+ ? Integrated flux in 072-080MHz image (int_flux_076)
+
+
+
+
+
+ ? Peak flux in 080-088MHz image (peak_flux_084)
+
+
+
+
+
+ ? Integrated flux in 080-088MHz image (int_flux_084)
+
+
+
+
+
+ ? Peak flux in 088-095MHz image (peak_flux_092)
+
+
+
+
+
+ ? Integrated flux in 088-095MHz image (int_flux_092)
+
+
+
+
+
+ ? Peak flux in 095-103MHz image (peak_flux_099)
+
+
+
+
+
+ ? Integrated flux in 095-103MHz image (int_flux_099)
+
+
+
+
+
+ ? Peak flux in 103-111MHz image (peak_flux_107)
+
+
+
+
+
+ ? Integrated flux in 103-111MHz image (int_flux_107)
+
+
+
+
+
+ ? Peak flux in 111-118MHz image (peak_flux_115)
+
+
+
+
+
+ ? Integrated flux in 111-118MHz image (int_flux_115)
+
+
+
+
+
+ ? Peak flux in 118-126MHz image (peak_flux_122)
+
+
+
+
+
+ ? Integrated flux in 118-126MHz image (int_flux_122)
+
+
+
+
+
+ ? Peak flux in 126-134MHz image (peak_flux_130)
+
+
+
+
+
+ ? Integrated flux in 126-134MHz image (int_flux_130)
+
+
+
+
+
+ ? Peak flux in 139-147MHz image (peak_flux_143)
+
+
+
+
+
+ ? Integrated flux in 139-147MHz image (int_flux_143)
+
+
+
+
+
+ ? Peak flux in 147-154MHz image (peak_flux_151)
+
+
+
+
+
+ ? Integrated flux in 147-154MHz image (int_flux_151)
+
+
+
+
+
+ ? Peak flux in 154-162MHz image (peak_flux_158)
+
+
+
+
+
+ ? Integrated flux in 154-162MHz image (int_flux_158)
+
+
+
+
+
+ ? Peak flux in 162-170MHz image (peak_flux_166)
+
+
+
+
+
+ ? Integrated flux in 162-170MHz image (int_flux_166)
+
+
+
+
+
+ ? Peak flux in 170-177MHz image (peak_flux_174)
+
+
+
+
+
+ ? Integrated flux in 170-177MHz image (int_flux_174)
+
+
+
+
+
+ ? Peak flux in 177-185MHz image (peak_flux_181)
+
+
+
+
+
+ ? Integrated flux in 177-185MHz image (int_flux_181)
+
+
+
+
+
+ ? Peak flux in 185-193MHz image (peak_flux_189)
+
+
+
+
+
+ ? Integrated flux in 185-193MHz image (int_flux_189)
+
+
+
+
+
+ ? Peak flux in 193-200MHz image (peak_flux_197)
+
+
+
+
+
+ ? Integrated flux in 193-200MHz image (int_flux_197)
+
+
+
+
+
+ ? Peak flux in 200-208MHz image (peak_flux_204)
+
+
+
+
+
+ ? Integrated flux in 200-208MHz image (int_flux_204)
+
+
+
+
+
+ ? Peak flux in 208-216MHz image (peak_flux_212)
+
+
+
+
+
+ ? Integrated flux in 208-216MHz image (int_flux_212)
+
+
+
+
+
+ ? Peak flux in 216-223MHz image (peak_flux_220)
+
+
+
+
+
+ ? Integrated flux in 216-223MHz image (int_flux_220)
+
+
+
+
+
+ ? Peak flux in 223-231MHz image (peak_flux_227)
+
+
+
+
+
+ ? Integrated flux in 223-231MHz image (int_flux_227)
+
+
+
+
+
+ ? Fitted spectral index (alpha)
+
+
+
+
+
+ ? Fitted 200MHz integrated flux density (int_flux_fit_200)
+
+
+
+
+
+
+ 0.00000000 |
+ 0.0000000 |
+ TEST |
+ 0.000000 |
+ -30.000000 |
+ 0.000000 |
+ 1.000000 |
+ 16959 |
+ 63 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+ 0.000000 |
+
+
+
+
+
+
diff --git a/pyradiosky/skymodel.py b/pyradiosky/skymodel.py
index ad366932..b1a4edfe 100644
--- a/pyradiosky/skymodel.py
+++ b/pyradiosky/skymodel.py
@@ -8,12 +8,20 @@
import numpy as np
from numpy.lib import recfunctions
from scipy.linalg import orthogonal_procrustes as ortho_procr
-from astropy.coordinates import Angle, EarthLocation, AltAz
+import scipy.io
+from astropy.coordinates import Angle, EarthLocation, AltAz, Latitude, Longitude
from astropy.time import Time
import astropy.units as units
from astropy.units import Quantity
from astropy.io import votable
-import scipy.io
+from pyuvdata.uvbase import UVBase
+from pyuvdata.parameter import UVParameter
+
+try:
+ from pyuvdata.uvbeam.cst_beam import CSTBeam
+except ImportError: # pragma: no cover
+ # backwards compatility for older pyuvdata versions
+ from pyuvdata.cst_beam import CSTBeam
from . import utils as skyutils
from . import spherical_coords_transforms as sct
@@ -27,6 +35,7 @@
"skymodel_to_array",
"array_to_skymodel",
"source_cuts",
+ "read_gleam_catalog",
"read_votable_catalog",
"read_text_catalog",
"read_idl_catalog",
@@ -61,7 +70,7 @@ class LunarTopo:
# Convert stokes and coherency to Astropy quantities.
-class SkyModel(object):
+class SkyModel(UVBase):
"""
Object to hold point source and diffuse models.
@@ -80,30 +89,26 @@ class SkyModel(object):
----------
name : array_like of str
Unique identifier for each source component, shape (Ncomponents,).
- ra : astropy Angle object
+ ra : astropy Longitude object
source RA in J2000 (or ICRS) coordinates, shape (Ncomponents,).
- dec : astropy Angle object
+ dec : astropy Latitude object
source Dec in J2000 (or ICRS) coordinates, shape (Ncomponents,).
stokes : array_like of float
4 element vector giving the source [I, Q, U, V], shape (4, Nfreqs, Ncomponents).
- freq : astropy Quantity
- Reference frequencies of flux values, shape (Ncomponents,).
spectral_type : str
Indicates how fluxes should be calculated at each frequency.
- spectral_index : array_like of float
- Spectral index of each source, shape (Nfreqs, Ncomponents).
- None if spectral_type is not 'spectral_index'.
Options:
- 'flat' : Flat spectrum.
- 'full' : Flux is defined by a saved value at each frequency.
- 'subband' : Flux is given at a set of band centers. (TODO)
- 'spectral_index' : Flux is given at a reference frequency. (TODO)
- rise_lst : array_like of float
- Approximate lst (radians) when the source rises, shape (Ncomponents,). Set by
- coarse horizon cut in simsetup. Default is nan, meaning the source never rises.
- set_lst : array_like of float
- Approximate lst (radians) when the source sets, shape (Ncomponents,).
- Default is None, meaning the source never sets.
+ freq_array : astropy Quantity
+ Array of frequencies that fluxes are provided for, shape (Nfreqs,).
+ reference_frequency : astropy Quantity
+ Reference frequencies of flux values, shape (Ncomponents,).
+ spectral_index : array_like of float
+ Spectral index of each source, shape (Ncomponents).
+ None if spectral_type is not 'spectral_index'.
pos_tol : float
position tolerance in degrees, defaults to minimum float in numpy
position tolerance in degrees
@@ -114,27 +119,8 @@ class SkyModel(object):
Beam amplitude at the source position, shape (4, Nfreqs, Ncomponents).
4 element vector corresponds to [XX, YY, XY, YX] instrumental
polarizations.
- """
- Ncomponents = None # Number of point source components represented here.
-
- _Ncomp_attrs = [
- "ra",
- "dec",
- "coherency_radec",
- "coherency_local",
- "stokes",
- "alt_az",
- "rise_lst",
- "set_lst",
- "freq",
- "pos_lmn",
- "name",
- "horizon_mask",
- ]
- _scalar_attrs = ["Ncomponents", "time", "pos_tol"]
-
- _member_funcs = ["coherency_calc", "update_positions"]
+ """
def __init__(
self,
@@ -142,85 +128,341 @@ def __init__(
ra,
dec,
stokes,
- freq_array,
spectral_type,
+ freq_array=None,
+ reference_frequency=None,
spectral_index=None,
- rise_lst=None,
- set_lst=None,
pos_tol=np.finfo(float).eps,
extended_model_group=None,
beam_amp=None,
):
+ # standard angle tolerance: 1 mas in radians.
+ angle_tol = Angle(1, units.arcsec)
+ self.future_angle_tol = Angle(1e-3, units.arcsec)
- if not isinstance(ra, Angle):
- raise ValueError(
- "ra must be an astropy Angle object. " "value was: {ra}".format(ra=ra)
- )
+ self._Ncomponents = UVParameter(
+ "Ncomponents", description="Number of components", expected_type=int
+ )
- if not isinstance(dec, Angle):
- raise ValueError(
- "dec must be an astropy Angle object. "
- "value was: {dec}".format(dec=dec)
- )
+ desc = (
+ "Number of frequencies if spectral_type is 'full' or 'subband', "
+ "1 otherwise."
+ )
+ self._Nfreqs = UVParameter("Nfreqs", description=desc, expected_type=int)
+
+ desc = "Right ascension of components in ICRS coordinates."
+ self._ra = UVParameter(
+ "ra",
+ description=desc,
+ form=("Ncomponents",),
+ expected_type=Longitude,
+ tols=angle_tol,
+ )
+
+ desc = "Declination of components in ICRS coordinates."
+ self._dec = UVParameter(
+ "dec",
+ description=desc,
+ form=("Ncomponents",),
+ expected_type=Latitude,
+ tols=angle_tol,
+ )
+
+ desc = "Component name."
+ self._name = UVParameter(
+ "name", description=desc, form=("Ncomponents",), expected_type=str
+ )
- self.Ncomponents = ra.size
+ desc = "Frequency array in Hz, only required if spectral_type is 'full' or 'subband'."
+ self._freq_array = UVParameter(
+ "freq_array",
+ description=desc,
+ form=("Nfreqs",),
+ expected_type=Quantity,
+ required=False,
+ )
+
+ desc = "Reference frequency in Hz, only required if spectral_type is 'spectral_index'."
+ self._reference_frequency = UVParameter(
+ "reference_frequency",
+ description=desc,
+ form=("Ncomponents",),
+ expected_type=Quantity,
+ required=False,
+ )
+
+ desc = "Component flux per frequency and Stokes parameter"
+ self._stokes = UVParameter(
+ "stokes",
+ description=desc,
+ form=(4, "Nfreqs", "Ncomponents"),
+ expected_type=float,
+ )
+
+ # The coherency is a 2x2 matrix giving electric field correlation in Jy
+ self._coherency_radec = UVParameter(
+ "coherency_radec",
+ description="Ra/Dec coherency per component",
+ form=(2, 2, "Nfreqs", "Ncomponents"),
+ expected_type=np.complex,
+ )
- self.name = np.atleast_1d(np.asarray(name))
- self.freq_array = np.atleast_1d(freq_array)
- self.Nfreqs = self.freq_array.size
- self.stokes = np.asarray(stokes)
+ desc = (
+ "Type of spectral flux specification, options are: "
+ "'full','flat', 'subband', 'spectral_index'."
+ )
+ self._spectral_type = UVParameter(
+ "spectral_type",
+ description=desc,
+ expected_type=str,
+ acceptable_vals=["full", "flat", "subband", "spectral_index"],
+ )
+
+ self._spectral_index = UVParameter(
+ "spectral_index",
+ description="Spectral indexm only required if spectral_type is 'spectral_index'.",
+ form=("Ncomponents",),
+ expected_type=float,
+ required=False,
+ )
+
+ self._beam_amp = UVParameter(
+ "beam_amp",
+ description=(
+ "Beam amplitude at the source position as a function "
+ "of instrument polarization and frequency."
+ ),
+ form=(4, "Nfreqs", "Ncomponents"),
+ expected_type=float,
+ required=False,
+ )
+
+ self._extended_model_group = UVParameter(
+ "extended_model_group",
+ description=(
+ "Identifier that groups components of an extended "
+ "source model. Set to -1 for point sources."
+ ),
+ form=("Ncomponents",),
+ expected_type=int,
+ required=False,
+ )
+
+ desc = "Time for local position calculations."
+ self._time = UVParameter(
+ "time", description=desc, expected_type=Time, required=False
+ )
+
+ desc = "Telescope Location for local position calculations."
+ self._telescope_location = UVParameter(
+ "telescope_location",
+ description=desc,
+ expected_type=EarthLocation,
+ required=False,
+ )
+ if hasmoon:
+ self._telescope_location.expected_type = (EarthLocation, MoonLocation)
+
+ desc = "Altitude and Azimuth of components in local coordinates."
+ self._alt_az = UVParameter(
+ "alt_az",
+ description=desc,
+ form=(2, "Ncomponents"),
+ expected_type=float,
+ tols=np.finfo(float).eps,
+ required=False,
+ )
+
+ desc = "Position cosines of components in local coordinates."
+ self._pos_lmn = UVParameter(
+ "pos_lmn",
+ description=desc,
+ form=(3, "Ncomponents"),
+ expected_type=float,
+ tols=np.finfo(float).eps,
+ required=False,
+ )
+
+ desc = (
+ "Boolean indicator of whether this source is below the horizon."
+ "True indicates the source is below the horizon."
+ )
+ self._horizon_mask = UVParameter(
+ "horizon_mask",
+ description=desc,
+ form=("Ncomponents",),
+ expected_type=bool,
+ required=False,
+ )
+
+ # initialize the underlying UVBase properties
+ super(SkyModel, self).__init__()
+
+ self.name = np.atleast_1d(name)
+ self.Ncomponents = self.name.size
self.ra = np.atleast_1d(ra)
self.dec = np.atleast_1d(dec)
- self.pos_tol = pos_tol
- self.spectral_type = spectral_type
- self.beam_amp = beam_amp
- self.extended_model_group = extended_model_group
- if self.spectral_type == "spectral_index":
- self.spectral_index = spectral_index
-
- self.has_rise_set_lsts = False
- if (rise_lst is not None) and (set_lst is not None):
- self.rise_lst = np.asarray(rise_lst)
- self.set_lst = np.asarray(set_lst)
- self.has_rise_set_lsts = True
+ # handle old parameter order
+ # (use to be: name, ra, dec, stokes, freq_array spectral_type)
+ if isinstance(spectral_type, (np.ndarray, list, float, Quantity)):
+ warnings.warn(
+ "The input parameters to SkyModel.__init__ have changed. Please "
+ "update the call.",
+ category=DeprecationWarning,
+ )
+ freqs_use = spectral_type
+ spectral_type = freq_array
- self.alt_az = np.zeros((2, self.Ncomponents), dtype=float)
- self.pos_lmn = np.zeros((3, self.Ncomponents), dtype=float)
+ if spectral_type == "flat" and np.unique(freqs_use).size == 1:
+ reference_frequency = np.zeros((self.Ncomponents), dtype=np.float)
+ reference_frequency.fill(freqs_use[0])
+ freq_array = None
+ else:
+ freq_array = freqs_use
+ reference_frequency = None
+
+ self.set_spectral_type_params(spectral_type)
+
+ if freq_array is not None:
+ if not isinstance(freq_array, (Quantity,)):
+ warnings.warn(
+ "In the future, the freq_array will be required to be an "
+ "astropy Quantity with units that are convertable to Hz. "
+ "Currently, floats are assumed to be in Hz.",
+ category=DeprecationWarning,
+ )
+ freq_array = freq_array * units.Hz
+ self.freq_array = np.atleast_1d(freq_array)
+ self.Nfreqs = self.freq_array.size
+ else:
+ self.Nfreqs = 1
+
+ if reference_frequency is not None:
+ if not isinstance(reference_frequency, (Quantity,)):
+ warnings.warn(
+ "In the future, the reference_frequency will be required to be an "
+ "astropy Quantity with units that are convertable to Hz. "
+ "Currently, floats are assumed to be in Hz.",
+ category=DeprecationWarning,
+ )
+ reference_frequency = reference_frequency * units.Hz
+ self.reference_frequency = np.atleast_1d(reference_frequency)
- self.horizon_mask = np.zeros(self.Ncomponents).astype(
- bool
- ) # If true, source component is below horizon.
+ if spectral_index is not None:
+ self.spectral_index = np.atleast_1d(spectral_index)
+ self.stokes = np.asarray(stokes, dtype=np.float)
if self.Ncomponents == 1:
self.stokes = self.stokes.reshape(4, self.Nfreqs, 1)
- # The coherency is a 2x2 matrix giving electric field correlation in Jy
- # Multiply by .5 to ensure that Trace sums to I not 2*I
- # Shape = (2,2,Ncomponents)
-
self.coherency_radec = skyutils.stokes_to_coherency(self.stokes)
- self.time = None
+ self.check()
- assert np.all(
- [
- self.Ncomponents == l
- for l in [self.ra.size, self.dec.size, self.stokes.shape[2]]
- ]
- ), "Inconsistent quantity dimensions."
+ def set_spectral_type_params(self, spectral_type):
+ """Set parameters depending on spectral_type."""
+ self.spectral_type = spectral_type
- def _calc_average_rotation_matrix(self, telescope_location):
+ if spectral_type == "spectral_index":
+ self._spectral_index.required = True
+ self._reference_frequency.required = True
+ self._Nfreqs.acceptable_vals = [1]
+ self._freq_array.required = False
+ elif spectral_type in ["full", "subband"]:
+ self._freq_array.required = True
+ self._spectral_index.required = False
+ self._reference_frequency.required = False
+ self._Nfreqs.acceptable_vals = None
+ elif spectral_type == "flat":
+ self._freq_array.required = False
+ self._spectral_index.required = False
+ self._reference_frequency.required = False
+ self._Nfreqs.acceptable_vals = [1]
+
+ def check(self, check_extra=True, run_check_acceptability=True):
"""
- Calculate the "average" rotation matrix from RA/Dec to AltAz.
+ Check that all required parameters are set reasonably.
- This gets us close to the right value, then need to calculate a correction
- for each source separately.
+ Check that required parameters exist and have appropriate shapes.
+ Optionally check if the values are acceptable.
Parameters
----------
- telescope_location : astropy EarthLocation or lunarsky MoonLocation object
- Location of the telescope.
+ check_extra : bool
+ Option to check optional parameters as well as required ones.
+ run_check_acceptability : bool
+ Option to check if values in required parameters are acceptable.
+
+ """
+ # first make sure the required parameters and forms are set properly
+ # for the spectral_type
+ self.set_spectral_type_params(self.spectral_type)
+
+ # make sure only one of freq_array and reference_frequency is defined
+ if self.freq_array is not None and self.reference_frequency is not None:
+ raise ValueError(
+ "Only one of freq_array and reference_frequency can be specified, not both."
+ )
+
+ # Run the basic check from UVBase
+ super(SkyModel, self).check(
+ check_extra=check_extra, run_check_acceptability=run_check_acceptability
+ )
+
+ # make sure freq_array or reference_frequency if present is compatible with Hz
+ if self.freq_array is not None:
+ try:
+ self.freq_array.to("Hz")
+ except (units.UnitConversionError) as e:
+ raise ValueError(
+ "freq_array must have a unit that can be converted to Hz."
+ ) from e
+
+ if self.reference_frequency is not None:
+ try:
+ self.reference_frequency.to("Hz")
+ except (units.UnitConversionError) as e:
+ raise ValueError(
+ "reference_frequency must have a unit that can be converted to Hz."
+ ) from e
+
+ def __eq__(self, other, check_extra=True):
+ """Check for equality, check for future equality."""
+ # Run the basic __eq__ from UVBase
+ equal = super(SkyModel, self).__eq__(other, check_extra=check_extra)
+
+ # Issue deprecation warning if ra/decs aren't close to future_angle_tol levels
+ if not np.allclose(self.ra, other.ra, rtol=0, atol=self.future_angle_tol):
+ warnings.warn(
+ "The _ra parameters are not within the future tolerance. "
+ f"Left is {self.ra}, right is {other.ra}",
+ category=DeprecationWarning,
+ )
+
+ if not np.allclose(self.dec, other.dec, rtol=0, atol=self.future_angle_tol):
+ warnings.warn(
+ "The _dec parameters are not within the future tolerance. "
+ f"Left is {self.dec}, right is {other.dec}",
+ category=DeprecationWarning,
+ )
+
+ if not equal:
+ warnings.warn(
+ "Future equality does not pass, probably because the "
+ "frequencies were not checked in the deprecated equality checking.",
+ category=DeprecationWarning,
+ )
+ equal = super(SkyModel, self).__eq__(other, check_extra=False)
+
+ return equal
+
+ def _calc_average_rotation_matrix(self):
+ """
+ Calculate the "average" rotation matrix from RA/Dec to AltAz.
+
+ This gets us close to the right value, then need to calculate a correction
+ for each source separately.
Returns
-------
@@ -238,7 +480,7 @@ def _calc_average_rotation_matrix(self, telescope_location):
y=y_c,
z=z_c,
obstime=self.time,
- location=telescope_location,
+ location=self.telescope_location,
frame="icrs",
representation_type="cartesian",
)
@@ -257,15 +499,10 @@ def _calc_average_rotation_matrix(self, telescope_location):
return R_really_orthogonal
- def _calc_rotation_matrix(self, telescope_location):
+ def _calc_rotation_matrix(self):
"""
Calculate the true rotation matrix from RA/Dec to AltAz for each component.
- Parameters
- ----------
- telescope_location : astropy EarthLocation or lunarsky MoonLocation object
- Location of the telescope.
-
Returns
-------
array of floats
@@ -284,7 +521,7 @@ def _calc_rotation_matrix(self, telescope_location):
altaz_vec = sct.r_hat(theta_altaz, phi_altaz)
assert altaz_vec.shape == (3, self.Ncomponents)
- R_avg = self._calc_average_rotation_matrix(telescope_location)
+ R_avg = self._calc_average_rotation_matrix()
R_exact = np.zeros((3, 3, self.Ncomponents), dtype=np.float)
@@ -297,22 +534,17 @@ def _calc_rotation_matrix(self, telescope_location):
return R_exact
- def _calc_coherency_rotation(self, telescope_location):
+ def _calc_coherency_rotation(self):
"""
Calculate the rotation matrix to apply to the RA/Dec coherency to get it into alt/az.
- Parameters
- ----------
- telescope_location : astropy EarthLocation or lunarsky MoonLocation object
- Location of the telescope.
-
Returns
-------
array of floats
Rotation matrix that takes the coherency from (RA,Dec) --> (Alt,Az),
shape (2, 2, Ncomponents).
"""
- basis_rotation_matrix = self._calc_rotation_matrix(telescope_location)
+ basis_rotation_matrix = self._calc_rotation_matrix()
# Find mathematical points and vectors for RA/Dec
theta_radec = np.pi / 2.0 - self.dec.rad
@@ -336,7 +568,7 @@ def _calc_coherency_rotation(self, telescope_location):
return coherency_rot_matrix
- def coherency_calc(self, telescope_location):
+ def coherency_calc(self, deprecated_location=None):
"""
Calculate the local coherency in alt/az basis for this source at a time & location.
@@ -346,22 +578,31 @@ def coherency_calc(self, telescope_location):
Parameters
----------
- telescope_location : astropy EarthLocation or lunarsky MoonLocation object
- location of the telescope.
+ deprecated_location : EarthLocation object
+ This keyword is deprecated. It is preserved to maintain backwards
+ compatibility and sets the EarthLocation on this SkyModel object.
Returns
-------
array of float
- local coherency in alt/az basis, shape (2, 2, Ncomponents)
+ local coherency in alt/az basis, shape (2, 2, Nfreqs, Ncomponents)
"""
- if not isinstance(telescope_location, (EarthLocation, MoonLocation)):
+ if deprecated_location is not None:
+ warnings.warn(
+ "Passing telescope_location to SkyModel.coherency_calc is "
+ "deprecated. Set the telescope_location via SkyModel.update_positions.",
+ category=DeprecationWarning,
+ )
+ self.update_positions(self.time, deprecated_location)
+
+ if not isinstance(self.telescope_location, (EarthLocation, MoonLocation)):
errm = "telescope_location must be an astropy EarthLocation object"
if hasmoon:
errm += " or a lunarsky MoonLocation object "
errm += ". "
raise ValueError(
- errm + "value was: {al}".format(al=str(telescope_location))
+ errm + "value was: {al}".format(al=str(self.telescope_location))
)
Ionly_mask = np.sum(self.stokes[1:, :, :], axis=0) == 0.0
@@ -372,7 +613,7 @@ def coherency_calc(self, telescope_location):
if NstokesI < self.Ncomponents:
# If there are any polarized sources, do rotation.
- rotation_matrix = self._calc_coherency_rotation(telescope_location)
+ rotation_matrix = self._calc_coherency_rotation()
polarized_sources = np.where(~Ionly_mask)[0]
@@ -412,7 +653,7 @@ def update_positions(self, time, telescope_location):
"""
if not isinstance(time, Time):
raise ValueError(
- "time must be an astropy Time object. " "value was: {t}".format(t=time)
+ "time must be an astropy Time object. value was: {t}".format(t=time)
)
if not isinstance(telescope_location, (EarthLocation, MoonLocation)):
@@ -425,22 +666,23 @@ def update_positions(self, time, telescope_location):
errm + "value was: {al}".format(al=str(telescope_location))
)
- if self.time == time: # Don't repeat calculations
+ # Don't repeat calculations
+ if self.time == time and self.telescope_location == telescope_location:
return
+ self.time = time
+ self.telescope_location = telescope_location
+
skycoord_use = SkyCoord(self.ra, self.dec, frame="icrs")
- if isinstance(telescope_location, MoonLocation):
+ if isinstance(self.telescope_location, MoonLocation):
source_altaz = skycoord_use.transform_to(
- LunarTopo(obstime=time, location=telescope_location)
+ LunarTopo(obstime=self.time, location=self.telescope_location)
)
else:
source_altaz = skycoord_use.transform_to(
- AltAz(obstime=time, location=telescope_location)
+ AltAz(obstime=self.time, location=self.telescope_location)
)
- time.location = telescope_location
-
- self.time = time
alt_az = np.array([source_altaz.alt.rad, source_altaz.az.rad])
self.alt_az = alt_az
@@ -449,6 +691,8 @@ def update_positions(self, time, telescope_location):
pos_m = np.cos(alt_az[1, :]) * np.cos(alt_az[0, :])
pos_n = np.sin(alt_az[0, :])
+ if self.pos_lmn is None:
+ self.pos_lmn = np.zeros((3, self.Ncomponents), dtype=float)
self.pos_lmn[0, :] = pos_l
self.pos_lmn[1, :] = pos_m
self.pos_lmn[2, :] = pos_n
@@ -456,18 +700,6 @@ def update_positions(self, time, telescope_location):
# Horizon mask:
self.horizon_mask = self.alt_az[0, :] < 0.0
- def __eq__(self, other):
- """Test equality with another sky model."""
- time_check = self.time is None and other.time is None
- if not time_check:
- time_check = np.isclose(self.time, other.time)
- return (
- np.allclose(self.ra.deg, other.ra.deg, atol=self.pos_tol)
- and np.allclose(self.stokes, other.stokes)
- and np.all(self.name == other.name)
- and time_check
- )
-
def read_healpix_hdf5(hdf5_filename):
"""
@@ -616,13 +848,13 @@ def healpix_to_sky(hpmap, indices, freqs):
stokes[0] = (hpmap.T / skyutils.jy_to_ksr(freq)).T
stokes[0] = stokes[0] * astropy_healpix.nside_to_pixel_area(nside)
- sky = SkyModel(indices.astype("str"), ra, dec, stokes, freq, "full")
+ sky = SkyModel(indices.astype("str"), ra, dec, stokes, "full", freq_array=freq)
return sky
def skymodel_to_array(sky):
"""
- Make a recarrayof source components from a SkyModel object.
+ Make a recarray of source components from a SkyModel object.
Parameters
----------
@@ -635,20 +867,74 @@ def skymodel_to_array(sky):
recarray to turn into a SkyModel object.
"""
- fieldtypes = ["U10", "f8", "f8", "f8", "f8"]
- fieldnames = ["source_id", "ra_j2000", "dec_j2000", "flux_density_I", "frequency"]
- fieldshapes = [()] * 3 + [(sky.Nfreqs,)] * 2
+ sky.check()
+ max_name_len = np.max([len(name) for name in sky.name])
+ fieldtypes = ["U" + str(max_name_len), "f8", "f8", "f8"]
+ fieldnames = ["source_id", "ra_j2000", "dec_j2000", "flux_density_I"]
+ fieldshapes = [()] * 3
+ if sky.freq_array is not None:
+ if sky.spectral_type == "subband":
+ fieldnames.append("subband_frequency")
+ else:
+ fieldnames.append("frequency")
+ fieldtypes.append("f8")
+ fieldshapes.extend([(sky.Nfreqs,)] * 2)
+ elif sky.reference_frequency is not None:
+ # add frequency field (a copy of reference_frequency) for backwards
+ # compatibility.
+ warnings.warn(
+ "The frequency field is included in the recarray as a copy of the "
+ "reference_frequency field for backwards compatibility. In future "
+ "only the reference_frequency will be included.",
+ category=DeprecationWarning,
+ )
+ fieldnames.extend(["frequency", "reference_frequency"])
+ fieldtypes.extend(["f8"] * 2)
+ fieldshapes.extend([()] * 3)
+ if sky.spectral_index is not None:
+ fieldnames.append("spectral_index")
+ fieldtypes.append("f8")
+ fieldshapes.append(())
+ else:
+ # flat spectrum, no freq info
+ fieldshapes.append(())
+
+ if hasattr(sky, "_rise_lst"):
+ fieldnames.append("rise_lst")
+ fieldtypes.append("f8")
+ fieldshapes.append(())
+
+ if hasattr(sky, "_set_lst"):
+ fieldnames.append("set_lst")
+ fieldtypes.append("f8")
+ fieldshapes.append(())
dt = np.dtype(list(zip(fieldnames, fieldtypes, fieldshapes)))
- if sky.Nfreqs == 1:
- sky.freq_array = sky.freq_array[:, None]
arr = np.empty(sky.Ncomponents, dtype=dt)
arr["source_id"] = sky.name
arr["ra_j2000"] = sky.ra.deg
arr["dec_j2000"] = sky.dec.deg
- arr["flux_density_I"] = sky.stokes[0, :, :].T # Swaps component and frequency axes
- arr["frequency"] = sky.freq_array
+ if sky.freq_array is not None:
+ if sky.spectral_type == "subband":
+ arr["subband_frequency"] = sky.freq_array
+ else:
+ arr["frequency"] = sky.freq_array
+ arr["flux_density_I"] = sky.stokes[0, :, :].T
+ elif sky.reference_frequency is not None:
+ arr["reference_frequency"] = sky.reference_frequency
+ arr["frequency"] = sky.reference_frequency
+ arr["flux_density_I"] = np.squeeze(sky.stokes[0, :, :])
+ if sky.spectral_index is not None:
+ arr["spectral_index"] = sky.spectral_index
+ else:
+ # flat spectral type, no freq info
+ arr["flux_density_I"] = np.squeeze(sky.stokes[0, :, :])
+
+ if hasattr(sky, "_rise_lst"):
+ arr["rise_lst"] = sky._rise_lst
+ if hasattr(sky, "_set_lst"):
+ arr["set_lst"] = sky._set_lst
return arr
@@ -667,37 +953,81 @@ def array_to_skymodel(catalog_table):
:class:`pyradiosky.SkyModel`
"""
- ra = Angle(catalog_table["ra_j2000"], units.deg)
- dec = Angle(catalog_table["dec_j2000"], units.deg)
- ids = catalog_table["source_id"]
+ ra = Longitude(catalog_table["ra_j2000"], units.deg)
+ dec = Latitude(catalog_table["dec_j2000"], units.deg)
+ ids = np.asarray(catalog_table["source_id"]).astype(str)
flux_I = np.atleast_1d(catalog_table["flux_density_I"])
- if flux_I.ndim == 1:
- flux_I = flux_I[:, None]
- stokes = np.pad(np.expand_dims(flux_I, 2), ((0, 0), (0, 0), (0, 3)), "constant").T
rise_lst = None
set_lst = None
- source_freqs = np.atleast_1d(catalog_table["frequency"][0])
+
+ fieldnames = catalog_table.dtype.names
+ if "reference_frequency" in fieldnames:
+ reference_frequency = Quantity(
+ np.atleast_1d(catalog_table["reference_frequency"]), "hertz"
+ )
+ if "spectral_index" in fieldnames:
+ spectral_index = np.atleast_1d(catalog_table["spectral_index"])
+ spectral_type = "spectral_index"
+ else:
+ spectral_type = "flat"
+ spectral_index = None
+ freq_array = None
+ elif "frequency" in fieldnames or "subband_frequency" in fieldnames:
+ if "frequency" in fieldnames:
+ freq_array = Quantity(np.atleast_1d(catalog_table["frequency"]), "hertz")
+ else:
+ spectral_type = "subband"
+ freq_array = Quantity(
+ np.atleast_1d(catalog_table["subband_frequency"]), "hertz"
+ )
+ # freq_array gets copied for every component, so its zeroth axis is
+ # length Ncomponents. Just take the first one.
+ freq_array = freq_array[0, :]
+ if freq_array.size > 1:
+ if "subband_frequency" not in fieldnames:
+ spectral_type = "full"
+ else:
+ spectral_type = "flat"
+ reference_frequency = None
+ spectral_index = None
+ else:
+ # flat spectrum, no freq info
+ spectral_type = "flat"
+ freq_array = None
+ reference_frequency = None
+ spectral_index = None
if "rise_lst" in catalog_table.dtype.names:
rise_lst = catalog_table["rise_lst"]
set_lst = catalog_table["set_lst"]
- spectral_type = "flat"
- if source_freqs.size > 1:
- spectral_type = "full"
- sourcelist = SkyModel(
+ if freq_array is not None:
+ flux_I = flux_I.T
+
+ if flux_I.ndim == 1:
+ flux_I = flux_I[np.newaxis, :]
+
+ # pad non-I flux values with zeros
+ stokes = np.pad(flux_I[np.newaxis, :, :], ((0, 3), (0, 0), (0, 0)))
+
+ skymodel = SkyModel(
ids,
ra,
dec,
stokes,
- source_freqs,
spectral_type,
- rise_lst=rise_lst,
- set_lst=set_lst,
+ freq_array=freq_array,
+ reference_frequency=reference_frequency,
+ spectral_index=spectral_index,
)
- return sourcelist
+ if rise_lst is not None:
+ skymodel._rise_lst = rise_lst
+ if set_lst is not None:
+ skymodel._set_lst = set_lst
+
+ return skymodel
def source_cuts(
@@ -706,6 +1036,7 @@ def source_cuts(
horizon_buffer=0.04364,
min_flux=None,
max_flux=None,
+ freq_range=None,
):
"""
Perform flux and horizon selections on recarray of source components.
@@ -714,7 +1045,9 @@ def source_cuts(
----------
catalog_table : recarray
recarray of source catalog information. Must have the columns:
- 'source_id', 'ra_j2000', 'dec_j2000', 'flux_density_I', 'frequency'
+ 'source_id', 'ra_j2000', 'dec_j2000', 'flux_density_I'
+ may also have the colums:
+ 'frequency' or 'reference_frequency'
latitude_deg : float
Latitude of telescope in degrees. Used to estimate rise/set lst.
horizon_buffer : float
@@ -730,6 +1063,9 @@ def source_cuts(
Minimum stokes I flux to select [Jy]
max_flux : float
Maximum stokes I flux to select [Jy]
+ freq_range : astropy Quantity
+ Frequency range over which the min and max flux tests should be performed.
+ Must be length 2. If None, use the range over which the object is defined.
Returns
-------
@@ -742,14 +1078,74 @@ def source_cuts(
lat_rad = np.radians(latitude_deg)
buff = horizon_buffer
- if min_flux:
- catalog_table = catalog_table[catalog_table["flux_density_I"] > min_flux]
-
- if max_flux:
- catalog_table = catalog_table[catalog_table["flux_density_I"] < max_flux]
-
- ra = Angle(catalog_table["ra_j2000"], units.deg)
- dec = Angle(catalog_table["dec_j2000"], units.deg)
+ if freq_range is not None:
+ if not isinstance(freq_range, (Quantity,)):
+ raise ValueError("freq_range must be an astropy Quantity.")
+ if not np.atleast_1d(freq_range).size == 2:
+ raise ValueError("freq_range must have 2 elements.")
+
+ fieldnames = catalog_table.dtype.names
+ if min_flux or max_flux:
+ if "reference_frequency" in fieldnames:
+ if "spectral_index" in fieldnames:
+ raise NotImplementedError(
+ "Flux cuts with spectral index type objects is not supported yet."
+ )
+ else:
+ # flat spectral type
+ if min_flux:
+ catalog_table = catalog_table[
+ catalog_table["flux_density_I"] > min_flux
+ ]
+ if max_flux:
+ catalog_table = catalog_table[
+ catalog_table["flux_density_I"] < max_flux
+ ]
+ elif "frequency" in fieldnames or "subband_frequency" in fieldnames:
+ if "frequency" in fieldnames:
+ freq_array = Quantity(
+ np.atleast_1d(catalog_table["frequency"]), "hertz"
+ )
+ else:
+ freq_array = Quantity(
+ np.atleast_1d(catalog_table["subband_frequency"]), "hertz"
+ )
+ # freq_array gets copied for every component, so its zeroth axis is
+ # length Ncomponents. Just take the first one.
+ freq_array = freq_array[0, :]
+ if freq_range is not None:
+ freqs_inds_use = np.where(
+ (freq_array >= np.min(freq_range))
+ & (freq_array <= np.max(freq_range))
+ )[0]
+ if freqs_inds_use.size == 0:
+ raise ValueError("No frequencies in freq_range.")
+ else:
+ freqs_inds_use = np.arange(freq_array.size)
+ flux_data = np.atleast_1d(catalog_table["flux_density_I"])
+ if flux_data.ndim > 1:
+ flux_data = flux_data[:, freqs_inds_use]
+ if min_flux:
+ row_inds = np.where(np.min(flux_data, axis=1) > min_flux)
+ catalog_table = catalog_table[row_inds]
+ flux_data = flux_data[row_inds]
+ if max_flux:
+ catalog_table = catalog_table[
+ np.where(np.max(flux_data, axis=1) < max_flux)
+ ]
+ else:
+ # flat spectral type
+ if min_flux:
+ catalog_table = catalog_table[
+ catalog_table["flux_density_I"] > min_flux
+ ]
+ if max_flux:
+ catalog_table = catalog_table[
+ catalog_table["flux_density_I"] < max_flux
+ ]
+
+ ra = Longitude(catalog_table["ra_j2000"], units.deg)
+ dec = Latitude(catalog_table["dec_j2000"], units.deg)
if coarse_horizon_cut:
tans = np.tan(lat_rad) * np.tan(dec.rad)
@@ -777,22 +1173,87 @@ def source_cuts(
catalog_table, ["rise_lst", "set_lst"], [rise_lst, set_lst], usemask=False
)
+ if len(catalog_table) == 0:
+ warnings.warn("All sources eliminated by cuts.")
+
return catalog_table
-def read_votable_catalog(gleam_votable, source_select_kwds={}, return_table=False):
+def _get_matching_fields(
+ name_to_match, name_list, exclude_start_pattern=None, brittle=True
+):
+ match_list = [name for name in name_list if name_to_match.lower() in name.lower()]
+ if len(match_list) > 1:
+ # try requiring exact match
+ match_list_temp = [
+ name for name in match_list if name_to_match.lower() == name.lower()
+ ]
+ if len(match_list_temp) == 1:
+ match_list = match_list_temp
+ elif exclude_start_pattern is not None:
+ # try excluding columns which start with exclude_start_pattern
+ match_list_temp = [
+ name
+ for name in match_list
+ if not name.startswith(exclude_start_pattern)
+ ]
+ if len(match_list_temp) == 1:
+ match_list = match_list_temp
+ if len(match_list) > 1:
+ if brittle:
+ raise ValueError(
+ f"More than one match for {name_to_match} in {name_list}."
+ )
+ else:
+ return match_list
+ elif len(match_list) == 0:
+ if brittle:
+ raise ValueError(f"No match for {name_to_match} in {name_list}.")
+ else:
+ return None
+ return match_list[0]
+
+
+def read_votable_catalog(
+ votable_file,
+ table_name="GLEAM",
+ id_column="GLEAM",
+ ra_column="RAJ2000",
+ dec_column="DEJ2000",
+ flux_columns="Fintwide",
+ reference_frequency=200e6 * units.Hz,
+ freq_array=None,
+ spectral_index_column=None,
+ source_select_kwds=None,
+ return_table=False,
+):
"""
- Create a list of pyradiosky source objects from a votable catalog.
+ Create a SkyModel object from a votable catalog.
Tested on: GLEAM EGC catalog, version 2
Parameters
----------
- gleam_votable: str
+ votable_file : str
Path to votable catalog file.
- return_table: bool, optional
+ table_name : str
+ Part of expected table name. Should match only one table name in votable_file.
+ id_column : str
+ Part of expected ID column. Should match only one column in the table.
+ ra_column : str
+ Part of expected RA column. Should match only one column in the table.
+ dec_column : str
+ Part of expected Dec column. Should match only one column in the table.
+ flux_columns : str or list of str
+ Part of expected Flux column(s). Each one should match only one column in the table.
+ reference_frequency : astropy Quantity
+ Reference frequency for flux values, assumed to be the same value for all rows.
+ freq_array : astropy Quantity
+ Frequencies corresponding to flux_columns (should be same length).
+ Required for multiple flux columns.
+ return_table : bool, optional
Whether to return the astropy table instead of a list of Source objects.
- source_select_kwds: dict, optional
+ source_select_kwds : dict, optional
Dictionary of keywords for source selection Valid options:
* `lst_array`: For coarse RA horizon cuts, lsts used in the simulation [radians]
@@ -809,55 +1270,212 @@ def read_votable_catalog(gleam_votable, source_select_kwds={}, return_table=Fals
recarray or :class:`pyradiosky.SkyModel`
if return_table, recarray of source parameters, otherwise :class:`pyradiosky.SkyModel` instance
"""
- resources = votable.parse(gleam_votable).resources
+ parsed_vo = votable.parse(votable_file)
- class Found(Exception):
- pass
+ tables = list(parsed_vo.iter_tables())
+ table_ids = [table._ID for table in tables]
- try:
- for rs in resources:
- for tab in rs.tables:
- if "GLEAM" in tab.array.dtype.names:
- raise Found
- except Found:
- table = tab.to_table() # Convert to astropy Table
-
- fieldnames = ["GLEAM", "RAJ2000", "DEJ2000", "Fintwide"]
- newnames = ["source_id", "ra_j2000", "dec_j2000", "flux_density_I", "frequency"]
- data = table[fieldnames]
- Nsrcs = len(data)
- freq = 200e6
- for t in data.colnames:
- i = fieldnames.index(t)
- data[t] = data[t]
- data[t].name = newnames[i]
- data.add_column(table.Column(np.ones(Nsrcs) * freq, name="frequency"))
- data = data.as_array().data
-
- if len(source_select_kwds) > 0:
- data = source_cuts(data, **source_select_kwds)
+ if None not in table_ids:
+ table_name_use = _get_matching_fields(table_name, table_ids)
+ table_match = [table for table in tables if table._ID == table_name_use][0]
+ else:
+ warnings.warn(
+ f"File {votable_file} contains tables with no name or ID, Support for "
+ "such files is deprecated.",
+ category=DeprecationWarning,
+ )
+ # Find correct table using the field names
+ tables_match = []
+ for table in tables:
+ id_col_use = _get_matching_fields(
+ id_column, table.to_table().colnames, brittle=False
+ )
+ if id_col_use is not None:
+ tables_match.append(table)
+ if len(tables_match) > 1:
+ raise ValueError("More than one matching table.")
+ else:
+ table_match = tables_match[0]
+
+ # Convert to astropy Table
+ astropy_table = table_match.to_table()
+
+ # get ID column
+ id_col_use = _get_matching_fields(id_column, astropy_table.colnames)
+
+ # get RA & Dec columns, if multiple matches, exclude VizieR calculated columns
+ # which start with an underscore
+ ra_col_use = _get_matching_fields(
+ ra_column, astropy_table.colnames, exclude_start_pattern="_"
+ )
+ dec_col_use = _get_matching_fields(
+ dec_column, astropy_table.colnames, exclude_start_pattern="_"
+ )
+
+ if isinstance(flux_columns, (str)):
+ flux_columns = [flux_columns]
+ flux_cols_use = []
+ for col in flux_columns:
+ flux_cols_use.append(_get_matching_fields(col, astropy_table.colnames))
+
+ if len(flux_columns) > 1 and freq_array is None:
+ raise ValueError("freq_array must be provided for multiple flux columns.")
+
+ if reference_frequency is not None or len(flux_cols_use) == 1:
+ if reference_frequency is not None:
+ if not isinstance(reference_frequency, (Quantity,)):
+ raise ValueError("reference_frequency must be an astropy Quantity.")
+ reference_frequency = (
+ np.array([reference_frequency.value] * len(astropy_table))
+ * reference_frequency.unit
+ )
+ if spectral_index_column is not None:
+ spectral_type = "spectral_index"
+ spec_index_col_use = _get_matching_fields(
+ spectral_index_column, astropy_table.colnames
+ )
+ spectral_index = astropy_table[spec_index_col_use].data.data
+ else:
+ spectral_type = "flat"
+ spectral_index = None
+ else:
+ spectral_type = "subband"
+ spectral_index = None
+
+ stokes = np.zeros((4, len(flux_cols_use), len(astropy_table)), dtype=np.float)
+ for index, col in enumerate(flux_cols_use):
+ stokes[0, index, :] = astropy_table[col].data.data
+
+ skymodel_obj = SkyModel(
+ astropy_table[id_col_use].data.data.astype("str"),
+ Longitude(astropy_table[ra_col_use].data.data, astropy_table[ra_col_use].unit),
+ Latitude(astropy_table[dec_col_use].data.data, astropy_table[dec_col_use].unit),
+ stokes,
+ spectral_type,
+ freq_array=freq_array,
+ reference_frequency=reference_frequency,
+ spectral_index=spectral_index,
+ )
+
+ if source_select_kwds is not None:
+ sky_array = skymodel_to_array(skymodel_obj)
+ sky_array = source_cuts(sky_array, **source_select_kwds)
+ if return_table:
+ return sky_array
+
+ skymodel_obj = array_to_skymodel(sky_array)
if return_table:
- return data
+ return skymodel_to_array(skymodel_obj)
+
+ return skymodel_obj
+
+
+def read_gleam_catalog(
+ votable_file, spectral_type="flat", source_select_kwds=None, return_table=False
+):
+ """
+ Create a SkyModel object from the GLEAM votable catalog.
+
+ Tested on: GLEAM EGC catalog, version 2
+
+ Parameters
+ ----------
+ votable_file : str
+ Path to GLEAM votable catalog file.
+ spectral_type : str
+ One of 'flat', 'subband' or 'spectral_index'. If set to 'flat', the
+ wide band integrated flux will be used, if set to 'spectral_index' the
+ fitted flux at 200 MHz will be used for the flux column.
+ return_table : bool, optional
+ Whether to return the astropy table instead of a SkyModel object.
+ source_select_kwds : dict, optional
+ Dictionary of keywords for source selection Valid options:
+
+ * `lst_array`: For coarse RA horizon cuts, lsts used in the simulation [radians]
+ * `latitude_deg`: Latitude of telescope in degrees. Used for declination coarse
+ horizon cut.
+ * `horizon_buffer`: Angle (float, in radians) of buffer for coarse horizon cut.
+ Default is about 10 minutes of sky rotation. (See caveats in
+ :func:`array_to_skymodel` docstring)
+ * `min_flux`: Minimum stokes I flux to select [Jy]
+ * `max_flux`: Maximum stokes I flux to select [Jy]
+
+ Returns
+ -------
+ recarray or :class:`pyradiosky.SkyModel`
+ if return_table, recarray of source parameters, otherwise :class:`pyradiosky.SkyModel` instance
+ """
+ spec_type_list = ["flat", "spectral_index", "subband"]
+ if spectral_type not in spec_type_list:
+ raise ValueError(
+ f"spectral_type {spectral_type} is not an allowed type. "
+ f"Allowed types are: {spec_type_list}"
+ )
+
+ if spectral_type == "flat":
+ flux_columns = "Fintwide"
+ reference_frequency = 200e6 * units.Hz
+ freq_array = None
+ spectral_index_column = None
+ elif spectral_type == "spectral_index":
+ flux_columns = "Fintfit200"
+ reference_frequency = 200e6 * units.Hz
+ spectral_index_column = "alpha"
+ freq_array = None
+ else:
+ # fmt: off
+ flux_columns = ["Fint076", "Fint084", "Fint092", "Fint099", "Fint107",
+ "Fint115", "Fint122", "Fint130", "Fint143", "Fint151",
+ "Fint158", "Fint166", "Fint174", "Fint181", "Fint189",
+ "Fint197", "Fint204", "Fint212", "Fint220", "Fint227"]
+ freq_array = [76, 84, 92, 99, 107, 115, 122, 130, 143, 151, 158, 166,
+ 174, 181, 189, 197, 204, 212, 220, 227]
+ freq_array = np.array(freq_array) * 1e6 * units.Hz
+ reference_frequency = None
+ spectral_index_column = None
+ # fmt: on
+
+ gleam_skymodel = read_votable_catalog(
+ votable_file,
+ table_name="GLEAM",
+ id_column="GLEAM",
+ ra_column="RAJ2000",
+ dec_column="DEJ2000",
+ flux_columns=flux_columns,
+ freq_array=freq_array,
+ reference_frequency=reference_frequency,
+ spectral_index_column=spectral_index_column,
+ source_select_kwds=source_select_kwds,
+ return_table=return_table,
+ )
- return array_to_skymodel(data)
+ return gleam_skymodel
-def read_text_catalog(catalog_csv, source_select_kwds={}, return_table=False):
+def read_text_catalog(catalog_csv, source_select_kwds=None, return_table=False):
"""
Read in a text file of sources.
Parameters
----------
catalog_csv: str
- Path to tab separated value file with the following expected columns
- (For now, all sources are flat spectrum):
-
+ Path to tab separated value file with the following required columns:
* `Source_ID`: source name as a string of maximum 10 characters
* `ra_j2000`: right ascension at J2000 epoch, in decimal degrees
* `dec_j2000`: declination at J2000 epoch, in decimal degrees
- * `flux_density_I`: Stokes I flux density in Janskys
- * `frequency`: reference frequency (for future spectral indexing) [Hz]
+ * `Flux [Jy]`: Stokes I flux density in Janskys
+
+ If flux is specified at multiple frequencies (must be the same set for all
+ components), the frequencies must be included in each column name,
+ e.g. `Flux at 150 MHz [Jy]`. Recognized units are ('Hz', 'kHz', 'MHz' or 'GHz'):
+
+ If flux is only specified at one reference frequency (can be different per
+ component), a frequency column should be added (note: assumed to be in Hz):
+ * `Frequency`: reference frequency [Hz]
+
+ Optionally a spectral index can be specified per component with:
+ * `Spectral_Index`: spectral index
source_select_kwds : dict, optional
Dictionary of keywords for source selection. Valid options:
@@ -881,25 +1499,115 @@ def read_text_catalog(catalog_csv, source_select_kwds={}, return_table=False):
header = [
h.strip() for h in header.split() if not h[0] == "["
] # Ignore units in header
- dt = np.format_parser(
- ["U10", "f8", "f8", "f8", "f8"],
- ["source_id", "ra_j2000", "dec_j2000", "flux_density_I", "frequency"],
- header,
- )
+
+ flux_fields = [colname for colname in header if colname.lower().startswith("flux")]
+ flux_fields_lower = [colname.lower() for colname in flux_fields]
+
+ header_lower = [colname.lower() for colname in header]
+
+ expected_cols = ["source_id", "ra_j2000", "dec_j2000"]
+ if "frequency" in header_lower:
+ if len(flux_fields) != 1:
+ raise ValueError(
+ "If frequency column is present, only one flux column allowed."
+ )
+ freq_array = None
+ expected_cols.extend([flux_fields_lower[0], "frequency"])
+ if "spectral_index" in header_lower:
+ spectral_type = "spectral_index"
+ expected_cols.append("spectral_index")
+ freq_array = None
+ else:
+ spectral_type = "flat"
+ n_freqs = 1
+ else:
+ frequencies = []
+ for fluxname in flux_fields:
+ if "Hz" in fluxname:
+ cst_obj = CSTBeam()
+ freq = cst_obj.name2freq(fluxname)
+ frequencies.append(freq)
+ else:
+ if len(flux_fields) > 1:
+ raise ValueError(
+ "Multiple flux fields, but they do not all contain a frequency."
+ )
+ if len(frequencies) > 0:
+ n_freqs = len(frequencies)
+ if "subband" in flux_fields[0]:
+ spectral_type = "subband"
+ else:
+ if len(frequencies) > 1:
+ spectral_type = "full"
+ else:
+ spectral_type = "flat"
+ # This has a freq_array
+ expected_cols.extend(flux_fields_lower)
+ freq_array = np.array(frequencies) * units.Hz
+ else:
+ # This is a flat spectrum (no freq info)
+ n_freqs = 1
+ spectral_type = "flat"
+ freq_array = None
+ expected_cols.append("flux")
+
+ if expected_cols != header_lower:
+ raise ValueError(
+ "Header does not match expectations. Expected columns"
+ f"are: {expected_cols}, header columns were: {header_lower}"
+ )
catalog_table = np.genfromtxt(
- catalog_csv, autostrip=True, skip_header=1, dtype=dt.dtype
+ catalog_csv, autostrip=True, skip_header=1, dtype=None, encoding="utf-8"
)
catalog_table = np.atleast_1d(catalog_table)
- if len(source_select_kwds) > 0:
- catalog_table = source_cuts(catalog_table, **source_select_kwds)
+ col_names = catalog_table.dtype.names
+
+ names = catalog_table[col_names[0]].astype("str")
+ ras = Longitude(catalog_table[col_names[1]], units.deg)
+ decs = Latitude(catalog_table[col_names[2]], units.deg)
+
+ stokes = np.zeros((4, n_freqs, len(catalog_table)), dtype=np.float)
+ for ind in np.arange(n_freqs):
+ stokes[0, ind, :] = catalog_table[col_names[ind + 3]]
+
+ if "frequency" in header_lower and freq_array is None:
+ freq_ind = np.where(np.array(header_lower) == "frequency")[0][0]
+ reference_frequency = catalog_table[col_names[freq_ind]] * units.Hz
+ if "spectral_index" in header_lower:
+ si_ind = np.where(np.array(header_lower) == "spectral_index")[0][0]
+ spectral_index = catalog_table[col_names[si_ind]]
+ else:
+ spectral_index = None
+ else:
+ reference_frequency = None
+ spectral_index = None
+
+ skymodel_obj = SkyModel(
+ names,
+ ras,
+ decs,
+ stokes,
+ spectral_type,
+ freq_array=freq_array,
+ reference_frequency=reference_frequency,
+ spectral_index=spectral_index,
+ )
+
+ if source_select_kwds is not None:
+ sky_array = skymodel_to_array(skymodel_obj)
+ sky_array = source_cuts(sky_array, **source_select_kwds)
+ if return_table:
+ return sky_array
+
+ skymodel_obj = array_to_skymodel(sky_array)
if return_table:
- return catalog_table
+ return skymodel_to_array(skymodel_obj)
- return array_to_skymodel(catalog_table)
+ return skymodel_obj
def read_idl_catalog(filename_sav, expand_extended=True):
@@ -920,7 +1628,7 @@ def read_idl_catalog(filename_sav, expand_extended=True):
:class:`pyradiosky.SkyModel`
"""
catalog = scipy.io.readsav(filename_sav)["catalog"]
- ids = catalog["id"]
+ ids = catalog["id"].astype(str)
ra = catalog["ra"]
dec = catalog["dec"]
source_freqs = catalog["freq"]
@@ -1007,16 +1715,16 @@ def read_idl_catalog(filename_sav, expand_extended=True):
spectral_index = np.insert(spectral_index, use_index, src["alpha"])
source_inds = np.insert(source_inds, use_index, np.full(Ncomps, ext))
- ra = Angle(ra, units.deg)
- dec = Angle(dec, units.deg)
+ ra = Longitude(ra, units.deg)
+ dec = Latitude(dec, units.deg)
stokes = stokes[:, np.newaxis, :] # Add frequency axis
sourcelist = SkyModel(
ids,
ra,
dec,
stokes,
- source_freqs,
spectral_type="spectral_index",
+ reference_frequency=Quantity(source_freqs, "hertz"),
spectral_index=spectral_index,
beam_amp=beam_amp,
extended_model_group=extended_model_group,
@@ -1024,29 +1732,69 @@ def read_idl_catalog(filename_sav, expand_extended=True):
return sourcelist
-def write_catalog_to_file(filename, catalog):
+def write_catalog_to_file(filename, skymodel):
"""
Write out a catalog to a text file.
- Readable with :meth:`simsetup.read_catalog_text()`.
+ Readable with :meth:`read_text_catalog()`.
Parameters
----------
filename : str
Path to output file (string)
- catalog : :class:`SkyModel`
+ skymodel : :class:`SkyModel`
The sky model to write to file.
"""
+ header = "SOURCE_ID\tRA_J2000 [deg]\tDec_J2000 [deg]"
+ format_str = "{}\t{:0.8f}\t{:0.8f}"
+ if skymodel.reference_frequency is not None:
+ header += "\tFlux [Jy]\tFrequency [Hz]"
+ format_str += "\t{:0.8f}\t{:0.8f}"
+ if skymodel.spectral_index is not None:
+ header += "\tSpectral_Index"
+ format_str += "\t{:0.8f}"
+ elif skymodel.freq_array is not None:
+ for freq in skymodel.freq_array:
+ freq_hz_val = freq.to(units.Hz).value
+ if freq_hz_val > 1e9:
+ freq_str = "{:g}_GHz".format(freq_hz_val * 1e-9)
+ elif freq_hz_val > 1e6:
+ freq_str = "{:g}_MHz".format(freq_hz_val * 1e-6)
+ elif freq_hz_val > 1e3:
+ freq_str = "{:g}_kHz".format(freq_hz_val * 1e-3)
+ else:
+ freq_str = "{:g}_Hz".format(freq_hz_val)
+
+ if skymodel.spectral_type == "subband":
+ header += f"\tFlux_subband_{freq_str} [Jy]"
+ else:
+ header += f"\tFlux_{freq_str} [Jy]"
+ format_str += "\t{:0.8f}"
+ else:
+ # flat spectral response, no freq info
+ header += "\tFlux [Jy]"
+ format_str += "\t{:0.8f}"
+
+ header += "\n"
+ format_str += "\n"
+
with open(filename, "w+") as fo:
- fo.write(
- "SOURCE_ID\tRA_J2000 [deg]\tDec_J2000 [deg]\tFlux [Jy]\tFrequency [Hz]\n"
- )
- arr = skymodel_to_array(catalog)
+ fo.write(header)
+ arr = skymodel_to_array(skymodel)
for src in arr:
- srcid, ra, dec, flux_i, freq = src
-
- fo.write(
- "{}\t{:f}\t{:f}\t{:0.2f}\t{:0.2f}\n".format(
- srcid, ra, dec, flux_i[0], freq[0]
- )
- )
+ if skymodel.reference_frequency is not None:
+ if skymodel.spectral_index is not None:
+ srcid, ra, dec, flux_i, rfreq, freq, spec_index = src
+ fo.write(
+ format_str.format(srcid, ra, dec, flux_i, freq, spec_index)
+ )
+ else:
+ srcid, ra, dec, flux_i, rfreq, freq = src
+ fo.write(format_str.format(srcid, ra, dec, flux_i, rfreq))
+ elif skymodel.freq_array is not None:
+ srcid, ra, dec, flux_i, freq = src
+ fo.write(format_str.format(srcid, ra, dec, *flux_i))
+ else:
+ # flat spectral response, no freq info
+ srcid, ra, dec, flux_i = src
+ fo.write(format_str.format(srcid, ra, dec, flux_i))
diff --git a/pyradiosky/tests/test_skymodel.py b/pyradiosky/tests/test_skymodel.py
index 0231165b..7e213b39 100644
--- a/pyradiosky/tests/test_skymodel.py
+++ b/pyradiosky/tests/test_skymodel.py
@@ -3,11 +3,20 @@
# Licensed under the 3-clause BSD License
import os
+import fileinput
import pytest
import numpy as np
+import warnings
from astropy import units
-from astropy.coordinates import SkyCoord, EarthLocation, Angle, AltAz
+from astropy.coordinates import (
+ SkyCoord,
+ EarthLocation,
+ Angle,
+ AltAz,
+ Longitude,
+ Latitude,
+)
from astropy.time import Time, TimeDelta
import scipy.io
@@ -19,11 +28,111 @@
GLEAM_vot = os.path.join(SKY_DATA_PATH, "gleam_50srcs.vot")
-def test_source_zenith_from_icrs():
- """Test single source position at zenith constructed using icrs."""
+@pytest.fixture
+def time_location():
array_location = EarthLocation(lat="-30d43m17.5s", lon="21d25m41.9s", height=1073.0)
+
time = Time("2018-03-01 00:00:00", scale="utc", location=array_location)
+ return time, array_location
+
+
+@pytest.fixture
+def zenith_skycoord(time_location):
+ time, array_location = time_location
+
+ source_coord = SkyCoord(
+ alt=Angle(90, unit=units.deg),
+ az=Angle(0, unit=units.deg),
+ obstime=time,
+ frame="altaz",
+ location=array_location,
+ )
+ icrs_coord = source_coord.transform_to("icrs")
+
+ return icrs_coord
+
+
+@pytest.fixture
+def zenith_skymodel(zenith_skycoord):
+ icrs_coord = zenith_skycoord
+
+ ra = icrs_coord.ra
+ dec = icrs_coord.dec
+
+ names = "zen_source"
+ stokes = [1.0, 0, 0, 0]
+ zenith_source = skymodel.SkyModel(names, ra, dec, stokes, "flat")
+
+ return zenith_source
+
+
+@pytest.fixture
+def moonsky():
+ pytest.importorskip("lunarsky")
+
+ from lunarsky import MoonLocation, SkyCoord as SkyC
+
+ # Tranquility base
+ array_location = MoonLocation(lat="00d41m15s", lon="23d26m00s", height=0.0)
+
+ time = Time.now()
+ zen_coord = SkyC(
+ alt=Angle(90, unit=units.deg),
+ az=Angle(0, unit=units.deg),
+ obstime=time,
+ frame="lunartopo",
+ location=array_location,
+ )
+
+ icrs_coord = zen_coord.transform_to("icrs")
+
+ ra = icrs_coord.ra
+ dec = icrs_coord.dec
+ names = "zen_source"
+ stokes = [1.0, 0, 0, 0]
+ zenith_source = skymodel.SkyModel(names, ra, dec, stokes, "flat")
+
+ zenith_source.update_positions(time, array_location)
+
+ yield zenith_source
+
+
+@pytest.fixture
+def healpix_data():
+ pytest.importorskip("astropy_healpix")
+ import astropy_healpix
+
+ nside = 32
+ npix = astropy_healpix.nside_to_npix(nside)
+ hp_obj = astropy_healpix.HEALPix(nside=nside)
+
+ frequencies = np.linspace(100, 110, 10)
+ pixel_area = astropy_healpix.nside_to_pixel_area(nside)
+
+ # Note that the cone search includes any pixels that overlap with the search
+ # region. With such a low resolution, this returns some slightly different
+ # results from the equivalent healpy search. Subtracting(0.75 * pixres) from
+ # the pixel area resolves this discrepancy for the test.
+
+ pixres = hp_obj.pixel_resolution.to("deg").value
+ ipix_disc = hp_obj.cone_search_lonlat(
+ 135 * units.deg, 0 * units.deg, radius=(10 - pixres * 0.75) * units.deg
+ )
+
+ return {
+ "nside": nside,
+ "npix": npix,
+ "frequencies": frequencies,
+ "pixel_area": pixel_area,
+ "ipix_disc": ipix_disc,
+ }
+
+
+def test_source_zenith_from_icrs(time_location):
+ """Test single source position at zenith constructed using icrs."""
+ time, array_location = time_location
+
lst = time.sidereal_time("apparent")
tee_ra = lst
@@ -41,52 +150,235 @@ def test_source_zenith_from_icrs():
ra = icrs_coord.ra
dec = icrs_coord.dec
- # Check error cases
- with pytest.raises(ValueError) as cm:
- skymodel.SkyModel("icrs_zen", ra.rad, dec.rad, [1, 0, 0, 0], 1e8, "flat")
- assert str(cm.value).startswith(
- "ra must be an astropy Angle object. " "value was: 3.14"
- )
-
- with pytest.raises(ValueError) as cm:
- skymodel.SkyModel("icrs_zen", ra, dec.rad, [1, 0, 0, 0], 1e8, "flat")
- assert str(cm.value).startswith(
- "dec must be an astropy Angle object. " "value was: -0.53"
- )
- zenith_source = skymodel.SkyModel("icrs_zen", ra, dec, [1, 0, 0, 0], 1e8, "flat")
+ zenith_source = skymodel.SkyModel("icrs_zen", ra, dec, [1.0, 0, 0, 0], "flat")
zenith_source.update_positions(time, array_location)
zenith_source_lmn = zenith_source.pos_lmn.squeeze()
assert np.allclose(zenith_source_lmn, np.array([0, 0, 1]), atol=1e-5)
-def test_source_zenith():
+def test_source_zenith(time_location, zenith_skymodel):
"""Test single source position at zenith."""
- time = Time("2018-03-01 00:00:00", scale="utc")
+ time, array_location = time_location
- array_location = EarthLocation(lat="-30d43m17.5s", lon="21d25m41.9s", height=1073.0)
+ zenith_skymodel.update_positions(time, array_location)
+ zenith_source_lmn = zenith_skymodel.pos_lmn.squeeze()
+ assert np.allclose(zenith_source_lmn, np.array([0, 0, 1]))
- source_coord = SkyCoord(
- alt=Angle(90, unit=units.deg),
- az=Angle(0, unit=units.deg),
- obstime=time,
- frame="altaz",
- location=array_location,
- )
- icrs_coord = source_coord.transform_to("icrs")
+
+def test_skymodel_init_errors(zenith_skycoord):
+ icrs_coord = zenith_skycoord
ra = icrs_coord.ra
dec = icrs_coord.dec
- names = "zen_source"
- stokes = [1, 0, 0, 0]
- freqs = [1e8]
- zenith_source = skymodel.SkyModel(names, ra, dec, stokes, freqs, "flat")
+ # Check error cases
+ with pytest.raises(
+ ValueError,
+ match=(
+ "UVParameter _ra is not the appropriate type. Is: float64. "
+ "Should be: "
+ ),
+ ):
+ skymodel.SkyModel("icrs_zen", ra.rad, dec, [1.0, 0, 0, 0], "flat")
+
+ with pytest.raises(
+ ValueError,
+ match=(
+ "UVParameter _dec is not the appropriate type. Is: float64. "
+ "Should be: "
+ ),
+ ):
+ skymodel.SkyModel("icrs_zen", ra, dec.rad, [1.0, 0, 0, 0], "flat")
+
+ with pytest.raises(
+ ValueError,
+ match=(
+ "Only one of freq_array and reference_frequency can be specified, not both."
+ ),
+ ):
+ skymodel.SkyModel(
+ "icrs_zen",
+ ra,
+ dec,
+ [1.0, 0, 0, 0],
+ "flat",
+ reference_frequency=[1e8] * units.Hz,
+ freq_array=[1e8] * units.Hz,
+ )
- zenith_source.update_positions(time, array_location)
- zenith_source_lmn = zenith_source.pos_lmn.squeeze()
- assert np.allclose(zenith_source_lmn, np.array([0, 0, 1]))
+ with pytest.raises(
+ ValueError, match=("freq_array must have a unit that can be converted to Hz.")
+ ):
+ skymodel.SkyModel(
+ "icrs_zen", ra, dec, [1.0, 0, 0, 0], "flat", freq_array=[1e8] * units.m
+ )
+
+ with pytest.raises(
+ ValueError,
+ match=("reference_frequency must have a unit that can be converted to Hz."),
+ ):
+ skymodel.SkyModel(
+ "icrs_zen",
+ ra,
+ dec,
+ [1.0, 0, 0, 0],
+ "flat",
+ reference_frequency=[1e8] * units.m,
+ )
+
+
+def test_skymodel_deprecated():
+ """Test that old init works with deprecation."""
+ source_new = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ [1.0, 0.0, 0.0, 0.0],
+ "flat",
+ reference_frequency=np.array([1e8]) * units.Hz,
+ )
+
+ with pytest.warns(
+ DeprecationWarning,
+ match="The input parameters to SkyModel.__init__ have changed",
+ ):
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ [1.0, 0.0, 0.0, 0.0],
+ np.array([1e8]) * units.Hz,
+ "flat",
+ )
+ assert source_new == source_old
+
+ with pytest.warns(
+ DeprecationWarning,
+ match="In the future, the reference_frequency will be required to be an astropy Quantity",
+ ):
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ [1.0, 0.0, 0.0, 0.0],
+ "flat",
+ reference_frequency=np.array([1e8]),
+ )
+ assert source_new == source_old
+
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ [1.0, 0.0, 0.0, 0.0],
+ "flat",
+ reference_frequency=np.array([1.5e8]) * units.Hz,
+ )
+ with pytest.warns(
+ DeprecationWarning,
+ match=(
+ "Future equality does not pass, probably because the frequencies "
+ "were not checked"
+ ),
+ ):
+ assert source_new == source_old
+
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg + 2e-3 * units.arcsec),
+ [1.0, 0.0, 0.0, 0.0],
+ "flat",
+ reference_frequency=np.array([1e8]) * units.Hz,
+ )
+ with pytest.warns(
+ DeprecationWarning,
+ match=("The _dec parameters are not within the future tolerance"),
+ ):
+ assert source_new == source_old
+
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(Longitude(12.0 * units.hr) + Longitude(2e-3 * units.arcsec)),
+ Latitude(-30.0 * units.deg),
+ [1.0, 0.0, 0.0, 0.0],
+ "flat",
+ reference_frequency=np.array([1e8]) * units.Hz,
+ )
+ with pytest.warns(
+ DeprecationWarning,
+ match=("The _ra parameters are not within the future tolerance"),
+ ):
+ assert source_new == source_old
+
+ stokes = np.zeros((4, 2, 1), dtype=np.float)
+ stokes[0, :, :] = 1.0
+ source_new = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ stokes,
+ "subband",
+ freq_array=np.array([1e8, 1.5e8]) * units.Hz,
+ )
+ with pytest.warns(
+ DeprecationWarning,
+ match="The input parameters to SkyModel.__init__ have changed",
+ ):
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ stokes,
+ np.array([1e8, 1.5e8]) * units.Hz,
+ "subband",
+ )
+ assert source_new == source_old
+
+ with pytest.warns(
+ DeprecationWarning,
+ match="In the future, the freq_array will be required to be an astropy Quantity",
+ ):
+ source_old = skymodel.SkyModel(
+ "Test",
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
+ stokes,
+ "subband",
+ freq_array=np.array([1e8, 1.5e8]),
+ )
+ assert source_new == source_old
+
+ telescope_location = EarthLocation(
+ lat="-30d43m17.5s", lon="21d25m41.9s", height=1073.0
+ )
+ time = Time("2018-01-01 00:00")
+ with pytest.warns(
+ DeprecationWarning,
+ match="Passing telescope_location to SkyModel.coherency_calc is deprecated",
+ ):
+ source_new.update_positions(time, telescope_location)
+ source_new.coherency_calc(telescope_location)
+
+
+def test_update_position_errors(zenith_skymodel, time_location):
+ time, array_location = time_location
+ with pytest.raises(ValueError, match=("time must be an astropy Time object.")):
+ zenith_skymodel.update_positions("2018-03-01 00:00:00", array_location)
+
+
+def test_coherency_calc_errors():
+ """Test that correct errors are raised when providing invalid location object."""
+ coord = SkyCoord(ra=30.0 * units.deg, dec=40 * units.deg, frame="icrs")
+
+ stokes_radec = [1, -0.2, 0.3, 0.1]
+
+ source = skymodel.SkyModel("test", coord.ra, coord.dec, stokes_radec, "flat")
+
+ with pytest.raises(ValueError, match="telescope_location must be an"):
+ source.coherency_calc().squeeze()
def test_calc_basis_rotation_matrix():
@@ -102,15 +394,14 @@ def test_calc_basis_rotation_matrix():
source = skymodel.SkyModel(
"Test",
- Angle(12.0 * units.hr),
- Angle(-30.0 * units.deg),
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
[1.0, 0.0, 0.0, 0.0],
- 1e8,
"flat",
)
source.update_positions(time, telescope_location)
- basis_rot_matrix = source._calc_average_rotation_matrix(telescope_location)
+ basis_rot_matrix = source._calc_average_rotation_matrix()
assert np.allclose(np.matmul(basis_rot_matrix, basis_rot_matrix.T), np.eye(3))
assert np.allclose(np.matmul(basis_rot_matrix.T, basis_rot_matrix), np.eye(3))
@@ -129,15 +420,14 @@ def test_calc_vector_rotation():
source = skymodel.SkyModel(
"Test",
- Angle(12.0 * units.hr),
- Angle(-30.0 * units.deg),
+ Longitude(12.0 * units.hr),
+ Latitude(-30.0 * units.deg),
[1.0, 0.0, 0.0, 0.0],
- 1e8,
"flat",
)
source.update_positions(time, telescope_location)
- coherency_rotation = np.squeeze(source._calc_coherency_rotation(telescope_location))
+ coherency_rotation = np.squeeze(source._calc_coherency_rotation())
assert np.isclose(np.linalg.det(coherency_rotation), 1)
@@ -192,10 +482,9 @@ def test_polarized_source_visibilities():
source = skymodel.SkyModel(
"icrs_zen",
- zenith_icrs.ra + raoff,
- zenith_icrs.dec + decoff,
+ Longitude(zenith_icrs.ra + raoff),
+ Latitude(zenith_icrs.dec + decoff),
stokes_radec,
- 1e8,
"flat",
)
@@ -210,7 +499,7 @@ def test_polarized_source_visibilities():
alts[ti] = alt
azs[ti] = az
- coherency_tmp = source.coherency_calc(array_location).squeeze()
+ coherency_tmp = source.coherency_calc().squeeze()
coherency_matrix_local[:, :, ti] = coherency_tmp
zas = np.pi / 2.0 - alts
@@ -291,7 +580,7 @@ def test_polarized_source_smooth_visibilities():
stokes_radec = [1, -0.2, 0.3, 0.1]
source = skymodel.SkyModel(
- "icrs_zen", zenith_icrs.ra, zenith_icrs.dec, stokes_radec, 1e8, "flat"
+ "icrs_zen", zenith_icrs.ra, zenith_icrs.dec, stokes_radec, "flat"
)
coherency_matrix_local = np.zeros([2, 2, ntimes], dtype="complex128")
@@ -305,7 +594,7 @@ def test_polarized_source_smooth_visibilities():
alts[ti] = alt
azs[ti] = az
- coherency_tmp = source.coherency_calc(array_location).squeeze()
+ coherency_tmp = source.coherency_calc().squeeze()
coherency_matrix_local[:, :, ti] = coherency_tmp
zas = np.pi / 2.0 - alts
@@ -338,117 +627,83 @@ def test_polarized_source_smooth_visibilities():
assert np.all(imag_stokes == 0)
-def test_coherency_calc_errors():
- """Test that correct errors are raised when providing invalid location object."""
- coord = SkyCoord(ra=30.0 * units.deg, dec=40 * units.deg, frame="icrs")
+def test_read_healpix_hdf5(healpix_data):
+ m = np.arange(healpix_data["npix"])
+ m[healpix_data["ipix_disc"]] = healpix_data["npix"] - 1
- stokes_radec = [1, -0.2, 0.3, 0.1]
-
- source = skymodel.SkyModel("test", coord.ra, coord.dec, stokes_radec, 1e8, "flat")
- time = Time.now()
- array_location = None
- with pytest.raises(ValueError) as err:
- source.update_positions(time, telescope_location=array_location)
- assert str(err.value).startswith("telescope_location must be an")
-
- with pytest.raises(ValueError) as err:
- source.coherency_calc(array_location).squeeze()
- assert str(err.value).startswith("telescope_location must be an")
+ indices = np.arange(healpix_data["npix"])
+ hpmap, inds, freqs = skymodel.read_healpix_hdf5(
+ os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
+ )
-class TestHealpixHdf5:
- def setup(self):
- # Common features of tests
- pytest.importorskip("astropy_healpix")
- import astropy_healpix
-
- self.nside = 32
- self.Npix = astropy_healpix.nside_to_npix(self.nside)
- hp_obj = astropy_healpix.HEALPix(nside=self.nside)
- self.frequencies = np.linspace(100, 110, 10)
- self.pixel_area = astropy_healpix.nside_to_pixel_area(self.nside)
- # Note that the cone search includes any pixels that overlap with the search
- # region. With such a low resolution, this returns some slightly different
- # results from the equivalent healpy search. Subtracting(0.75 * pixres) from
- # the pixel area resolves this discrepancy for the test.
-
- pixres = hp_obj.pixel_resolution.to("deg").value
- self.ipix_disc = hp_obj.cone_search_lonlat(
- 135 * units.deg, 0 * units.deg, radius=(10 - pixres * 0.75) * units.deg
- )
+ assert np.allclose(hpmap[0, :], m)
+ assert np.allclose(inds, indices)
+ assert np.allclose(freqs, healpix_data["frequencies"])
- def test_read_healpix_hdf5(self):
- m = np.arange(self.Npix)
- m[self.ipix_disc] = self.Npix - 1
- indices = np.arange(self.Npix)
+def test_healpix_to_sky(healpix_data):
+ hpmap, inds, freqs = skymodel.read_healpix_hdf5(
+ os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
+ )
- hpmap, inds, freqs = skymodel.read_healpix_hdf5(
- os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
- )
+ hmap_orig = np.arange(healpix_data["npix"])
+ hmap_orig[healpix_data["ipix_disc"]] = healpix_data["npix"] - 1
- assert np.allclose(hpmap[0, :], m)
- assert np.allclose(inds, indices)
- assert np.allclose(freqs, self.frequencies)
+ hmap_orig = np.repeat(hmap_orig[None, :], 10, axis=0)
+ hmap_orig = (hmap_orig.T / skyutils.jy_to_ksr(freqs)).T
+ hmap_orig = hmap_orig * healpix_data["pixel_area"]
+ sky = skymodel.healpix_to_sky(hpmap, inds, freqs)
- def test_healpix_to_sky(self):
- hpmap, inds, freqs = skymodel.read_healpix_hdf5(
- os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
- )
+ assert np.allclose(sky.stokes[0], hmap_orig.value)
- hmap_orig = np.arange(self.Npix)
- hmap_orig[self.ipix_disc] = self.Npix - 1
- hmap_orig = np.repeat(hmap_orig[None, :], 10, axis=0)
- hmap_orig = (hmap_orig.T / skyutils.jy_to_ksr(freqs)).T
- hmap_orig = hmap_orig * self.pixel_area
- sky = skymodel.healpix_to_sky(hpmap, inds, freqs)
+def test_units_healpix_to_sky(healpix_data):
+ hpmap, inds, freqs = skymodel.read_healpix_hdf5(
+ os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
+ )
+ freqs = freqs * units.Hz
- assert np.allclose(sky.stokes[0], hmap_orig.value)
+ brightness_temperature_conv = units.brightness_temperature(
+ freqs, beam_area=healpix_data["pixel_area"]
+ )
+ stokes = (hpmap.T * units.K).to(units.Jy, brightness_temperature_conv).T
+ sky = skymodel.healpix_to_sky(hpmap, inds, freqs)
- def test_units_healpix_to_sky(self):
- hpmap, inds, freqs = skymodel.read_healpix_hdf5(
- os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
- )
- freqs = freqs * units.Hz
+ assert np.allclose(sky.stokes[0, 0], stokes.value[0])
- brightness_temperature_conv = units.brightness_temperature(
- freqs, beam_area=self.pixel_area
- )
- stokes = (hpmap.T * units.K).to(units.Jy, brightness_temperature_conv).T
- sky = skymodel.healpix_to_sky(hpmap, inds, freqs)
- assert np.allclose(sky.stokes[0, 0], stokes.value[0])
+def test_read_write_healpix(healpix_data):
+ hpmap, inds, freqs = skymodel.read_healpix_hdf5(
+ os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
+ )
+ freqs = freqs * units.Hz
+ filename = "tempfile.hdf5"
+ with pytest.raises(ValueError) as verr:
+ skymodel.write_healpix_hdf5(filename, hpmap, inds[:10], freqs.to("Hz").value)
+ assert str(verr.value).startswith(
+ "Need to provide nside if giving a subset of the map."
+ )
- def test_read_write_healpix(self):
- hpmap, inds, freqs = skymodel.read_healpix_hdf5(
- os.path.join(SKY_DATA_PATH, "healpix_disk.hdf5")
+ with pytest.raises(ValueError) as verr:
+ skymodel.write_healpix_hdf5(
+ filename,
+ hpmap,
+ inds[:10],
+ freqs.to("Hz").value,
+ nside=healpix_data["nside"],
)
- freqs = freqs * units.Hz
- filename = "tempfile.hdf5"
- with pytest.raises(ValueError) as verr:
- skymodel.write_healpix_hdf5(
- filename, hpmap, inds[:10], freqs.to("Hz").value
- )
- assert str(verr.value).startswith(
- "Need to provide nside if giving a subset of the map."
- )
-
- with pytest.raises(ValueError) as verr:
- skymodel.write_healpix_hdf5(
- filename, hpmap, inds[:10], freqs.to("Hz").value, nside=self.nside
- )
- assert str(verr.value).startswith("Invalid map shape")
+ assert str(verr.value).startswith("Invalid map shape")
- skymodel.write_healpix_hdf5(filename, hpmap, inds, freqs.to("Hz").value)
+ skymodel.write_healpix_hdf5(filename, hpmap, inds, freqs.to("Hz").value)
- hpmap_new, inds_new, freqs_new = skymodel.read_healpix_hdf5(filename)
+ hpmap_new, inds_new, freqs_new = skymodel.read_healpix_hdf5(filename)
- os.remove(filename)
+ os.remove(filename)
- assert np.allclose(hpmap_new, hpmap)
- assert np.allclose(inds_new, inds)
- assert np.allclose(freqs_new, freqs.to("Hz").value)
+ assert np.allclose(hpmap_new, hpmap)
+ assert np.allclose(inds_new, inds)
+ assert np.allclose(freqs_new, freqs.to("Hz").value)
def test_healpix_import_err():
@@ -525,6 +780,55 @@ def test_healpix_positions():
assert np.isclose(src_lmn[2][ipix], src_n)
+@pytest.mark.parametrize("spec_type", ["flat", "subband", "spectral_index", "full"])
+def test_array_to_skymodel_loop(spec_type):
+ if spec_type == "full":
+ spectral_type = "subband"
+ else:
+ spectral_type = spec_type
+
+ sky = skymodel.read_gleam_catalog(GLEAM_vot, spectral_type=spectral_type)
+ if spec_type == "full":
+ sky.spectral_type = "full"
+
+ # This should be removed after pyuvdata PR #790 is merged.
+ # GLEAM has NaNs for the spectral_index of some sources
+ # Currently, arrays with NaNs are never equal even if they are equal where
+ # they are not nan and are nan in the same locations.
+ # So remove those components for the round trip equality test.
+ if spectral_type == "spectral_index":
+ wh_not_nan = np.squeeze(np.argwhere(~np.isnan(sky.spectral_index)))
+ sky.Ncomponents = wh_not_nan.size
+ sky.ra = sky.ra[wh_not_nan]
+ sky.dec = sky.dec[wh_not_nan]
+ sky.name = sky.name[wh_not_nan]
+ sky.reference_frequency = sky.reference_frequency[wh_not_nan]
+ sky.spectral_index = sky.spectral_index[wh_not_nan]
+ sky.stokes = sky.stokes[:, :, wh_not_nan]
+ sky.coherency_radec = skyutils.stokes_to_coherency(sky.stokes)
+
+ arr = skymodel.skymodel_to_array(sky)
+ sky2 = skymodel.array_to_skymodel(arr)
+
+ assert sky == sky2
+
+ if spec_type == "flat":
+ # again with no reference_frequency field
+ reference_frequency = sky.reference_frequency
+ sky.reference_frequency = None
+ arr = skymodel.skymodel_to_array(sky)
+ sky2 = skymodel.array_to_skymodel(arr)
+
+ assert sky == sky2
+
+ # again with flat & freq_array
+ sky.freq_array = np.atleast_1d(np.unique(reference_frequency))
+ arr = skymodel.skymodel_to_array(sky)
+ sky2 = skymodel.array_to_skymodel(arr)
+
+ assert sky == sky2
+
+
def test_param_flux_cuts():
# Check that min/max flux limits in test params work.
@@ -537,34 +841,358 @@ def test_param_flux_cuts():
assert np.all(0.2 < sI < 1.5)
-def test_point_catalog_reader():
- catfile = os.path.join(SKY_DATA_PATH, "pointsource_catalog.txt")
- srcs = skymodel.read_text_catalog(catfile)
+@pytest.mark.parametrize(
+ "spec_type, init_kwargs, cut_kwargs",
+ [
+ ("flat", {}, {}),
+ ("flat", {"reference_frequency": np.ones(20) * 200e6 * units.Hz}, {}),
+ ("full", {"freq_array": np.array([1e8, 1.5e8]) * units.Hz}, {}),
+ (
+ "subband",
+ {"freq_array": np.array([1e8, 1.5e8]) * units.Hz},
+ {"freq_range": np.array([0.9e8, 2e8]) * units.Hz},
+ ),
+ (
+ "subband",
+ {"freq_array": np.array([1e8, 1.5e8]) * units.Hz},
+ {"freq_range": np.array([1.1e8, 2e8]) * units.Hz},
+ ),
+ (
+ "flat",
+ {"freq_array": np.array([1e8]) * units.Hz},
+ {"freq_range": np.array([0.9e8, 2e8]) * units.Hz},
+ ),
+ ],
+)
+def test_flux_cuts(spec_type, init_kwargs, cut_kwargs):
+ Nsrcs = 20
- with open(catfile, "r") as fhandle:
- header = fhandle.readline()
- header = [h.strip() for h in header.split()]
- dt = np.format_parser(
- ["U10", "f8", "f8", "f8", "f8"],
- ["source_id", "ra_j2000", "dec_j2000", "flux_density_I", "frequency"],
- header,
+ minflux = 0.5
+ maxflux = 3.0
+
+ ids = ["src{}".format(i) for i in range(Nsrcs)]
+ ras = Longitude(np.random.uniform(0, 360.0, Nsrcs), units.deg)
+ decs = Latitude(np.linspace(-90, 90, Nsrcs), units.deg)
+ stokes = np.zeros((4, 1, Nsrcs), dtype=np.float)
+ if spec_type == "flat":
+ stokes[0, :, :] = np.linspace(minflux, maxflux, Nsrcs)
+ else:
+ stokes = np.zeros((4, 2, Nsrcs), dtype=np.float)
+ stokes[0, 0, :] = np.linspace(minflux, maxflux / 2.0, Nsrcs)
+ stokes[0, 1, :] = np.linspace(minflux * 2.0, maxflux, Nsrcs)
+
+ skymodel_obj = skymodel.SkyModel(ids, ras, decs, stokes, spec_type, **init_kwargs)
+ catalog_table = skymodel.skymodel_to_array(skymodel_obj)
+
+ minI_cut = 1.0
+ maxI_cut = 2.3
+
+ cut_sourcelist = skymodel.source_cuts(
+ catalog_table,
+ latitude_deg=30.0,
+ min_flux=minI_cut,
+ max_flux=maxI_cut,
+ **cut_kwargs,
)
- catalog_table = np.genfromtxt(
- catfile, autostrip=True, skip_header=1, dtype=dt.dtype
+ if "freq_range" in cut_kwargs and np.min(
+ cut_kwargs["freq_range"] > np.min(init_kwargs["freq_array"])
+ ):
+ assert np.all(cut_sourcelist["flux_density_I"] < maxI_cut)
+ else:
+ assert np.all(cut_sourcelist["flux_density_I"] > minI_cut)
+ assert np.all(cut_sourcelist["flux_density_I"] < maxI_cut)
+
+
+@pytest.mark.parametrize(
+ "spec_type, init_kwargs, cut_kwargs, error_category, error_message",
+ [
+ (
+ "spectral_index",
+ {
+ "reference_frequency": np.ones(20) * 200e6 * units.Hz,
+ "spectral_index": np.ones(20) * 0.8,
+ },
+ {},
+ NotImplementedError,
+ "Flux cuts with spectral index type objects is not supported yet.",
+ ),
+ (
+ "full",
+ {"freq_array": np.array([1e8, 1.5e8]) * units.Hz},
+ {"freq_range": [0.9e8, 2e8]},
+ ValueError,
+ "freq_range must be an astropy Quantity.",
+ ),
+ (
+ "subband",
+ {"freq_array": np.array([1e8, 1.5e8]) * units.Hz},
+ {"freq_range": 0.9e8 * units.Hz},
+ ValueError,
+ "freq_range must have 2 elements.",
+ ),
+ (
+ "subband",
+ {"freq_array": np.array([1e8, 1.5e8]) * units.Hz},
+ {"freq_range": np.array([1.1e8, 1.4e8]) * units.Hz},
+ ValueError,
+ "No frequencies in freq_range.",
+ ),
+ ],
+)
+def test_source_cut_error(
+ spec_type, init_kwargs, cut_kwargs, error_category, error_message
+):
+ Nsrcs = 20
+
+ minflux = 0.5
+ maxflux = 3.0
+
+ ids = ["src{}".format(i) for i in range(Nsrcs)]
+ ras = Longitude(np.random.uniform(0, 360.0, Nsrcs), units.deg)
+ decs = Latitude(np.linspace(-90, 90, Nsrcs), units.deg)
+ stokes = np.zeros((4, 1, Nsrcs), dtype=np.float)
+ stokes = np.zeros((4, 1, Nsrcs), dtype=np.float)
+ if spec_type == "flat" or spec_type == "spectral_index":
+ stokes[0, :, :] = np.linspace(minflux, maxflux, Nsrcs)
+ else:
+ stokes = np.zeros((4, 2, Nsrcs), dtype=np.float)
+ stokes[0, 0, :] = np.linspace(minflux, maxflux / 2.0, Nsrcs)
+ stokes[0, 1, :] = np.linspace(minflux * 2.0, maxflux, Nsrcs)
+
+ skymodel_obj = skymodel.SkyModel(ids, ras, decs, stokes, spec_type, **init_kwargs)
+ catalog_table = skymodel.skymodel_to_array(skymodel_obj)
+
+ minI_cut = 1.0
+ maxI_cut = 2.3
+
+ with pytest.raises(error_category, match=error_message):
+ skymodel.source_cuts(
+ catalog_table,
+ latitude_deg=30.0,
+ min_flux=minI_cut,
+ max_flux=maxI_cut,
+ **cut_kwargs,
+ )
+
+
+def test_circumpolar_nonrising(time_location):
+ # Check that the source_cut function correctly identifies sources that are circumpolar or
+ # won't rise.
+ # Working with an observatory at the HERA latitude.
+
+ time, location = time_location
+
+ Ntimes = 100
+ Nras = 20
+ Ndecs = 20
+ Nsrcs = Nras * Ndecs
+
+ times = time + TimeDelta(np.linspace(0, 1.0, Ntimes), format="jd")
+ lon = location.lon.deg
+ ra = np.linspace(lon - 90, lon + 90, Nras)
+ dec = np.linspace(-90, 90, Ndecs)
+
+ ra, dec = map(np.ndarray.flatten, np.meshgrid(ra, dec))
+ ra = Longitude(ra, units.deg)
+ dec = Latitude(dec, units.deg)
+
+ names = ["src{}".format(i) for i in range(Nsrcs)]
+ stokes = np.zeros((4, 1, Nsrcs))
+ stokes[0, ...] = 1.0
+
+ sky = skymodel.SkyModel(names, ra, dec, stokes, "flat")
+
+ src_arr = skymodel.skymodel_to_array(sky)
+ src_arr = skymodel.source_cuts(src_arr, latitude_deg=location.lat.deg)
+
+ # Boolean array identifying nonrising sources that were removed by source_cuts
+ nonrising = np.array(
+ [sky.name[ind] not in src_arr["source_id"] for ind in range(Nsrcs)]
)
+ is_below_horizon = np.zeros(Nsrcs).astype(bool)
+ is_below_horizon[nonrising] = True
+
+ alts, azs = [], []
+ for ti in range(Ntimes):
+ sky.update_positions(times[ti], location)
+ alts.append(sky.alt_az[0])
+ azs.append(sky.alt_az[1])
+
+ # Check that sources below the horizon by coarse cut are
+ # indeed below the horizon.
+ lst = times[ti].sidereal_time("mean").rad
+ dt0 = lst - src_arr["rise_lst"]
+ dt1 = src_arr["set_lst"] - src_arr["rise_lst"]
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", message="invalid value encountered", category=RuntimeWarning
+ )
- assert sorted(srcs.name) == sorted(catalog_table["source_id"])
- assert srcs.ra.deg in catalog_table["ra_j2000"]
- assert srcs.dec.deg in catalog_table["dec_j2000"]
- assert srcs.stokes[0] in catalog_table["flux_density_I"]
+ dt0[dt0 < 0] += 2 * np.pi
+ dt1[dt1 < 0] += 2 * np.pi
+
+ is_below_horizon[~nonrising] = dt0 > dt1
+ assert np.all(sky.alt_az[0][is_below_horizon] < 0.0)
+
+ alts = np.degrees(alts)
+
+ # Check that the circumpolar and nonrising sources match expectation
+ # from the tan(lat) * tan(dec) values.
+ nonrising_test = np.where(np.all(alts < 0, axis=0))[0]
+ circumpolar_test = np.where(np.all(alts > 0, axis=0))[0]
+
+ tans = np.tan(location.lat.rad) * np.tan(dec.rad)
+ nonrising_calc = np.where(tans < -1)
+ circumpolar_calc = np.where(tans > 1)
+ assert np.all(circumpolar_calc == circumpolar_test)
+ assert np.all(nonrising_calc == nonrising_test)
+
+ # Confirm that the source cuts excluded the non-rising sources.
+ assert np.all(np.where(nonrising)[0] == nonrising_test)
+
+ # check that rise_lst and set_lst get added to object when converted
+ new_sky = skymodel.array_to_skymodel(src_arr)
+ assert hasattr(new_sky, "_rise_lst")
+ assert hasattr(new_sky, "_set_lst")
+
+ # and that it's round tripped
+ src_arr2 = skymodel.skymodel_to_array(new_sky)
+ assert src_arr.dtype == src_arr2.dtype
+ assert len(src_arr) == len(src_arr2)
+
+ for name in src_arr.dtype.names:
+ if isinstance(src_arr[name][0], (str,)):
+ assert np.array_equal(src_arr[name], src_arr2[name])
+ else:
+ assert np.allclose(src_arr[name], src_arr2[name], equal_nan=True)
+
+
+@pytest.mark.parametrize(
+ "name_to_match, name_list, kwargs, result",
+ [
+ ("gle", ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"], {}, "GLEAM"),
+ (
+ "raj2000",
+ ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"],
+ {},
+ "RAJ2000",
+ ),
+ (
+ "ra",
+ ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"],
+ {"exclude_start_pattern": "_"},
+ "RAJ2000",
+ ),
+ (
+ "foo",
+ ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"],
+ {"brittle": False},
+ None,
+ ),
+ ],
+)
+def test_get_matching_fields(name_to_match, name_list, kwargs, result):
+ assert skymodel._get_matching_fields(name_to_match, name_list, **kwargs) == result
+
+
+@pytest.mark.parametrize(
+ "name_to_match, name_list, error_message",
+ [
+ (
+ "j2000",
+ ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"],
+ "More than one match for j2000 in",
+ ),
+ (
+ "foo",
+ ["_RAJ2000", "_DEJ2000", "RAJ2000", "DEJ2000", "GLEAM"],
+ "No match for foo in",
+ ),
+ ],
+)
+def test_get_matching_fields_errors(name_to_match, name_list, error_message):
+ with pytest.raises(ValueError, match=error_message):
+ skymodel._get_matching_fields(name_to_match, name_list)
+
+
+@pytest.mark.parametrize("spec_type", ["flat", "subband"])
+def test_read_gleam(spec_type):
+ skymodel_obj = skymodel.read_gleam_catalog(GLEAM_vot, spectral_type=spec_type)
+
+ assert skymodel_obj.Ncomponents == 50
+ if spec_type == "subband":
+ assert skymodel_obj.Nfreqs == 20
# Check cuts
- source_select_kwds = {"min_flux": 1.0}
- catalog = skymodel.read_text_catalog(
- catfile, source_select_kwds=source_select_kwds, return_table=True
+ source_select_kwds = {"min_flux": 0.5}
+ cut_catalog = skymodel.read_gleam_catalog(
+ GLEAM_vot,
+ spectral_type=spec_type,
+ source_select_kwds=source_select_kwds,
+ return_table=True,
)
- assert len(catalog) == 2
+
+ assert len(cut_catalog) < skymodel_obj.Ncomponents
+
+ cut_obj = skymodel.read_gleam_catalog(
+ GLEAM_vot, spectral_type=spec_type, source_select_kwds=source_select_kwds
+ )
+
+ assert len(cut_catalog) == cut_obj.Ncomponents
+
+ source_select_kwds = {"min_flux": 10.0}
+ with pytest.warns(UserWarning, match="All sources eliminated by cuts."):
+ skymodel.read_gleam_catalog(
+ GLEAM_vot,
+ spectral_type=spec_type,
+ source_select_kwds=source_select_kwds,
+ return_table=True,
+ )
+
+
+def test_read_gleam_errors():
+ with pytest.raises(ValueError, match="spectral_type full is not an allowed type"):
+ skymodel.read_gleam_catalog(GLEAM_vot, spectral_type="full")
+
+
+def test_read_deprecated_votable():
+ votable_file = os.path.join(SKY_DATA_PATH, "single_source.vot")
+
+ with pytest.warns(
+ DeprecationWarning,
+ match=(
+ f"File {votable_file} contains tables with no name or ID, "
+ "Support for such files is deprecated."
+ ),
+ ):
+ skymodel_obj = skymodel.read_votable_catalog(votable_file)
+
+ assert skymodel_obj.Ncomponents == 1
+
+ with pytest.raises(ValueError, match=("More than one matching table.")):
+ skymodel.read_votable_catalog(votable_file, id_column="de")
+
+
+def test_read_votable_errors():
+
+ # fmt: off
+ flux_columns = ["Fint076", "Fint084", "Fint092", "Fint099", "Fint107",
+ "Fint115", "Fint122", "Fint130", "Fint143", "Fint151",
+ "Fint158", "Fint166", "Fint174", "Fint181", "Fint189",
+ "Fint197", "Fint204", "Fint212", "Fint220", "Fint227"]
+ # fmt: on
+ with pytest.raises(
+ ValueError, match="freq_array must be provided for multiple flux columns."
+ ):
+ skymodel.read_votable_catalog(
+ GLEAM_vot, flux_columns=flux_columns, reference_frequency=200e6 * units.Hz
+ )
+
+ with pytest.raises(
+ ValueError, match="reference_frequency must be an astropy Quantity."
+ ):
+ skymodel.read_votable_catalog(GLEAM_vot, reference_frequency=200e6)
def test_idl_catalog_reader():
@@ -587,90 +1215,34 @@ def test_idl_catalog_reader_extended_sources():
assert len(sourcelist.ra) == len(catalog) - len(ext_inds) + sum(ext_Ncomps)
-def test_flux_cuts():
- Nsrcs = 20
+def test_point_catalog_reader():
+ catfile = os.path.join(SKY_DATA_PATH, "pointsource_catalog.txt")
+ srcs = skymodel.read_text_catalog(catfile)
+ with open(catfile, "r") as fhandle:
+ header = fhandle.readline()
+ header = [h.strip() for h in header.split()]
dt = np.format_parser(
["U10", "f8", "f8", "f8", "f8"],
["source_id", "ra_j2000", "dec_j2000", "flux_density_I", "frequency"],
- [],
- )
-
- minflux = 0.5
- maxflux = 3.0
-
- catalog_table = np.recarray(Nsrcs, dtype=dt.dtype)
- catalog_table["source_id"] = ["src{}".format(i) for i in range(Nsrcs)]
- catalog_table["ra_j2000"] = np.random.uniform(0, 360.0, Nsrcs)
- catalog_table["dec_j2000"] = np.linspace(-90, 90, Nsrcs)
- catalog_table["flux_density_I"] = np.linspace(minflux, maxflux, Nsrcs)
- catalog_table["frequency"] = np.ones(Nsrcs) * 200e6
-
- minI_cut = 1.0
- maxI_cut = 2.3
-
- cut_sourcelist = skymodel.source_cuts(
- catalog_table, latitude_deg=30.0, min_flux=minI_cut, max_flux=maxI_cut
+ header,
)
- assert np.all(cut_sourcelist["flux_density_I"] > minI_cut)
- assert np.all(cut_sourcelist["flux_density_I"] < maxI_cut)
-
-def test_circumpolar_nonrising():
- # Check that the source_cut function correctly identifies sources that are circumpolar or
- # won't rise.
- # Working with an observatory at the HERA latitude
-
- lat = -31.0
- lon = 0.0
-
- Ntimes = 100
- Nsrcs = 50
-
- j2000 = 2451545.0
- times = Time(
- np.linspace(j2000 - 0.5, j2000 + 0.5, Ntimes), format="jd", scale="utc"
+ catalog_table = np.genfromtxt(
+ catfile, autostrip=True, skip_header=1, dtype=dt.dtype
)
- ra = np.zeros(Nsrcs)
- dec = np.linspace(-90, 90, Nsrcs)
-
- ra = Angle(ra, units.deg)
- dec = Angle(dec, units.deg)
-
- coord = SkyCoord(ra=ra, dec=dec, frame="icrs")
- alts = []
- azs = []
-
- loc = EarthLocation.from_geodetic(lat=lat, lon=lon)
- for i in range(Ntimes):
- altaz = coord.transform_to(AltAz(obstime=times[i], location=loc))
- alts.append(altaz.alt.deg)
- azs.append(altaz.az.deg)
- alts = np.array(alts)
-
- nonrising = np.where(np.all(alts < 0, axis=0))[0]
- circumpolar = np.where(np.all(alts > 0, axis=0))[0]
-
- tans = np.tan(np.radians(lat)) * np.tan(dec.rad)
- nonrising_check = np.where(tans < -1)
- circumpolar_check = np.where(tans > 1)
- assert np.all(circumpolar_check == circumpolar)
- assert np.all(nonrising_check == nonrising)
-
-
-def test_read_gleam():
- sourcelist = skymodel.read_votable_catalog(GLEAM_vot)
-
- assert sourcelist.Ncomponents == 50
+ assert sorted(srcs.name) == sorted(catalog_table["source_id"])
+ assert srcs.ra.deg in catalog_table["ra_j2000"]
+ assert srcs.dec.deg in catalog_table["dec_j2000"]
+ assert srcs.stokes[0] in catalog_table["flux_density_I"]
# Check cuts
source_select_kwds = {"min_flux": 1.0}
- catalog = skymodel.read_votable_catalog(
- GLEAM_vot, source_select_kwds=source_select_kwds, return_table=True
+ catalog = skymodel.read_text_catalog(
+ catfile, source_select_kwds=source_select_kwds, return_table=True
)
-
- assert len(catalog) < sourcelist.Ncomponents
+ assert len(catalog) == 2
def test_catalog_file_writer():
@@ -690,9 +1262,8 @@ def test_catalog_file_writer():
dec = icrs_coord.dec
names = "zen_source"
- stokes = [1, 0, 0, 0]
- freqs = [1e8]
- zenith_source = skymodel.SkyModel(names, ra, dec, stokes, freqs, "flat")
+ stokes = [1.0, 0, 0, 0]
+ zenith_source = skymodel.SkyModel(names, ra, dec, stokes, "flat")
fname = os.path.join(SKY_DATA_PATH, "temp_cat.txt")
@@ -702,72 +1273,172 @@ def test_catalog_file_writer():
os.remove(fname)
-def test_array_to_skymodel_loop():
- sky = skymodel.read_votable_catalog(GLEAM_vot)
- sky.ra = Angle(sky.ra.rad, "rad")
- sky.dec = Angle(sky.dec.rad, "rad")
- arr = skymodel.skymodel_to_array(sky)
- sky2 = skymodel.array_to_skymodel(arr)
+@pytest.mark.parametrize("spec_type", ["flat", "subband", "spectral_index", "full"])
+def test_text_catalog_loop(spec_type):
+ if spec_type == "full":
+ spectral_type = "subband"
+ else:
+ spectral_type = spec_type
+
+ sky = skymodel.read_gleam_catalog(GLEAM_vot, spectral_type=spectral_type)
+ if spec_type == "full":
+ sky.spectral_type = "full"
+
+ # This should be removed after pyuvdata PR #790 is merged.
+ # GLEAM has NaNs for the spectral_index of some sources
+ # Currently, arrays with NaNs are never equal even if they are equal where
+ # they are not nan and are nan in the same locations.
+ # So remove those components for the round trip equality test.
+ if spectral_type == "spectral_index":
+ wh_not_nan = np.squeeze(np.argwhere(~np.isnan(sky.spectral_index)))
+ sky.Ncomponents = wh_not_nan.size
+ sky.ra = sky.ra[wh_not_nan]
+ sky.dec = sky.dec[wh_not_nan]
+ sky.name = sky.name[wh_not_nan]
+ sky.reference_frequency = sky.reference_frequency[wh_not_nan]
+ sky.spectral_index = sky.spectral_index[wh_not_nan]
+ sky.stokes = sky.stokes[:, :, wh_not_nan]
+ sky.coherency_radec = skyutils.stokes_to_coherency(sky.stokes)
- assert np.allclose((sky.ra - sky2.ra).rad, 0.0)
- assert np.allclose((sky.dec - sky2.dec).rad, 0.0)
+ fname = os.path.join(SKY_DATA_PATH, "temp_cat.txt")
+ skymodel.write_catalog_to_file(fname, sky)
+ sky2 = skymodel.read_text_catalog(fname)
+ sky_arr2 = skymodel.read_text_catalog(fname, return_table=True)
+ sky3 = skymodel.array_to_skymodel(sky_arr2)
+ os.remove(fname)
+ assert sky == sky2
+ assert sky == sky3
-class TestMoon:
- """
- Series of tests for Moon-based observers
- """
+ if spec_type == "flat":
+ # again with no reference_frequency field
+ reference_frequency = sky.reference_frequency
+ sky.reference_frequency = None
+ arr = skymodel.skymodel_to_array(sky)
+ sky2 = skymodel.array_to_skymodel(arr)
- def setup(self):
- pytest.importorskip("lunarsky")
+ assert sky == sky2
- from lunarsky import MoonLocation, SkyCoord as SkyC
+ # again with flat & freq_array
+ sky.freq_array = np.atleast_1d(np.unique(reference_frequency))
+ arr = skymodel.skymodel_to_array(sky)
+ sky2 = skymodel.array_to_skymodel(arr)
- # Tranquility base
- self.array_location = MoonLocation(lat="00d41m15s", lon="23d26m00s", height=0.0)
+ assert sky == sky2
- self.time = Time.now()
- self.zen_coord = SkyC(
- alt=Angle(90, unit=units.deg),
- az=Angle(0, unit=units.deg),
- obstime=self.time,
- frame="lunartopo",
- location=self.array_location,
- )
- icrs_coord = self.zen_coord.transform_to("icrs")
+@pytest.mark.parametrize("freq_mult", [1e-6, 1e-3, 1e3])
+def test_text_catalog_loop_other_freqs(freq_mult):
+ sky = skymodel.read_gleam_catalog(GLEAM_vot)
+ sky.freq_array = np.atleast_1d(np.unique(sky.reference_frequency) * freq_mult)
+ sky.reference_frequency = None
+
+ fname = os.path.join(SKY_DATA_PATH, "temp_cat.txt")
+ skymodel.write_catalog_to_file(fname, sky)
+ sky2 = skymodel.read_text_catalog(fname)
+ print(sky.spectral_type)
+ print(sky2.spectral_type)
+ os.remove(fname)
+
+ assert sky == sky2
+
+
+@pytest.mark.parametrize("spec_type", ["flat", "subband"])
+def test_read_text_source_cuts(spec_type):
+
+ sky = skymodel.read_gleam_catalog(GLEAM_vot, spectral_type=spec_type)
+ fname = os.path.join(SKY_DATA_PATH, "temp_cat.txt")
+ skymodel.write_catalog_to_file(fname, sky)
+
+ source_select_kwds = {"min_flux": 0.5}
+ cut_catalog = skymodel.read_text_catalog(
+ fname, source_select_kwds=source_select_kwds, return_table=True,
+ )
+
+ assert len(cut_catalog) < sky.Ncomponents
+
+ cut_obj = skymodel.read_text_catalog(fname, source_select_kwds=source_select_kwds)
+ os.remove(fname)
+
+ assert len(cut_catalog) == cut_obj.Ncomponents
+
+
+def test_pyuvsim_mock_catalog_read():
+ mock_cat_file = os.path.join(SKY_DATA_PATH, "mock_hera_text_2458098.27471.txt")
+
+ mock_sky = skymodel.read_text_catalog(mock_cat_file)
+ expected_names = ["src" + str(val) for val in np.arange(mock_sky.Ncomponents)]
+ assert mock_sky.name.tolist() == expected_names
+
- ra = icrs_coord.ra
- dec = icrs_coord.dec
- names = "zen_source"
- stokes = [1, 0, 0, 0]
- freqs = [1e8]
- self.zenith_source = skymodel.SkyModel(names, ra, dec, stokes, freqs, "flat")
+def test_read_text_errors():
+ sky = skymodel.read_gleam_catalog(GLEAM_vot, spectral_type="subband")
- self.zenith_source.update_positions(self.time, self.array_location)
+ fname = os.path.join(SKY_DATA_PATH, "temp_cat.txt")
+ skymodel.write_catalog_to_file(fname, sky)
+ with fileinput.input(files=fname, inplace=True) as infile:
+ for line in infile:
+ line = line.replace("Flux_subband_76_MHz [Jy]", "Frequency [Hz]")
+ print(line, end="")
+
+ with pytest.raises(
+ ValueError,
+ match="If frequency column is present, only one flux column allowed.",
+ ):
+ skymodel.read_text_catalog(fname)
+
+ skymodel.write_catalog_to_file(fname, sky)
+ with fileinput.input(files=fname, inplace=True) as infile:
+ for line in infile:
+ line = line.replace("Flux_subband_76_MHz [Jy]", "Flux [Jy]")
+ print(line, end="")
+
+ with pytest.raises(
+ ValueError,
+ match="Multiple flux fields, but they do not all contain a frequency.",
+ ):
+ skymodel.read_text_catalog(fname)
+
+ skymodel.write_catalog_to_file(fname, sky)
+ with fileinput.input(files=fname, inplace=True) as infile:
+ for line in infile:
+ line = line.replace("SOURCE_ID", "NAME")
+ print(line, end="")
+
+ with pytest.raises(ValueError, match="Header does not match expectations."):
+ skymodel.read_text_catalog(fname)
+
+ os.remove(fname)
+
+
+def test_zenith_on_moon(moonsky):
+ """Source at zenith from the Moon."""
+
+ zenith_source = moonsky
+ zenith_source.check()
+
+ zenith_source_lmn = zenith_source.pos_lmn.squeeze()
+ assert np.allclose(zenith_source_lmn, np.array([0, 0, 1]))
- def test_zenith_on_moon(self):
- """Source at zenith from the Moon."""
- zenith_source_lmn = self.zenith_source.pos_lmn.squeeze()
- assert np.allclose(zenith_source_lmn, np.array([0, 0, 1]))
+def test_source_motion(moonsky):
+ """ Check that period is about 28 days."""
- def test_source_motion(self):
- """ Check that period is about 28 days."""
+ zenith_source = moonsky
- Ntimes = 500
- ets = np.linspace(0, 4 * 28 * 24 * 3600, Ntimes)
- times = self.time + TimeDelta(ets, format="sec")
+ Ntimes = 500
+ ets = np.linspace(0, 4 * 28 * 24 * 3600, Ntimes)
+ times = zenith_source.time + TimeDelta(ets, format="sec")
- lmns = np.zeros((Ntimes, 3))
- for ti in range(Ntimes):
- self.zenith_source.update_positions(times[ti], self.array_location)
- lmns[ti] = self.zenith_source.pos_lmn.squeeze()
- _els = np.fft.fft(lmns[:, 0])
- dt = np.diff(ets)[0]
- _freqs = np.fft.fftfreq(Ntimes, d=dt)
+ lmns = np.zeros((Ntimes, 3))
+ for ti in range(Ntimes):
+ zenith_source.update_positions(times[ti], zenith_source.telescope_location)
+ lmns[ti] = zenith_source.pos_lmn.squeeze()
+ _els = np.fft.fft(lmns[:, 0])
+ dt = np.diff(ets)[0]
+ _freqs = np.fft.fftfreq(Ntimes, d=dt)
- f_28d = 1 / (28 * 24 * 3600.0)
+ f_28d = 1 / (28 * 24 * 3600.0)
- maxf = _freqs[np.argmax(np.abs(_els[_freqs > 0]) ** 2)]
- assert np.isclose(maxf, f_28d, atol=2 / ets[-1])
+ maxf = _freqs[np.argmax(np.abs(_els[_freqs > 0]) ** 2)]
+ assert np.isclose(maxf, f_28d, atol=2 / ets[-1])
diff --git a/pyradiosky/utils.py b/pyradiosky/utils.py
index 17f10537..0a0c4dcb 100644
--- a/pyradiosky/utils.py
+++ b/pyradiosky/utils.py
@@ -82,7 +82,7 @@ def stokes_to_coherency(stokes_vector):
Returns
-------
coherency matrix : array of float
- Array of coherencies, shape (2, 2) or (2, 2, Ncomponents)
+ Array of coherencies, shape (2, 2) or (2, 2, Nfreqs, Ncomponents)
"""
stokes_arr = np.atleast_1d(np.asarray(stokes_vector))
initial_shape = stokes_arr.shape
diff --git a/setup.py b/setup.py
index 7528ba3a..7c2d1293 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ class MockVersion:
"scripts": glob.glob("scripts/*"),
"version": version.version,
"include_package_data": True,
- "install_requires": ["numpy>=1.15", "scipy", "astropy>=4.0", "h5py"],
+ "install_requires": ["numpy>=1.15", "scipy", "astropy>=4.0", "h5py", "pyuvdata"],
"tests_require": ["pytest"],
"extras_require": {
"healpix": ["astropy-healpix"],