From 2f8b5d4f506c25e9595183f0caec63f87ed21373 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 18 Apr 2024 14:44:01 -0700 Subject: [PATCH 01/59] get telescope refactor working on UVData --- pyuvdata/__init__.py | 2 +- pyuvdata/ms_utils.py | 4 +- pyuvdata/telescopes.py | 454 ++++++++++++++------- pyuvdata/uvbase.py | 53 ++- pyuvdata/uvdata/fhd.py | 15 +- pyuvdata/uvdata/initializers.py | 11 +- pyuvdata/uvdata/mir.py | 12 +- pyuvdata/uvdata/mir_parser.py | 6 +- pyuvdata/uvdata/miriad.py | 61 ++- pyuvdata/uvdata/ms.py | 15 +- pyuvdata/uvdata/mwa_corr_fits.py | 15 +- pyuvdata/uvdata/tests/conftest.py | 6 +- pyuvdata/uvdata/tests/test_fhd.py | 18 +- pyuvdata/uvdata/tests/test_initializers.py | 4 +- pyuvdata/uvdata/tests/test_mir.py | 4 +- pyuvdata/uvdata/tests/test_miriad.py | 26 +- pyuvdata/uvdata/tests/test_ms.py | 39 +- pyuvdata/uvdata/tests/test_uvdata.py | 94 ++--- pyuvdata/uvdata/tests/test_uvfits.py | 57 +-- pyuvdata/uvdata/tests/test_uvh5.py | 25 +- pyuvdata/uvdata/uvdata.py | 403 +++++++++--------- pyuvdata/uvdata/uvfits.py | 30 +- pyuvdata/uvdata/uvh5.py | 51 +-- 23 files changed, 753 insertions(+), 652 deletions(-) diff --git a/pyuvdata/__init__.py b/pyuvdata/__init__.py index 4d90ba393f..99980d70a9 100644 --- a/pyuvdata/__init__.py +++ b/pyuvdata/__init__.py @@ -30,7 +30,7 @@ warnings.filterwarnings("ignore", message="numpy.dtype size changed") warnings.filterwarnings("ignore", message="numpy.ufunc size changed") -from .telescopes import Telescope, get_telescope, known_telescopes # noqa +from .telescopes import Telescope, known_telescopes # noqa from .uvbeam import UVBeam # noqa from .uvcal import UVCal # noqa from .uvdata import FastUVH5Meta # noqa diff --git a/pyuvdata/ms_utils.py b/pyuvdata/ms_utils.py index 4258373bbf..63c7825817 100644 --- a/pyuvdata/ms_utils.py +++ b/pyuvdata/ms_utils.py @@ -408,8 +408,8 @@ def write_ms_antenna( antenna_positions = uvobj.antenna_positions antenna_diameters = uvobj.antenna_diameters telescope_location = uvobj.telescope_location - telescope_frame = uvobj._telescope_location.frame - telescope_ellipsoid = uvobj._telescope_location.ellipsoid + telescope_frame = uvobj.telescope._location.frame + telescope_ellipsoid = uvobj.telescope._location.ellipsoid tabledesc = tables.required_ms_desc("ANTENNA") dminfo = tables.makedminfo(tabledesc) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 77a5858f87..c81aa28ac5 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -4,6 +4,7 @@ """Telescope information and known telescope list.""" import os +import warnings import numpy as np from astropy.coordinates import Angle, EarthLocation @@ -11,9 +12,10 @@ from pyuvdata.data import DATA_PATH from . import parameter as uvp +from . import utils as uvutils from . import uvbase -__all__ = ["Telescope", "known_telescopes", "get_telescope"] +__all__ = ["Telescope", "known_telescopes"] # We use astropy sites for telescope locations. The dict below is for # telescopes not in astropy sites, or to include extra information for a telescope. @@ -39,7 +41,7 @@ "latitude": Angle("-30.72152612068925d").radian, "longitude": Angle("21.42830382686301d").radian, "altitude": 1051.69, - "diameters": 14.0, + "antenna_diameters": 14.0, "antenna_positions_file": "hera_ant_pos.csv", "citation": ( "value taken from hera_mc geo.py script " @@ -71,6 +73,59 @@ } +def _parse_antpos_file(antenna_positions_file): + """ + Interpret the antenna positions file. + + Parameters + ---------- + antenna_positions_file : str + Name of the antenna_positions_file, which is assumed to be in DATA_PATH. + Should contain antenna names, numbers and ECEF positions relative to the + telescope location. + + Returns + ------- + antenna_names : array of str + Antenna names. + antenna_names : array of int + Antenna numbers. + antenna_positions : array of float + Antenna positions in ECEF relative to the telescope location. + + """ + columns = ["name", "number", "x", "y", "z"] + formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble] + + dt = np.format_parser(formats, columns, []) + ant_array = np.genfromtxt( + antenna_positions_file, + delimiter=",", + autostrip=True, + skip_header=1, + dtype=dt.dtype, + ) + antenna_names = ant_array["name"] + antenna_numbers = ant_array["number"] + antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T + + return antenna_names, antenna_numbers, antenna_positions.astype("float") + + +def known_telescopes(): + """ + Get list of known telescopes. + + Returns + ------- + list of str + List of known telescope names. + """ + astropy_sites = [site for site in EarthLocation.get_site_names() if site != ""] + known_telescopes = list(set(astropy_sites + list(KNOWN_TELESCOPES.keys()))) + return known_telescopes + + class Telescope(uvbase.UVBase): """ A class for defining a telescope for use with UVData objects. @@ -94,212 +149,305 @@ def __init__(self): # use the same names as in UVData so they can be automatically set self.citation = None - self._telescope_name = uvp.UVParameter( - "telescope_name", description="name of telescope " "(string)", form="str" + self._name = uvp.UVParameter( + "name", description="name of telescope (string)", form="str" ) desc = ( "telescope location: xyz in ITRF (earth-centered frame). " "Can also be set using telescope_location_lat_lon_alt or " "telescope_location_lat_lon_alt_degrees properties" ) - self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - acceptable_range=(6.35e6, 6.39e6), - tols=1e-3, - ) - desc = ( - "Antenna diameters in meters. Used by CASA to " - "construct a default beam if no beam is supplied." - ) - self._antenna_diameters = uvp.UVParameter( - "antenna_diameters", + self._location = uvp.LocationParameter("location", description=desc, tols=1e-3) + + self._instrument = uvp.UVParameter( + "instrument", + description="Receiver or backend. Sometimes identical to telescope_name.", required=False, - description=desc, - expected_type=float, - tols=1e-3, # 1 mm + form="str", + expected_type=str, ) desc = "Number of antennas in the array." - self._Nants_telescope = uvp.UVParameter( - "Nants_telescope", required=False, description=desc, expected_type=int + self._Nants = uvp.UVParameter( + "Nants", required=False, description=desc, expected_type=int ) desc = ( - "List of antenna names, shape (Nants_telescope), " + "List of antenna names, shape (Nants), " "with numbers given by antenna_numbers." ) self._antenna_names = uvp.UVParameter( "antenna_names", - required=False, description=desc, - form=("Nants_telescope",), + required=False, + form=("Nants",), expected_type=str, ) desc = ( "List of integer antenna numbers corresponding to antenna_names, " - "shape (Nants_telescope)." + "shape (Nants)." ) self._antenna_numbers = uvp.UVParameter( "antenna_numbers", - required=False, description=desc, - form=("Nants_telescope",), + required=False, + form=("Nants",), expected_type=int, ) desc = ( "Array giving coordinates of antennas relative to " - "telescope_location (ITRF frame), shape (Nants_telescope, 3), " + "telescope_location (ITRF frame), shape (Nants, 3), " "units meters. See the tutorial page in the documentation " "for an example of how to convert this to topocentric frame." ) self._antenna_positions = uvp.UVParameter( "antenna_positions", - required=False, description=desc, - form=("Nants_telescope", 3), + required=False, + form=("Nants", 3), expected_type=float, tols=1e-3, # 1 mm ) - super(Telescope, self).__init__() - - -def known_telescopes(): - """ - Get list of known telescopes. - - Returns - ------- - list of str - List of known telescope names. - """ - astropy_sites = [site for site in EarthLocation.get_site_names() if site != ""] - known_telescopes = list(set(astropy_sites + list(KNOWN_TELESCOPES.keys()))) - return known_telescopes + desc = ( + "Orientation of the physical dipole corresponding to what is " + "labelled as the x polarization. Options are 'east' " + "(indicating east/west orientation) and 'north (indicating " + "north/south orientation)." + ) + self._x_orientation = uvp.UVParameter( + "x_orientation", + description=desc, + required=False, + expected_type=str, + acceptable_vals=["east", "north"], + ) + desc = ( + "Antenna diameters in meters. Used by CASA to " + "construct a default beam if no beam is supplied." + ) + self._antenna_diameters = uvp.UVParameter( + "antenna_diameters", + description=desc, + required=False, + form=("Nants",), + expected_type=float, + tols=1e-3, # 1 mm + ) -def _parse_antpos_file(antenna_positions_file): - """ - Interpret the antenna positions file. + super(Telescope, self).__init__() - Parameters - ---------- - antenna_positions_file : str - Name of the antenna_positions_file, which is assumed to be in DATA_PATH. - Should contain antenna names, numbers and ECEF positions relative to the - telescope location. + def check(self, *, check_extra=True, run_check_acceptability=True): + """ + Add some extra checks on top of checks on UVBase class. - Returns - ------- - antenna_names : array of str - Antenna names. - antenna_names : array of int - Antenna numbers. - antenna_positions : array of float - Antenna positions in ECEF relative to the telescope location. + Check that required parameters exist. Check that parameters have + appropriate shapes and optionally that the values are acceptable. - """ - columns = ["name", "number", "x", "y", "z"] - formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble] + Parameters + ---------- + check_extra : bool + If true, check all parameters, otherwise only check required parameters. + run_check_acceptability : bool + Option to check if values in parameters are acceptable. - dt = np.format_parser(formats, columns, []) - ant_array = np.genfromtxt( - antenna_positions_file, - delimiter=",", - autostrip=True, - skip_header=1, - dtype=dt.dtype, - ) - antenna_names = ant_array["name"] - antenna_numbers = ant_array["number"] - antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T + Returns + ------- + bool + True if check passes - return antenna_names, antenna_numbers, antenna_positions.astype("float") + Raises + ------ + ValueError + if parameter shapes or types are wrong or do not have acceptable + values (if run_check_acceptability is True) + """ + # first run the basic check from UVBase -def get_telescope(telescope_name, telescope_dict_in=None): - """ - Get Telescope object for a telescope in telescope_dict. + super(Telescope, self).check( + check_extra=check_extra, run_check_acceptability=run_check_acceptability + ) - Parameters - ---------- - telescope_name : str - Name of a telescope - telescope_dict_in: dict - telescope info dict. Default is None, meaning use KNOWN_TELESCOPES - (other values are only used for testing) + if run_check_acceptability: + # Check antenna positions + uvutils.check_surface_based_positions( + antenna_positions=self.antenna_positions, + telescope_loc=self.location, + telescope_frame=self._location.frame, + raise_error=False, + ) - Returns - ------- - Telescope object - The Telescope object associated with telescope_name. - """ - if telescope_dict_in is None: - telescope_dict_in = KNOWN_TELESCOPES - - astropy_sites = EarthLocation.get_site_names() - telescope_keys = list(telescope_dict_in.keys()) - telescope_list = [tel.lower() for tel in telescope_keys] - if telescope_name in astropy_sites: - telescope_loc = EarthLocation.of_site(telescope_name) - - obj = Telescope() - obj.telescope_name = telescope_name - obj.citation = "astropy sites" - obj.telescope_location = np.array( - [telescope_loc.x.value, telescope_loc.y.value, telescope_loc.z.value] - ) + return True + + def update_params_from_known_telescopes( + self, + *, + known_telescope_dict=KNOWN_TELESCOPES, + overwrite=False, + warn=True, + run_check=True, + check_extra=True, + run_check_acceptability=True, + ): + """ + Set the parameters based on telescope in known_telescopes. + + Parameters + ---------- + known_telescope_dict: dict + telescope info dict. Default is KNOWN_TELESCOPES + (other values are only used for testing) + + """ + astropy_sites = EarthLocation.get_site_names() + telescope_keys = list(known_telescope_dict.keys()) + telescope_list = [tel.lower() for tel in telescope_keys] + + astropy_sites_list = [] + known_telescope_list = [] + # first deal with location. + if overwrite or self.location is None: + if self.name in astropy_sites: + tel_loc = EarthLocation.of_site(self.name) + + self.citation = "astropy sites" + self.location = np.array( + [tel_loc.x.value, tel_loc.y.value, tel_loc.z.value] + ) + astropy_sites_list.append("telescope_location") + + elif self.name.lower() in telescope_list: + telescope_index = telescope_list.index(self.name.lower()) + telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] + self.citation = telescope_dict["citation"] + + known_telescope_list.append("telescope_location") + if telescope_dict["center_xyz"] is not None: + self.location = telescope_dict["center_xyz"] + else: + if ( + telescope_dict["latitude"] is None + or telescope_dict["longitude"] is None + or telescope_dict["altitude"] is None + ): + raise ValueError( + "Bad location information in known_telescopes_dict " + f"for telescope {self.name}. Either the center_xyz " + "or the latitude, longitude and altitude of the " + "telescope must be specified." + ) + self.location_lat_lon_alt = ( + telescope_dict["latitude"], + telescope_dict["longitude"], + telescope_dict["altitude"], + ) + else: + # no telescope matching this name + raise ValueError( + f"Telescope {self.name} is not in astropy_sites or " + "known_telescopes_dict." + ) - elif telescope_name.lower() in telescope_list: - telescope_index = telescope_list.index(telescope_name.lower()) - telescope_dict = telescope_dict_in[telescope_keys[telescope_index]] - obj = Telescope() - obj.citation = telescope_dict["citation"] - obj.telescope_name = telescope_keys[telescope_index] - if telescope_dict["center_xyz"] is not None: - obj.telescope_location = telescope_dict["center_xyz"] - else: - if ( - telescope_dict["latitude"] is None - or telescope_dict["longitude"] is None - or telescope_dict["altitude"] is None + # check for extra info + if self.name.lower() in telescope_list: + telescope_index = telescope_list.index(self.name.lower()) + telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] + + if "antenna_positions_file" in telescope_dict.keys() and ( + overwrite + or self.antenna_names is None + or self.antenna_numbers is None + or self.antenna_positions is None + or self.Nants is None ): - raise ValueError( - "either the center_xyz or the " - "latitude, longitude and altitude of the " - "telescope must be specified" + antpos_file = os.path.join( + DATA_PATH, telescope_dict["antenna_positions_file"] ) - obj.telescope_location_lat_lon_alt = ( - telescope_dict["latitude"], - telescope_dict["longitude"], - telescope_dict["altitude"], - ) - else: - # no telescope matching this name - return False - - # check for extra info - if telescope_name.lower() in telescope_list: - telescope_index = telescope_list.index(telescope_name.lower()) - telescope_dict = telescope_dict_in[telescope_keys[telescope_index]] - if "diameters" in telescope_dict.keys(): - obj.antenna_diameters = telescope_dict["diameters"] - - if "antenna_positions_file" in telescope_dict.keys(): - antpos_file = os.path.join( - DATA_PATH, telescope_dict["antenna_positions_file"] - ) - antenna_names, antenna_numbers, antenna_positions = _parse_antpos_file( - antpos_file - ) - obj.Nants_telescope = antenna_names.size - obj.antenna_names = antenna_names - obj.antenna_numbers = antenna_numbers - obj.antenna_positions = antenna_positions + antenna_names, antenna_numbers, antenna_positions = _parse_antpos_file( + antpos_file + ) + ant_info = { + "Nants": antenna_names.size, + "antenna_names": antenna_names, + "antenna_numbers": antenna_numbers, + "antenna_positions": antenna_positions, + } + for key, value in ant_info.items(): + if overwrite or getattr(self, key) is None: + known_telescope_list.append(key) + setattr(self, key, value) + + if "antenna_diameters" in telescope_dict.keys() and ( + overwrite or self.antenna_diameters is None + ): + antenna_diameters = np.atleast_1d(telescope_dict["antenna_diameters"]) + if antenna_diameters.size == 1: + known_telescope_list.append("antenna_diameters") + self.antenna_diameters = ( + np.zeros(self.Nants, dtype=float) + antenna_diameters[0] + ) + elif antenna_diameters.size == self.Nants: + known_telescope_list.append("antenna_diameters") + self.antenna_diameters = antenna_diameters + else: + if warn: + warnings.warn( + "antenna_diameters are not set because the number " + "of antenna_diameters on known_telescopes_dict is " + "more than one and does not match Nants for " + f"telescope {self.name}." + ) + + if "x_orientation" in telescope_dict.keys() and ( + overwrite or self.x_orientation is None + ): + known_telescope_list.append("x_orientation") + self.x_orientation = telescope_dict["x_orientation"] + + full_list = astropy_sites_list + known_telescope_list + if warn and len(full_list) > 0: + warn_str = ", ".join(full_list) + " are not set or are being overwritten. " + specific_str = [] + if len(astropy_sites_list) > 0: + specific_str.append( + ", ".join(astropy_sites_list) + " are set using values from " + f"astropy sites for {self.name}." + ) + if len(known_telescope_list) > 0: + specific_str.append( + ", ".join(known_telescope_list) + " are set using values " + f"from known telescopes for {self.name}." + ) + warn_str += " ".join(specific_str) + warnings.warn(warn_str) - obj.check(run_check_acceptability=True) + if run_check: + self.check( + check_extra=check_extra, run_check_acceptability=run_check_acceptability + ) - return obj + @classmethod + def get_telescope_from_known_telescopes(cls, name: str): + """ + Create a new Telescope object using information from known_telescopes. + + Parameters + ---------- + name : str + Name of the telescope. + + Returns + ------- + Telescope + A new Telescope object populated with information from + known_telescopes. + + """ + tel_obj = cls() + tel_obj.name = name + tel_obj.update_params_from_known_telescopes(warn=False) + return tel_obj diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index 52f843eb3b..2e02d5d2a0 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -331,11 +331,20 @@ def __iter__(self, uvparams_only=True): Object attributes, exclusively UVParameter objects if uvparams_only is True. """ - attribute_list = [ - a - for a in dir(self) - if not a.startswith("__") and not callable(getattr(self, a)) - ] + if uvparams_only: + attribute_list = [ + a + for a in dir(self) + if a.startswith("_") + and not a.startswith("__") + and not callable(getattr(self, a)) + ] + else: + attribute_list = [ + a + for a in dir(self) + if not a.startswith("__") and not callable(getattr(self, a)) + ] param_list = [] for a in attribute_list: if uvparams_only: @@ -360,7 +369,9 @@ def required(self): attribute_list = [ a for a in dir(self) - if not a.startswith("__") and not callable(getattr(self, a)) + if a.startswith("_") + and not a.startswith("__") + and not callable(getattr(self, a)) ] required_list = [] for a in attribute_list: @@ -427,14 +438,18 @@ def __eq__( """ if isinstance(other, self.__class__): # only check that required parameters are identical + if hasattr(self, "metadata_only"): + self.metadata_only + other.metadata_only + self_required = set(self.required()) other_required = set(other.required()) if self_required != other_required: if not silent: print( - "Sets of required parameters do not match. " - f"Left is {self_required}," - f" right is {other_required}. Left has " + "Sets of required parameters do not match. \n" + f"Left is {self_required},\n" + f" right is {other_required}.\n\n Left has " f"{self_required.difference(other_required)} extra." f" Right has {other_required.difference(self_required)} extra." ) @@ -479,12 +494,20 @@ def __eq__( self_param = getattr(self, param) other_param = getattr(other, param) if self_param.__ne__(other_param, silent=silent): - if not silent: - print( - f"parameter {param} does not match. Left is " - f"{self_param.value}, right is {other_param.value}." - ) - p_equal = False + if isinstance(self_param.value, UVBase): + if self_param.value.__ne__( + other_param.value, check_extra=check_extra, silent=silent + ): + if not silent: + print(f"parameter {param} does not match.") + p_equal = False + else: + if not silent: + print( + f"parameter {param} does not match. Left is " + f"{self_param.value}, right is {other_param.value}." + ) + p_equal = False if allowed_failures is not None: for param in allowed_failures: diff --git a/pyuvdata/uvdata/fhd.py b/pyuvdata/uvdata/fhd.py index bc9cd5f1f4..65de5cd877 100644 --- a/pyuvdata/uvdata/fhd.py +++ b/pyuvdata/uvdata/fhd.py @@ -13,7 +13,7 @@ from docstring_parser import DocstringStyle from scipy.io import readsav -from .. import telescopes as uvtel +from .. import Telescope from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning @@ -284,16 +284,21 @@ def get_fhd_layout_info( # this is a known issue with FHD runs on cotter uvfits # files for the MWA # compare with the known_telescopes values - telescope_obj = uvtel.get_telescope(telescope_name) + try: + telescope_obj = Telescope.get_telescope_from_known_telescopes( + telescope_name + ) + except ValueError: + telescope_obj = None # start warning message message = ( "Telescope location derived from obs lat/lon/alt " "values does not match the location in the layout file." ) - if telescope_obj is not False: + if telescope_obj is not None: message += " Using the value from known_telescopes." - telescope_location = telescope_obj.telescope_location + telescope_location = telescope_obj.location else: message += ( " Telescope is not in known_telescopes. " @@ -648,7 +653,7 @@ def read_fhd( longitude=longitude, altitude=altitude, radian_tol=uvutils.RADIAN_TOL, - loc_tols=self._telescope_location.tols, + loc_tols=self.telescope._location.tols, obs_tile_names=obs_tile_names, run_check_acceptability=True, ) diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 1e0e34ea72..cc716e1bdd 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -13,7 +13,7 @@ from astropy.coordinates import EarthLocation from astropy.time import Time -from .. import __version__, utils +from .. import Telescope, __version__, utils try: from lunarsky import MoonLocation @@ -596,16 +596,19 @@ def new_uvdata( ) # Now set all the metadata + # initialize telescope object first + obj.telescope = Telescope() + obj.freq_array = freq_array obj.polarization_array = polarization_array obj.antenna_positions = antenna_positions - obj.telescope_location = [ + obj.telescope.location = [ telescope_location.x.to_value("m"), telescope_location.y.to_value("m"), telescope_location.z.to_value("m"), ] - obj._telescope_location.frame = telescope_frame - obj._telescope_location.ellipsoid = ellipsoid + obj.telescope._location.frame = telescope_frame + obj.telescope._location.ellipsoid = ellipsoid obj.telescope_name = telescope_name obj.baseline_array = baseline_array obj.ant_1_array = ant_1_array diff --git a/pyuvdata/uvdata/mir.py b/pyuvdata/uvdata/mir.py index 83e8206e85..bc030871c7 100644 --- a/pyuvdata/uvdata/mir.py +++ b/pyuvdata/uvdata/mir.py @@ -11,11 +11,11 @@ from astropy.time import Time from docstring_parser import DocstringStyle -from .. import get_telescope +from pyuvdata import Telescope, UVData + from .. import utils as uvutils from ..docstrings import copy_replace_short_description from . import mir_parser -from .uvdata import UVData __all__ = ["generate_sma_antpos_dict", "Mir"] @@ -60,7 +60,9 @@ def generate_sma_antpos_dict(filepath): # We need the antenna positions in ECEF, rather than the native rotECEF format that # they are stored in. Get the longitude info, and use the appropriate function in # utils to get these values the way that we want them. - _, lon, _ = get_telescope("SMA")._telescope_location.lat_lon_alt() + _, lon, _ = Telescope.get_telescope_from_known_telescopes( + "SMA" + ).location_lat_lon_alt mir_antpos["xyz_pos"] = uvutils.ECEF_from_rotECEF(mir_antpos["xyz_pos"], lon) # Create a dictionary that can be used for updates. @@ -543,7 +545,9 @@ def _init_from_mir_parser( ] # Get the coordinates from the entry in telescope.py - lat, lon, alt = get_telescope("SMA")._telescope_location.lat_lon_alt() + lat, lon, alt = Telescope.get_telescope_from_known_telescopes( + "SMA" + ).location_lat_lon_alt self.telescope_location_lat_lon_alt = (lat, lon, alt) # Calculate antenna positions in ECEF frame. Note that since both diff --git a/pyuvdata/uvdata/mir_parser.py b/pyuvdata/uvdata/mir_parser.py index ef2322efef..379568c8cf 100644 --- a/pyuvdata/uvdata/mir_parser.py +++ b/pyuvdata/uvdata/mir_parser.py @@ -4130,7 +4130,7 @@ def _make_v3_compliant(self): from astropy.time import Time - from .. import get_telescope + from .. import Telescope from .. import utils as uvutils # First thing -- we only want modern (i.e., SWARM) data, since the older (ASIC) @@ -4139,7 +4139,9 @@ def _make_v3_compliant(self): # if swarm_only: # self.select(where=("correlator", "eq", 1)) # Get SMA coordinates for various data-filling stuff - sma_lat, sma_lon, sma_alt = get_telescope("SMA").telescope_location_lat_lon_alt + sma_lat, sma_lon, sma_alt = Telescope.get_telescope_from_known_telescopes( + "SMA" + ).location_lat_lon_alt # in_data updates: mjd, lst, ara, adec # First sort out the time stamps using the day reference inside codes_data, and diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index 50abb233b2..9875833509 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -15,7 +15,7 @@ from astropy.time import Time from docstring_parser import DocstringStyle -from .. import telescopes as uvtel +from .. import Telescope from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning, reporting_request @@ -296,46 +296,43 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): except KeyError: # get info from known telescopes. # Check to make sure the lat/lon values match reasonably well - telescope_obj = uvtel.get_telescope(self.telescope_name) - if telescope_obj is not False: + try: + telescope_obj = Telescope.get_telescope_from_known_telescopes( + self.telescope_name + ) + except ValueError: + telescope_obj = None + if telescope_obj is not None: tol = 2 * np.pi * 1e-3 / (60.0 * 60.0 * 24.0) # 1mas in radians lat_close = np.isclose( - telescope_obj.telescope_location_lat_lon_alt[0], - latitude, - rtol=0, - atol=tol, + telescope_obj.location_lat_lon_alt[0], latitude, rtol=0, atol=tol ) lon_close = np.isclose( - telescope_obj.telescope_location_lat_lon_alt[1], - longitude, - rtol=0, - atol=tol, + telescope_obj.location_lat_lon_alt[1], longitude, rtol=0, atol=tol ) if correct_lat_lon: self.telescope_location_lat_lon_alt = ( - telescope_obj.telescope_location_lat_lon_alt + telescope_obj.location_lat_lon_alt ) else: self.telescope_location_lat_lon_alt = ( latitude, longitude, - telescope_obj.telescope_location_lat_lon_alt[2], + telescope_obj.location_lat_lon_alt[2], ) if lat_close and lon_close: if correct_lat_lon: warnings.warn( "Altitude is not present in Miriad file, " "using known location values for " - "{telescope_name}.".format( - telescope_name=telescope_obj.telescope_name - ) + f"{telescope_obj.name}." ) else: warnings.warn( "Altitude is not present in Miriad file, " "using known location altitude value " - "for {telescope_name} and lat/lon from " - "file.".format(telescope_name=telescope_obj.telescope_name) + f"for {telescope_obj.name} and lat/lon from " + "file." ) else: warn_string = "Altitude is not present in file " @@ -356,29 +353,23 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): ) if correct_lat_lon: warn_string = ( - warn_string + "for {telescope_name} in known " - "telescopes. Using values from known " - "telescopes.".format( - telescope_name=telescope_obj.telescope_name - ) + warn_string + f"for {telescope_obj.name} in known " + "telescopes. Using values from known telescopes." ) warnings.warn(warn_string) else: warn_string = ( - warn_string + "for {telescope_name} in known " + warn_string + f"for {telescope_obj.name} in known " "telescopes. Using altitude value from known " - "telescopes and lat/lon from " - "file.".format(telescope_name=telescope_obj.telescope_name) + "telescopes and lat/lon from file." ) warnings.warn(warn_string) else: warnings.warn( "Altitude is not present in Miriad file, and " - "telescope {telescope_name} is not in " + f"telescope {self.telescope_name} is not in " "known_telescopes. Telescope location will be " - "set using antenna positions.".format( - telescope_name=self.telescope_name - ) + "set using antenna positions." ) def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): @@ -459,7 +450,7 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): self.telescope_location = np.mean(ecef_antpos[good_antpos, :], axis=0) valid_location = uvutils.check_surface_based_positions( telescope_loc=self.telescope_location, - telescope_frame=self._telescope_location.frame, + telescope_frame=self.telescope._location.frame, raise_error=False, raise_warning=False, ) @@ -1580,7 +1571,11 @@ def read_miriad( # in. self._set_app_coords_helper(pa_only=record_app) try: - self.set_telescope_params() + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) except ValueError as ve: warnings.warn(str(ve)) @@ -1680,7 +1675,7 @@ def write_miriad( """ from . import aipy_extracts - if self._telescope_location.frame != "itrs": + if self.telescope._location.frame != "itrs": raise ValueError( "Only ITRS telescope locations are supported in Miriad files." ) diff --git a/pyuvdata/uvdata/ms.py b/pyuvdata/uvdata/ms.py index 0ee5902d5c..76f2d38a53 100644 --- a/pyuvdata/uvdata/ms.py +++ b/pyuvdata/uvdata/ms.py @@ -14,7 +14,7 @@ from astropy.time import Time from docstring_parser import DocstringStyle -from .. import ms_utils +from .. import Telescope, ms_utils from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning @@ -969,10 +969,17 @@ def read_ms( and self.telescope_name in self.known_telescopes() ): # get it from known telescopes - self.set_telescope_params() + telescope_obj = Telescope.get_telescope_from_known_telescopes( + self.telescope_name + ) + warnings.warn( + "Setting telescope_location to value in known_telescopes for " + f"{self.telescope_name}." + ) + self.telescope_location = telescope_obj.location else: - self._telescope_location.frame = xyz_telescope_frame - self._telescope_location.ellipsoid = xyz_telescope_ellipsoid + self.telescope._location.frame = xyz_telescope_frame + self.telescope._location.ellipsoid = xyz_telescope_ellipsoid if "telescope_location" in obs_dict: self.telescope_location = np.squeeze(obs_dict["telescope_location"]) diff --git a/pyuvdata/uvdata/mwa_corr_fits.py b/pyuvdata/uvdata/mwa_corr_fits.py index 780d53bea1..51627f1d50 100644 --- a/pyuvdata/uvdata/mwa_corr_fits.py +++ b/pyuvdata/uvdata/mwa_corr_fits.py @@ -19,8 +19,7 @@ from pyuvdata.data import DATA_PATH -from .. import _corr_fits -from .. import telescopes as uvtel +from .. import Telescope, _corr_fits from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning @@ -73,20 +72,18 @@ def read_metafits( antenna_positions[:, 1] = meta_tbl["North"][1::2] antenna_positions[:, 2] = meta_tbl["Height"][1::2] - mwa_telescope_obj = uvtel.get_telescope("mwa") + mwa_telescope_obj = Telescope.get_telescope_from_known_telescopes("mwa") # convert antenna positions from enu to ecef # antenna positions are "relative to # the centre of the array in local topocentric \"east\", \"north\", # \"height\". Units are meters." - latitude, longitude, altitude = mwa_telescope_obj.telescope_location_lat_lon_alt + latitude, longitude, altitude = mwa_telescope_obj.location_lat_lon_alt antenna_positions_ecef = uvutils.ECEF_from_ENU( antenna_positions, latitude=latitude, longitude=longitude, altitude=altitude ) # make antenna positions relative to telescope location - antenna_positions = ( - antenna_positions_ecef - mwa_telescope_obj.telescope_location - ) + antenna_positions = antenna_positions_ecef - mwa_telescope_obj.location # reorder antenna parameters from metafits ordering reordered_inds = antenna_inds.argsort() @@ -99,7 +96,7 @@ def read_metafits( if telescope_info_only: return { "telescope_name": telescope_name, - "telescope_location": mwa_telescope_obj.telescope_location, + "telescope_location": mwa_telescope_obj.location, "instrument": instrument, "antenna_numbers": antenna_numbers, "antenna_names": antenna_names, @@ -198,7 +195,7 @@ def read_metafits( meta_dict = { "telescope_name": telescope_name, - "telescope_location": mwa_telescope_obj.telescope_location, + "telescope_location": mwa_telescope_obj.location, "instrument": instrument, "antenna_inds": antenna_inds, "antenna_numbers": antenna_numbers, diff --git a/pyuvdata/uvdata/tests/conftest.py b/pyuvdata/uvdata/tests/conftest.py index d84ed66b2f..2e75c917a3 100644 --- a/pyuvdata/uvdata/tests/conftest.py +++ b/pyuvdata/uvdata/tests/conftest.py @@ -24,11 +24,7 @@ def casa_uvfits_main(): """Read in CASA tutorial uvfits file.""" uv_in = UVData() with uvtest.check_warnings( - UserWarning, - [ - "Telescope EVLA is not in known_telescopes", - "The uvw_array does not match the expected values", - ], + UserWarning, "The uvw_array does not match the expected values" ): uv_in.read(casa_tutorial_uvfits, use_future_array_shapes=True) diff --git a/pyuvdata/uvdata/tests/test_fhd.py b/pyuvdata/uvdata/tests/test_fhd.py index e03b8166b9..f017a96ed5 100644 --- a/pyuvdata/uvdata/tests/test_fhd.py +++ b/pyuvdata/uvdata/tests/test_fhd.py @@ -233,8 +233,11 @@ def test_fhd_antenna_pos(fhd_data): use_future_array_shapes=True, ) - assert fhd_data._antenna_names == mwa_corr_obj._antenna_names - assert fhd_data._antenna_positions == mwa_corr_obj._antenna_positions + assert fhd_data.telescope._antenna_names == mwa_corr_obj.telescope._antenna_names + assert ( + fhd_data.telescope._antenna_positions + == mwa_corr_obj.telescope._antenna_positions + ) cotter_file = os.path.join(DATA_PATH, "1061316296.uvfits") cotter_obj = UVData() @@ -243,10 +246,15 @@ def test_fhd_antenna_pos(fhd_data): # don't test antenna_numbers, they will not match. # mwa_corr_fits now uses antenna_numbers that correspond to antenna_names # instead of following the cotter convention of using 0-127. - assert fhd_data._antenna_names == cotter_obj._antenna_names - assert fhd_data._antenna_positions == cotter_obj._antenna_positions + assert fhd_data.telescope._antenna_names == cotter_obj.telescope._antenna_names + assert ( + fhd_data.telescope._antenna_positions == cotter_obj.telescope._antenna_positions + ) - assert mwa_corr_obj._antenna_positions == cotter_obj._antenna_positions + assert ( + mwa_corr_obj.telescope._antenna_positions + == cotter_obj.telescope._antenna_positions + ) def test_read_fhd_write_read_uvfits_variant_flag(tmp_path, fhd_data_files): diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index e1715d0e35..e50d073a60 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -76,8 +76,8 @@ def test_simplest_new_uvdata(simplest_working_params: dict[str, Any]): def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: str): uvd = UVData.new(**lunar_simple_params, ellipsoid=selenoid) - assert uvd._telescope_location.frame == "mcmf" - assert uvd._telescope_location.ellipsoid == selenoid + assert uvd.telescope._location.frame == "mcmf" + assert uvd.telescope._location.ellipsoid == selenoid def test_bad_inputs(simplest_working_params: dict[str, Any]): diff --git a/pyuvdata/uvdata/tests/test_mir.py b/pyuvdata/uvdata/tests/test_mir.py index b0a829c232..bb25fc5d91 100644 --- a/pyuvdata/uvdata/tests/test_mir.py +++ b/pyuvdata/uvdata/tests/test_mir.py @@ -74,6 +74,9 @@ def test_read_mir_write_uvfits(sma_mir, tmp_path, future_shapes): sma_mir.use_current_array_shapes() sma_mir.write_uvfits(testfile) uvfits_uv.read_uvfits(testfile, use_future_array_shapes=future_shapes) + print("sma_mir instrument", sma_mir.instrument) + print("uvfits_uv instrument", uvfits_uv.instrument) + assert sma_mir.instrument == uvfits_uv.instrument for item in ["dut1", "earth_omega", "gst0", "rdate", "timesys"]: # Check to make sure that the UVFITS-specific paramters are set on the # UVFITS-based obj, and not on our original object. Then set it to None for the @@ -138,7 +141,6 @@ def test_read_mir_write_uvfits(sma_mir, tmp_path, future_shapes): assert sma_mir.filename == ["sma_test.mir"] assert uvfits_uv.filename == ["outtest_mir.uvfits"] sma_mir.filename = uvfits_uv.filename - assert sma_mir == uvfits_uv assert sma_mir == uvfits_uv diff --git a/pyuvdata/uvdata/tests/test_miriad.py b/pyuvdata/uvdata/tests/test_miriad.py index bf05698df0..a948a39197 100644 --- a/pyuvdata/uvdata/tests/test_miriad.py +++ b/pyuvdata/uvdata/tests/test_miriad.py @@ -84,7 +84,6 @@ "telescope_position will be set using the mean " "of the antenna altitudes" ), - "unknown_telescope_foo": "Telescope foo is not in known_telescopes.", "unclear_projection": ( "It is not clear from the file if the data are projected or not." ), @@ -178,13 +177,12 @@ def test_read_write_read_atca(tmp_path, future_shapes): atca_file = os.path.join(DATA_PATH, "atca_miriad/") testfile = os.path.join(tmp_path, "outtest_atca_miriad.uv") with uvtest.check_warnings( - [UserWarning] * 6 + [DeprecationWarning], + [UserWarning] * 5 + [DeprecationWarning], [ ( "Altitude is not present in Miriad file, and " "telescope ATCA is not in known_telescopes. " ), - "Telescope ATCA is not in known_telescopes.", "Altitude is not present", warn_dict["telescope_at_sealevel"], warn_dict["uvw_mismatch"], @@ -227,12 +225,8 @@ def test_read_nrao_write_miriad_read_miriad(casa_uvfits, tmp_path): # check that setting projected also works with uvtest.check_warnings( - [DeprecationWarning, UserWarning, UserWarning], - match=[ - warn_dict["phase_type_deprecated"], - warn_dict["uvw_mismatch"], - "Telescope EVLA is not", - ], + [DeprecationWarning, UserWarning], + match=[warn_dict["phase_type_deprecated"], warn_dict["uvw_mismatch"]], ): miriad_uv2 = UVData.from_file( writefile, phase_type="phased", use_future_array_shapes=True @@ -576,7 +570,6 @@ def test_wronglatlon(): warn_dict["altitude_missing_miriad"], warn_dict["altitude_missing_miriad"], warn_dict["no_telescope_loc"], - warn_dict["unknown_telescope_foo"], warn_dict["unclear_projection"], ], ): @@ -628,7 +621,6 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): warn_dict["altitude_missing_foo"], warn_dict["altitude_missing_foo"], # raised twice warn_dict["no_telescope_loc"], - warn_dict["unknown_telescope_foo"], warn_dict["uvw_mismatch"], ], ): @@ -659,7 +651,6 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): warn_dict["altitude_missing_foo"], warn_dict["telescope_at_sealevel_foo"], warn_dict["projection_false_offset"], - warn_dict["unknown_telescope_foo"], warn_dict["uvw_mismatch"], ], ): @@ -696,7 +687,6 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): "value does not match" ), warn_dict["projection_false_offset"], - warn_dict["unknown_telescope_foo"], warn_dict["uvw_mismatch"], ], ): @@ -732,7 +722,6 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): warn_dict["altitude_missing_miriad"], warn_dict["telescope_at_sealevel_lat_long"], warn_dict["projection_false_offset"], - warn_dict["unknown_telescope_foo"], warn_dict["uvw_mismatch"], ], ): @@ -782,7 +771,6 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): "does not give a telescope_location on the " "surface of the earth." ), - warn_dict["unknown_telescope_foo"], warn_dict["uvw_mismatch"], ], ): @@ -918,7 +906,7 @@ def test_miriad_only_itrs(tmp_path, paper_miriad): enu_antpos, _ = uv_in.get_ENU_antpos() latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in._telescope_location.frame = "mcmf" + uv_in.telescope._location.frame = "mcmf" uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( enu=enu_antpos, @@ -927,6 +915,7 @@ def test_miriad_only_itrs(tmp_path, paper_miriad): altitude=altitude, frame="mcmf", ) + uv_in.antenna_positions = new_full_antpos - uv_in.telescope_location uv_in.set_lsts_from_time_array() uv_in.check() @@ -938,7 +927,6 @@ def test_miriad_only_itrs(tmp_path, paper_miriad): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.parametrize("cut_ephem_pts", [True, False]) @pytest.mark.parametrize("extrapolate", [True, False]) def test_miriad_ephem(tmp_path, casa_uvfits, cut_ephem_pts, extrapolate): @@ -1871,7 +1859,7 @@ def test_rwr_miriad_antpos_issues(uv_in_paper, tmp_path): warn=warn_dict["default_vals"], ) with uvtest.check_warnings( - UserWarning, "Antenna positions are not present in the file.", nwarnings=2 + UserWarning, match=["Antenna positions are not present in the file."] * 2 ): uv_out.read(write_file, run_check=False, use_future_array_shapes=True) @@ -1927,7 +1915,7 @@ def test_rwr_miriad_antpos_issues(uv_in_paper, tmp_path): warn=warn_dict["default_vals"], ) with uvtest.check_warnings( - UserWarning, "Antenna positions are not present in the file.", nwarnings=2 + UserWarning, match=["Antenna positions are not present in the file."] * 2 ): uv_out.read(write_file, run_check=False, use_future_array_shapes=True) diff --git a/pyuvdata/uvdata/tests/test_ms.py b/pyuvdata/uvdata/tests/test_ms.py index 77e5c9f4ea..53c1e53b7f 100644 --- a/pyuvdata/uvdata/tests/test_ms.py +++ b/pyuvdata/uvdata/tests/test_ms.py @@ -21,8 +21,6 @@ pytest.importorskip("casacore") -allowed_failures = "filename" - def is_within_directory(directory, target): abs_directory = os.path.abspath(directory) @@ -45,7 +43,12 @@ def check_members(tar, path): def safe_extract(tar, path=".", members=None, *, numeric_owner=False): # this is factored this way (splitting out the `check_members` function) # to appease bandit. - tar.extractall(path, members=check_members(tar, path), numeric_owner=numeric_owner) + tar.extractall( + path, + members=check_members(tar, path), + numeric_owner=numeric_owner, + filter="data", + ) @pytest.fixture(scope="session") @@ -86,7 +89,7 @@ def nrao_uv_legacy(): @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @pytest.mark.filterwarnings("ignore:ITRF coordinate frame detected,") @pytest.mark.filterwarnings("ignore:UVW orientation appears to be flipped,") -@pytest.mark.filterwarnings("ignore:Nants_telescope, antenna_names") +@pytest.mark.filterwarnings("ignore:Setting telescope_location to value") def test_cotter_ms(): """Test reading in an ms made from MWA data with cotter (no dysco compression)""" uvobj = UVData() @@ -100,10 +103,7 @@ def test_cotter_ms(): [UserWarning] * 3 + [DeprecationWarning], match=[ "Warning: select on read keyword set", - ( - "telescope_location are not set or are being overwritten. Using known " - "values for MWA." - ), + "Setting telescope_location to value in known_telescopes for MWA.", "UVW orientation appears to be flipped,", _future_array_shapes_warning, ], @@ -125,8 +125,8 @@ def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): pytest.importorskip("lunarsky") enu_antpos, _ = uvobj.get_ENU_antpos() latitude, longitude, altitude = uvobj.telescope_location_lat_lon_alt - uvobj._telescope_location.frame = "mcmf" - uvobj._telescope_location.ellipsoid = selenoid + uvobj.telescope._location.frame = "mcmf" + uvobj.telescope._location.ellipsoid = selenoid uvobj.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( enu=enu_antpos, @@ -164,7 +164,7 @@ def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): assert uvobj2.filename == ["ms_testfile.ms"] uvobj.filename = uvobj2.filename - assert uvobj._telescope_location.frame == uvobj2._telescope_location.frame + assert uvobj.telescope._location.frame == uvobj2.telescope._location.frame # Test that the scan numbers are equal assert (uvobj.scan_number_array == uvobj2.scan_number_array).all() @@ -207,7 +207,7 @@ def test_read_lwa(tmp_path): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:telescope_location are not set") +@pytest.mark.filterwarnings("ignore:Setting telescope_location to value") def test_no_spw(): """Test reading in a PAPER ms converted by CASA from a uvfits with no spw axis.""" uvobj = UVData() @@ -218,7 +218,7 @@ def test_no_spw(): @pytest.mark.filterwarnings("ignore:Coordinate reference frame not detected,") @pytest.mark.filterwarnings("ignore:UVW orientation appears to be flipped,") -@pytest.mark.filterwarnings("ignore:telescope_location are not set") +@pytest.mark.filterwarnings("ignore:Setting telescope_location to value") def test_extra_pol_setup(tmp_path): """Test reading in an ms file with extra polarization setups (not used in data).""" uvobj = UVData() @@ -239,7 +239,6 @@ def test_extra_pol_setup(tmp_path): shutil.rmtree(new_filename) -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.filterwarnings("ignore:The older phase attributes") def test_read_ms_read_uvfits(nrao_uv, casa_uvfits): @@ -280,7 +279,7 @@ def test_read_ms_read_uvfits(nrao_uv, casa_uvfits): # they are equal if only required parameters are checked: # scan numbers only defined for the MS - assert uvfits_uv.__eq__(ms_uv, check_extra=False, allowed_failures=allowed_failures) + assert uvfits_uv.__eq__(ms_uv, check_extra=False) # set those parameters to none to check that the rest of the objects match ms_uv.antenna_diameters = None @@ -307,7 +306,6 @@ def test_read_ms_read_uvfits(nrao_uv, casa_uvfits): assert uvfits_uv == ms_uv -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_read_ms_write_uvfits(nrao_uv, tmp_path): """ @@ -343,7 +341,6 @@ def test_read_ms_write_uvfits(nrao_uv, tmp_path): assert uvfits_uv == ms_uv -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_read_ms_write_miriad(nrao_uv, tmp_path): """ @@ -379,7 +376,6 @@ def test_read_ms_write_miriad(nrao_uv, tmp_path): assert miriad_uv == ms_uv -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.filterwarnings("ignore:The older phase attributes") @pytest.mark.filterwarnings("ignore:Writing in the MS file that the units of the data") @@ -459,7 +455,7 @@ def test_multi_files(casa_uvfits, axis, tmp_path): uv_multi.filename = uv_full.filename uv_multi._filename.form = (1,) - assert uv_multi.__eq__(uv_full, allowed_failures=allowed_failures) + assert uv_multi == uv_full del uv_full del uv_multi @@ -924,7 +920,7 @@ def test_antenna_diameter_handling(hera_uvh5, tmp_path): uv_obj2._consolidate_phase_center_catalogs( reference_catalog=uv_obj.phase_center_catalog ) - assert uv_obj2.__eq__(uv_obj, allowed_failures=allowed_failures) + assert uv_obj2 == uv_obj def test_no_source(sma_mir, tmp_path): @@ -942,7 +938,7 @@ def test_no_source(sma_mir, tmp_path): assert uv == uv2 -@pytest.mark.filterwarnings("ignore:Nants_telescope, antenna_names") +@pytest.mark.filterwarnings("ignore:Setting telescope_location to value") @pytest.mark.filterwarnings("ignore:UVW orientation appears to be flipped") @pytest.mark.filterwarnings("ignore:Fixing auto-correlations to be be real-only") def test_timescale_handling(): @@ -998,7 +994,6 @@ def test_ms_bad_history(sma_mir, tmp_path): assert sma_mir.history in sma_ms.history -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_flip_conj(nrao_uv, tmp_path): filename = os.path.join(tmp_path, "flip_conj.ms") diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index 5165bae3cb..490ec1247d 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -58,16 +58,10 @@ def uvdata_props(): "spw_array", "integration_time", "channel_width", - "telescope_name", - "instrument", - "telescope_location", + "telescope", "history", "vis_units", "Nants_data", - "Nants_telescope", - "antenna_names", - "antenna_numbers", - "antenna_positions", "flex_spw", "phase_center_app_ra", "phase_center_app_dec", @@ -81,8 +75,6 @@ def uvdata_props(): extra_properties = [ "extra_keywords", - "x_orientation", - "antenna_diameters", "blt_order", "gst0", "rdate", @@ -101,7 +93,16 @@ def uvdata_props(): ] extra_parameters = ["_" + prop for prop in extra_properties] - other_properties = [ + other_attributes = [ + "telescope_name", + "telescope_location", + "instrument", + "Nants_telescope", + "antenna_names", + "antenna_nubers", + "antenna_positions", + "x_orientation", + "antenna_diameters", "telescope_location_lat_lon_alt", "telescope_location_lat_lon_alt_degrees", "phase_center_ra_degrees", @@ -119,7 +120,7 @@ def uvdata_props(): "required_properties", "extra_parameters", "extra_properties", - "other_properties", + "other_attributes", ], ) @@ -129,7 +130,7 @@ def uvdata_props(): required_properties, extra_parameters, extra_properties, - other_properties, + other_attributes, ) # yields the data we need but will continue to the del call after tests yield uvdata_props @@ -269,11 +270,7 @@ def bda_test_file_main(): uv_object = UVData() testfile = os.path.join(DATA_PATH, "simulated_bda_file.uvh5") with uvtest.check_warnings( - UserWarning, - match=[ - "Unknown phase types are no longer supported", - "Telescope mock-HERA is not in known_telescopes", - ], + UserWarning, match="Unknown phase types are no longer supported" ): uv_object.read(testfile, use_future_array_shapes=True) @@ -459,7 +456,7 @@ def test_unexpected_attributes(uvdata_props): expected_attributes = ( uvdata_props.required_properties + uvdata_props.extra_properties - + uvdata_props.other_properties + + uvdata_props.other_attributes ) attributes = [i for i in uvdata_props.uv_object.__dict__.keys() if i[0] != "_"] for a in attributes: @@ -894,8 +891,8 @@ def test_hera_diameters(paper_uvh5): with uvtest.check_warnings( UserWarning, match=( - "antenna_diameters are not set or are being overwritten. Using known " - "values for HERA." + "antenna_diameters are not set or are being overwritten. " + "antenna_diameters are set using values from known telescopes for HERA." ), ): uv_in.set_telescope_params() @@ -2179,12 +2176,11 @@ def test_select_lsts_too_big(casa_uvfits, tmp_path): UserWarning, match=warn_msg + [ - "Telescope EVLA is not in known_telescopes.", ( "The lst_array is not self-consistent with the time_array and telescope" " location. Consider recomputing with the `set_lsts_from_time_array`" " method" - ), + ) ], ): UVData.from_file(test_filename, use_future_array_shapes=True) @@ -2227,11 +2223,7 @@ def test_select_lst_range(casa_uvfits, tmp_path): uv_object.write_uvh5(testfile) with uvtest.check_warnings( - UserWarning, - [ - "Telescope EVLA is not in known_telescopes", - "The uvw_array does not match the expected values", - ], + UserWarning, "The uvw_array does not match the expected values" ): uv2_in = UVData.from_file( testfile, lst_range=lst_range, use_future_array_shapes=True @@ -3560,17 +3552,17 @@ def test_sum_vis(casa_uvfits, future_shapes): # check override_params uv_overrides = uv_full.copy() - uv_overrides.instrument = "test_telescope" + uv_overrides.timesys = "foo" uv_overrides.telescope_location = [ -1601183.15377712, -5042003.74810822, 3554841.17192104, ] uv_overrides_2 = uv_overrides.sum_vis( - uv_full, override_params=["instrument", "telescope_location"] + uv_full, override_params=["timesys", "telescope"] ) - assert uv_overrides_2.instrument == "test_telescope" + assert uv_overrides_2.timesys == "foo" assert uv_overrides_2.telescope_location == [ -1601183.15377712, -5042003.74810822, @@ -3587,7 +3579,7 @@ def test_sum_vis(casa_uvfits, future_shapes): [["use_current_array_shapes"], {}, {}, "Both objects must have the same `futu"], [[], {}, {"override": ["fake"]}, "Provided parameter fake is not a recogniza"], [[], {"__class__": UVCal}, {}, "Only UVData (or subclass) objects can be"], - [[], {"instrument": "foo"}, {"inplace": True}, "UVParameter instrument does"], + [[], {"timesys": "foo"}, {"inplace": True}, "UVParameter timesys does"], ], ) def test_sum_vis_errors(hera_uvh5, attr_to_get, attr_to_set, arg_dict, msg): @@ -5376,19 +5368,18 @@ def test_telescope_loc_xyz_check(paper_uvh5, tmp_path): fname = str(tmp_path / "test.uvh5") uv.write_uvh5(fname, run_check=False, check_extra=False, clobber=True) - # try to read file without checks (passing is implicit) + # try to read file without checks, should be no warnings uv.read(fname, run_check=False, use_future_array_shapes=True) - # try to read without checks: assert it fails + # try to read without checks: should be warnings with uvtest.check_warnings( UserWarning, - [ - "The uvw_array does not match the expected", - ( - "itrs position vector magnitudes must be on the order " - "of the radius of Earth -- they appear to lie well below this." - ), - ], + match=["The uvw_array does not match the expected"] + + [ + "itrs position vector magnitudes must be on the order " + "of the radius of Earth -- they appear to lie well below this." + ] + * 3, ): uv.read(fname, use_future_array_shapes=True) @@ -8589,7 +8580,6 @@ def test_upsample_downsample_in_time_metadata_only(hera_uvh5): @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:Unknown phase types are no longer supported,") -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline") @pytest.mark.parametrize("future_shapes", [True, False]) @pytest.mark.parametrize("driftscan", [True, False]) @@ -8658,7 +8648,6 @@ def test_resample_in_time(bda_test_file, future_shapes, driftscan, partial_phase return -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline") def test_resample_in_time_downsample_only(bda_test_file): """Test resample_in_time with downsampling only""" @@ -8711,7 +8700,6 @@ def test_resample_in_time_downsample_only(bda_test_file): return -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline") def test_resample_in_time_only_upsample(bda_test_file): """Test resample_in_time with only upsampling""" @@ -8760,7 +8748,6 @@ def test_resample_in_time_only_upsample(bda_test_file): return -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline") def test_resample_in_time_partial_flags(bda_test_file): """Test resample_in_time with partial flags""" @@ -9904,7 +9891,6 @@ def test_multifile_read_check_long_list(hera_uvh5, tmp_path, err_type): return -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_read_background_lsts(): """Test reading a file with the lst calc in the background.""" @@ -11495,7 +11481,6 @@ def test_multi_phase_downselect(hera_uvh5_split, cat_type, future_shapes): @pytest.mark.filterwarnings("ignore:Unknown phase types are no longer supported") -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") def test_eq_allowed_failures(bda_test_file, capsys): """ Test that the allowed_failures keyword on the __eq__ method works as intended. @@ -11504,15 +11489,14 @@ def test_eq_allowed_failures(bda_test_file, capsys): uv2 = uv1.copy() # adjust optional parameters to be different - uv1.x_orientation = "NORTH" - uv2.x_orientation = "EAST" - assert uv1.__eq__(uv2, check_extra=True, allowed_failures=["x_orientation"]) + uv1.extra_keywords = {"foo": 2} + uv2.extra_keywords = {"foo": 4} + assert uv1.__eq__(uv2, check_extra=True, allowed_failures=["extra_keywords"]) captured = capsys.readouterr() assert ( - captured.out - == "x_orientation parameter value is a string, values are different\n" - "parameter _x_orientation does not match, but is not required to for equality. " - "Left is NORTH, right is EAST.\n" + captured.out == "extra_keywords parameter is a dict, key foo is not equal\n" + "parameter _extra_keywords does not match, but is not required to for " + "equality. Left is {'foo': 2}, right is {'foo': 4}.\n" ) # make sure that objects are not equal without specifying allowed_failures @@ -11522,7 +11506,6 @@ def test_eq_allowed_failures(bda_test_file, capsys): @pytest.mark.filterwarnings("ignore:Unknown phase types are no longer supported") -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") def test_eq_allowed_failures_filename(bda_test_file, capsys): """ Test that the `filename` parameter does not trip up the __eq__ method. @@ -11545,7 +11528,6 @@ def test_eq_allowed_failures_filename(bda_test_file, capsys): @pytest.mark.filterwarnings("ignore:Unknown phase types are no longer supported") -@pytest.mark.filterwarnings("ignore:Telescope mock-HERA is not in known_telescopes") def test_eq_allowed_failures_filename_string(bda_test_file, capsys): """ Try passing a string to the __eq__ method instead of an iterable. @@ -11590,7 +11572,6 @@ def test_set_data(hera_uvh5, future_shapes): @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes") @pytest.mark.parametrize("future_shapes", [True, False]) def test_set_data_evla(future_shapes): """ @@ -12446,7 +12427,6 @@ def test_normalize_by_autos_flag_noautos(hera_uvh5): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes") @pytest.mark.parametrize("multi_phase", [True, False]) def test_split_write_comb_read(tmp_path, multi_phase): """Pulled from a failed tutorial example.""" diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index a651a2c85a..bd74bc6290 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -119,7 +119,6 @@ def uvfits_nospw(uvfits_nospw_main): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_read_nrao(casa_uvfits): """Test reading in a CASA tutorial uvfits file.""" uvobj = casa_uvfits @@ -192,7 +191,6 @@ def test_time_precision(tmp_path): ) -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_break_read_uvfits(tmp_path): """Test errors on reading in a uvfits file with subarrays and other problems.""" uvobj = UVData() @@ -241,7 +239,6 @@ def test_break_read_uvfits(tmp_path): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:UVFITS file is missing AIPS SU table") def test_source_group_params(casa_uvfits, tmp_path): # make a file with a single source to test that it works @@ -288,7 +285,6 @@ def test_source_group_params(casa_uvfits, tmp_path): assert uv_in == uv_out -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The entry name") @pytest.mark.filterwarnings("ignore:The provided name") @pytest.mark.parametrize("frame", [["icrs"], ["fk5"], ["fk4"], ["fk5", "icrs"]]) @@ -337,7 +333,6 @@ def test_read_write_multi_source(casa_uvfits, tmp_path, frame, high_precision): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The older phase attributes") @pytest.mark.parametrize("frame", ["icrs", "fk4"]) @pytest.mark.filterwarnings("ignore:UVFITS file is missing AIPS SU table") @@ -379,7 +374,6 @@ def test_source_frame_defaults(casa_uvfits, tmp_path, frame): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The entry name") @pytest.mark.filterwarnings("ignore:The provided name") @pytest.mark.parametrize("frame_list", [["fk5", "fk5"], ["fk4", "fk4"], ["fk4", "fk5"]]) @@ -442,7 +436,6 @@ def test_multi_source_frame_defaults(casa_uvfits, tmp_path, frame_list): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_missing_aips_su_table(casa_uvfits, tmp_path): # make a file with multiple sources to test error condition uv_in = casa_uvfits @@ -477,10 +470,9 @@ def test_missing_aips_su_table(casa_uvfits, tmp_path): hdulist.close() with uvtest.check_warnings( - [UserWarning] * 3 + [DeprecationWarning], + [UserWarning] * 2 + [DeprecationWarning], [ "UVFITS file is missing AIPS SU table, which is required when ", - "Telescope EVLA is not", "The uvw_array does not match the expected values", _future_array_shapes_warning, ], @@ -493,10 +485,7 @@ def test_casa_nonascii_bytes_antenna_names(): uv1 = UVData() testfile = os.path.join(DATA_PATH, "corrected2_zen.2458106.28114.ant012.HH.uvfits") # this file has issues with the telescope location so turn checking off - with uvtest.check_warnings( - UserWarning, "Telescope mock-HERA is not in known_telescopes." - ): - uv1.read(testfile, run_check=False, use_future_array_shapes=True) + uv1.read(testfile, run_check=False, use_future_array_shapes=True) # fmt: off expected_ant_names = [ 'HH0', 'HH1', 'HH2', 'H2', 'H2', 'H2', 'H2', 'H2', 'H2', 'H2', @@ -523,7 +512,6 @@ def test_casa_nonascii_bytes_antenna_names(): @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize("future_shapes", [True, False]) @pytest.mark.parametrize(["telescope_frame", "selenoid"], frame_selenoid) def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, selenoid): @@ -542,8 +530,8 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se pytest.importorskip("lunarsky") enu_antpos, _ = uv_in.get_ENU_antpos() latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in._telescope_location.frame = "mcmf" - uv_in._telescope_location.ellipsoid = selenoid + uv_in.telescope._location.frame = "mcmf" + uv_in.telescope._location.ellipsoid = selenoid uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( enu=enu_antpos, @@ -570,8 +558,8 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se assert uv_out.filename == ["outtest_casa.uvfits"] uv_in.filename = uv_out.filename - assert uv_in._telescope_location.frame == uv_out._telescope_location.frame - assert uv_in._telescope_location.ellipsoid == uv_out._telescope_location.ellipsoid + assert uv_in.telescope._location.frame == uv_out.telescope._location.frame + assert uv_in.telescope._location.ellipsoid == uv_out.telescope._location.ellipsoid uv_out._consolidate_phase_center_catalogs( reference_catalog=uv_in.phase_center_catalog @@ -582,7 +570,6 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.parametrize("uvw_suffix", ["---SIN", "---NCP"]) def test_uvw_coordinate_suffixes(casa_uvfits, tmp_path, uvw_suffix): uv_in = casa_uvfits @@ -619,7 +606,6 @@ def test_uvw_coordinate_suffixes(casa_uvfits, tmp_path, uvw_suffix): with uvtest.check_warnings( UserWarning, match=[ - "Telescope EVLA is not in known_telescopes.", ( "The baseline coordinates (uvws) in this file are specified in the " "---NCP coordinate system" @@ -641,7 +627,6 @@ def test_uvw_coordinate_suffixes(casa_uvfits, tmp_path, uvw_suffix): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.parametrize( "uvw_suffix", [["---SIN", "", ""], ["", "---NCP", ""], ["", "---NCP", "---SIN"]] ) @@ -684,7 +669,6 @@ def test_uvw_coordinate_suffix_errors(casa_uvfits, tmp_path, uvw_suffix): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_no_lst(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() @@ -708,7 +692,6 @@ def test_readwriteread_no_lst(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_x_orientation(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() @@ -733,7 +716,6 @@ def test_readwriteread_x_orientation(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_antenna_diameters(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() @@ -760,7 +742,6 @@ def test_readwriteread_antenna_diameters(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_large_antnums(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() @@ -804,7 +785,6 @@ def test_readwriteread_large_antnums(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize("lat_lon_alt", [True, False]) def test_readwriteread_missing_info(tmp_path, casa_uvfits, lat_lon_alt): uv_in = casa_uvfits @@ -851,7 +831,6 @@ def test_readwriteread_missing_info(tmp_path, casa_uvfits, lat_lon_alt): "ignorance. Defaulting the frame to 'itrs', but this may lead to other " "warnings or errors." ), - "Telescope EVLA is not in known_telescopes.", ( "The uvw_array does not match the expected values given the antenna " "positions." @@ -866,7 +845,6 @@ def test_readwriteread_missing_info(tmp_path, casa_uvfits, lat_lon_alt): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_error_timesys(tmp_path, casa_uvfits): uv_in = casa_uvfits write_file = str(tmp_path / "outtest_casa.uvfits") @@ -883,7 +861,6 @@ def test_readwriteread_error_timesys(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_error_single_time(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() @@ -924,15 +901,8 @@ def test_readwriteread_error_single_time(tmp_path, casa_uvfits): ValueError, match="Required UVParameter _integration_time has not been set" ): with uvtest.check_warnings( + [erfa.core.ErfaWarning, erfa.core.ErfaWarning, UserWarning, UserWarning], [ - UserWarning, - erfa.core.ErfaWarning, - erfa.core.ErfaWarning, - UserWarning, - UserWarning, - ], - [ - "Telescope EVLA is not", "ERFA function 'utcut1' yielded 1 of 'dubious year (Note 3)'", "ERFA function 'utctai' yielded 1 of 'dubious year (Note 3)'", "LST values stored in this file are not self-consistent", @@ -945,7 +915,6 @@ def test_readwriteread_error_single_time(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_readwriteread_unflagged_data_warnings(tmp_path, casa_uvfits): uv_in = casa_uvfits write_file = str(tmp_path / "outtest_casa.uvfits") @@ -969,7 +938,6 @@ def test_readwriteread_unflagged_data_warnings(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize( "kwd_name,kwd_value,warnstr,errstr", ( @@ -1025,7 +993,6 @@ def test_extra_keywords_errors( @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize( "kwd_names,kwd_values", ( @@ -1067,7 +1034,6 @@ def test_extra_keywords(casa_uvfits, tmp_path, kwd_names, kwd_values): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize("order", ["time", "bda"]) def test_roundtrip_blt_order(casa_uvfits, order, tmp_path): uv_in = casa_uvfits @@ -1090,7 +1056,6 @@ def test_roundtrip_blt_order(casa_uvfits, order, tmp_path): assert uv_in == uv_out -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.parametrize( "select_kwargs", @@ -1176,7 +1141,6 @@ def test_select_read_nospw(uvfits_nospw, tmp_path, select_kwargs): assert uvfits_uv == uvfits_uv2 -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_select_read_nospw_pol(casa_uvfits, tmp_path): # this requires writing a new file because the no spw file we have has only 1 pol @@ -1242,7 +1206,6 @@ def test_select_read_nospw_pol(casa_uvfits, tmp_path): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_read_uvfits_write_miriad(casa_uvfits, tmp_path): """ read uvfits, write miriad test. @@ -1311,7 +1274,6 @@ def test_read_uvfits_write_miriad(casa_uvfits, tmp_path): assert miriad_uv == uvfits_uv -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_multi_files(casa_uvfits, tmp_path): """ @@ -1358,7 +1320,6 @@ def test_multi_files(casa_uvfits, tmp_path): assert uv1 == uv_full -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_multi_files_axis(casa_uvfits, tmp_path): """ @@ -1401,7 +1362,6 @@ def test_multi_files_axis(casa_uvfits, tmp_path): assert uv1 == uv_full -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_multi_files_metadata_only(casa_uvfits, tmp_path): """ @@ -1446,7 +1406,6 @@ def test_multi_files_metadata_only(casa_uvfits, tmp_path): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") def test_read_ms_write_uvfits_casa_history(tmp_path): """ read in .ms file. @@ -1508,7 +1467,6 @@ def test_cotter_telescope_frame(tmp_path): @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.parametrize("future_shapes", [True, False]) def test_readwriteread_reorder_pols(tmp_path, casa_uvfits, future_shapes): """ @@ -1711,7 +1669,6 @@ def test_uvfits_phasing_errors(hera_uvh5, tmp_path): hera_uvh5.write_uvfits(tmp_path) -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.filterwarnings( "ignore:The shapes of several attributes will be changing " diff --git a/pyuvdata/uvdata/tests/test_uvh5.py b/pyuvdata/uvdata/tests/test_uvh5.py index 0cdbad7a2b..435debc559 100644 --- a/pyuvdata/uvdata/tests/test_uvh5.py +++ b/pyuvdata/uvdata/tests/test_uvh5.py @@ -205,8 +205,8 @@ def test_read_uvfits_write_uvh5_read_uvh5( pytest.importorskip("lunarsky") enu_antpos, _ = uv_in.get_ENU_antpos() latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in._telescope_location.frame = "mcmf" - uv_in._telescope_location.ellipsoid = selenoid + uv_in.telescope._location.frame = "mcmf" + uv_in.telescope._location.ellipsoid = selenoid uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( enu=enu_antpos, @@ -220,8 +220,8 @@ def test_read_uvfits_write_uvh5_read_uvh5( uv_in.set_lsts_from_time_array() uv_in.check() - assert uv_in._telescope_location.frame == telescope_frame - assert uv_in._telescope_location.ellipsoid == selenoid + assert uv_in.telescope._location.frame == telescope_frame + assert uv_in.telescope._location.ellipsoid == selenoid uv_out = UVData() fname = f"outtest_{telescope_frame}_uvfits.uvh5" @@ -229,8 +229,8 @@ def test_read_uvfits_write_uvh5_read_uvh5( uv_in.write_uvh5(testfile, clobber=True) uv_out.read(testfile, use_future_array_shapes=True) - assert uv_out._telescope_location.frame == telescope_frame - assert uv_out._telescope_location.ellipsoid == selenoid + assert uv_out.telescope._location.frame == telescope_frame + assert uv_out.telescope._location.ellipsoid == selenoid # make sure filenames are what we expect assert uv_in.filename == ["day2_TDEM0003_10s_norx_1src_1spw.uvfits"] @@ -315,9 +315,8 @@ def test_write_uvh5_errors(casa_uvfits, tmp_path): # use clobber=True to write out anyway uv_in.write_uvh5(testfile, clobber=True) with uvtest.check_warnings( - [UserWarning, UserWarning, DeprecationWarning], + [UserWarning, DeprecationWarning], match=[ - "Telescope EVLA is not in known_telescopes.", "The uvw_array does not match the expected values", _future_array_shapes_warning, ], @@ -434,7 +433,6 @@ def test_uvh5_compression_options(casa_uvfits, tmp_path): return -@pytest.mark.filterwarnings("ignore:Telescope EVLA is not in known_telescopes.") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_uvh5_read_multiple_files(casa_uvfits, tmp_path): """ @@ -3511,11 +3509,10 @@ def test_old_phase_attributes_header(casa_uvfits, tmp_path): with uvtest.check_warnings( UserWarning, match=[ - "Telescope EVLA is not in known_telescopes.", "This data appears to have been phased-up using the old `phase` method, " "which is incompatible with the current set of methods. Please run the " "`fix_phase` method (or set `fix_old_proj=True` when loading the dataset) " - "to address this issue.", + "to address this issue." ], ): uvd = UVData.from_file( @@ -3525,11 +3522,7 @@ def test_old_phase_attributes_header(casa_uvfits, tmp_path): assert uvd == casa_uvfits with uvtest.check_warnings( - UserWarning, - match=[ - "Telescope EVLA is not in known_telescopes.", - "Fixing phases using antenna positions.", - ], + UserWarning, match=["Fixing phases using antenna positions."] ): uvd2 = UVData.from_file(testfile, use_future_array_shapes=True) diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 22ba5fdab5..a45b9e7ad0 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -21,8 +21,8 @@ from docstring_parser import DocstringStyle from scipy import ndimage as nd +from .. import Telescope, known_telescopes from .. import parameter as uvp -from .. import telescopes as uvtel from .. import utils as uvutils from ..docstrings import combine_docstrings, copy_replace_short_description from ..uvbase import UVBase @@ -361,33 +361,6 @@ def __init__(self): "channel_width", description=desc, expected_type=float, tols=1e-3 ) # 1 mHz - self._telescope_name = uvp.UVParameter( - "telescope_name", - description="Name of telescope or array (string).", - form="str", - expected_type=str, - ) - - self._instrument = uvp.UVParameter( - "instrument", - description="Receiver or backend. Sometimes identical to telescope_name.", - form="str", - expected_type=str, - ) - - desc = ( - "Telescope location: xyz position in specified frame (default ITRS). " - "Can also be accessed using telescope_location_lat_lon_alt or " - "telescope_location_lat_lon_alt_degrees properties." - ) - self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - frame="itrs", - ellipsoid=None, - tols=1e-3, - ) - self._history = uvp.UVParameter( "history", description="String of history, units English.", @@ -562,71 +535,13 @@ def __init__(self): "Nants_data", description=desc, expected_type=int ) - desc = ( - "Number of antennas in the array. May be larger " - "than the number of antennas with data." - ) - self._Nants_telescope = uvp.UVParameter( - "Nants_telescope", description=desc, expected_type=int - ) - - desc = ( - "List of antenna names, shape (Nants_telescope), " - "with numbers given by antenna_numbers (which can be matched " - "to ant_1_array and ant_2_array). There must be one entry " - "here for each unique entry in ant_1_array and " - "ant_2_array, but there may be extras as well. " - ) - self._antenna_names = uvp.UVParameter( - "antenna_names", - description=desc, - form=("Nants_telescope",), - expected_type=str, - ) - - desc = ( - "List of integer antenna numbers corresponding to antenna_names, " - "shape (Nants_telescope). There must be one " - "entry here for each unique entry in ant_1_array and " - "ant_2_array, but there may be extras as well." - "Note that these are not indices -- they do not need to start " - "at zero or be continuous." - ) - self._antenna_numbers = uvp.UVParameter( - "antenna_numbers", - description=desc, - form=("Nants_telescope",), - expected_type=int, - ) - - desc = ( - "Array giving coordinates of antennas relative to telescope_location " - "(in frame _telescope_location.frame), shape (Nants_telescope, 3), units " - "meters. See the UVData tutorial page in the documentation for an example " - "of how to convert this to topocentric frame." - ) - self._antenna_positions = uvp.UVParameter( - "antenna_positions", - description=desc, - form=("Nants_telescope", 3), - expected_type=float, - tols=1e-3, # 1 mm + self._telescope = uvp.UVParameter( + "telescope", + description="Telescope object containing the telescope metadata.", + expected_type=Telescope, ) # -------- extra, non-required parameters ---------- - desc = ( - "Orientation of the physical dipole corresponding to what is " - "labelled as the x polarization. Options are 'east' " - "(indicating east/west orientation) and 'north (indicating " - "north/south orientation)." - ) - self._x_orientation = uvp.UVParameter( - "x_orientation", - description=desc, - required=False, - expected_type=str, - acceptable_vals=["east", "north"], - ) blt_order_options = ["time", "baseline", "ant1", "ant2", "bda"] desc = ( @@ -686,19 +601,6 @@ def __init__(self): expected_type=dict, ) - desc = ( - "Array of antenna diameters in meters. Used by CASA to " - "construct a default beam if no beam is supplied." - ) - self._antenna_diameters = uvp.UVParameter( - "antenna_diameters", - required=False, - description=desc, - form=("Nants_telescope",), - expected_type=float, - tols=1e-3, # 1 mm - ) - # --- other stuff --- # the below are copied from AIPS memo 117, but could be revised to # merge with other sources of data. @@ -777,8 +679,140 @@ def __init__(self): self.__antpair2ind_cache = {} self.__key2ind_cache = {} + # initialize the telescope object + self.telescope = Telescope() + + # set the appropriate telescope attributes as required + self.telescope._instrument.required = True + self.telescope._Nants.required = True + self.telescope._antenna_names.required = True + self.telescope._antenna_numbers.required = True + self.telescope._antenna_positions.required = True + super(UVData, self).__init__() + @property + def telescope_name(self): + """The telescope name (stored on the Telescope object internally).""" + return self.telescope.name + + @telescope_name.setter + def telescope_name(self, val): + self.telescope.name = val + + @property + def instrument(self): + """The instrument name (stored on the Telescope object internally).""" + return self.telescope.instrument + + @instrument.setter + def instrument(self, val): + self.telescope.instrument = val + + @property + def telescope_location(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location + + @telescope_location.setter + def telescope_location(self, val): + self.telescope.location = val + + @property + def telescope_location_lat_lon_alt(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt + + @telescope_location_lat_lon_alt.setter + def telescope_location_lat_lon_alt(self, val): + self.telescope.location_lat_lon_alt = val + + @property + def telescope_location_lat_lon_alt_degrees(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt_degrees + + @telescope_location_lat_lon_alt_degrees.setter + def telescope_location_lat_lon_alt_degrees(self, val): + self.telescope.location_lat_lon_alt_degrees = val + + @property + def Nants_telescope(self): + """ + The number of antennas in the telescope. + + This property is stored on the Telescope object internally. + """ + return self.telescope.Nants + + @Nants_telescope.setter + def Nants_telescope(self, val): + self.telescope.Nants = val + + @property + def antenna_names(self): + """The antenna names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_names + + @antenna_names.setter + def antenna_names(self, val): + self.telescope.antenna_names = val + + @property + def antenna_numbers(self): + """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_numbers + + @antenna_numbers.setter + def antenna_numbers(self, val): + self.telescope.antenna_numbers = val + + @property + def antenna_positions(self): + """The antenna positions coordinates of antennas relative to telescope_location. + + The coordinates are in the ITRF frame, shape (Nants_telescope, 3). + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_positions + + @antenna_positions.setter + def antenna_positions(self, val): + self.telescope.antenna_positions = val + + @property + def x_orientation(self): + """Orientation of the physical dipole corresponding to the x label. + + Options are 'east' (indicating east/west orientation) and 'north (indicating + north/south orientation). + This property is stored on the Telescope object internally. + """ + return self.telescope.x_orientation + + @x_orientation.setter + def x_orientation(self, val): + self.telescope.x_orientation = val + + @property + def antenna_diameters(self): + """The antenna diameters in meters. + + Used by CASA to construct a default beam if no beam is supplied. + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_diameters + + @antenna_diameters.setter + def antenna_diameters(self, val): + self.telescope.antenna_diameters = val + @staticmethod def _clear_antpair2ind_cache(obj): """Clear the antpair2ind cache.""" @@ -1770,15 +1804,23 @@ def known_telescopes(self): list of str List of names of known telescopes """ - return uvtel.known_telescopes() + return known_telescopes() - def set_telescope_params(self, *, overwrite=False, warn=True): + def set_telescope_params( + self, + *, + overwrite=False, + warn=True, + run_check=True, + check_extra=True, + run_check_acceptability=True, + ): """ Set telescope related parameters. - If the telescope_name is in the known_telescopes, set any missing - telescope-associated parameters (e.g. telescope location) to the value - for the known telescope. + If the telescope_name is in astropy sites or known_telescopes, set any + missing telescope parameters (e.g. telescope location, antenna information) + to the value(s) from astropy sites or known telescopes. Parameters ---------- @@ -1789,64 +1831,15 @@ def set_telescope_params(self, *, overwrite=False, warn=True): Raises ------ ValueError - if the telescope_name is not in known telescopes - """ - telescope_obj = uvtel.get_telescope(self.telescope_name) - if telescope_obj is not False: - params_set = [] - for p in telescope_obj: - telescope_param = getattr(telescope_obj, p) - self_param = getattr(self, p) - if telescope_param.value is not None and ( - overwrite is True or self_param.value is None - ): - telescope_shape = telescope_param.expected_shape(telescope_obj) - self_shape = self_param.expected_shape(self) - if telescope_shape == self_shape: - params_set.append(self_param.name) - prop_name = self_param.name - setattr(self, prop_name, getattr(telescope_obj, prop_name)) - else: - # expected shapes aren't equal. This can happen - # e.g. with diameters, - # which is a single value on the telescope object but is - # an array of length Nants_telescope on the UVData object - - # use an assert here because we want an error if this condition - # isn't true, but it's really an internal consistency check. - # This will error if there are changes to the Telescope - # object definition, but nothing that a normal user - # does will cause an error - assert telescope_shape == () and self_shape != "str", ( - "There is a mismatch in one or more array sizes of the " - "UVData and telescope objects. This should not happen, " - "if you see this error please make an issue in the " - "pyuvdata issue log." - ) - # this parameter is as of this comment most likely a float - # since only diameters and antenna positions will probably - # trigger this else statement - # assign float64 as the type of the array - array_val = ( - np.zeros(self_shape, dtype=np.float64) - + telescope_param.value - ) - params_set.append(self_param.name) - prop_name = self_param.name - setattr(self, prop_name, array_val) - - if len(params_set) > 0: - if warn: - params_set_str = ", ".join(params_set) - warnings.warn( - f"{params_set_str} are not set or are being " - "overwritten. Using known values for " - f"{telescope_obj.telescope_name}." - ) - else: - raise ValueError( - f"Telescope {self.telescope_name} is not in known_telescopes." - ) + If the telescope_name is not in astropy sites or known telescopes. + """ + self.telescope.update_params_from_known_telescopes( + overwrite=overwrite, + warn=warn, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) def _calc_single_integration_time(self): """ @@ -1874,8 +1867,8 @@ def _set_lsts_helper(self, *, astrometry_library=None): latitude=latitude, longitude=longitude, altitude=altitude, - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, astrometry_library=astrometry_library, ) return @@ -1928,8 +1921,8 @@ def _set_app_coords_helper(self, *, pa_only=False): time_array=self.time_array[select_mask], lst_array=self.lst_array[select_mask], telescope_loc=self.telescope_location_lat_lon_alt, - telescope_frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + telescope_frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, coord_type=cat_type, ) @@ -1951,8 +1944,8 @@ def _set_app_coords_helper(self, *, pa_only=False): telescope_loc=self.telescope_location_lat_lon_alt, ref_frame=frame, ref_epoch=epoch, - telescope_frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + telescope_frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) self.phase_center_app_ra = app_ra self.phase_center_app_dec = app_dec @@ -2725,6 +2718,11 @@ def check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) logger.debug("... Done UVBase Check") + self.telescope.check( + check_extra=check_extra, run_check_acceptability=run_check_acceptability + ) + + # then run telescope object check # Check blt axis rectangularity arguments if self.time_axis_faster_than_bls and not self.blts_are_rectangular: @@ -2823,7 +2821,7 @@ def check( uvutils.check_surface_based_positions( antenna_positions=self.antenna_positions, telescope_loc=self.telescope_location, - telescope_frame=self._telescope_location.frame, + telescope_frame=self.telescope._location.frame, raise_error=False, ) @@ -2836,8 +2834,8 @@ def check( longitude=lon, altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) # create a metadata copy to do operations on @@ -3792,8 +3790,8 @@ def get_ENU_antpos(self, *, center=False, pick_data_ants=False): latitude=latitude, longitude=longitude, altitude=altitude, - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) ants = self.antenna_numbers @@ -5299,8 +5297,8 @@ def phase( vrad=phase_dict["cat_vrad"], dist=phase_dict["cat_dist"], telescope_loc=self.telescope_location_lat_lon_alt, - telescope_frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + telescope_frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) # Now calculate position angles. @@ -5312,8 +5310,8 @@ def phase( telescope_loc=self.telescope_location_lat_lon_alt, ref_frame=phase_frame, ref_epoch=epoch, - telescope_frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + telescope_frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) else: new_frame_pa = np.zeros(time_array.shape, dtype=float) @@ -5851,16 +5849,7 @@ def __add__( ) # Define parameters that must be the same to add objects - compatibility_params = [ - "_vis_units", - "_telescope_name", - "_instrument", - "_telescope_location", - "_Nants_telescope", - "_antenna_names", - "_antenna_numbers", - "_antenna_positions", - ] + compatibility_params = ["_vis_units", "_telescope"] if not this.future_array_shapes and not this.flex_spw: compatibility_params.append("_channel_width") @@ -6734,16 +6723,7 @@ def fast_concat( # Because self was at the beginning of the list, # all the phase centers are merged into it at the end of this loop - compatibility_params = [ - "_vis_units", - "_telescope_name", - "_instrument", - "_telescope_location", - "_Nants_telescope", - "_antenna_names", - "_antenna_numbers", - "_antenna_positions", - ] + compatibility_params = ["_vis_units", "_telescope"] if not this.future_array_shapes and not this.flex_spw: compatibility_params.append("_channel_width") @@ -7831,16 +7811,25 @@ def _select_by_index( if ind_arr is None: continue - for param in self: + if key == "Nants_telescope": + # need to iterate over params in self.telescope NOT in self + obj_use = self.telescope + key_use = "Nants" + else: + obj_use = self + key_use = key + + for param in obj_use: # For each attribute, if the value is None, then bail, otherwise # attempt to figure out along which axis ind_arr will apply. - attr = getattr(self, param) + + attr = getattr(obj_use, param) if attr.value is not None: try: - sel_axis = attr.form.index(key) + sel_axis = attr.form.index(key_use) except (AttributeError, ValueError): # If form is not a tuple/list (and therefore not - # array-like), it'll throw an AttributeError, and if key is + # array-like), it'll throw an AttributeError, and if key_use is # not found in the tuple/list, it'll throw a ValueError. # In both cases, skip! continue @@ -7849,7 +7838,7 @@ def _select_by_index( # If we're working with an ndarray, use take to slice along # the axis that we want to grab from. attr.value = attr.value.take(ind_arr, axis=sel_axis) - attr.setter(self) + attr.setter(obj_use) elif isinstance(attr.value, list): # If this is a list, it _should_ always have 1-dimension. assert sel_axis == 0, ( @@ -7858,7 +7847,7 @@ def _select_by_index( "issue in our GitHub issue log so that we can fix it." ) attr.value = [attr.value[idx] for idx in ind_arr] - attr.setter(self) + attr.setter(obj_use) if key == "Nblts": # Process post blt-specific selection actions, including counting diff --git a/pyuvdata/uvdata/uvfits.py b/pyuvdata/uvdata/uvfits.py index aa2462e997..dc10e42680 100644 --- a/pyuvdata/uvdata/uvfits.py +++ b/pyuvdata/uvdata/uvfits.py @@ -70,8 +70,8 @@ def _get_parameter_data( longitude=longitude, altitude=altitude, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) else: @@ -570,7 +570,7 @@ def read_uvfits( if "FRAME" in ant_hdu.header.keys(): if ant_hdu.header["FRAME"] == "ITRF": # uvfits uses ITRF, astropy uses itrs. They are the same. - self._telescope_location.frame = "itrs" + self.telescope._location.frame = "itrs" elif ant_hdu.header["FRAME"] == "????": # default to itrs, but use the lat/lon/alt to set the location # if they are available. @@ -580,7 +580,7 @@ def read_uvfits( "may lead to other warnings or errors." ) prefer_lat_lon_alt = True - self._telescope_location.frame = "itrs" + self.telescope._location.frame = "itrs" else: telescope_frame = ant_hdu.header["FRAME"].lower() if telescope_frame not in ["itrs", "mcmf"]: @@ -588,19 +588,19 @@ def read_uvfits( f"Telescope frame in file is {telescope_frame}. " "Only 'itrs' and 'mcmf' are currently supported." ) - self._telescope_location.frame = telescope_frame + self.telescope._location.frame = telescope_frame if ( telescope_frame != "itrs" and "ELLIPSOI" in ant_hdu.header.keys() ): - self._telescope_location.ellipsoid = ant_hdu.header["ELLIPSOI"] + self.telescope._location.ellipsoid = ant_hdu.header["ELLIPSOI"] else: warnings.warn( "Required Antenna keyword 'FRAME' not set; " "Assuming frame is 'ITRF'." ) - self._telescope_location.frame = "itrs" + self.telescope._location.frame = "itrs" # get telescope location and antenna positions. # VLA incorrectly sets ARRAYX/ARRAYY/ARRAYZ to 0, and puts array center @@ -627,8 +627,8 @@ def read_uvfits( rot_ecef_positions = ant_hdu.data.field("STABXYZ") _, longitude, altitude = uvutils.LatLonAlt_from_XYZ( np.array([x_telescope, y_telescope, z_telescope]), - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, check_acceptability=run_check_acceptability, ) self.antenna_positions = uvutils.ECEF_from_rotECEF( @@ -682,7 +682,11 @@ def read_uvfits( self.antenna_diameters = ant_hdu.data.field("DIAMETER") try: - self.set_telescope_params() + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) except ValueError as ve: warnings.warn(str(ve)) @@ -1334,13 +1338,13 @@ def write_uvfits( ant_hdu.header["ARRAYX"] = self.telescope_location[0] ant_hdu.header["ARRAYY"] = self.telescope_location[1] ant_hdu.header["ARRAYZ"] = self.telescope_location[2] - if self._telescope_location.frame == "itrs": + if self.telescope._location.frame == "itrs": # uvfits uses "ITRF" rather than "ITRS". They are the same thing. ant_hdu.header["FRAME"] = "ITRF" else: - ant_hdu.header["FRAME"] = self._telescope_location.frame.upper() + ant_hdu.header["FRAME"] = self.telescope._location.frame.upper() # use ELLIPSOI because of FITS 8 character limit for header items - ant_hdu.header["ELLIPSOI"] = self._telescope_location.ellipsoid + ant_hdu.header["ELLIPSOI"] = self.telescope._location.ellipsoid # TODO Karto: Do this more intelligently in the future if self.future_array_shapes: diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index 17cb2f00af..a616a695c8 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -561,6 +561,8 @@ def _read_header_with_fast_meta( self, filename: str | Path | FastUVH5Meta, *, + run_check: bool = True, + check_extra: bool = True, run_check_acceptability: bool = True, blt_order: tuple[str] | None | Literal["determine"] = None, blts_are_rectangular: bool | None = None, @@ -585,9 +587,9 @@ def _read_header_with_fast_meta( # background if desired. self.time_array = obj.time_array # must set the frame before setting the location using lat/lon/alt - self._telescope_location.frame = obj.telescope_frame - if self._telescope_location.frame == "mcmf": - self._telescope_location.ellipsoid = obj.ellipsoid + self.telescope._location.frame = obj.telescope_frame + if self.telescope._location.frame == "mcmf": + self.telescope._location.ellipsoid = obj.ellipsoid self.telescope_location_lat_lon_alt_degrees = ( obj.telescope_location_lat_lon_alt_degrees ) @@ -595,26 +597,23 @@ def _read_header_with_fast_meta( if "lst_array" in obj.header: self.lst_array = obj.header["lst_array"][:] proc = None + + if run_check_acceptability: + lat, lon, alt = self.telescope_location_lat_lon_alt_degrees + uvutils.check_lsts_against_times( + jd_array=self.time_array, + lst_array=self.lst_array, + latitude=lat, + longitude=lon, + altitude=alt, + lst_tols=(0, uvutils.LST_RAD_TOL), + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, + ) else: proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library ) - # This only checks the LSTs, which is not necessary if they are being - # computed now - run_check_acceptability = False - - if run_check_acceptability: - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees - uvutils.check_lsts_against_times( - jd_array=self.time_array, - lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, - lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self._telescope_location.frame, - ellipsoid=self._telescope_location.ellipsoid, - ) # Required parameters for attr in [ @@ -733,7 +732,11 @@ def _read_header_with_fast_meta( self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id # set telescope params try: - self.set_telescope_params() + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) except ValueError as ve: warnings.warn(str(ve)) @@ -1157,6 +1160,8 @@ def read_uvh5( # open hdf5 file for reading self._read_header( meta, + run_check=run_check, + check_extra=check_extra, run_check_acceptability=run_check_acceptability, background_lsts=background_lsts, astrometry_library=astrometry_library, @@ -1266,9 +1271,9 @@ def _write_header(self, header): header["version"] = np.string_("1.2") # write out telescope and source information - header["telescope_frame"] = np.string_(self._telescope_location.frame) - if self._telescope_location.frame == "mcmf": - header["ellipsoid"] = self._telescope_location.ellipsoid + header["telescope_frame"] = np.string_(self.telescope._location.frame) + if self.telescope._location.frame == "mcmf": + header["ellipsoid"] = self.telescope._location.ellipsoid header["latitude"] = self.telescope_location_lat_lon_alt_degrees[0] header["longitude"] = self.telescope_location_lat_lon_alt_degrees[1] header["altitude"] = self.telescope_location_lat_lon_alt_degrees[2] From a241c23d7bb74c890470e327ce687aeaab7c3c69 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 18 Apr 2024 14:43:37 -0700 Subject: [PATCH 02/59] get telescope refactor working on uvcal --- pyuvdata/conftest.py | 39 ++- pyuvdata/telescopes.py | 111 ++++++- pyuvdata/tests/test_telescopes.py | 69 +++-- pyuvdata/uvcal/calfits.py | 22 +- pyuvdata/uvcal/calh5.py | 4 +- pyuvdata/uvcal/fhd_cal.py | 6 +- pyuvdata/uvcal/initializers.py | 236 ++++++++++----- pyuvdata/uvcal/ms_cal.py | 4 +- pyuvdata/uvcal/tests/conftest.py | 12 +- pyuvdata/uvcal/tests/test_calfits.py | 7 +- pyuvdata/uvcal/tests/test_calh5.py | 5 +- pyuvdata/uvcal/tests/test_fhd_cal.py | 5 +- pyuvdata/uvcal/tests/test_initializers.py | 32 +- pyuvdata/uvcal/tests/test_ms_cal.py | 54 +--- pyuvdata/uvcal/tests/test_uvcal.py | 108 +++++-- pyuvdata/uvcal/uvcal.py | 328 ++++++++++----------- pyuvdata/uvdata/tests/test_initializers.py | 3 +- pyuvdata/uvdata/uvdata.py | 10 +- 18 files changed, 646 insertions(+), 409 deletions(-) diff --git a/pyuvdata/conftest.py b/pyuvdata/conftest.py index f3502b5c83..8d90a7071f 100644 --- a/pyuvdata/conftest.py +++ b/pyuvdata/conftest.py @@ -49,10 +49,16 @@ def uvcalibrate_init_data_main(): use_future_array_shapes=True, ) uvcal = UVCal() - uvcal.read_calfits( - os.path.join(DATA_PATH, "zen.2458098.45361.HH.omni.calfits_downselected"), - use_future_array_shapes=True, - ) + with uvtest.check_warnings( + UserWarning, + match="telescope_location, antenna_positions, antenna_diameters are " + "not set or are being overwritten. telescope_location, antenna_positions, " + "antenna_diameters are set using values from known telescopes for HERA.", + ): + uvcal.read_calfits( + os.path.join(DATA_PATH, "zen.2458098.45361.HH.omni.calfits_downselected"), + use_future_array_shapes=True, + ) yield uvdata, uvcal @@ -76,24 +82,17 @@ def uvcalibrate_data_main(uvcalibrate_init_data_main): uvdata = uvdata_in.copy() uvcal = uvcal_in.copy() - with uvtest.check_warnings( - UserWarning, - match=[ - "telescope_location is not set. Using known values for HERA.", - "antenna_positions, antenna_names, antenna_numbers, Nants_telescope are " - "not set or are being overwritten. Using known values for HERA.", - ], - ): + warn_str = ( + "telescope_location, Nants, antenna_names, antenna_numbers, " + "antenna_positions, antenna_diameters are not set or are being " + "overwritten. telescope_location, Nants, antenna_names, " + "antenna_numbers, antenna_positions, antenna_diameters are set " + "using values from known telescopes for HERA." + ) + with uvtest.check_warnings(UserWarning, warn_str): uvcal.set_telescope_params(overwrite=True) - with uvtest.check_warnings( - UserWarning, - match=[ - "Nants_telescope, antenna_diameters, antenna_names, antenna_numbers, " - "antenna_positions, telescope_location, telescope_name are not set or are " - "being overwritten. Using known values for HERA." - ], - ): + with uvtest.check_warnings(UserWarning, warn_str): uvdata.set_telescope_params(overwrite=True) yield uvdata, uvcal diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index c81aa28ac5..2e8d75f914 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -173,7 +173,7 @@ def __init__(self): ) desc = ( - "List of antenna names, shape (Nants), " + "Array of antenna names, shape (Nants), " "with numbers given by antenna_numbers." ) self._antenna_names = uvp.UVParameter( @@ -185,7 +185,7 @@ def __init__(self): ) desc = ( - "List of integer antenna numbers corresponding to antenna_names, " + "Array of integer antenna numbers corresponding to antenna_names, " "shape (Nants)." ) self._antenna_numbers = uvp.UVParameter( @@ -283,6 +283,59 @@ def check(self, *, check_extra=True, run_check_acceptability=True): return True + @property + def location_obj(self): + """The location as an EarthLocation or MoonLocation object.""" + if self.location is None: + return None + + if self._location.frame == "itrs": + return EarthLocation.from_geocentric(*self.location, unit="m") + elif uvutils.hasmoon and self._location.frame == "mcmf": + moon_loc = uvutils.MoonLocation.from_selenocentric(*self.location, unit="m") + moon_loc.ellipsoid = self._location.ellipsoid + return moon_loc + + @location_obj.setter + def location_obj(self, val): + if isinstance(val, EarthLocation): + self._location.frame = "itrs" + elif isinstance(val, uvutils.MoonLocation): + self._location.frame = "mcmf" + self._location.ellipsoid = val.ellipsoid + else: + raise ValueError( + "location_obj is not a recognized location object. Must be an " + "EarthLocation or MoonLocation object." + ) + self.location = np.array( + [val.x.to("m").value, val.y.to("m").value, val.z.to("m").value] + ) + + def _set_uvcal_requirements(self): + """Set the UVParameter required fields appropriately for UVCal.""" + self._name.required = True + self._location.required = True + self._instrument.required = False + self._Nants.required = True + self._antenna_names.required = True + self._antenna_numbers.required = True + self._antenna_positions.required = True + self._antenna_diameters.required = False + self._x_orientation.required = True + + def _set_uvdata_requirements(self): + """Set the UVParameter required fields appropriately for UVData.""" + self._name.required = True + self._location.required = True + self._instrument.required = True + self._Nants.required = True + self._antenna_names.required = True + self._antenna_numbers.required = True + self._antenna_positions.required = True + self._x_orientation.required = False + self._antenna_diameters.required = False + def update_params_from_known_telescopes( self, *, @@ -376,10 +429,52 @@ def update_params_from_known_telescopes( "antenna_numbers": antenna_numbers, "antenna_positions": antenna_positions, } - for key, value in ant_info.items(): - if overwrite or getattr(self, key) is None: + if overwrite or all( + getattr(self, key) is None for key in ant_info.keys() + ): + for key, value in ant_info.items(): known_telescope_list.append(key) setattr(self, key, value) + elif ( + self.antenna_positions is None + and self.antenna_names is not None + and self.antenna_numbers is not None + ): + ant_inds = [] + telescope_ant_inds = [] + # first try to match using names only + for index, antname in enumerate(self.antenna_names): + if antname in ant_info["antenna_names"]: + ant_inds.append(index) + telescope_ant_inds.append( + np.where(ant_info["antenna_names"] == antname)[0][0] + ) + # next try using numbers + if len(ant_inds) != self.Nants: + for index, antnum in enumerate(self.antenna_numbers): + # only update if not already found + if ( + index not in ant_inds + and antnum in ant_info["antenna_numbers"] + ): + this_ant_ind = np.where( + ant_info["antenna_numbers"] == antnum + )[0][0] + # make sure we don't already have this antenna + # associated with another antenna + if this_ant_ind not in telescope_ant_inds: + ant_inds.append(index) + telescope_ant_inds.append(this_ant_ind) + if len(ant_inds) != self.Nants: + warnings.warn( + "Not all antennas have positions in the " + "known_telescope data. Not setting antenna_positions." + ) + else: + known_telescope_list.append("antenna_positions") + self.antenna_positions = ant_info["antenna_positions"][ + telescope_ant_inds, : + ] if "antenna_diameters" in telescope_dict.keys() and ( overwrite or self.antenna_diameters is None @@ -431,7 +526,9 @@ def update_params_from_known_telescopes( ) @classmethod - def get_telescope_from_known_telescopes(cls, name: str): + def get_telescope_from_known_telescopes( + cls, name: str, *, known_telescope_dict: dict = KNOWN_TELESCOPES + ): """ Create a new Telescope object using information from known_telescopes. @@ -449,5 +546,7 @@ def get_telescope_from_known_telescopes(cls, name: str): """ tel_obj = cls() tel_obj.name = name - tel_obj.update_params_from_known_telescopes(warn=False) + tel_obj.update_params_from_known_telescopes( + warn=False, known_telescope_dict=known_telescope_dict + ) return tel_obj diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index a15fad53ef..8449e8bf9b 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -12,24 +12,28 @@ from astropy.coordinates import EarthLocation import pyuvdata -from pyuvdata import UVData +from pyuvdata import Telescope, UVData from pyuvdata.data import DATA_PATH -required_parameters = ["_telescope_name", "_telescope_location"] -required_properties = ["telescope_name", "telescope_location"] +required_parameters = ["_name", "_location"] +required_properties = ["name", "location"] extra_parameters = [ "_antenna_diameters", - "_Nants_telescope", + "_Nants", "_antenna_names", "_antenna_numbers", "_antenna_positions", + "_x_orientation", + "_instrument", ] extra_properties = [ "antenna_diameters", - "Nants_telescope", + "Nants", "antenna_names", "antenna_numbers", "antenna_positions", + "x_orientation", + "instrument", ] other_attributes = [ "citation", @@ -124,10 +128,10 @@ def test_known_telescopes(): assert sorted(pyuvdata.known_telescopes()) == sorted(expected_known_telescopes) -def test_get_telescope(): +def test_get_telescope_from_known(): for inst in pyuvdata.known_telescopes(): - telescope_obj = pyuvdata.get_telescope(inst) - assert telescope_obj.telescope_name == inst + telescope_obj = Telescope.get_telescope_from_known_telescopes(inst) + assert telescope_obj.name == inst def test_get_telescope_center_xyz(): @@ -149,19 +153,19 @@ def test_get_telescope_center_xyz(): "citation": "", }, } - telescope_obj = pyuvdata.get_telescope( - "test", telescope_dict_in=test_telescope_dict + telescope_obj = Telescope.get_telescope_from_known_telescopes( + "test", known_telescope_dict=test_telescope_dict ) - telescope_obj_ext = pyuvdata.Telescope() + telescope_obj_ext = Telescope() telescope_obj_ext.citation = "" - telescope_obj_ext.telescope_name = "test" - telescope_obj_ext.telescope_location = ref_xyz + telescope_obj_ext.name = "test" + telescope_obj_ext.location = ref_xyz assert telescope_obj == telescope_obj_ext - telescope_obj_ext.telescope_name = "test2" - telescope_obj2 = pyuvdata.get_telescope( - "test2", telescope_dict_in=test_telescope_dict + telescope_obj_ext.name = "test2" + telescope_obj2 = Telescope.get_telescope_from_known_telescopes( + "test2", known_telescope_dict=test_telescope_dict ) assert telescope_obj2 == telescope_obj_ext @@ -176,12 +180,27 @@ def test_get_telescope_no_loc(): "citation": "", } } - pytest.raises( + with pytest.raises( ValueError, - pyuvdata.get_telescope, - "test", - telescope_dict_in=test_telescope_dict, - ) + match="Bad location information in known_telescopes_dict for telescope " + "test. Either the center_xyz or the latitude, longitude and altitude of " + "the telescope must be specified.", + ): + Telescope.get_telescope_from_known_telescopes( + "test", known_telescope_dict=test_telescope_dict + ) + + +def test_bad_location_obj(): + tel = Telescope() + tel.name = "foo" + + with pytest.raises( + ValueError, + match="location_obj is not a recognized location object. Must be an " + "EarthLocation or MoonLocation object.", + ): + tel.location_obj = (-2562123.42683, 5094215.40141, -2848728.58869) def test_hera_loc(): @@ -191,11 +210,11 @@ def test_hera_loc(): hera_file, read_data=False, file_type="uvh5", use_future_array_shapes=True ) - telescope_obj = pyuvdata.get_telescope("HERA") + telescope_obj = Telescope.get_telescope_from_known_telescopes("HERA") assert np.allclose( - telescope_obj.telescope_location, + telescope_obj.location, hera_data.telescope_location, - rtol=hera_data._telescope_location.tols[0], - atol=hera_data._telescope_location.tols[1], + rtol=hera_data.telescope._location.tols[0], + atol=hera_data.telescope._location.tols[1], ) diff --git a/pyuvdata/uvcal/calfits.py b/pyuvdata/uvcal/calfits.py index 14b5fefa36..76a69ff1b7 100644 --- a/pyuvdata/uvcal/calfits.py +++ b/pyuvdata/uvcal/calfits.py @@ -574,20 +574,18 @@ def read_calfits( # Remove the padded entries. self.ant_array = self.ant_array[np.where(self.ant_array >= 0)[0]] - if anthdu.header["TFIELDS"] > 3: + if "ANTXYZ" in antdata.names: self.antenna_positions = antdata["ANTXYZ"] - try: - self.antenna_diameters = np.array(list(map(float, antdata["ANTDIAM"]))) - except KeyError: - # No field by this name, which means ant diams not recorded. - # Move along... - pass + if "ANTDIAM" in antdata.names: + self.antenna_diameters = antdata["ANTDIAM"] self.channel_width = hdr.pop("CHWIDTH") self.integration_time = hdr.pop("INTTIME") self.telescope_name = hdr.pop("TELESCOP") + self.x_orientation = hdr.pop("XORIENT") + x_telescope = hdr.pop("ARRAYX", None) y_telescope = hdr.pop("ARRAYY", None) z_telescope = hdr.pop("ARRAYZ", None) @@ -604,11 +602,10 @@ def read_calfits( ) elif lat is not None and lon is not None and alt is not None: self.telescope_location_lat_lon_alt_degrees = (lat, lon, alt) - if self.telescope_location is None or self.antenna_positions is None: - try: - self.set_telescope_params() - except ValueError as ve: - warnings.warn(str(ve)) + try: + self.set_telescope_params() + except ValueError as ve: + warnings.warn(str(ve)) self.history = str(hdr.get("HISTORY", "")) @@ -622,7 +619,6 @@ def read_calfits( self.gain_convention = hdr.pop("GNCONVEN") self.gain_scale = hdr.pop("GNSCALE", None) - self.x_orientation = hdr.pop("XORIENT") self.cal_type = hdr.pop("CALTYPE") # old files might have a freq range for gain types but we don't want them diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index 71a44fdf2e..0e14910247 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -303,7 +303,7 @@ def _read_header( longitude=lon, altitude=alt, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) if self.time_range is not None: uvutils.check_lsts_against_times( @@ -313,7 +313,7 @@ def _read_header( longitude=lon, altitude=alt, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) def _get_data( diff --git a/pyuvdata/uvcal/fhd_cal.py b/pyuvdata/uvcal/fhd_cal.py index 57f85120fa..ee1b58659a 100644 --- a/pyuvdata/uvcal/fhd_cal.py +++ b/pyuvdata/uvcal/fhd_cal.py @@ -164,7 +164,7 @@ def read_fhd_cal( longitude=longitude, altitude=altitude, radian_tol=uvutils.RADIAN_TOL, - loc_tols=self._telescope_location.tols, + loc_tols=self.telescope._location.tols, obs_tile_names=obs_tile_names, run_check_acceptability=True, ) @@ -202,6 +202,8 @@ def read_fhd_cal( self.antenna_names = np.asarray(self.antenna_names) + self.x_orientation = "east" + try: self.set_telescope_params() except ValueError as ve: @@ -215,8 +217,6 @@ def read_fhd_cal( self._set_sky() self.gain_convention = "divide" - self.x_orientation = "east" - self._set_gain() # currently don't have branch info. may change in future. diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index 952921521c..a34b307b00 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -8,10 +8,9 @@ from typing import Literal import numpy as np -from astropy.coordinates import EarthLocation from astropy.time import Time -from .. import __version__, utils +from .. import Telescope, __version__, utils from ..docstrings import combine_docstrings from ..uvdata.initializers import ( XORIENTMAP, @@ -25,13 +24,9 @@ def new_uvcal( *, - antenna_positions: np.ndarray | dict[str | int, np.ndarray], - telescope_location: Locations, - telescope_name: str, cal_style: Literal["sky", "redundant"], gain_convention: Literal["divide", "multiply"], jones_array: np.ndarray | str, - x_orientation: Literal["east", "north", "e", "n", "ew", "ns"], time_array: np.ndarray | None = None, time_range: np.ndarray | None = None, freq_array: np.ndarray | None = None, @@ -39,6 +34,11 @@ def new_uvcal( cal_type: Literal["delay", "gain"] | None = None, integration_time: float | np.ndarray | None = None, channel_width: float | np.ndarray | None = None, + telescope: Telescope | None = None, + telescope_location: Locations | None = None, + telescope_name: str | None = None, + x_orientation: Literal["east", "north", "e", "n", "ew", "ns"] | None = None, + antenna_positions: np.ndarray | dict[str | int, np.ndarray] | None = None, antenna_names: list[str] | None = None, antenna_numbers: list[int] | None = None, antname_format: str = "{0:03d}", @@ -64,16 +64,6 @@ def new_uvcal( two-dimensional array with shape (Ntimes, 2) where the second axis gives the start time and stop time (in that order). Only one of time_array or time_range should be supplied. - antenna_positions : ndarray of float or dict of ndarray of float - Array of antenna positions in ECEF coordinates in meters. - If a dict, keys can either be antenna numbers or antenna names, and values are - position arrays. Keys are interpreted as antenna numbers if they are integers, - otherwise they are interpreted as antenna names if strings. You cannot - provide a mix of different types of keys. - telescope_location : ndarray of float or str - Telescope location as an astropy EarthLocation object or MoonLocation object. - telescope_name : str - Telescope name. cal_style : str Calibration style. Options are 'sky' or 'redundant'. freq_array : ndarray of float, optional @@ -84,8 +74,6 @@ def new_uvcal( the calibration is assumed to be wide band. The array shape should be (Nspws, 2) gain_convention : str Gain convention. Options are 'divide' or 'multiply'. - x_orientation : str - Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', 'ew', 'ns'. jones_array : ndarray of int or str Array of Jones polarization integers. If a string, options are 'linear' or 'circular' (which will be converted to the appropriate Jones integers as @@ -110,15 +98,37 @@ def new_uvcal( If not provided and freq_array is length-one, the channel_width will be set to 1 Hz (and a warning issued). If an ndarray is provided, it must have the same shape as freq_array. + telescope : pyuvdata.Telescope + Telescope object containing the telescope-related metadata including + telescope name and location, x_orientation and antenna names, numbers + and positions. + telescope_location : ndarray of float or str + Telescope location as an astropy EarthLocation object or MoonLocation + object. Not required or used if a Telescope object is passed to `telescope`. + telescope_name : str + Telescope name. Not required or used if a Telescope object is passed to + `telescope`. + x_orientation : str + Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', 'ew', 'ns'. + Not required or used if a Telescope object is passed to `telescope`. + antenna_positions : ndarray of float or dict of ndarray of float + Array of antenna positions in ECEF coordinates in meters. + If a dict, keys can either be antenna numbers or antenna names, and values are + position arrays. Keys are interpreted as antenna numbers if they are integers, + otherwise they are interpreted as antenna names if strings. You cannot + provide a mix of different types of keys. + Not required or used if a Telescope object is passed to `telescope`. antenna_names : list of str, optional List of antenna names. If not provided, antenna numbers will be used to form - the antenna_names, according to the antname_format. antenna_names need not be - provided if antenna_positions is a dict with string keys. + the antenna_names, according to the antname_format. Not required or used + if a Telescope object is passed to `telescope` or if antenna_positions + is a dict with string keys. antenna_numbers : list of int, optional List of antenna numbers. If not provided, antenna names will be used to form the antenna_numbers, but in this case the antenna_names must be strings that - can be converted to integers. antenna_numbers need not be provided if - antenna_positions is a dict with integer keys. + can be converted to integers. Not required or used + if a Telescope object is passed to `telescope` or if antenna_positions + is a dict with integer keys. antname_format : str, optional Format string for antenna names. Default is '{0:03d}'. ant_array : ndarray of int, optional @@ -166,12 +176,37 @@ def new_uvcal( uvc = UVCal() - antenna_positions, antenna_names, antenna_numbers = get_antenna_params( - antenna_positions=antenna_positions, - antenna_names=antenna_names, - antenna_numbers=antenna_numbers, - antname_format=antname_format, - ) + if telescope is None: + required_without_tel = { + "antenna_positions": antenna_positions, + "telescope_location": telescope_location, + "telescope_name": telescope_name, + "x_orientation": x_orientation, + } + for key, value in required_without_tel.items(): + if value is None: + raise ValueError(f"{key} is required if telescope is not provided.") + else: + required_on_tel = { + "antenna_positions": antenna_positions, + "x_orientation": x_orientation, + } + for key, value in required_on_tel.items(): + if value is None and getattr(telescope, key) is None: + raise ValueError(f"{key} is required if it is not set on telescope.") + + if telescope is not None: + antenna_numbers = telescope.antenna_numbers + telescope_location = telescope.location_obj + else: + antenna_positions, antenna_names, antenna_numbers = get_antenna_params( + antenna_positions=antenna_positions, + antenna_names=antenna_names, + antenna_numbers=antenna_numbers, + antname_format=antname_format, + ) + x_orientation = XORIENTMAP[x_orientation.lower()] + if ant_array is None: ant_array = antenna_numbers else: @@ -233,8 +268,6 @@ def new_uvcal( if cal_type not in ("gain", "delay"): raise ValueError(f"cal_type must be either 'gain' or 'delay', got {cal_type}") - x_orientation = XORIENTMAP[x_orientation.lower()] - if isinstance(jones_array, str): if jones_array == "linear": jones_array = np.array([-5, -6, -7, -8]) @@ -259,16 +292,25 @@ def new_uvcal( ) # Now set all the metadata + if telescope is not None: + uvc.telescope = telescope + # set the appropriate telescope attributes as required + uvc.telescope._set_uvcal_requirements() + else: + new_telescope = Telescope() + new_telescope._set_uvcal_requirements() + + new_telescope.name = telescope_name + new_telescope.location_obj = telescope_location + new_telescope.antenna_names = antenna_names + new_telescope.antenna_numbers = antenna_numbers + new_telescope.antenna_positions = antenna_positions + new_telescope.x_orientation = x_orientation + + uvc.telescope = new_telescope + uvc.freq_array = freq_array - uvc.antenna_positions = antenna_positions - uvc.telescope_location = np.array( - [ - telescope_location.x.to_value("m"), - telescope_location.y.to_value("m"), - telescope_location.z.to_value("m"), - ] - ) - uvc.telescope_name = telescope_name + if time_array is not None: uvc.time_array = time_array uvc.lst_array = lst_array @@ -279,15 +321,11 @@ def new_uvcal( uvc.Ntimes = time_range.shape[0] uvc.integration_time = integration_time uvc.channel_width = channel_width - uvc.antenna_names = antenna_names - uvc.antenna_numbers = antenna_numbers uvc.history = history uvc.ant_array = ant_array - uvc.telescope_name = telescope_name uvc.cal_style = cal_style uvc.cal_type = cal_type uvc.gain_convention = gain_convention - uvc.x_orientation = x_orientation uvc.ref_antenna_name = ref_antenna_name uvc.sky_catalog = sky_catalog uvc.jones_array = jones_array @@ -467,6 +505,9 @@ def new_uvcal_from_uvdata( # not computable directly from flex_spw_id_array in this case kwargs["spw_array"] = spw_array + new_telescope = uvdata.telescope.copy() + new_telescope._set_uvcal_requirements() + # Figure out how to mesh the antenna parameters given with those in the uvd if "antenna_positions" not in kwargs: if "antenna_numbers" in kwargs or "antenna_names" in kwargs: @@ -483,32 +524,82 @@ def new_uvcal_from_uvdata( if antenna_numbers is not None: idx = [ i - for i, v in enumerate(uvdata.antenna_numbers) + for i, v in enumerate(new_telescope.antenna_numbers) if v in antenna_numbers ] elif antenna_names is not None: idx = [ - i for i, v in enumerate(uvdata.antenna_names) if v in antenna_names + i + for i, v in enumerate(new_telescope.antenna_names) + if v in antenna_names ] - antenna_numbers = np.asarray(uvdata.antenna_numbers)[idx] - antenna_names = np.asarray(uvdata.antenna_names)[idx] - antenna_positions = uvdata.antenna_positions[idx] - else: - antenna_numbers = uvdata.antenna_numbers - antenna_names = uvdata.antenna_names - antenna_positions = uvdata.antenna_positions + antenna_numbers = np.asarray(new_telescope.antenna_numbers)[idx] + antenna_names = np.asarray(new_telescope.antenna_names)[idx] + antenna_positions = new_telescope.antenna_positions[idx] + if ( + new_telescope.antenna_diameters is not None + and "antenna_diameters" not in kwargs + ): + antenna_diameters = new_telescope.antenna_diameters[idx] + elif "antenna_diameters" in kwargs: + antenna_diameters = kwargs.pop("antenna_diameters") + else: + antenna_diameters = None - # Sort them, because that's what the old function did - sort_idx = np.argsort(antenna_numbers) - antenna_numbers = np.asarray(antenna_numbers)[sort_idx] - antenna_names = ((np.asarray(antenna_names))[sort_idx]).tolist() - antenna_positions = antenna_positions[sort_idx] + # Sort them, because that's what the old function did + sort_idx = np.argsort(antenna_numbers) + new_telescope.antenna_numbers = np.asarray(antenna_numbers)[sort_idx] + new_telescope.antenna_names = np.asarray(antenna_names)[sort_idx] + new_telescope.antenna_positions = antenna_positions[sort_idx] + if antenna_diameters is not None: + new_telescope.antenna_diameters = antenna_diameters + new_telescope.Nants = new_telescope.antenna_numbers.size else: - antenna_positions = kwargs.pop("antenna_positions") - antenna_numbers = kwargs.pop("antenna_numbers", None) - antenna_names = kwargs.pop("antenna_names", None) + ant_metadata_kwargs = {} + for param in [ + "antenna_positions", + "antenna_numbers", + "antenna_names", + "antname_format", + ]: + if param in kwargs: + ant_metadata_kwargs[param] = kwargs.pop(param) + antenna_positions, antenna_names, antenna_numbers = get_antenna_params( + **ant_metadata_kwargs + ) + new_telescope.antenna_positions = antenna_positions + new_telescope.antenna_names = antenna_names + new_telescope.antenna_numbers = antenna_numbers + new_telescope.Nants = np.asarray(new_telescope.antenna_numbers).size + + # map other UVCal telescope parameters to their names on a Telescope object + other_tele_params = { + "telescope_name": "name", + "telescope_location": "location", + "instrument": "instrument", + "antenna_diameters": "antenna_diameters", + } + for param, tele_name in other_tele_params.items(): + if param in kwargs.keys(): + setattr(new_telescope, tele_name, kwargs.pop(param)) + + if "x_orientation" in kwargs: + new_telescope.x_orientation = XORIENTMAP[kwargs.pop("x_orientation").lower()] + + new_telescope.check() + for param in [ + "telescope_name", + "telescope_location", + "instrument", + "x_orientation", + "antenna_positions", + "antenna_numbers", + "antenna_names", + "antenna_diameters", + ]: + assert param not in kwargs, f"{param} still in kwargs, value: {kwargs[param]}" ant_array = kwargs.pop( "ant_array", np.union1d(uvdata.ant_1_array, uvdata.ant_2_array) @@ -519,14 +610,9 @@ def new_uvcal_from_uvdata( if not isinstance(ant_array, np.ndarray): ant_array = np.asarray(ant_array) - if antenna_numbers is not None: - ant_array = np.intersect1d( - ant_array, np.asarray(antenna_numbers, dtype=ant_array.dtype) - ) - elif isinstance(antenna_positions, dict): - ant_array = np.intersect1d( - ant_array, np.asarray(list(antenna_positions.keys()), dtype=ant_array.dtype) - ) + ant_array = np.intersect1d( + ant_array, np.asarray(new_telescope.antenna_numbers, dtype=ant_array.dtype) + ) if jones_array is None: if np.all(uvdata.polarization_array < -4): @@ -558,25 +644,13 @@ def new_uvcal_from_uvdata( cal_style=cal_style, gain_convention=gain_convention, jones_array=jones_array, - x_orientation=kwargs.pop("x_orientation", uvdata.x_orientation), + telescope=new_telescope, freq_array=freq_array, freq_range=freq_range, cal_type=cal_type, time_array=time_array, time_range=time_range, - antenna_positions=antenna_positions, - antenna_names=antenna_names, - antenna_numbers=antenna_numbers, ant_array=ant_array, - telescope_location=kwargs.pop( - "telescope_location", - EarthLocation( - lat=uvdata.telescope_location_lat_lon_alt_degrees[0], - lon=uvdata.telescope_location_lat_lon_alt_degrees[1], - height=uvdata.telescope_location_lat_lon_alt_degrees[2], - ), - ), - telescope_name=kwargs.pop("telescope_name", uvdata.telescope_name), integration_time=integration_time, channel_width=channel_width, flex_spw_id_array=flex_spw_id_array, diff --git a/pyuvdata/uvcal/ms_cal.py b/pyuvdata/uvcal/ms_cal.py index d66fe5f42e..0c2e28f69a 100644 --- a/pyuvdata/uvcal/ms_cal.py +++ b/pyuvdata/uvcal/ms_cal.py @@ -136,8 +136,8 @@ def read_ms_cal( self.observer = obs_info["observer"] self.telescope_name = obs_info["telescope_name"] - self._telescope_location.frame = ant_info["telescope_frame"] - self._telescope_location.ellipsoid = ant_info["telescope_ellipsoid"] + self.telescope._location.frame = ant_info["telescope_frame"] + self.telescope._location.ellipsoid = ant_info["telescope_ellipsoid"] # check to see if a TELESCOPE_LOCATION column is present in the observation # table. This is non-standard, but inserted by pyuvdata diff --git a/pyuvdata/uvcal/tests/conftest.py b/pyuvdata/uvcal/tests/conftest.py index 257ef0b70a..36f59a00ce 100644 --- a/pyuvdata/uvcal/tests/conftest.py +++ b/pyuvdata/uvcal/tests/conftest.py @@ -21,9 +21,9 @@ def gain_data_main(): with uvtest.check_warnings( UserWarning, match=[ - "telescope_location is not set. Using known values for HERA.", - "antenna_positions are not set or are being overwritten. Using known " - "values for HERA.", + "telescope_location, antenna_positions, antenna_diameters are not " + "set or are being overwritten. telescope_location, antenna_positions, " + "antenna_diameters are set using values from known telescopes for HERA." ], ): gain_object = UVCal.from_file(gainfile, use_future_array_shapes=True) @@ -51,9 +51,9 @@ def delay_data_main(): with uvtest.check_warnings( UserWarning, match=[ - "telescope_location is not set. Using known values for HERA.", - "antenna_positions are not set or are being overwritten. Using known " - "values for HERA.", + "telescope_location, antenna_positions, antenna_diameters are not " + "set or are being overwritten. telescope_location, antenna_positions, " + "antenna_diameters are set using values from known telescopes for HERA.", "When converting a delay-style cal to future array shapes the flag_array" " (and input_flag_array if it exists) must drop the frequency axis", ], diff --git a/pyuvdata/uvcal/tests/test_calfits.py b/pyuvdata/uvcal/tests/test_calfits.py index f2a71c2bd4..7d094f9130 100644 --- a/pyuvdata/uvcal/tests/test_calfits.py +++ b/pyuvdata/uvcal/tests/test_calfits.py @@ -18,11 +18,6 @@ from pyuvdata.uvcal.tests import extend_jones_axis, time_array_to_time_range from pyuvdata.uvcal.uvcal import _future_array_shapes_warning -pytestmark = pytest.mark.filterwarnings( - "ignore:telescope_location is not set. Using known values", - "ignore:antenna_positions are not set or are being overwritten. Using known values", -) - @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated") @@ -134,6 +129,7 @@ def test_write_inttime_equal_timediff(future_shapes, gain_data, delay_data, tmp_ return +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize( "filein,caltype", [ @@ -172,6 +168,7 @@ def test_readwriteread_no_freq_range(gain_data, tmp_path): return +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_readwriteread_no_time_range(tmp_path): # test without time_range parameter testfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.gain.calfits") diff --git a/pyuvdata/uvcal/tests/test_calh5.py b/pyuvdata/uvcal/tests/test_calh5.py index 65e72f5161..03c2eddf29 100644 --- a/pyuvdata/uvcal/tests/test_calh5.py +++ b/pyuvdata/uvcal/tests/test_calh5.py @@ -199,10 +199,7 @@ def test_calh5_unknown_telescope(gain_data, tmp_path): testfile = str(tmp_path / "unknown_telescope.calh5") cal_obj.write_calh5(testfile) - with uvtest.check_warnings( - UserWarning, match="Telescope foo is not in known_telescopes." - ): - cal_obj2 = UVCal.from_file(testfile, use_future_array_shapes=True) + cal_obj2 = UVCal.from_file(testfile, use_future_array_shapes=True) assert cal_obj2 == cal_obj diff --git a/pyuvdata/uvcal/tests/test_fhd_cal.py b/pyuvdata/uvcal/tests/test_fhd_cal.py index a88b8bd553..b47c142383 100644 --- a/pyuvdata/uvcal/tests/test_fhd_cal.py +++ b/pyuvdata/uvcal/tests/test_fhd_cal.py @@ -297,7 +297,6 @@ def test_unknown_telescope(): with uvtest.check_warnings( UserWarning, match=[ - "Telescope foo is not in known_telescopes.", "Telescope location derived from obs lat/lon/alt", "Some FHD input files do not have the expected subfolder so FHD folder " "matching could not be done. The affected file types are: ['obs']", @@ -364,8 +363,8 @@ def test_break_read_fhdcal(cal_file, obs_file, layout_file, settings_file, nfile message_list = [ "No layout file, antenna_postions will not be defined.", - "antenna_positions are not set or are being overwritten. Using known values " - "for mwa.", + "antenna_positions are not set or are being overwritten. " + "antenna_positions are set using values from known telescopes for mwa.", ] * nfiles + ["UVParameter diffuse_model does not match"] * (nfiles - 1) warning_list = [UserWarning] * (3 * nfiles - 1) diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index 6b83e11f64..6a29c6a772 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -8,6 +8,7 @@ import pytest from astropy.coordinates import EarthLocation +from pyuvdata.tests.test_utils import selenoids from pyuvdata.uvcal import UVCal from pyuvdata.uvcal.initializers import new_uvcal, new_uvcal_from_uvdata from pyuvdata.uvdata.initializers import new_uvdata @@ -56,6 +57,21 @@ def test_new_uvcal_simplest(uvc_kw): assert uvc.Ntimes == 12 +@pytest.mark.parametrize("selenoid", selenoids) +def test_new_uvcal_simple_moon(uvc_kw, selenoid): + pytest.importorskip("lunarsky") + from pyuvdata.utils import MoonLocation + + uvc_kw["telescope_location"] = MoonLocation.from_selenodetic( + 0, 0, 0, ellipsoid=selenoid + ) + uvc = UVCal.new(**uvc_kw) + assert uvc.telescope._location.frame == "mcmf" + assert uvc.telescope._location.ellipsoid == selenoid + assert uvc.telescope.location_obj == uvc_kw["telescope_location"] + assert uvc.telescope.location_obj.ellipsoid == selenoid + + def test_new_uvcal_time_range(uvc_kw): tdiff = np.mean(np.diff(uvc_kw["time_array"])) tstarts = uvc_kw["time_array"] - tdiff / 2 @@ -202,12 +218,17 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): 0: uvd_kw["antenna_positions"][0], 1: uvd_kw["antenna_positions"][1], }, + antenna_diameters=[10.0, 10.0], **uvc_only_kw ) assert np.all(uvc.antenna_positions[0] == uvd_kw["antenna_positions"][0]) assert len(uvc.antenna_positions) == 2 + uvd.antenna_diameters = np.zeros(uvd.Nants_telescope, dtype=float) + 5.0 + uvc = new_uvcal_from_uvdata(uvd, **uvc_only_kw) + assert np.all(uvc.antenna_diameters == uvd.antenna_diameters) + def test_new_uvcal_set_freq_range_for_gain_type(uvd_kw, uvc_only_kw): uvd = new_uvdata(**uvd_kw) @@ -241,9 +262,15 @@ def test_new_uvcal_get_freq_range_without_spwids(uvd_kw, uvc_only_kw): assert uvc.freq_range.max() == uvd.freq_array.max() -def test_new_uvcal_from_uvdata_specify_numbers_names(uvd_kw, uvc_only_kw): +@pytest.mark.parametrize("diameters", ["uvdata", "kwargs", None, "both"]) +def test_new_uvcal_from_uvdata_specify_numbers_names(uvd_kw, uvc_only_kw, diameters): uvd = new_uvdata(**uvd_kw) + if diameters in ["uvdata", "both"]: + uvd.antenna_diameters = np.zeros(uvd.Nants_telescope, dtype=float) + 5.0 + elif diameters in ["kwargs", "both"]: + uvc_only_kw["antenna_diameters"] = np.zeros(1, dtype=float) + 5.0 + with pytest.raises( ValueError, match="Cannot specify both antenna_numbers and antenna_names" ): @@ -260,6 +287,9 @@ def test_new_uvcal_from_uvdata_specify_numbers_names(uvd_kw, uvc_only_kw): uvc2 = new_uvcal_from_uvdata( uvd, antenna_names=uvd.antenna_names[:1], **uvc_only_kw ) + if diameters is not None: + assert np.all(uvc.antenna_diameters == 5.0) + uvc.history = uvc2.history assert uvc == uvc2 diff --git a/pyuvdata/uvcal/tests/test_ms_cal.py b/pyuvdata/uvcal/tests/test_ms_cal.py index a052853ee7..2dd472ac08 100644 --- a/pyuvdata/uvcal/tests/test_ms_cal.py +++ b/pyuvdata/uvcal/tests/test_ms_cal.py @@ -24,19 +24,21 @@ ) +sma_warnings = [ + "Unknown polarization basis for solutions, jones_array values may " "be spurious.", + "Unknown x_orientation basis for solutions, assuming", + "key CASA_Version in extra_keywords is longer than 8 characters. " + "It will be truncated to 8 if written to a calfits file format.", + "telescope_location are not set or are being overwritten. " + "telescope_location are set using values from known telescopes for SMA.", +] + + @pytest.fixture(scope="session") def sma_pcal_main(): uvobj = UVCal() testfile = os.path.join(DATA_PATH, "sma.ms.pha.gcal") - with uvtest.check_warnings( - UserWarning, - [ - "Unknown polarization", - "Unknown x_orientation", - "key CASA_Version", - "telescope_location is not set", - ], - ): + with uvtest.check_warnings(UserWarning, match=sma_warnings): uvobj.read(testfile) uvobj.gain_scale = "Jy" @@ -55,15 +57,7 @@ def sma_pcal(sma_pcal_main): def sma_dcal_main(): uvobj = UVCal() testfile = os.path.join(DATA_PATH, "sma.ms.dcal") - with uvtest.check_warnings( - UserWarning, - [ - "Unknown polarization", - "Unknown x_orientation", - "key CASA_Version", - "telescope_location is not set", - ], - ): + with uvtest.check_warnings(UserWarning, match=sma_warnings): uvobj.read(testfile) yield uvobj @@ -80,15 +74,7 @@ def sma_dcal(sma_dcal_main): def sma_bcal_main(): uvobj = UVCal() testfile = os.path.join(DATA_PATH, "sma.ms.bcal") - with uvtest.check_warnings( - UserWarning, - [ - "Unknown polarization", - "Unknown x_orientation", - "key CASA_Version", - "telescope_location is not set", - ], - ): + with uvtest.check_warnings(UserWarning, match=sma_warnings): uvobj.read(testfile) uvobj.gain_scale = "Jy" @@ -220,24 +206,14 @@ def test_ms_default_setting(): uvc1 = UVCal() uvc2 = UVCal() testfile = os.path.join(DATA_PATH, "sma.ms.pha.gcal") - with uvtest.check_warnings( - UserWarning, ["key CASA_Version", "telescope_location is not set"] - ): + with uvtest.check_warnings(UserWarning, match=sma_warnings[2:]): uvc1.read_ms_cal( testfile, default_x_orientation="north", default_jones_array=np.array([-5, -6]), ) - with uvtest.check_warnings( - UserWarning, - [ - "Unknown polarization", - "Unknown x_orientation", - "key CASA_Version", - "telescope_location is not set", - ], - ): + with uvtest.check_warnings(UserWarning, match=sma_warnings): uvc2.read(testfile) assert uvc1.x_orientation == "north" diff --git a/pyuvdata/uvcal/tests/test_uvcal.py b/pyuvdata/uvcal/tests/test_uvcal.py index 864855c4e5..2cd3aba776 100644 --- a/pyuvdata/uvcal/tests/test_uvcal.py +++ b/pyuvdata/uvcal/tests/test_uvcal.py @@ -23,9 +23,7 @@ from pyuvdata.uvcal.uvcal import _future_array_shapes_warning pytestmark = pytest.mark.filterwarnings( - "ignore:telescope_location is not set. Using known values", - "ignore:antenna_positions are not set or are being overwritten. Using known values", - "ignore:key CASA_Version in extra_keywords is longer than 8 characters", + "ignore:key CASA_Version in extra_keywords is longer than 8 characters" ) @@ -75,12 +73,9 @@ def uvcal_data(): "Ntimes", "Nspws", "Nants_data", - "Nants_telescope", "wide_band", - "antenna_names", - "antenna_numbers", "ant_array", - "telescope_name", + "telescope", "freq_array", "channel_width", "spw_array", @@ -91,15 +86,12 @@ def uvcal_data(): "flag_array", "cal_type", "cal_style", - "x_orientation", "future_array_shapes", "history", ] required_parameters = ["_" + prop for prop in required_properties] extra_properties = [ - "telescope_location", - "antenna_positions", "lst_array", "lst_range", "gain_array", @@ -127,13 +119,25 @@ def uvcal_data(): "scan_number_array", "phase_center_catalog", "phase_center_id_array", - "antenna_diameters", "Nphase", "ref_antenna_array", ] extra_parameters = ["_" + prop for prop in extra_properties] - other_properties = ["pyuvdata_version_str"] + other_attributes = [ + "pyuvdata_version_str", + "telescope_name", + "telescope_location", + "instrument", + "Nants_telescope", + "antenna_names", + "antenna_numbers", + "antenna_positions", + "x_orientation", + "antenna_diameters", + "telescope_location_lat_lon_alt", + "telescope_location_lat_lon_alt_degrees", + ] uv_cal_object = UVCal() @@ -144,7 +148,7 @@ def uvcal_data(): required_properties, extra_parameters, extra_properties, - other_properties, + other_attributes, ) # some post-test object cleanup @@ -199,10 +203,10 @@ def test_unexpected_parameters(uvcal_data): def test_unexpected_attributes(uvcal_data): """Test for extra attributes.""" - (uv_cal_object, _, required_properties, _, extra_properties, other_properties) = ( + (uv_cal_object, _, required_properties, _, extra_properties, other_attributes) = ( uvcal_data ) - expected_attributes = required_properties + extra_properties + other_properties + expected_attributes = required_properties + extra_properties + other_attributes attributes = [i for i in uv_cal_object.__dict__.keys() if i[0] != "_"] for a in attributes: assert a in expected_attributes, "unexpected attribute " + a + " found in UVCal" @@ -518,11 +522,13 @@ def test_unknown_telescopes(gain_data, tmp_path): ValueError, match="Required UVParameter _antenna_positions has not been set." ): with uvtest.check_warnings( - [UserWarning], match=["Telescope foo is not in known_telescopes"] + UserWarning, + match="Telescope foo is not in astropy_sites or known_telescopes_dict.", ): UVCal.from_file(write_file2, use_future_array_shapes=True) with uvtest.check_warnings( - [UserWarning], match=["Telescope foo is not in known_telescopes"] + UserWarning, + match="Telescope foo is not in astropy_sites or known_telescopes_dict.", ): UVCal.from_file(write_file2, use_future_array_shapes=True, run_check=False) @@ -536,6 +542,11 @@ def test_nants_data_telescope_larger(gain_data): gain_data.antenna_positions = np.concatenate( (gain_data.antenna_positions, np.zeros((1, 3), dtype=float)) ) + if gain_data.antenna_diameters is not None: + gain_data.antenna_diameters = np.concatenate( + (gain_data.antenna_diameters, np.ones((1,), dtype=float)) + ) + assert gain_data.check() @@ -545,6 +556,8 @@ def test_ant_array_not_in_antnums(gain_data): gain_data.antenna_names = gain_data.antenna_names[1:] gain_data.antenna_numbers = gain_data.antenna_numbers[1:] gain_data.antenna_positions = gain_data.antenna_positions[1:, :] + if gain_data.antenna_diameters is not None: + gain_data.antenna_diameters = gain_data.antenna_diameters[1:] gain_data.Nants_telescope = gain_data.antenna_numbers.size with pytest.raises(ValueError) as cm: gain_data.check() @@ -3545,7 +3558,7 @@ def test_add_errors( # test compatibility param mismatch calobj2.telescope_name = "PAPER" - with pytest.raises(ValueError, match="Parameter telescope_name does not match"): + with pytest.raises(ValueError, match="Parameter telescope does not match"): getattr(calobj, method)(calobj2, **kwargs) # test array shape mismatch @@ -4082,8 +4095,8 @@ def test_match_antpos_antname(gain_data, antnamefix, tmp_path): with uvtest.check_warnings( UserWarning, - match="antenna_positions are not set or are being overwritten. Using known " - "values for HERA.", + match="antenna_positions are not set or are being overwritten. " + "antenna_positions are set using values from known telescopes for HERA.", ): gain_data2 = UVCal.from_file(write_file2, use_future_array_shapes=True) @@ -4138,8 +4151,9 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): with uvtest.check_warnings( [UserWarning], match=[ - "Not all antennas have positions in the telescope object. " - "Not setting antenna_positions." + "Not all antennas have positions in the known_telescope data. " + "Not setting antenna_positions.", + "Required UVParameter _antenna_positions has not been set.", ], ): gain_data2 = UVCal.from_file(write_file2, use_future_array_shapes=True) @@ -4147,8 +4161,9 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): with uvtest.check_warnings( UserWarning, match=[ - "Not all antennas have positions in the telescope object. " - "Not setting antenna_positions." + "Not all antennas have positions in the known_telescope data. " + "Not setting antenna_positions.", + "Required UVParameter _antenna_positions has not been set.", ], ): gain_data2 = UVCal.from_file( @@ -4238,6 +4253,11 @@ def test_init_from_uvdata( uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4336,6 +4356,11 @@ def test_init_from_uvdata_setfreqs( )[:200], ) + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4418,6 +4443,11 @@ def test_init_from_uvdata_settimes( uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + if not metadata_only: uvc2.gain_array[:] = 1.0 uvc2.quality_array = None @@ -4478,6 +4508,11 @@ def test_init_from_uvdata_setjones(uvcalibrate_data): uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4528,6 +4563,11 @@ def test_init_single_pol(uvcalibrate_data, pol): uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4572,6 +4612,11 @@ def test_init_from_uvdata_circular_pol(uvcalibrate_data): uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4659,6 +4704,11 @@ def test_init_from_uvdata_sky( uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4766,6 +4816,11 @@ def test_init_from_uvdata_delay( uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance @@ -4865,6 +4920,11 @@ def test_init_from_uvdata_wideband( uvc_new.history = uvc2.history + # the new one has an instrument set because UVData requires it + assert uvc_new.instrument == uvd.instrument + # remove it to match uvc2 + uvc_new.instrument = None + # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences # in the lsts of 5.86770454e-09 which are larger than our tolerance diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index c85d0e9d7f..946040484e 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -11,8 +11,8 @@ import numpy as np from docstring_parser import DocstringStyle +from .. import Telescope from .. import parameter as uvp -from .. import telescopes as uvtel from .. import utils as uvutils from ..docstrings import combine_docstrings, copy_replace_short_description from ..uvbase import UVBase @@ -96,9 +96,10 @@ def __init__(self): expected_type=int, ) - desc = "Name of telescope. e.g. HERA. String." - self._telescope_name = uvp.UVParameter( - "telescope_name", description=desc, form="str", expected_type=str + self._telescope = uvp.UVParameter( + "telescope", + description="Telescope object containing the telescope metadata.", + expected_type=Telescope, ) desc = ( @@ -110,27 +111,6 @@ def __init__(self): "Nants_data", description=desc, expected_type=int ) - desc = ( - "Number of antennas in the antenna_numbers array. May be larger " - "than the number of antennas with gains associated with them." - ) - self._Nants_telescope = uvp.UVParameter( - "Nants_telescope", description=desc, expected_type=int - ) - - desc = ( - "Telescope location: xyz in ITRF (earth-centered frame). " - "Can also be accessed using telescope_location_lat_lon_alt or " - "telescope_location_lat_lon_alt_degrees properties" - ) - self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - tols=1e-3, - frame="itrs", - required=True, - ) - desc = ( "Array of integer antenna numbers that appear in self.gain_array," " with shape (Nants_data,). " @@ -141,47 +121,6 @@ def __init__(self): "ant_array", description=desc, expected_type=int, form=("Nants_data",) ) - desc = ( - "Array of antenna names with shape (Nants_telescope,). " - "Ordering of elements matches ordering of antenna_numbers." - ) - self._antenna_names = uvp.UVParameter( - "antenna_names", - description=desc, - form=("Nants_telescope",), - expected_type=str, - ) - - desc = ( - "Array of all integer-valued antenna numbers in the telescope with " - "shape (Nants_telescope,). Ordering of elements matches that of " - "antenna_names. This array is not necessarily identical to " - "ant_array, in that this array holds all antenna numbers " - "associated with the telescope, not just antennas with data, and " - "has an in principle non-specific ordering." - ) - self._antenna_numbers = uvp.UVParameter( - "antenna_numbers", - description=desc, - form=("Nants_telescope",), - expected_type=int, - ) - - desc = ( - "Array giving coordinates of antennas relative to " - "telescope_location (ITRF frame), shape (Nants_telescope, 3), " - "units meters. See the tutorial page in the documentation " - "for an example of how to convert this to topocentric frame." - ) - self._antenna_positions = uvp.UVParameter( - "antenna_positions", - description=desc, - form=("Nants_telescope", 3), - expected_type=float, - tols=1e-3, # 1 mm - required=True, - ) - desc = ( "Option to support 'wide-band' calibration solutions with gains or delays " "that apply over a range of frequencies rather than having distinct values " @@ -387,12 +326,6 @@ def __init__(self): '(indicating east/west orientation) and "north" (indicating ' "north/south orientation)" ) - self._x_orientation = uvp.UVParameter( - "x_orientation", - description=desc, - expected_type=str, - acceptable_vals=["east", "north"], - ) # --- cal_type parameters --- desc = "cal type parameter. Values are delay or gain." @@ -722,21 +655,136 @@ def __init__(self): required=False, ) - desc = ( - "Optional parameter, array of antenna diameters in meters. Used by CASA to " - "construct a default beam if no beam is supplied." - ) - self._antenna_diameters = uvp.UVParameter( - "antenna_diameters", - required=False, - description=desc, - form=("Nants_telescope",), - expected_type=float, - tols=1e-3, # 1 mm - ) + # initialize the telescope object + self.telescope = Telescope() + + # set the appropriate telescope attributes as required + self.telescope._set_uvcal_requirements() super(UVCal, self).__init__() + @property + def telescope_name(self): + """The telescope name (stored on the Telescope object internally).""" + return self.telescope.name + + @telescope_name.setter + def telescope_name(self, val): + self.telescope.name = val + + @property + def instrument(self): + """The instrument name (stored on the Telescope object internally).""" + return self.telescope.instrument + + @instrument.setter + def instrument(self, val): + self.telescope.instrument = val + + @property + def telescope_location(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location + + @telescope_location.setter + def telescope_location(self, val): + self.telescope.location = val + + @property + def telescope_location_lat_lon_alt(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt + + @telescope_location_lat_lon_alt.setter + def telescope_location_lat_lon_alt(self, val): + self.telescope.location_lat_lon_alt = val + + @property + def telescope_location_lat_lon_alt_degrees(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt_degrees + + @telescope_location_lat_lon_alt_degrees.setter + def telescope_location_lat_lon_alt_degrees(self, val): + self.telescope.location_lat_lon_alt_degrees = val + + @property + def Nants_telescope(self): # noqa + """ + The number of antennas in the telescope. + + This property is stored on the Telescope object internally. + """ + return self.telescope.Nants + + @Nants_telescope.setter + def Nants_telescope(self, val): # noqa + self.telescope.Nants = val + + @property + def antenna_names(self): + """The antenna names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_names + + @antenna_names.setter + def antenna_names(self, val): + self.telescope.antenna_names = val + + @property + def antenna_numbers(self): + """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_numbers + + @antenna_numbers.setter + def antenna_numbers(self, val): + self.telescope.antenna_numbers = val + + @property + def antenna_positions(self): + """The antenna positions coordinates of antennas relative to telescope_location. + + The coordinates are in the ITRF frame, shape (Nants_telescope, 3). + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_positions + + @antenna_positions.setter + def antenna_positions(self, val): + self.telescope.antenna_positions = val + + @property + def x_orientation(self): + """Orientation of the physical dipole corresponding to the x label. + + Options are 'east' (indicating east/west orientation) and 'north (indicating + north/south orientation). + This property is stored on the Telescope object internally. + """ + return self.telescope.x_orientation + + @x_orientation.setter + def x_orientation(self, val): + self.telescope.x_orientation = val + + @property + def antenna_diameters(self): + """The antenna diameters in meters. + + Used by CASA to construct a default beam if no beam is supplied. + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_diameters + + @antenna_diameters.setter + def antenna_diameters(self, val): + self.telescope.antenna_diameters = val + @staticmethod @combine_docstrings(initializers.new_uvcal, style=DocstringStyle.NUMPYDOC) def new(**kwargs): @@ -1509,7 +1557,15 @@ def convert_to_flex_jones(self): param = np.transpose(param, (0, 3, 1, 2)).reshape(new_shape) setattr(self, name, param) - def set_telescope_params(self, *, overwrite=False): + def set_telescope_params( + self, + *, + warn=True, + overwrite=False, + run_check=True, + check_extra=True, + run_check_acceptability=True, + ): """ Set telescope related parameters. @@ -1528,76 +1584,13 @@ def set_telescope_params(self, *, overwrite=False): ValueError if the telescope_name is not in known telescopes """ - telescope_obj = uvtel.get_telescope(self.telescope_name) - if telescope_obj is not False: - if self.telescope_location is None or overwrite is True: - warnings.warn( - "telescope_location is not set. Using known values " - f"for {telescope_obj.telescope_name}." - ) - self.telescope_location = telescope_obj.telescope_location - - if telescope_obj.antenna_positions is not None and ( - self.antenna_positions is None or overwrite is True - ): - ant_inds = [] - telescope_ant_inds = [] - # first try to match using names only - for index, antname in enumerate(self.antenna_names): - if antname in telescope_obj.antenna_names: - ant_inds.append(index) - telescope_ant_inds.append( - np.where(telescope_obj.antenna_names == antname)[0][0] - ) - # next try using numbers - if len(ant_inds) != self.Nants_telescope: - for index, antnum in enumerate(self.antenna_numbers): - # only update if not already found - if ( - index not in ant_inds - and antnum in telescope_obj.antenna_numbers - ): - this_ant_ind = np.where( - telescope_obj.antenna_numbers == antnum - )[0][0] - # make sure we don't already have this antenna associated - # with another antenna - if this_ant_ind not in telescope_ant_inds: - ant_inds.append(index) - telescope_ant_inds.append(this_ant_ind) - if len(ant_inds) != self.Nants_telescope: - warnings.warn( - "Not all antennas have positions in the telescope object. " - "Not setting antenna_positions." - ) - else: - params_set = ["antenna_positions"] - if overwrite: - self.antenna_names = telescope_obj.antenna_names - self.antenna_numbers = telescope_obj.antenna_numbers - self.antenna_positions = telescope_obj.antenna_positions - self.Nants_telescope = telescope_obj.Nants_telescope - params_set += [ - "antenna_names", - "antenna_numbers", - "Nants_telescope", - ] - else: - telescope_ant_inds = np.array(telescope_ant_inds) - self.antenna_positions = telescope_obj.antenna_positions[ - telescope_ant_inds, : - ] - params_set_str = ", ".join(params_set) - warnings.warn( - f"{params_set_str} are not set or are being " - "overwritten. Using known values for " - f"{telescope_obj.telescope_name}." - ) - - else: - raise ValueError( - f"Telescope {self.telescope_name} is not in known_telescopes." - ) + self.telescope.update_params_from_known_telescopes( + overwrite=overwrite, + warn=warn, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) def _set_lsts_helper(self, *, astrometry_library=None): latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees @@ -1608,7 +1601,7 @@ def _set_lsts_helper(self, *, astrometry_library=None): longitude=longitude, altitude=altitude, astrometry_library=astrometry_library, - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) if self.time_range is not None: @@ -1618,7 +1611,7 @@ def _set_lsts_helper(self, *, astrometry_library=None): longitude=longitude, altitude=altitude, astrometry_library=astrometry_library, - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) return @@ -2152,7 +2145,12 @@ def check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) - # deprecate having both arrays and ranges set for times and lsts + # then run telescope object check + self.telescope.check( + check_extra=check_extra, run_check_acceptability=run_check_acceptability + ) + + # deprecate having both time_array and time_range set time_like_pairs = [("time_array", "time_range"), ("lst_array", "lst_range")] for pair in time_like_pairs: if ( @@ -2259,7 +2257,7 @@ def check( uvutils.check_surface_based_positions( antenna_positions=self.antenna_positions, telescope_loc=self.telescope_location, - telescope_frame=self._telescope_location.frame, + telescope_frame=self.telescope._location.frame, raise_error=False, ) @@ -2272,7 +2270,7 @@ def check( longitude=lon, altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) if self.time_range is not None: uvutils.check_lsts_against_times( @@ -2282,7 +2280,7 @@ def check( longitude=lon, altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) return True @@ -2578,7 +2576,7 @@ def get_lst_array(self, *, astrometry_library=None): longitude=longitude, altitude=altitude, astrometry_library=astrometry_library, - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, ) else: return self.lst_array @@ -3386,9 +3384,8 @@ def __add__( # Check objects are compatible compatibility_params = [ "_cal_type", - "_telescope_name", + "_telescope", "_gain_convention", - "_x_orientation", "_cal_style", "_ref_antenna_name", ] @@ -4747,9 +4744,8 @@ def fast_concat( # Check objects are compatible compatibility_params = [ "_cal_type", - "_telescope_name", + "_telescope", "_gain_convention", - "_x_orientation", "_cal_style", "_ref_antenna_name", ] diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index e50d073a60..6b12df1314 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -13,6 +13,7 @@ from astropy.coordinates import EarthLocation from pyuvdata import UVData +from pyuvdata.tests.test_utils import selenoids from pyuvdata.utils import polnum2str from pyuvdata.uvdata.initializers import ( configure_blt_rectangularity, @@ -22,8 +23,6 @@ get_time_params, ) -selenoids = ["SPHERE", "GSFC", "GRAIL23", "CE-1-LAM-GEO"] - @pytest.fixture(scope="function") def simplest_working_params() -> dict[str, Any]: diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index a45b9e7ad0..7b604672f4 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -683,11 +683,7 @@ def __init__(self): self.telescope = Telescope() # set the appropriate telescope attributes as required - self.telescope._instrument.required = True - self.telescope._Nants.required = True - self.telescope._antenna_names.required = True - self.telescope._antenna_numbers.required = True - self.telescope._antenna_positions.required = True + self.telescope._set_uvdata_requirements() super(UVData, self).__init__() @@ -2718,12 +2714,12 @@ def check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) logger.debug("... Done UVBase Check") + + # then run telescope object check self.telescope.check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) - # then run telescope object check - # Check blt axis rectangularity arguments if self.time_axis_faster_than_bls and not self.blts_are_rectangular: raise ValueError( From 717b917ad45d3dd29428600c369e7ef90d2dcc5e Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 18 Apr 2024 14:52:15 -0700 Subject: [PATCH 03/59] more cal updates after rebase --- pyuvdata/hdf5_utils.py | 60 ++++++++++++++++++++++++++++ pyuvdata/uvcal/calfits.py | 19 +++++++++ pyuvdata/uvcal/calh5.py | 33 ++++++++------- pyuvdata/uvcal/tests/test_calfits.py | 42 +++++++++++++++++++ pyuvdata/uvcal/tests/test_calh5.py | 58 +++++++++++++++++++++------ pyuvdata/uvcal/uvcal.py | 5 +++ pyuvdata/uvdata/uvh5.py | 57 -------------------------- 7 files changed, 189 insertions(+), 85 deletions(-) diff --git a/pyuvdata/hdf5_utils.py b/pyuvdata/hdf5_utils.py index 2246f45a37..687de2f2e3 100644 --- a/pyuvdata/hdf5_utils.py +++ b/pyuvdata/hdf5_utils.py @@ -360,6 +360,66 @@ def __getattr__(self, name: str) -> Any: except KeyError as e: raise AttributeError(f"{name} not found in {self.path}") from e + @cached_property + def antpos_enu(self) -> np.ndarray: + """The antenna positions in ENU coordinates, in meters.""" + lat, lon, alt = self.telescope_location_lat_lon_alt + return uvutils.ENU_from_ECEF( + self.antenna_positions + self.telescope_location, + latitude=lat, + longitude=lon, + altitude=alt, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + + @cached_property + def telescope_location(self): + """The telescope location in ECEF coordinates, in meters.""" + return uvutils.XYZ_from_LatLonAlt( + *self.telescope_location_lat_lon_alt, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + + @property + def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: + """The telescope location in latitude, longitude, and altitude, in degrees.""" + return self.latitude * np.pi / 180, self.longitude * np.pi / 180, self.altitude + + @property + def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: + """The telescope location in latitude, longitude, and altitude, in degrees.""" + return self.latitude, self.longitude, self.altitude + + @property + def telescope_frame(self) -> str: + """The telescope frame.""" + h = self.header + if "telescope_frame" in h: + telescope_frame = bytes(h["telescope_frame"][()]).decode("utf8") + if telescope_frame not in ["itrs", "mcmf"]: + raise ValueError( + f"Telescope frame in file is {telescope_frame}. " + "Only 'itrs' and 'mcmf' are currently supported." + ) + return telescope_frame + else: + # default to ITRS + return "itrs" + + @property + def ellipsoid(self) -> str: + """The reference ellipsoid to use for lunar coordinates.""" + h = self.header + if self.telescope_frame == "mcmf": + if "ellipsoid" in h: + return bytes(h["ellipsoid"][()]).decode("utf8") + else: + return "SPHERE" + else: + return None + @cached_property def extra_keywords(self) -> dict: """The extra_keywords from the file.""" diff --git a/pyuvdata/uvcal/calfits.py b/pyuvdata/uvcal/calfits.py index 76a69ff1b7..112cab62af 100644 --- a/pyuvdata/uvcal/calfits.py +++ b/pyuvdata/uvcal/calfits.py @@ -208,6 +208,10 @@ def write_calfits( prihdr["ARRAYX"] = self.telescope_location[0] prihdr["ARRAYY"] = self.telescope_location[1] prihdr["ARRAYZ"] = self.telescope_location[2] + prihdr["FRAME"] = self.telescope._location.frame + if self.telescope._location.ellipsoid is not None: + # use ELLIPSOI because of FITS 8 character limit for header items + prihdr["ELLIPSOI"] = self.telescope._location.ellipsoid prihdr["LAT"] = self.telescope_location_lat_lon_alt_degrees[0] prihdr["LON"] = self.telescope_location_lat_lon_alt_degrees[1] prihdr["ALT"] = self.telescope_location_lat_lon_alt[2] @@ -261,6 +265,10 @@ def write_calfits( if self.observer: prihdr["OBSERVER"] = self.observer + + if self.instrument: + prihdr["INSTRUME"] = self.instrument + if self.git_origin_cal: prihdr["ORIGCAL"] = self.git_origin_cal if self.git_hash_cal: @@ -592,6 +600,12 @@ def read_calfits( lat = hdr.pop("LAT", None) lon = hdr.pop("LON", None) alt = hdr.pop("ALT", None) + + telescope_frame = hdr.pop("FRAME", "itrs") + ellipsoid = None + if telescope_frame != "itrs": + ellipsoid = hdr.pop("ELLIPSOI", "SPHERE") + if ( x_telescope is not None and y_telescope is not None @@ -602,6 +616,9 @@ def read_calfits( ) elif lat is not None and lon is not None and alt is not None: self.telescope_location_lat_lon_alt_degrees = (lat, lon, alt) + + self.telescope._location.frame = telescope_frame + self.telescope._location.ellipsoid = ellipsoid try: self.set_telescope_params() except ValueError as ve: @@ -643,6 +660,8 @@ def read_calfits( self.diffuse_model = hdr.pop("DIFFUSE", None) self.observer = hdr.pop("OBSERVER", None) + self.instrument = hdr.pop("INSTRUME", None) + self.git_origin_cal = hdr.pop("ORIGCAL", None) self.git_hash_cal = hdr.pop("HASHCAL", None) diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index 0e14910247..bcf82ab103 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -71,6 +71,7 @@ class FastCalH5Meta(hdf5_utils.HDF5Meta): "ref_antenna_name", "sky_catalog", "sky_field", + "instrument", "version", } ) @@ -118,21 +119,6 @@ def pols(self) -> list[str]: uvutils.jnum2str(self.jones_array, x_orientation=self.x_orientation) ) - @cached_property - def telescope_location(self): - """The telescope location in ECEF coordinates, in meters.""" - return uvutils.XYZ_from_LatLonAlt(*self.telescope_location_lat_lon_alt) - - @property - def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude * np.pi / 180, self.longitude * np.pi / 180, self.altitude - - @property - def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude, self.longitude, self.altitude - def to_uvcal( self, *, check_lsts: bool = False, astrometry_library: str | None = None ) -> UVCal: @@ -194,9 +180,16 @@ def _read_header( """ # First, get the things relevant for setting LSTs, so that can be run in the # background if desired. + + # must set the frame before setting the location using lat/lon/alt + self.telescope._location.frame = meta.telescope_frame + if self.telescope._location.frame == "mcmf": + self.telescope._location.ellipsoid = meta.ellipsoid + self.telescope_location_lat_lon_alt_degrees = ( meta.telescope_location_lat_lon_alt_degrees ) + if "time_array" in meta.header: self.time_array = meta.time_array if "lst_array" in meta.header: @@ -277,6 +270,7 @@ def _read_header( "scan_number_array", "sky_catalog", "sky_field", + "instrument", ]: try: setattr(self, attr, getattr(meta, attr)) @@ -304,6 +298,7 @@ def _read_header( altitude=alt, lst_tols=(0, uvutils.LST_RAD_TOL), frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) if self.time_range is not None: uvutils.check_lsts_against_times( @@ -314,6 +309,7 @@ def _read_header( altitude=alt, lst_tols=(0, uvutils.LST_RAD_TOL), frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) def _get_data( @@ -733,6 +729,9 @@ def _write_header(self, header): header["version"] = np.string_("0.1") # write out telescope and source information + header["telescope_frame"] = np.string_(self.telescope._location.frame) + if self.telescope._location.frame == "mcmf": + header["ellipsoid"] = self.telescope._location.ellipsoid header["latitude"] = self.telescope_location_lat_lon_alt_degrees[0] header["longitude"] = self.telescope_location_lat_lon_alt_degrees[1] header["altitude"] = self.telescope_location_lat_lon_alt_degrees[2] @@ -823,6 +822,10 @@ def _write_header(self, header): else: this_group[key] = value + # extra telescope-related parameters + if self.instrument is not None: + header["instrument"] = np.string_(self.instrument) + # write out extra keywords if it exists and has elements if self.extra_keywords: extra_keywords = header.create_group("extra_keywords") diff --git a/pyuvdata/uvcal/tests/test_calfits.py b/pyuvdata/uvcal/tests/test_calfits.py index 7d094f9130..c5ecd379e6 100644 --- a/pyuvdata/uvcal/tests/test_calfits.py +++ b/pyuvdata/uvcal/tests/test_calfits.py @@ -15,6 +15,7 @@ import pyuvdata.utils as uvutils from pyuvdata import UVCal from pyuvdata.data import DATA_PATH +from pyuvdata.tests.test_utils import selenoids from pyuvdata.uvcal.tests import extend_jones_axis, time_array_to_time_range from pyuvdata.uvcal.uvcal import _future_array_shapes_warning @@ -63,6 +64,9 @@ def test_readwriteread( cal_in.total_quality_array = np.ones( cal_in._total_quality_array.expected_shape(cal_in) ) + # add instrument and antenna_diameters + cal_in.instrument = cal_in.telescope_name + cal_in.antenna_diameters = np.zeros((cal_in.Nants_telescope,), dtype=float) + 5.0 write_file = str(tmp_path / "outtest.fits") cal_in.write_calfits(write_file, clobber=True) @@ -96,6 +100,44 @@ def test_readwriteread( return +@pytest.mark.parametrize("selenoid", selenoids) +def test_moon_loopback(tmp_path, gain_data, selenoid): + pytest.importorskip("lunarsky") + cal_in = gain_data + + latitude, longitude, altitude = cal_in.telescope_location_lat_lon_alt + enu_antpos = uvutils.ENU_from_ECEF( + (cal_in.antenna_positions + cal_in.telescope_location), + latitude=latitude, + longitude=longitude, + altitude=altitude, + frame=cal_in.telescope._location.frame, + ellipsoid=cal_in.telescope._location.ellipsoid, + ) + + cal_in.telescope._location.frame = "mcmf" + cal_in.telescope._location.ellipsoid = selenoid + cal_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, + latitude=latitude, + longitude=longitude, + altitude=altitude, + frame="mcmf", + ellipsoid=selenoid, + ) + cal_in.antenna_positions = new_full_antpos - cal_in.telescope_location + cal_in.set_lsts_from_time_array() + cal_in.check() + + write_file = str(tmp_path / "outtest.fits") + cal_in.write_calfits(write_file, clobber=True) + + cal_out = UVCal.from_file(write_file, use_future_array_shapes=True) + + assert cal_in == cal_out + + @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.parametrize("future_shapes", [True, False]) def test_write_inttime_equal_timediff(future_shapes, gain_data, delay_data, tmp_path): diff --git a/pyuvdata/uvcal/tests/test_calh5.py b/pyuvdata/uvcal/tests/test_calh5.py index 03c2eddf29..c0326a6bf3 100644 --- a/pyuvdata/uvcal/tests/test_calh5.py +++ b/pyuvdata/uvcal/tests/test_calh5.py @@ -10,8 +10,10 @@ import pytest import pyuvdata.tests as uvtest +import pyuvdata.utils as uvutils from pyuvdata import UVCal from pyuvdata.data import DATA_PATH +from pyuvdata.tests.test_utils import selenoids from pyuvdata.uvcal import FastCalH5Meta from pyuvdata.uvcal.tests import extend_jones_axis, time_array_to_time_range from pyuvdata.uvcal.uvcal import _future_array_shapes_warning @@ -33,6 +35,8 @@ def test_calh5_write_read_loop_gain(gain_data, tmp_path, time_range, future_shap calobj.total_quality_array = np.ones( calobj._total_quality_array.expected_shape(calobj) ) + # add instrument + calobj.instrument = calobj.telescope_name write_file = str(tmp_path / "outtest.calh5") calobj.write_calh5(write_file, clobber=True) @@ -109,6 +113,47 @@ def test_calh5_loop_bitshuffle(gain_data, tmp_path): assert calobj == calobj2 +@pytest.mark.parametrize("selenoid", selenoids) +def test_calh5_loop_moon(tmp_path, gain_data, selenoid): + pytest.importorskip("lunarsky") + cal_in = gain_data + + latitude, longitude, altitude = cal_in.telescope_location_lat_lon_alt + enu_antpos = uvutils.ENU_from_ECEF( + (cal_in.antenna_positions + cal_in.telescope_location), + latitude=latitude, + longitude=longitude, + altitude=altitude, + frame=cal_in.telescope._location.frame, + ellipsoid=cal_in.telescope._location.ellipsoid, + ) + + cal_in.telescope._location.frame = "mcmf" + cal_in.telescope._location.ellipsoid = selenoid + cal_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, + latitude=latitude, + longitude=longitude, + altitude=altitude, + frame="mcmf", + ellipsoid=selenoid, + ) + cal_in.antenna_positions = new_full_antpos - cal_in.telescope_location + cal_in.set_lsts_from_time_array() + cal_in.check() + + write_file = str(tmp_path / "outtest.calh5") + cal_in.write_calh5(write_file, clobber=True) + + cal_out = UVCal.from_file(write_file, use_future_array_shapes=True) + + assert cal_out.telescope._location.frame == "mcmf" + assert cal_out.telescope._location.ellipsoid == selenoid + + assert cal_in == cal_out + + def test_calh5_meta(gain_data, tmp_path): calobj = gain_data @@ -191,19 +236,6 @@ def test_none_extra_keywords(gain_data, tmp_path): return -def test_calh5_unknown_telescope(gain_data, tmp_path): - cal_obj = gain_data - - cal_obj.telescope_name = "foo" - - testfile = str(tmp_path / "unknown_telescope.calh5") - cal_obj.write_calh5(testfile) - - cal_obj2 = UVCal.from_file(testfile, use_future_array_shapes=True) - - assert cal_obj2 == cal_obj - - @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") def test_write_calh5_errors(gain_data, tmp_path): """ diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index 946040484e..e82ceb0508 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -1602,6 +1602,7 @@ def _set_lsts_helper(self, *, astrometry_library=None): altitude=altitude, astrometry_library=astrometry_library, frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) if self.time_range is not None: @@ -1612,6 +1613,7 @@ def _set_lsts_helper(self, *, astrometry_library=None): altitude=altitude, astrometry_library=astrometry_library, frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) return @@ -2271,6 +2273,7 @@ def check( altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) if self.time_range is not None: uvutils.check_lsts_against_times( @@ -2281,6 +2284,7 @@ def check( altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) return True @@ -2577,6 +2581,7 @@ def get_lst_array(self, *, astrometry_library=None): altitude=altitude, astrometry_library=astrometry_library, frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) else: return self.lst_array diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index a616a695c8..2d4dbadaad 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -443,63 +443,6 @@ def pols(self) -> list[str]: for p in self.polarization_array ] - @cached_property - def antpos_enu(self) -> np.ndarray: - """The antenna positions in ENU coordinates, in meters.""" - lat, lon, alt = self.telescope_location_lat_lon_alt - return uvutils.ENU_from_ECEF( - self.antenna_positions + self.telescope_location, - latitude=lat, - longitude=lon, - altitude=alt, - frame="itrs", - ) - - @cached_property - def telescope_location(self): - """The telescope location in ECEF coordinates, in meters.""" - return uvutils.XYZ_from_LatLonAlt( - *self.telescope_location_lat_lon_alt, frame=self.telescope_frame - ) - - @property - def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude * np.pi / 180, self.longitude * np.pi / 180, self.altitude - - @property - def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude, self.longitude, self.altitude - - @property - def telescope_frame(self) -> str: - """The telescope frame.""" - h = self.header - if "telescope_frame" in h: - telescope_frame = bytes(h["telescope_frame"][()]).decode("utf8") - if telescope_frame not in ["itrs", "mcmf"]: - raise ValueError( - f"Telescope frame in file is {telescope_frame}. " - "Only 'itrs' and 'mcmf' are currently supported." - ) - return telescope_frame - else: - # default to ITRS - return "itrs" - - @property - def ellipsoid(self) -> str: - """The reference ellipsoid to use for lunar coordinates.""" - h = self.header - if self.telescope_frame == "mcmf": - if "ellipsoid" in h: - return bytes(h["ellipsoid"][()]).decode("utf8") - else: - return "SPHERE" - else: - return None - @cached_property def vis_units(self) -> str: """The visibility units in the file, as a string.""" From 62b562dfd0b8139cea1408de32d899268c3156c0 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 27 Mar 2024 19:33:12 -0700 Subject: [PATCH 04/59] make telescope refactor work on uvflag --- pyuvdata/telescopes.py | 92 +++--- pyuvdata/tests/test_utils.py | 3 + pyuvdata/uvcal/tests/test_uvcal.py | 8 +- pyuvdata/uvflag/tests/test_uvflag.py | 171 +++++++---- pyuvdata/uvflag/uvflag.py | 430 ++++++++++++++------------- 5 files changed, 397 insertions(+), 307 deletions(-) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 2e8d75f914..15b1d9f7bb 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -336,6 +336,18 @@ def _set_uvdata_requirements(self): self._x_orientation.required = False self._antenna_diameters.required = False + def _set_uvflag_requirements(self): + """Set the UVParameter required fields appropriately for UVCal.""" + self._name.required = True + self._location.required = True + self._instrument.required = False + self._Nants.required = True + self._antenna_names.required = True + self._antenna_numbers.required = True + self._antenna_positions.required = True + self._antenna_diameters.required = False + self._x_orientation.required = False + def update_params_from_known_telescopes( self, *, @@ -429,52 +441,62 @@ def update_params_from_known_telescopes( "antenna_numbers": antenna_numbers, "antenna_positions": antenna_positions, } - if overwrite or all( - getattr(self, key) is None for key in ant_info.keys() - ): + ant_params_missing = [] + for key in ant_info.keys(): + if getattr(self, key) is None: + ant_params_missing.append(key) + if overwrite or len(ant_params_missing) == len(ant_info.keys()): for key, value in ant_info.items(): known_telescope_list.append(key) setattr(self, key, value) - elif ( - self.antenna_positions is None - and self.antenna_names is not None - and self.antenna_numbers is not None - ): + elif self.antenna_names is not None or self.antenna_numbers is not None: ant_inds = [] telescope_ant_inds = [] # first try to match using names only - for index, antname in enumerate(self.antenna_names): - if antname in ant_info["antenna_names"]: - ant_inds.append(index) - telescope_ant_inds.append( - np.where(ant_info["antenna_names"] == antname)[0][0] - ) + if self.antenna_names is not None: + for index, antname in enumerate(self.antenna_names): + if antname in ant_info["antenna_names"]: + ant_inds.append(index) + telescope_ant_inds.append( + np.where(ant_info["antenna_names"] == antname)[0][0] + ) # next try using numbers - if len(ant_inds) != self.Nants: - for index, antnum in enumerate(self.antenna_numbers): - # only update if not already found - if ( - index not in ant_inds - and antnum in ant_info["antenna_numbers"] - ): - this_ant_ind = np.where( - ant_info["antenna_numbers"] == antnum - )[0][0] - # make sure we don't already have this antenna - # associated with another antenna - if this_ant_ind not in telescope_ant_inds: - ant_inds.append(index) - telescope_ant_inds.append(this_ant_ind) + if self.antenna_numbers is not None: + if len(ant_inds) != self.Nants: + for index, antnum in enumerate(self.antenna_numbers): + # only update if not already found + if ( + index not in ant_inds + and antnum in ant_info["antenna_numbers"] + ): + this_ant_ind = np.where( + ant_info["antenna_numbers"] == antnum + )[0][0] + # make sure we don't already have this antenna + # associated with another antenna + if this_ant_ind not in telescope_ant_inds: + ant_inds.append(index) + telescope_ant_inds.append(this_ant_ind) if len(ant_inds) != self.Nants: warnings.warn( - "Not all antennas have positions in the " - "known_telescope data. Not setting antenna_positions." + "Not all antennas have metadata in the " + f"known_telescope data. Not setting {ant_params_missing}." ) else: - known_telescope_list.append("antenna_positions") - self.antenna_positions = ant_info["antenna_positions"][ - telescope_ant_inds, : - ] + known_telescope_list.extend(ant_params_missing) + if "antenna_positions" in ant_params_missing: + self.antenna_positions = ant_info["antenna_positions"][ + telescope_ant_inds, : + ] + if "antenna_names" in ant_params_missing: + self.antenna_names = ant_info["antenna_names"][ + telescope_ant_inds + ] + + if "antenna_numbers" in ant_params_missing: + self.antenna_numbers = ant_info["antenna_numbers"][ + telescope_ant_inds + ] if "antenna_diameters" in telescope_dict.keys() and ( overwrite or self.antenna_diameters is None diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 6101bb8a89..9f1841036e 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -3619,6 +3619,7 @@ def test_and_collapse_errors(): @pytest.mark.filterwarnings("ignore:Fixing auto-correlations to be be real-only,") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_uvcalibrate_apply_gains_oldfiles(): # read data uvd = UVData() @@ -3683,6 +3684,7 @@ def test_uvcalibrate_apply_gains_oldfiles(): @pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array") @pytest.mark.filterwarnings("ignore:Fixing auto-correlations to be be real-only,") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize("uvd_future_shapes", [True, False]) @pytest.mark.parametrize("uvc_future_shapes", [True, False]) def test_uvcalibrate_delay_oldfiles(uvd_future_shapes, uvc_future_shapes): @@ -4246,6 +4248,7 @@ def test_uvcalibrate_wideband_gain(uvcalibrate_data): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array") @pytest.mark.filterwarnings("ignore:Nfreqs will be required to be 1 for wide_band cals") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_uvcalibrate_delay_multispw(): uvd = UVData() uvd.read( diff --git a/pyuvdata/uvcal/tests/test_uvcal.py b/pyuvdata/uvcal/tests/test_uvcal.py index 2cd3aba776..5a44621d13 100644 --- a/pyuvdata/uvcal/tests/test_uvcal.py +++ b/pyuvdata/uvcal/tests/test_uvcal.py @@ -4151,8 +4151,8 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): with uvtest.check_warnings( [UserWarning], match=[ - "Not all antennas have positions in the known_telescope data. " - "Not setting antenna_positions.", + "Not all antennas have metadata in the known_telescope data. Not " + "setting ['antenna_positions'].", "Required UVParameter _antenna_positions has not been set.", ], ): @@ -4161,8 +4161,8 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): with uvtest.check_warnings( UserWarning, match=[ - "Not all antennas have positions in the known_telescope data. " - "Not setting antenna_positions.", + "Not all antennas have metadata in the known_telescope data. Not " + "setting ['antenna_positions'].", "Required UVParameter _antenna_positions has not been set.", ], ): diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 140795b106..83f6e9dfc8 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -17,6 +17,7 @@ from pyuvdata import UVCal, UVData, UVFlag, __version__ from pyuvdata import utils as uvutils from pyuvdata.data import DATA_PATH +from pyuvdata.tests.test_utils import frame_selenoid from pyuvdata.uvflag.uvflag import _future_array_shapes_warning from ..uvflag import and_rows_cols, flags2waterfall @@ -74,14 +75,7 @@ def uvdata_obj(uvdata_obj_main): # This data file has a different telescope location and other weirdness. # Set them to the known HERA values to allow combinations with the test_f_file # which appears to have the known HERA values. - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="Nants_telescope, antenna_diameters, antenna_names, " - "antenna_numbers, antenna_positions, telescope_location, telescope_name " - "are not set or are being overwritten. Using known values for HERA.", - ) - uvdata_object.set_telescope_params(overwrite=True) + uvdata_object.set_telescope_params(overwrite=True, warn=False) uvdata_object.set_lsts_from_time_array() yield uvdata_object @@ -95,7 +89,13 @@ def uvdata_obj(uvdata_obj_main): @pytest.fixture(scope="session") def uvcal_obj_main(): uvc = UVCal() - uvc.read_calfits(test_c_file, use_future_array_shapes=True) + with uvtest.check_warnings( + UserWarning, + match="telescope_location, antenna_positions, antenna_diameters are " + "not set or are being overwritten. telescope_location, antenna_positions, " + "antenna_diameters are set using values from known telescopes for HERA.", + ): + uvc.read_calfits(test_c_file, use_future_array_shapes=True) yield uvc @@ -112,14 +112,7 @@ def uvcal_obj(uvcal_obj_main): # This cal file has a different antenna names and other weirdness. # Set them to the known HERA values to allow combinations with the test_f_file # which appears to have the known HERA values. - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="antenna_positions, antenna_names, antenna_numbers, " - "Nants_telescope are not set or are being overwritten. Using known values " - "for HERA.", - ) - uvc.set_telescope_params(overwrite=True) + uvc.set_telescope_params(overwrite=True, warn=False) yield uvc # cleanup @@ -349,6 +342,7 @@ def test_check_flex_spw_id_array(uvf_from_data): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_init_bad_mode(uvdata_obj): uv = uvdata_obj with pytest.raises(ValueError) as cm: @@ -527,6 +521,7 @@ def test_init_uvdata_mode_flag(uvdata_obj, uvd_future_shapes, uvf_future_shapes) assert pyuvdata_version_str in uvf.history +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_init_uvcal(): uvc = UVCal() uvc.read_calfits(test_c_file, use_future_array_shapes=True) @@ -595,6 +590,7 @@ def test_init_uvcal_mode_flag(uvcal_obj, uvc_future_shapes, uvf_future_shapes): @pytest.mark.filterwarnings("ignore:The shapes of several attributes will be changing") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize("uvc_future_shapes", [True, False]) @pytest.mark.parametrize("uvf_future_shapes", [True, False]) def test_init_cal_copy_flags(uvc_future_shapes, uvf_future_shapes): @@ -667,6 +663,7 @@ def test_init_waterfall_uvd(uvdata_obj, uvd_future_shapes, uvf_future_shapes): @pytest.mark.filterwarnings("ignore:The shapes of several attributes will be changing") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize("uvc_future_shapes", [True, False]) @pytest.mark.parametrize("uvf_future_shapes", [True, False]) def test_init_waterfall_uvc(uvc_future_shapes, uvf_future_shapes): @@ -691,6 +688,7 @@ def test_init_waterfall_uvc(uvc_future_shapes, uvf_future_shapes): assert pyuvdata_version_str in uvf.history +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_init_waterfall_flag_uvcal(): uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) @@ -724,6 +722,7 @@ def test_init_waterfall_flag_uvdata(uvdata_obj): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_init_waterfall_copy_flags(uvdata_obj): uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) @@ -762,9 +761,9 @@ def test_from_uvcal_error(uvdata_obj): with uvtest.check_warnings( UserWarning, match=[ - "telescope_location is not set. Using known values for HERA.", - "antenna_positions are not set or are being overwritten. Using known " - "values for HERA.", + "telescope_location, antenna_positions, antenna_diameters are not " + "set or are being overwritten. telescope_location, antenna_positions, " + "antenna_diameters are set using values from known telescopes for HERA.", "When converting a delay-style cal to future array shapes", ], ): @@ -783,6 +782,7 @@ def test_from_uvcal_error(uvdata_obj): uvf.from_uvcal(delay_object) +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_from_uvdata_error(): uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) @@ -859,12 +859,34 @@ def test_read_write_loop( uvf2.use_current_array_shapes() else: uvf2.use_future_array_shapes() + assert uvf.__eq__(uvf2, check_history=True) assert uvf2.filename == [os.path.basename(test_outfile)] -def test_read_write_loop_spw(uvdata_obj, test_outfile): +@pytest.mark.parametrize(["telescope_frame", "selenoid"], frame_selenoid) +def test_read_write_loop_spw(uvdata_obj, test_outfile, telescope_frame, selenoid): uv = uvdata_obj + + if telescope_frame == "mcmf": + pytest.importorskip("lunarsky") + enu_antpos, _ = uv.get_ENU_antpos() + latitude, longitude, altitude = uv.telescope_location_lat_lon_alt + uv.telescope._location.frame = "mcmf" + uv.telescope._location.ellipsoid = selenoid + uv.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, + latitude=latitude, + longitude=longitude, + altitude=altitude, + frame="mcmf", + ellipsoid=selenoid, + ) + uv.antenna_positions = new_full_antpos - uv.telescope_location + uv.set_lsts_from_time_array() + uv.check() + uvf = UVFlag(uv, label="test", use_future_array_shapes=True) uvf.Nspws = 2 @@ -898,6 +920,7 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize( ["uvf_type", "param_list", "warn_type", "msg", "uv_mod"], [ @@ -917,14 +940,21 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) "baseline", ["telescope_location"], UserWarning, - ["telescope_location are not set or are being overwritten. Using known"], + [ + "telescope_location are not set or are being overwritten. " + "telescope_location are set using values from known telescopes " + "for HERA." + ], "reset_telescope_params", ), ( "baseline", ["antenna_names"], UserWarning, - ["antenna_names are not set or are being overwritten. Using known"], + [ + "antenna_names are not set or are being overwritten. " + "antenna_names are set using values from known telescopes for HERA." + ], "reset_telescope_params", ), ( @@ -932,8 +962,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_names"], UserWarning, [ - "Not all antennas with data have metadata in the telescope object. " - "Not setting antenna metadata.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_names']", "antenna_names not in file, setting based on antenna_numbers", ], "change_ant_numbers", @@ -942,14 +972,20 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) "baseline", ["antenna_numbers"], UserWarning, - ["antenna_numbers are not set or are being overwritten. Using known"], + [ + "antenna_numbers are not set or are being overwritten. " + "antenna_numbers are set using values from known telescopes for HERA." + ], "reset_telescope_params", ), ( "baseline", ["antenna_positions"], UserWarning, - ["antenna_positions are not set or are being overwritten. Using known"], + [ + "antenna_positions are not set or are being overwritten. " + "antenna_positions are set using values from known telescopes for HERA." + ], "reset_telescope_params", ), ("baseline", ["Nants_telescope"], None, [], "reset_telescope_params"), @@ -1000,8 +1036,10 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_names", "antenna_numbers", "antenna_positions"], UserWarning, [ - "Nants_telescope, antenna_names, antenna_numbers, antenna_positions " - "are not set or are being overwritten. Using known values for HERA." + "Nants, antenna_names, antenna_numbers, antenna_positions are " + "not set or are being overwritten. Nants, antenna_names, " + "antenna_numbers, antenna_positions are set using values from " + "known telescopes for HERA." ], "reset_telescope_params", ), @@ -1010,8 +1048,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_numbers"], [UserWarning, UserWarning], [ - "antenna_numbers is not set but cannot be set using known values for " - "HERA because the expected shapes don't match.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_numbers'].", "antenna_numbers not in file, cannot be set based on ant_1_array and " "ant_2_array because Nants_telescope is greater than Nants_data.", ], @@ -1022,9 +1060,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_names"], UserWarning, [ - "antenna_names is not set but cannot be set using known values for " - "HERA because the expected shapes don't match.", - "antenna_names not in file, setting based on antenna_numbers", + "antenna_names are not set or are being overwritten. " + "antenna_names are set using values from known telescopes for HERA." ], None, ), @@ -1033,8 +1070,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_numbers"], UserWarning, [ - "antenna_numbers is not set but cannot be set using known values for " - "HERA because the expected shapes don't match.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_numbers']", "antenna_numbers not in file, setting based on ant_1_array and " "ant_2_array.", ], @@ -1045,8 +1082,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_numbers"], [UserWarning, UserWarning], [ - "antenna_numbers is not set but cannot be set using known values for " - "HERA because the expected shapes don't match.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_numbers']", "antenna_numbers not in file, cannot be set based on ant_array because " "Nants_telescope is greater than Nants_data.", ], @@ -1057,8 +1094,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_numbers"], [UserWarning, UserWarning], [ - "antenna_numbers is not set but cannot be set using known values for " - "HERA because the expected shapes don't match.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_numbers']", "antenna_numbers not in file, setting based on ant_array.", ], "remove_extra_metadata", @@ -1068,8 +1105,8 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ["antenna_numbers"], [UserWarning, UserWarning], [ - "Not all antennas with data have metadata in the telescope object. " - "Not setting antenna metadata.", + "Not all antennas have metadata in the known_telescope data. " + "Not setting ['antenna_numbers']", "antenna_numbers not in file, cannot be set based on ant_array " "because Nants_telescope is greater than Nants_data. This will result " "in errors when the object is checked.", @@ -1098,8 +1135,11 @@ def test_read_write_loop_missing_telescope_info( if uv_mod == "reset_telescope_params": with uvtest.check_warnings( UserWarning, - match="Nants_telescope, antenna_diameters, antenna_names, antenna_numbers, " - "antenna_positions, telescope_location, telescope_name", + match="telescope_location, Nants, antenna_names, antenna_numbers, " + "antenna_positions, antenna_diameters are not set or are being " + "overwritten. telescope_location, Nants, antenna_names, " + "antenna_numbers, antenna_positions, antenna_diameters are set " + "using values from known telescopes for HERA.", ): uv.set_telescope_params(overwrite=True) elif uv_mod == "remove_extra_metadata": @@ -1108,6 +1148,8 @@ def test_read_write_loop_missing_telescope_info( uv.antenna_names = uv.antenna_names[ant_inds_keep] uv.antenna_numbers = uv.antenna_numbers[ant_inds_keep] uv.antenna_positions = uv.antenna_positions[ant_inds_keep] + if uv.antenna_diameters is not None: + uv.antenna_diameters = uv.antenna_diameters[ant_inds_keep] uv.Nants_telescope = ant_inds_keep.size uv.check() else: @@ -1148,7 +1190,7 @@ def test_read_write_loop_missing_telescope_info( if uv_mod is None: if param_list == ["antenna_names"]: - assert np.array_equal(uvf2.antenna_names, uvf2.antenna_numbers.astype(str)) + assert not np.array_equal(uvf2.antenna_names, uvf.antenna_numbers) uvf2.antenna_names = uvf.antenna_names else: for param in param_list: @@ -1208,8 +1250,9 @@ def test_missing_telescope_info_mwa(test_outfile): "data were taken. Since that was not passed, the antenna metadata will be " "filled in from a static csv file containing all the antennas that could " "have been connected.", - "Nants_telescope, antenna_names, antenna_numbers, antenna_positions " - "are not set or are being overwritten. Using known values for mwa.", + "Nants, antenna_names, antenna_numbers, antenna_positions are not " + "set or are being overwritten. Nants, antenna_names, antenna_numbers, " + "antenna_positions are set using values from known telescopes for mwa.", ], ): uvf2 = UVFlag(test_outfile, use_future_array_shapes=True, telescope_name="mwa") @@ -1432,6 +1475,7 @@ def test_read_write_ant( assert uvf.__eq__(uvf2, check_history=True) +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_read_missing_nants_data(test_outfile): uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) @@ -1452,6 +1496,7 @@ def test_read_missing_nants_data(test_outfile): @pytest.mark.filterwarnings("ignore:The shapes of several attributes will be changing") +@pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") @pytest.mark.parametrize("future_shapes", [True, False]) def test_read_missing_nspws(test_outfile, future_shapes): uv = UVCal() @@ -1655,10 +1700,9 @@ def test_set_lsts(uvf_from_data, background): assert uvf2._lst_array == uvf._lst_array -@pytest.mark.filterwarnings("ignore:Nants_telescope, antenna_") def test_set_telescope_params(uvdata_obj): uvd = uvdata_obj - uvd.set_telescope_params(overwrite=True) + uvd.set_telescope_params(overwrite=True, warn=False) ants_with_data = np.union1d(uvd.ant_1_array, uvd.ant_2_array) uvd2 = uvd.select( antenna_nums=ants_with_data[: uvd.Nants_data // 2], @@ -1666,28 +1710,31 @@ def test_set_telescope_params(uvdata_obj): inplace=False, ) uvf = UVFlag(uvd2, use_future_array_shapes=True) - assert uvf._antenna_names == uvd2._antenna_names - assert uvf._antenna_numbers == uvd2._antenna_numbers - assert uvf._antenna_positions == uvd2._antenna_positions + assert uvf.telescope._antenna_names == uvd2.telescope._antenna_names + assert uvf.telescope._antenna_numbers == uvd2.telescope._antenna_numbers + assert uvf.telescope._antenna_positions == uvd2.telescope._antenna_positions - uvf.set_telescope_params(overwrite=True) - assert uvf._antenna_names == uvd._antenna_names - assert uvf._antenna_numbers == uvd._antenna_numbers - assert uvf._antenna_positions == uvd._antenna_positions + uvf.set_telescope_params(overwrite=True, warn=False) + assert uvf.telescope._antenna_names == uvd.telescope._antenna_names + assert uvf.telescope._antenna_numbers == uvd.telescope._antenna_numbers + assert uvf.telescope._antenna_positions == uvd.telescope._antenna_positions uvf = UVFlag(uvd2, use_future_array_shapes=True) uvf.antenna_positions = None with uvtest.check_warnings( UserWarning, - match="antenna_positions is not set but cannot be set using " - "known values for HERA because the expected shapes don't match.", + match="antenna_positions are not set or are being overwritten. " + "antenna_positions are set using values from known telescopes for HERA.", ): uvf.set_telescope_params() - assert uvf.antenna_positions is None uvf = UVFlag(uvd2, use_future_array_shapes=True) uvf.telescope_name = "foo" - with pytest.raises(ValueError, match="Telescope foo is not in known_telescopes."): + uvf.telescope_location = None + with pytest.raises( + ValueError, + match="Telescope foo is not in astropy_sites or known_telescopes_dict.", + ): uvf.set_telescope_params() @@ -2450,6 +2497,7 @@ def test_to_baseline_metric(uvdata_obj, uvd_future_shapes, uvf_future_shapes): uvf.metric_array[0, 10, 0] = 3.2 # Fill in time0, chan10 uvf.metric_array[1, 15, 0] = 2.1 # Fill in time1, chan15 + uvf.to_baseline(uv) assert uvf.telescope_name == uv.telescope_name assert np.all(uvf.telescope_location == uv.telescope_location) @@ -2548,8 +2596,7 @@ def test_to_baseline_from_antenna( with uvtest.check_warnings( UserWarning, - match=["Nants_telescope, antenna_names, antenna_numbers, antenna_positions"] - * 2, + match=["telescope_location, Nants, antenna_names, antenna_numbers, "] * 2, ): uvf.set_telescope_params(overwrite=True) uv.set_telescope_params(overwrite=True) diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 13ee0c9073..8bcc6a040a 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -12,8 +12,8 @@ import h5py import numpy as np +from .. import Telescope from .. import parameter as uvp -from .. import telescopes as uvtel from .. import utils as uvutils from ..uvbase import UVBase from ..uvcal import UVCal @@ -31,6 +31,19 @@ ) +telescope_params = { + "telescope_name": "name", + "telescope_location": "location", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + "antenna_diameters": "antenna_diameters", + "x_orientation": "x_orientation", + "instrument": "instrument", +} + + def and_rows_cols(waterfall): """Perform logical and over rows and cols of a waterfall. @@ -474,17 +487,6 @@ def __init__( form=("Npols",), ) - self._telescope_name = uvp.UVParameter( - "telescope_name", - description="Name of telescope or array (string).", - form="str", - expected_type=str, - ) - - self._telescope_location = uvp.LocationParameter( - "telescope_location", description=desc, tols=1e-3, frame="itrs" - ) - self._history = uvp.UVParameter( "history", description="String of history, units English", @@ -511,15 +513,6 @@ def __init__( "future_array_shapes", description=desc, expected_type=bool, value=False ) - # ---antenna information --- - desc = ( - "Number of antennas in the array. Only available for 'baseline' type " - "objects, used for calculating baseline numbers. " - "May be larger than the number of antennas with data." - ) - self._Nants_telescope = uvp.UVParameter( - "Nants_telescope", description=desc, expected_type=int, required=False - ) desc = ( "Number of antennas with data present. " "Only available for 'baseline' or 'antenna' type objects." @@ -529,65 +522,14 @@ def __init__( "Nants_data", description=desc, expected_type=int, required=False ) - desc = ( - "List of antenna names, shape (Nants_telescope), with numbers given by " - "antenna_numbers (which can be matched to ant_1_array and ant_2_array for " - "baseline type or ant_array for antenna type objects). Required for " - "baseline or antenna type objects. There must be one entry here for each " - "unique entry in ant_1_array and ant_2_array (for baseline type) or " - "ant_array (for antenna type), but there may be extras as well. " - ) - self._antenna_names = uvp.UVParameter( - "antenna_names", - description=desc, - form=("Nants_telescope",), - expected_type=str, - ) - - desc = ( - "List of integer antenna numbers corresponding to antenna_names, " - "shape (Nants_telescope). Required for baseline or antenna type objects. " - "There must be one entry here for each unique entry in ant_1_array and " - "ant_2_array (for baseline type) or ant_array (for antenna type), but " - "there may be extras as well. Note that these are not indices -- they do " - "not need to start at zero or be continuous." - ) - self._antenna_numbers = uvp.UVParameter( - "antenna_numbers", - description=desc, - form=("Nants_telescope",), - expected_type=int, - ) - - desc = ( - "Array giving coordinates of antennas relative to " - "telescope_location (ITRF frame), shape (Nants_telescope, 3), " - "units meters. See the tutorial page in the documentation " - "for an example of how to convert this to topocentric frame." - ) - self._antenna_positions = uvp.UVParameter( - "antenna_positions", - description=desc, - form=("Nants_telescope", 3), - expected_type=float, - tols=1e-3, # 1 mm + # ---telescope information --- + self._telescope = uvp.UVParameter( + "telescope", + description="Telescope object containing the telescope metadata.", + expected_type=Telescope, ) # --extra information --- - desc = ( - "Orientation of the physical dipole corresponding to what is " - 'labelled as the x polarization. Options are "east" ' - '(indicating east/west orientation) and "north" (indicating ' - "north/south orientation)" - ) - self._x_orientation = uvp.UVParameter( - "x_orientation", - description=desc, - required=False, - expected_type=str, - acceptable_vals=["east", "north"], - ) - desc = ( "List of strings containing the unique basenames (not the full path) of " "input files." @@ -596,6 +538,12 @@ def __init__( "filename", required=False, description=desc, expected_type=str ) + # initialize the telescope object + self.telescope = Telescope() + + # set the appropriate telescope attributes as required + self.telescope._set_uvflag_requirements() + # initialize the underlying UVBase properties super(UVFlag, self).__init__() @@ -683,6 +631,128 @@ def __init__( "list, tuple, string, pathlib.Path, UVData, or UVCal." ) + @property + def telescope_name(self): + """The telescope name (stored on the Telescope object internally).""" + return self.telescope.name + + @telescope_name.setter + def telescope_name(self, val): + self.telescope.name = val + + @property + def instrument(self): + """The instrument name (stored on the Telescope object internally).""" + return self.telescope.instrument + + @instrument.setter + def instrument(self, val): + self.telescope.instrument = val + + @property + def telescope_location(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location + + @telescope_location.setter + def telescope_location(self, val): + self.telescope.location = val + + @property + def telescope_location_lat_lon_alt(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt + + @telescope_location_lat_lon_alt.setter + def telescope_location_lat_lon_alt(self, val): + self.telescope.location_lat_lon_alt = val + + @property + def telescope_location_lat_lon_alt_degrees(self): + """The telescope location (stored on the Telescope object internally).""" + return self.telescope.location_lat_lon_alt_degrees + + @telescope_location_lat_lon_alt_degrees.setter + def telescope_location_lat_lon_alt_degrees(self, val): + self.telescope.location_lat_lon_alt_degrees = val + + @property + def Nants_telescope(self): # noqa + """ + The number of antennas in the telescope. + + This property is stored on the Telescope object internally. + """ + return self.telescope.Nants + + @Nants_telescope.setter + def Nants_telescope(self, val): # noqa + self.telescope.Nants = val + + @property + def antenna_names(self): + """The antenna names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_names + + @antenna_names.setter + def antenna_names(self, val): + self.telescope.antenna_names = val + + @property + def antenna_numbers(self): + """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). + + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_numbers + + @antenna_numbers.setter + def antenna_numbers(self, val): + self.telescope.antenna_numbers = val + + @property + def antenna_positions(self): + """The antenna positions coordinates of antennas relative to telescope_location. + + The coordinates are in the ITRF frame, shape (Nants_telescope, 3). + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_positions + + @antenna_positions.setter + def antenna_positions(self, val): + self.telescope.antenna_positions = val + + @property + def x_orientation(self): + """Orientation of the physical dipole corresponding to the x label. + + Options are 'east' (indicating east/west orientation) and 'north (indicating + north/south orientation). + This property is stored on the Telescope object internally. + """ + return self.telescope.x_orientation + + @x_orientation.setter + def x_orientation(self, val): + self.telescope.x_orientation = val + + @property + def antenna_diameters(self): + """The antenna diameters in meters. + + Used by CASA to construct a default beam if no beam is supplied. + This property is stored on the Telescope object internally. + """ + return self.telescope.antenna_diameters + + @antenna_diameters.setter + def antenna_diameters(self, val): + self.telescope.antenna_diameters = val + @property def _data_params(self): """List of strings giving the data-like parameters.""" @@ -858,7 +928,6 @@ def _set_type_antenna(self): self._baseline_array.required = False self._ant_1_array.required = False self._ant_2_array.required = False - self._Nants_telescope.required = False self._Nants_data.required = True self._Nbls.required = False self._Nblts.required = False @@ -883,7 +952,6 @@ def _set_type_baseline(self): self._baseline_array.required = True self._ant_1_array.required = True self._ant_2_array.required = True - self._Nants_telescope.required = True self._Nants_data.required = True self._Nbls.required = True self._Nblts.required = True @@ -912,7 +980,6 @@ def _set_type_waterfall(self): self._baseline_array.required = False self._ant_1_array.required = False self._ant_2_array.required = False - self._Nants_telescope.required = False self._Nants_data.required = False self._Nbls.required = False self._Nblts.required = False @@ -1041,7 +1108,7 @@ def check( uvutils.check_surface_based_positions( antenna_positions=self.antenna_positions, telescope_loc=self.telescope_location, - telescope_frame=self._telescope_location.frame, + telescope_frame=self.telescope._location.frame, raise_error=False, ) @@ -1053,7 +1120,8 @@ def check( longitude=lon, altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) return True @@ -1142,7 +1210,8 @@ def _set_lsts_helper(self, astrometry_library=None): longitude=longitude, altitude=altitude, astrometry_library=astrometry_library, - frame=self._telescope_location.frame, + frame=self.telescope._location.frame, + ellipsoid=self.telescope._location.ellipsoid, ) return @@ -1173,7 +1242,15 @@ def set_lsts_from_time_array(self, *, background=False, astrometry_library=None) proc.start() return proc - def set_telescope_params(self, *, overwrite=False, warn=True): + def set_telescope_params( + self, + *, + overwrite=False, + warn=True, + run_check=True, + check_extra=True, + run_check_acceptability=True, + ): """ Set telescope related parameters. @@ -1192,77 +1269,13 @@ def set_telescope_params(self, *, overwrite=False, warn=True): ValueError if the telescope_name is not in known telescopes """ - telescope_obj = uvtel.get_telescope(self.telescope_name) - if telescope_obj is not False: - params_set = [] - telescope_params = list(telescope_obj.__iter__()) - # ensure that the Nants_telescope comes first so shapes work out below - telescope_params.remove("_Nants_telescope") - telescope_params.insert(0, "_Nants_telescope") - - set_ant_metadata = True - if self.type != "waterfall" and "_antenna_numbers" in telescope_params: - # need to check that all antennas on the object are in the telescope's - # antenna_numbers - if self.type == "antenna": - ants_to_check = self.ant_array - else: - ants_to_check = np.union1d(self.ant_1_array, self.ant_2_array) - - if not all( - ant in telescope_obj.antenna_numbers for ant in ants_to_check - ): - warnings.warn( - "Not all antennas with data have metadata in the telescope " - "object. Not setting antenna metadata." - ) - set_ant_metadata = False - - if not set_ant_metadata: - ant_params_to_remove = [] - for p in telescope_params: - if "ant" in p: - ant_params_to_remove.append(p) - - for p in ant_params_to_remove: - telescope_params.remove(p) - - for p in telescope_params: - telescope_param = getattr(telescope_obj, p) - if p in self: - self_param = getattr(self, p) - else: - continue - if telescope_param.value is not None and ( - overwrite is True or self_param.value is None - ): - telescope_shape = telescope_param.expected_shape(telescope_obj) - self_shape = self_param.expected_shape(self) - if telescope_shape == self_shape: - params_set.append(self_param.name) - prop_name = self_param.name - setattr(self, prop_name, getattr(telescope_obj, prop_name)) - else: - # Note dropped handling for antenna diameters that appears in - # UVData because they don't exist on UVFlag. - warnings.warn( - f"{self_param.name} is not set but cannot be set using " - f"known values for {telescope_obj.telescope_name} " - "because the expected shapes don't match." - ) - - if len(params_set) > 0: - if warn: - params_set_str = ", ".join(params_set) - warnings.warn( - f"{params_set_str} are not set or are being " - "overwritten. Using known values for " - f"{telescope_obj.telescope_name}." - ) - else: - raise ValueError( - f"Telescope {self.telescope_name} is not in known_telescopes." - ) + self.telescope.update_params_from_known_telescopes( + overwrite=overwrite, + warn=warn, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) def antpair2ind(self, ant1, ant2): """Get blt indices for given (ordered) antenna pair. @@ -1751,8 +1764,12 @@ def to_baseline( ) else: # compare the UVParameter objects to properly handle tolerances - this_param = getattr(self, "_" + param) - uv_param = getattr(uv, "_" + param) + if param in telescope_params: + this_param = getattr(self.telescope, "_" + telescope_params[param]) + uv_param = getattr(uv.telescope, "_" + telescope_params[param]) + else: + this_param = getattr(self, "_" + param) + uv_param = getattr(uv, "_" + param) if this_param.value is not None and this_param != uv_param: raise ValueError( f"{param} is not the same this object and on uv. The value on " @@ -1921,19 +1938,12 @@ def to_baseline( self.lst_array = uv.lst_array self.Nblts = self.time_array.size - if self.telescope_name is None and self.telescope_location is None: - self.telescope_name = uv.telescope_name - self.telescope_location = uv.telescope_location - - if ( - self.antenna_numbers is None - and self.antenna_names is None - and self.antenna_positions is None - ): - self.antenna_numbers = uv.antenna_numbers - self.antenna_names = uv.antenna_names - self.antenna_positions = uv.antenna_positions - self.Nants_telescope = uv.Nants_telescope + for param in self.telescope: + this_param = getattr(self.telescope, param) + uvd_param = getattr(uv.telescope, param) + param_name = this_param.name + if this_param.value is None and uvd_param.value is not None: + setattr(self.telescope, param_name, uvd_param.value) self._set_type_baseline() self.clear_unused_attributes() @@ -2049,8 +2059,12 @@ def to_antenna( ) else: # compare the UVParameter objects to properly handle tolerances - this_param = getattr(self, "_" + param) - uv_param = getattr(uv, "_" + param) + if param in telescope_params: + this_param = getattr(self.telescope, "_" + telescope_params[param]) + uv_param = getattr(uv.telescope, "_" + telescope_params[param]) + else: + this_param = getattr(self, "_" + param) + uv_param = getattr(uv, "_" + param) if this_param.value is not None and this_param != uv_param: raise ValueError( f"{param} is not the same this object and on uv. The value on " @@ -2124,19 +2138,12 @@ def to_antenna( if not self.future_array_shapes: self.freq_array = np.atleast_2d(self.freq_array) - if self.telescope_name is None and self.telescope_location is None: - self.telescope_name = uv.telescope_name - self.telescope_location = uv.telescope_location - - if ( - self.antenna_numbers is None - and self.antenna_names is None - and self.antenna_positions is None - ): - self.antenna_numbers = uv.antenna_numbers - self.antenna_names = uv.antenna_names - self.antenna_positions = uv.antenna_positions - self.Nants_telescope = uv.Nants_telescope + for param in self.telescope: + this_param = getattr(self.telescope, param) + uvd_param = getattr(uv.telescope, param) + param_name = this_param.name + if this_param.value is None and uvd_param.value is not None: + setattr(self.telescope, param_name, uvd_param.value) if self.Nspws is None: self.Nspws = uv.Nspws @@ -2421,8 +2428,12 @@ def __add__( for param in warn_compatibility_params: # compare the UVParameter objects to properly handle tolerances - this_param = getattr(self, "_" + param) - other_param = getattr(other, "_" + param) + if param in telescope_params: + this_param = getattr(self.telescope, "_" + telescope_params[param]) + other_param = getattr(other.telescope, "_" + telescope_params[param]) + else: + this_param = getattr(self, "_" + param) + other_param = getattr(other, "_" + param) if this_param.value is not None and this_param != other_param: raise ValueError( f"{param} is not the same the two objects. The value on this " @@ -3532,6 +3543,8 @@ def read( if "x_orientation" in header.keys(): self.x_orientation = header["x_orientation"][()].decode("utf8") + if "instrument" in header.keys(): + self.instrument = header["instrument"][()].decode("utf8") self.time_array = header["time_array"][()] if "Ntimes" in header.keys(): @@ -3667,6 +3680,14 @@ def read( if "telescope_location" in header.keys(): self.telescope_location = header["telescope_location"][()] + if "telescope_frame" in header.keys(): + self.telescope._location.frame = header["telescope_frame"][ + () + ].decode("utf8") + if self.telescope._location.frame != "itrs": + self.telescope._location.ellipsoid = header["ellipsoid"][ + () + ].decode("utf8") if "antenna_numbers" in header.keys(): self.antenna_numbers = header["antenna_numbers"][()] @@ -3681,6 +3702,8 @@ def read( if "antenna_positions" in header.keys(): self.antenna_positions = header["antenna_positions"][()] + if "antenna_diameters" in header.keys(): + self.antenna_diameters = header["antenna_diameters"][()] self.history = header["history"][()].decode("utf8") @@ -3798,7 +3821,7 @@ def read( "will be filled in from a static csv file containing all " "the antennas that could have been connected." ) - self.set_telescope_params() + self.set_telescope_params(run_check=False) if self.antenna_numbers is None and self.type in [ "baseline", @@ -3905,6 +3928,9 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["telescope_name"] = np.string_(self.telescope_name) if self.telescope_location is not None: header["telescope_location"] = self.telescope_location + header["telescope_frame"] = np.string_(self.telescope._location.frame) + if self.telescope._location.frame == "mcmf": + header["ellipsoid"] = np.string_(self.telescope._location.ellipsoid) header["Ntimes"] = self.Ntimes header["time_array"] = self.time_array @@ -3923,6 +3949,10 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): if self.x_orientation is not None: header["x_orientation"] = np.string_(self.x_orientation) + if self.instrument is not None: + header["instrument"] = np.string_(self.instrument) + if self.antenna_diameters is not None: + header["antenna_diameters"] = self.antenna_diameters if isinstance(self.polarization_array.item(0), str): polarization_array = np.asarray( @@ -4067,7 +4097,6 @@ def from_uvdata( self.Nfreqs = indata.Nfreqs self.polarization_array = copy.deepcopy(indata.polarization_array) self.Npols = indata.Npols - self.Nants_telescope = indata.Nants_telescope self.Ntimes = indata.Ntimes if indata.future_array_shapes or indata.flex_spw: @@ -4075,11 +4104,9 @@ def from_uvdata( else: self.channel_width = np.full(self.Nfreqs, indata.channel_width) - self.telescope_name = indata.telescope_name - self.telescope_location = indata.telescope_location - self.antenna_names = copy.deepcopy(indata.antenna_names) - self.antenna_numbers = copy.deepcopy(indata.antenna_numbers) - self.antenna_positions = copy.deepcopy(indata.antenna_positions) + self.telescope = indata.telescope.copy() + self.telescope._set_uvflag_requirements() + self.Nspws = indata.Nspws self.spw_array = copy.deepcopy(indata.spw_array) if indata.flex_spw_id_array is not None: @@ -4168,9 +4195,6 @@ def from_uvdata( self.filename = indata.filename self._filename.form = indata._filename.form - if indata.x_orientation is not None: - self.x_orientation = indata.x_orientation - if self.mode == "metric": self.weights_array = np.ones(self.metric_array.shape) @@ -4266,7 +4290,6 @@ def from_uvcal( self.Nfreqs = indata.Nfreqs self.polarization_array = copy.deepcopy(indata.jones_array) self.Npols = indata.Njones - self.Nants_telescope = indata.Nants_telescope self.Ntimes = indata.Ntimes self.time_array = copy.deepcopy(indata.time_array) self.lst_array = copy.deepcopy(indata.lst_array) @@ -4276,11 +4299,9 @@ def from_uvcal( else: self.channel_width = np.full(self.Nfreqs, indata.channel_width) - self.telescope_name = indata.telescope_name - self.telescope_location = indata.telescope_location - self.antenna_names = copy.deepcopy(indata.antenna_names) - self.antenna_numbers = copy.deepcopy(indata.antenna_numbers) - self.antenna_positions = copy.deepcopy(indata.antenna_positions) + self.telescope = indata.telescope.copy() + self.telescope._set_uvflag_requirements() + self.Nspws = indata.Nspws self.spw_array = copy.deepcopy(indata.spw_array) if indata.flex_spw_id_array is not None: @@ -4374,9 +4395,6 @@ def from_uvcal( self.filename = indata.filename self._filename.form = indata._filename.form - if indata.x_orientation is not None: - self.x_orientation = indata.x_orientation - if history not in self.history: self.history += history self.label += label From 951da2f19d705729851a4c52bd643bde030d837d Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 28 Mar 2024 16:58:35 -0700 Subject: [PATCH 05/59] use location_obj throughout --- pyuvdata/telescopes.py | 2 +- pyuvdata/uvdata/initializers.py | 11 +---------- pyuvdata/uvdata/miriad.py | 6 ++---- pyuvdata/uvdata/uvdata.py | 10 +++------- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 15b1d9f7bb..e453c07889 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -300,7 +300,7 @@ def location_obj(self): def location_obj(self, val): if isinstance(val, EarthLocation): self._location.frame = "itrs" - elif isinstance(val, uvutils.MoonLocation): + elif uvutils.hasmoon and isinstance(val, uvutils.MoonLocation): self._location.frame = "mcmf" self._location.ellipsoid = val.ellipsoid else: diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index cc716e1bdd..b8ed979c58 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -529,9 +529,6 @@ def new_uvdata( if hasmoon and isinstance(telescope_location, MoonLocation): telescope_location.ellipsoid = ellipsoid - telescope_frame = "mcmf" - else: - telescope_frame = "itrs" lst_array, integration_time = get_time_params( telescope_location=telescope_location, @@ -602,13 +599,7 @@ def new_uvdata( obj.freq_array = freq_array obj.polarization_array = polarization_array obj.antenna_positions = antenna_positions - obj.telescope.location = [ - telescope_location.x.to_value("m"), - telescope_location.y.to_value("m"), - telescope_location.z.to_value("m"), - ] - obj.telescope._location.frame = telescope_frame - obj.telescope._location.ellipsoid = ellipsoid + obj.telescope.location_obj = telescope_location obj.telescope_name = telescope_name obj.baseline_array = baseline_array obj.ant_1_array = ant_1_array diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index 9875833509..f54c7a87d9 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -11,7 +11,7 @@ import numpy as np import scipy from astropy import constants as const -from astropy.coordinates import Angle, EarthLocation, SkyCoord +from astropy.coordinates import Angle, SkyCoord from astropy.time import Time from docstring_parser import DocstringStyle @@ -1999,9 +1999,7 @@ def write_miriad( az=np.zeros_like(times) + phase_dict["cat_lon"], frame="altaz", unit="rad", - location=EarthLocation.from_geocentric( - *self.telescope_location, unit="m" - ), + location=self.telescope.location_obj, obstime=Time(times, format="jd"), ) driftscan_coords[cat_id] = { diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 7b604672f4..953a7e371c 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -16,7 +16,7 @@ import numpy as np from astropy import constants as const from astropy import coordinates as coord -from astropy.coordinates import Angle, EarthLocation, SkyCoord +from astropy.coordinates import Angle, SkyCoord from astropy.time import Time from docstring_parser import DocstringStyle from scipy import ndimage as nd @@ -5415,9 +5415,7 @@ def phase_to_time( # Generate ra/dec of zenith at time in the phase_frame coordinate # system to use for phasing - telescope_location = EarthLocation.from_geocentric( - *self.telescope_location, unit="m" - ) + telescope_location = self.telescope.location_obj zenith_coord = SkyCoord( alt=Angle(90 * units.deg), @@ -5661,9 +5659,7 @@ def fix_phase(self, *, use_ant_pos=True): unique_times, _ = np.unique(self.time_array, return_index=True) - telescope_location = EarthLocation.from_geocentric( - *self.telescope_location, unit=units.m - ) + telescope_location = self.telescope.location_obj obs_times = Time(unique_times, format="jd") itrs_telescope_locations = telescope_location.get_itrs(obstime=obs_times) itrs_telescope_locations = SkyCoord(itrs_telescope_locations) From 6dc9961550f8ef851d8c4fb223b94dd2a9c389e3 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 28 Mar 2024 15:39:11 -0700 Subject: [PATCH 06/59] fix tutorial bug --- docs/uvdata_tutorial.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/uvdata_tutorial.rst b/docs/uvdata_tutorial.rst index 09308a5f53..06d8c1c5d8 100644 --- a/docs/uvdata_tutorial.rst +++ b/docs/uvdata_tutorial.rst @@ -1500,6 +1500,7 @@ and :meth:`pyuvdata.UVData.diff_vis` methods. .. code-block:: python >>> import os + >>> from astropy.time import Time >>> from pyuvdata import UVData >>> from pyuvdata.data import DATA_PATH >>> filename = os.path.join(DATA_PATH, 'day2_TDEM0003_10s_norx_1src_1spw.uvfits') @@ -1516,8 +1517,9 @@ and :meth:`pyuvdata.UVData.diff_vis` methods. >>> uvd1.sum_vis(uvd2, inplace=True) >>> # override a particular parameter - >>> uvd1.instrument = "test instrument" - >>> uvd1.sum_vis(uvd2, inplace=True, override_params=["instrument"]) + >>> rdate_obj = Time(np.floor(uvd1.time_array[0]), format="jd", scale="utc") + >>> uvd1.rdate = rdate_obj.strftime("%Y-%m-%d") + >>> uvd1.sum_vis(uvd2, inplace=True, override_params=["rdate"]) .. _large_files: From eb016ba64709fd358075a776cf0a39957f34ae6b Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 28 Mar 2024 16:35:15 -0700 Subject: [PATCH 07/59] Update the docs for the telescope refactor --- .gitignore | 1 + docs/conf.py | 3 ++ docs/fast_calh5_meta.rst | 5 ++ docs/known_telescopes.rst | 21 --------- docs/make_index.py | 3 +- docs/make_telescope.py | 96 +++++++++++++++++++++++++++++++++++++++ docs/make_uvbeam.py | 2 +- docs/make_uvcal.py | 9 ++-- docs/make_uvdata.py | 11 +++-- docs/make_uvflag.py | 6 ++- pyuvdata/uvcal/uvcal.py | 5 +- pyuvdata/uvdata/uvdata.py | 5 +- pyuvdata/uvflag/uvflag.py | 5 +- 13 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 docs/fast_calh5_meta.rst delete mode 100644 docs/known_telescopes.rst create mode 100644 docs/make_telescope.py diff --git a/.gitignore b/.gitignore index c9674e47e2..52dc32d40f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ docs/uvdata.rst docs/uvcal.rst docs/uvbeam.rst docs/uvflag.rst +docs/telescope.rst # PyBuilder target/ diff --git a/docs/conf.py b/docs/conf.py index 956eef141d..a3422bb211 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,7 @@ uvcal_file = os.path.join(os.path.abspath("../docs"), "uvcal.rst") uvbeam_file = os.path.join(os.path.abspath("../docs"), "uvbeam.rst") uvflag_file = os.path.join(os.path.abspath("../docs"), "uvflag.rst") +telescope_file = os.path.join(os.path.abspath("../docs"), "telescope.rst") # -- General configuration ------------------------------------------------ @@ -347,12 +348,14 @@ def build_custom_docs(app): import make_uvcal import make_uvbeam import make_uvflag + import make_telescope make_index.write_index_rst(readme_file=readme_file, write_file=index_file) make_uvdata.write_uvdata_rst(write_file=uvdata_file) make_uvcal.write_uvcal_rst(write_file=uvcal_file) make_uvbeam.write_uvbeam_rst(write_file=uvbeam_file) make_uvflag.write_uvflag_rst(write_file=uvflag_file) + make_telescope.write_telescope_rst(write_file=telescope_file) # this is to enable running python in the rst files. diff --git a/docs/fast_calh5_meta.rst b/docs/fast_calh5_meta.rst new file mode 100644 index 0000000000..3fecea1753 --- /dev/null +++ b/docs/fast_calh5_meta.rst @@ -0,0 +1,5 @@ +FastCalH5Meta +================= + +.. autoclass:: pyuvdata.uvcal.FastCalH5Meta + :members: diff --git a/docs/known_telescopes.rst b/docs/known_telescopes.rst deleted file mode 100644 index e5172b6910..0000000000 --- a/docs/known_telescopes.rst +++ /dev/null @@ -1,21 +0,0 @@ -Known Telescopes -================ - -Known Telescope Data --------------------- -pyuvdata uses `Astropy sites `_ -for telescope location information, in addition to the following telescope information -that is tracked within pyuvdata: - -.. exec:: - import json - from pyuvdata.telescopes import KNOWN_TELESCOPES - json_obj = json.dumps(KNOWN_TELESCOPES, sort_keys=True, indent=4) - json_obj = json_obj[:-1] + " }" - print('.. code-block:: JavaScript\n\n {json_str}\n\n'.format(json_str=json_obj)) - -Related class and functions ---------------------------- - -.. automodule:: pyuvdata.telescopes - :members: diff --git a/docs/make_index.py b/docs/make_index.py index a713bef015..a80bbdb673 100644 --- a/docs/make_index.py +++ b/docs/make_index.py @@ -49,9 +49,10 @@ def write_index_rst(readme_file=None, write_file=None): " uvcal\n" " uvbeam\n" " uvflag\n" + " telescope\n" " fast_uvh5_meta\n" + " fast_calh5_meta\n" " utility_functions\n" - " known_telescopes\n" " developer_docs\n" ) diff --git a/docs/make_telescope.py b/docs/make_telescope.py new file mode 100644 index 0000000000..aa501799b9 --- /dev/null +++ b/docs/make_telescope.py @@ -0,0 +1,96 @@ +# -*- mode: python; coding: utf-8 -*- + +""" +Format the Telescope object parameters into a sphinx rst file. + +""" +import inspect +import json +import os + +from astropy.time import Time + +from pyuvdata import Telescope +from pyuvdata.telescopes import KNOWN_TELESCOPES + + +def write_telescope_rst(write_file=None): + tel = Telescope() + out = "Telescope\n=========\n\n" + out += ( + "Telescope is a helper class for telescope-related metadata.\n" + "Several of the primary user classes need telescope metdata, so they " + "have a Telescope object as an attribute.\n\n" + "Attributes\n----------\n" + "The attributes on Telescope hold all of the metadata required to\n" + "describe interferometric telescopes. Under the hood, the attributes are\n" + "implemented as properties based on :class:`pyuvdata.parameter.UVParameter`\n" + "objects but this is fairly transparent to users.\n\n" + "When a new Telescope object is initialized, it has all of these \n" + "attributes defined but set to ``None``. The attributes\n" + "can be set directly on the object. Some of these attributes\n" + "are `required`_ to be set to have a fully defined object while others are\n" + "`optional`_. The :meth:`pyuvdata.Telescope.check` method can be called\n" + "on the object to verify that all of the required attributes have been\n" + "set in a consistent way.\n\n" + "Note that angle type attributes also have convenience properties named the\n" + "same thing with ``_degrees`` appended through which you can get or set the\n" + "value in degrees. Similarly location type attributes (which are given in\n" + "geocentric xyz coordinates) have convenience properties named the\n" + "same thing with ``_lat_lon_alt`` and ``_lat_lon_alt_degrees`` appended\n" + "through which you can get or set the values using latitude, longitude and\n" + "altitude values in radians or degrees and meters.\n\n" + ) + out += "Required\n********\n" + out += ( + "These parameters are required to have a basic well-defined Telescope object.\n" + ) + out += "\n\n" + for thing in tel.required(): + obj = getattr(tel, thing) + out += "**{name}**\n".format(name=obj.name) + out += " {desc}\n".format(desc=obj.description) + out += "\n" + + out += "Optional\n********\n" + out += ( + "These parameters are needed by by one or of the primary user classes\n" + "but are not always required. Some of them are required when attached to\n" + "the primary classes." + ) + out += "\n\n" + + for thing in tel.extra(): + obj = getattr(tel, thing) + out += "**{name}**\n".format(name=obj.name) + out += " {desc}\n".format(desc=obj.description) + out += "\n" + + out += "Methods\n-------\n.. autoclass:: pyuvdata.Telescope\n :members:\n\n" + + out += ( + "Known Telescopes\n================\n\n" + "Known Telescope Data\n--------------------\n" + "pyuvdata uses `Astropy sites\n" + "`_\n" + "for telescope location information, in addition to the following\n" + "telescope information that is tracked within pyuvdata. Note that for\n" + "some telescopes we store csv files giving antenna layout information\n" + "which can be used if data files are missing that information.\n\n" + ) + + json_obj = json.dumps(KNOWN_TELESCOPES, sort_keys=True, indent=4) + json_obj = json_obj[:-1] + " }" + out += ".. code-block:: JavaScript\n\n {json_str}\n\n".format(json_str=json_obj) + + t = Time.now() + t.format = "iso" + t.out_subfmt = "date" + out += "last updated: {date}".format(date=t.iso) + if write_file is None: + write_path = os.path.dirname(os.path.abspath(inspect.stack()[0][1])) + write_file = os.path.join(write_path, "telescope.rst") + F = open(write_file, "w") + F.write(out) + print("wrote " + write_file) diff --git a/docs/make_uvbeam.py b/docs/make_uvbeam.py index 3f69b64ba5..f930a0f5e2 100644 --- a/docs/make_uvbeam.py +++ b/docs/make_uvbeam.py @@ -43,7 +43,7 @@ def write_uvbeam_rst(write_file=None): ) out += "Required\n********\n" out += ( - "These parameters are required to have a sensible UVBeam object and \n" + "These parameters are required to have a well-defined UVBeam object and \n" "are required for most kinds of beam files." ) out += "\n\n" diff --git a/docs/make_uvcal.py b/docs/make_uvcal.py index 9aee25a6e5..d2384f279b 100644 --- a/docs/make_uvcal.py +++ b/docs/make_uvcal.py @@ -9,11 +9,12 @@ from astropy.time import Time -from pyuvdata import UVCal +from pyuvdata import Telescope, UVCal def write_uvcal_rst(write_file=None): cal = UVCal() + cal.telescope = Telescope() out = "UVCal\n=====\n" out += ( "UVCal is the main user class for calibration solutions for interferometric\n" @@ -26,7 +27,9 @@ def write_uvcal_rst(write_file=None): "work with calibration solutions for interferometric data sets. Under the\n" "hood, the attributes are implemented as properties based on\n" ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" - "transparent to users.\n\n" + "transparent to users.\n" + "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" + "object, with its attributes available on the UVData object as properties.\n\n" "UVCal objects can be initialized as an empty object (as ``cal = UVCal()``).\n" "When an empty UVCal object is initialized, it has all of these attributes\n" "defined but set to ``None``. The attributes can be set by reading in a data\n" @@ -49,7 +52,7 @@ def write_uvcal_rst(write_file=None): ) out += "Required\n********\n" out += ( - "These parameters are required to have a sensible UVCal object and \n" + "These parameters are required to have a well-defined UVCal object and \n" "are required for most kinds of uv cal files." ) out += "\n\n" diff --git a/docs/make_uvdata.py b/docs/make_uvdata.py index 7424afb329..c12581076d 100644 --- a/docs/make_uvdata.py +++ b/docs/make_uvdata.py @@ -9,11 +9,12 @@ from astropy.time import Time -from pyuvdata import UVData +from pyuvdata import Telescope, UVData def write_uvdata_rst(write_file=None): UV = UVData() + UV.telescope = Telescope() out = "UVData\n======\n\n" out += ( "UVData is the main user class for intereferometric data (visibilities).\n" @@ -25,7 +26,9 @@ def write_uvdata_rst(write_file=None): "The attributes on UVData hold all of the metadata and data required to\n" "analyze interferometric data sets. Under the hood, the attributes are\n" "implemented as properties based on :class:`pyuvdata.parameter.UVParameter`\n" - "objects but this is fairly transparent to users.\n\n" + "objects but this is fairly transparent to users.\n" + "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" + "object, with its attributes available on the UVData object as properties.\n" "UVData objects can be initialized from a file using the\n" ":meth:`pyuvdata.UVData.from_file` class method\n" "(as ``uvd = UVData.from_file()``) or be initialized as an empty\n" @@ -45,14 +48,14 @@ def write_uvdata_rst(write_file=None): "Note that angle type attributes also have convenience properties named the\n" "same thing with ``_degrees`` appended through which you can get or set the\n" "value in degrees. Similarly location type attributes (which are given in\n" - "topocentric xyz coordinates) have convenience properties named the\n" + "geocentric xyz coordinates) have convenience properties named the\n" "same thing with ``_lat_lon_alt`` and ``_lat_lon_alt_degrees`` appended\n" "through which you can get or set the values using latitude, longitude and\n" "altitude values in radians or degrees and meters.\n\n" ) out += "Required\n********\n" out += ( - "These parameters are required to have a sensible UVData object and\n" + "These parameters are required to have a well-defined UVData object and\n" "are required for most kinds of interferometric data files." ) out += "\n\n" diff --git a/docs/make_uvflag.py b/docs/make_uvflag.py index 135151ee10..7ac785cef7 100644 --- a/docs/make_uvflag.py +++ b/docs/make_uvflag.py @@ -31,7 +31,9 @@ def write_uvflag_rst(write_file=None): "specify flagging and metric information for interferometric data sets.\n" "Under the hood, the attributes are implemented as properties based on\n" ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" - "transparent to users.\n\n" + "transparent to users.\n" + "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" + "object, with its attributes available on the UVData object as properties.\n\n" "UVFlag objects can be initialized from a file or a :class:`pyuvdata.UVData`\n" "or :class:`pyuvdata.UVCal` object\n" "(as ``flag = UVFlag()``). Some of these attributes\n" @@ -42,7 +44,7 @@ def write_uvflag_rst(write_file=None): ) out += "Required\n********\n" out += ( - "These parameters are required to have a sensible UVFlag object and \n" + "These parameters are required to have a well-defined UVFlag object and \n" "are required for most kinds of uv data files." ) out += "\n\n" diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index e82ceb0508..20d1cb4164 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -98,7 +98,10 @@ def __init__(self): self._telescope = uvp.UVParameter( "telescope", - description="Telescope object containing the telescope metadata.", + description=( + ":class:`pyuvdata.Telescope` object containing the telescope " + "metadata." + ), expected_type=Telescope, ) diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 953a7e371c..c8a6afe9c0 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -537,7 +537,10 @@ def __init__(self): self._telescope = uvp.UVParameter( "telescope", - description="Telescope object containing the telescope metadata.", + description=( + ":class:`pyuvdata.Telescope` object containing the telescope " + "metadata." + ), expected_type=Telescope, ) diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 8bcc6a040a..74a694e9f9 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -525,7 +525,10 @@ def __init__( # ---telescope information --- self._telescope = uvp.UVParameter( "telescope", - description="Telescope object containing the telescope metadata.", + description=( + ":class:`pyuvdata.Telescope` object containing the telescope " + "metadata." + ), expected_type=Telescope, ) From 64a0ca64b3b9ab8ed4720b676f4731353e065a31 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 29 Mar 2024 08:47:47 -0700 Subject: [PATCH 08/59] address some of the review comments --- docs/make_uvcal.py | 2 +- docs/make_uvflag.py | 2 +- pyuvdata/telescopes.py | 99 ++++++++++++++++--------------- pyuvdata/tests/test_telescopes.py | 12 ++-- pyuvdata/uvcal/initializers.py | 20 +------ pyuvdata/uvcal/uvcal.py | 14 ++++- pyuvdata/uvdata/fhd.py | 4 +- pyuvdata/uvdata/mir.py | 8 +-- pyuvdata/uvdata/mir_parser.py | 2 +- pyuvdata/uvdata/miriad.py | 4 +- pyuvdata/uvdata/ms.py | 4 +- pyuvdata/uvdata/mwa_corr_fits.py | 2 +- pyuvdata/uvdata/uvdata.py | 14 ++++- pyuvdata/uvflag/uvflag.py | 18 +++++- 14 files changed, 110 insertions(+), 95 deletions(-) diff --git a/docs/make_uvcal.py b/docs/make_uvcal.py index d2384f279b..a038a077b6 100644 --- a/docs/make_uvcal.py +++ b/docs/make_uvcal.py @@ -29,7 +29,7 @@ def write_uvcal_rst(write_file=None): ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" "transparent to users.\n" "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" - "object, with its attributes available on the UVData object as properties.\n\n" + "object, with its attributes available on the UVCal object as properties.\n\n" "UVCal objects can be initialized as an empty object (as ``cal = UVCal()``).\n" "When an empty UVCal object is initialized, it has all of these attributes\n" "defined but set to ``None``. The attributes can be set by reading in a data\n" diff --git a/docs/make_uvflag.py b/docs/make_uvflag.py index 7ac785cef7..1b50e53ff5 100644 --- a/docs/make_uvflag.py +++ b/docs/make_uvflag.py @@ -33,7 +33,7 @@ def write_uvflag_rst(write_file=None): ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" "transparent to users.\n" "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" - "object, with its attributes available on the UVData object as properties.\n\n" + "object, with its attributes available on the UVFlag object as properties.\n\n" "UVFlag objects can be initialized from a file or a :class:`pyuvdata.UVData`\n" "or :class:`pyuvdata.UVCal` object\n" "(as ``flag = UVFlag()``). Some of these attributes\n" diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index e453c07889..928afcf0bb 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -312,60 +312,44 @@ def location_obj(self, val): [val.x.to("m").value, val.y.to("m").value, val.z.to("m").value] ) - def _set_uvcal_requirements(self): - """Set the UVParameter required fields appropriately for UVCal.""" - self._name.required = True - self._location.required = True - self._instrument.required = False - self._Nants.required = True - self._antenna_names.required = True - self._antenna_numbers.required = True - self._antenna_positions.required = True - self._antenna_diameters.required = False - self._x_orientation.required = True - - def _set_uvdata_requirements(self): - """Set the UVParameter required fields appropriately for UVData.""" - self._name.required = True - self._location.required = True - self._instrument.required = True - self._Nants.required = True - self._antenna_names.required = True - self._antenna_numbers.required = True - self._antenna_positions.required = True - self._x_orientation.required = False - self._antenna_diameters.required = False - - def _set_uvflag_requirements(self): - """Set the UVParameter required fields appropriately for UVCal.""" - self._name.required = True - self._location.required = True - self._instrument.required = False - self._Nants.required = True - self._antenna_names.required = True - self._antenna_numbers.required = True - self._antenna_positions.required = True - self._antenna_diameters.required = False - self._x_orientation.required = False - def update_params_from_known_telescopes( self, *, - known_telescope_dict=KNOWN_TELESCOPES, - overwrite=False, - warn=True, - run_check=True, - check_extra=True, - run_check_acceptability=True, + overwrite: bool = False, + warn: bool = True, + run_check: bool = True, + check_extra: bool = True, + run_check_acceptability: bool = True, + known_telescope_dict: dict = KNOWN_TELESCOPES, ): """ - Set the parameters based on telescope in known_telescopes. + Update the parameters based on telescope in known_telescopes. + + This fills in any missing parameters (or to overwrite parameters that + have inaccurate values) on self that are available for a telescope from + either Astropy sites and/or from the KNOWN_TELESCOPES dict. This is + primarily used on UVData, UVCal and UVFlag to fill in information that + is missing, especially in older files. Parameters ---------- + overwrite : bool + If set, overwrite parameters with information from Astropy sites + and/or from the KNOWN_TELESCOPES dict. Defaults to False. + warn : bool + Option to issue a warning listing all modified parameters. + Defaults to True. + run_check : bool + Option to check for the existence and proper shapes of parameters + after updating. + check_extra : bool + Option to check optional parameters as well as required ones. + run_check_acceptability : bool + Option to check acceptable range of the values of parameters after + updating. known_telescope_dict: dict - telescope info dict. Default is KNOWN_TELESCOPES - (other values are only used for testing) + This should only be used for testing. This allows passing in a + different dict to use in place of the KNOWN_TELESCOPES dict. """ astropy_sites = EarthLocation.get_site_names() @@ -548,8 +532,14 @@ def update_params_from_known_telescopes( ) @classmethod - def get_telescope_from_known_telescopes( - cls, name: str, *, known_telescope_dict: dict = KNOWN_TELESCOPES + def from_known_telescopes( + cls, + name: str, + *, + run_check: bool = True, + check_extra: bool = True, + run_check_acceptability: bool = True, + known_telescope_dict: dict = KNOWN_TELESCOPES, ): """ Create a new Telescope object using information from known_telescopes. @@ -558,6 +548,15 @@ def get_telescope_from_known_telescopes( ---------- name : str Name of the telescope. + run_check : bool + Option to check for the existence and proper shapes of parameters. + check_extra : bool + Option to check optional parameters as well as required ones. + run_check_acceptability : bool + Option to check acceptable range of the values of parameters. + known_telescope_dict: dict + This should only be used for testing. This allows passing in a + different dict to use in place of the KNOWN_TELESCOPES dict. Returns ------- @@ -569,6 +568,10 @@ def get_telescope_from_known_telescopes( tel_obj = cls() tel_obj.name = name tel_obj.update_params_from_known_telescopes( - warn=False, known_telescope_dict=known_telescope_dict + warn=False, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + known_telescope_dict=known_telescope_dict, ) return tel_obj diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 8449e8bf9b..28831a7cf2 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -128,9 +128,9 @@ def test_known_telescopes(): assert sorted(pyuvdata.known_telescopes()) == sorted(expected_known_telescopes) -def test_get_telescope_from_known(): +def test_from_known(): for inst in pyuvdata.known_telescopes(): - telescope_obj = Telescope.get_telescope_from_known_telescopes(inst) + telescope_obj = Telescope.from_known_telescopes(inst) assert telescope_obj.name == inst @@ -153,7 +153,7 @@ def test_get_telescope_center_xyz(): "citation": "", }, } - telescope_obj = Telescope.get_telescope_from_known_telescopes( + telescope_obj = Telescope.from_known_telescopes( "test", known_telescope_dict=test_telescope_dict ) telescope_obj_ext = Telescope() @@ -164,7 +164,7 @@ def test_get_telescope_center_xyz(): assert telescope_obj == telescope_obj_ext telescope_obj_ext.name = "test2" - telescope_obj2 = Telescope.get_telescope_from_known_telescopes( + telescope_obj2 = Telescope.from_known_telescopes( "test2", known_telescope_dict=test_telescope_dict ) assert telescope_obj2 == telescope_obj_ext @@ -186,7 +186,7 @@ def test_get_telescope_no_loc(): "test. Either the center_xyz or the latitude, longitude and altitude of " "the telescope must be specified.", ): - Telescope.get_telescope_from_known_telescopes( + Telescope.from_known_telescopes( "test", known_telescope_dict=test_telescope_dict ) @@ -210,7 +210,7 @@ def test_hera_loc(): hera_file, read_data=False, file_type="uvh5", use_future_array_shapes=True ) - telescope_obj = Telescope.get_telescope_from_known_telescopes("HERA") + telescope_obj = Telescope.from_known_telescopes("HERA") assert np.allclose( telescope_obj.location, diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index a34b307b00..c8677cae63 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -294,11 +294,8 @@ def new_uvcal( # Now set all the metadata if telescope is not None: uvc.telescope = telescope - # set the appropriate telescope attributes as required - uvc.telescope._set_uvcal_requirements() else: new_telescope = Telescope() - new_telescope._set_uvcal_requirements() new_telescope.name = telescope_name new_telescope.location_obj = telescope_location @@ -309,6 +306,9 @@ def new_uvcal( uvc.telescope = new_telescope + # set the appropriate telescope attributes as required + uvc._set_telescope_requirements() + uvc.freq_array = freq_array if time_array is not None: @@ -506,7 +506,6 @@ def new_uvcal_from_uvdata( kwargs["spw_array"] = spw_array new_telescope = uvdata.telescope.copy() - new_telescope._set_uvcal_requirements() # Figure out how to mesh the antenna parameters given with those in the uvd if "antenna_positions" not in kwargs: @@ -588,19 +587,6 @@ def new_uvcal_from_uvdata( if "x_orientation" in kwargs: new_telescope.x_orientation = XORIENTMAP[kwargs.pop("x_orientation").lower()] - new_telescope.check() - for param in [ - "telescope_name", - "telescope_location", - "instrument", - "x_orientation", - "antenna_positions", - "antenna_numbers", - "antenna_names", - "antenna_diameters", - ]: - assert param not in kwargs, f"{param} still in kwargs, value: {kwargs[param]}" - ant_array = kwargs.pop( "ant_array", np.union1d(uvdata.ant_1_array, uvdata.ant_2_array) ) diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index 20d1cb4164..2502c0aff2 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -662,10 +662,22 @@ def __init__(self): self.telescope = Telescope() # set the appropriate telescope attributes as required - self.telescope._set_uvcal_requirements() + self._set_telescope_requirements() super(UVCal, self).__init__() + def _set_telescope_requirements(self): + """Set the UVParameter required fields appropriately for UVCal.""" + self.telescope._name.required = True + self.telescope._location.required = True + self.telescope._instrument.required = False + self.telescope._Nants.required = True + self.telescope._antenna_names.required = True + self.telescope._antenna_numbers.required = True + self.telescope._antenna_positions.required = True + self.telescope._antenna_diameters.required = False + self.telescope._x_orientation.required = True + @property def telescope_name(self): """The telescope name (stored on the Telescope object internally).""" diff --git a/pyuvdata/uvdata/fhd.py b/pyuvdata/uvdata/fhd.py index 65de5cd877..9d9c69fa67 100644 --- a/pyuvdata/uvdata/fhd.py +++ b/pyuvdata/uvdata/fhd.py @@ -285,9 +285,7 @@ def get_fhd_layout_info( # files for the MWA # compare with the known_telescopes values try: - telescope_obj = Telescope.get_telescope_from_known_telescopes( - telescope_name - ) + telescope_obj = Telescope.from_known_telescopes(telescope_name) except ValueError: telescope_obj = None # start warning message diff --git a/pyuvdata/uvdata/mir.py b/pyuvdata/uvdata/mir.py index bc030871c7..b1441e706e 100644 --- a/pyuvdata/uvdata/mir.py +++ b/pyuvdata/uvdata/mir.py @@ -60,9 +60,7 @@ def generate_sma_antpos_dict(filepath): # We need the antenna positions in ECEF, rather than the native rotECEF format that # they are stored in. Get the longitude info, and use the appropriate function in # utils to get these values the way that we want them. - _, lon, _ = Telescope.get_telescope_from_known_telescopes( - "SMA" - ).location_lat_lon_alt + _, lon, _ = Telescope.from_known_telescopes("SMA").location_lat_lon_alt mir_antpos["xyz_pos"] = uvutils.ECEF_from_rotECEF(mir_antpos["xyz_pos"], lon) # Create a dictionary that can be used for updates. @@ -545,9 +543,7 @@ def _init_from_mir_parser( ] # Get the coordinates from the entry in telescope.py - lat, lon, alt = Telescope.get_telescope_from_known_telescopes( - "SMA" - ).location_lat_lon_alt + lat, lon, alt = Telescope.from_known_telescopes("SMA").location_lat_lon_alt self.telescope_location_lat_lon_alt = (lat, lon, alt) # Calculate antenna positions in ECEF frame. Note that since both diff --git a/pyuvdata/uvdata/mir_parser.py b/pyuvdata/uvdata/mir_parser.py index 379568c8cf..8e72cd0bd4 100644 --- a/pyuvdata/uvdata/mir_parser.py +++ b/pyuvdata/uvdata/mir_parser.py @@ -4139,7 +4139,7 @@ def _make_v3_compliant(self): # if swarm_only: # self.select(where=("correlator", "eq", 1)) # Get SMA coordinates for various data-filling stuff - sma_lat, sma_lon, sma_alt = Telescope.get_telescope_from_known_telescopes( + sma_lat, sma_lon, sma_alt = Telescope.from_known_telescopes( "SMA" ).location_lat_lon_alt diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index f54c7a87d9..f9fa2280cf 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -297,9 +297,7 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): # get info from known telescopes. # Check to make sure the lat/lon values match reasonably well try: - telescope_obj = Telescope.get_telescope_from_known_telescopes( - self.telescope_name - ) + telescope_obj = Telescope.from_known_telescopes(self.telescope_name) except ValueError: telescope_obj = None if telescope_obj is not None: diff --git a/pyuvdata/uvdata/ms.py b/pyuvdata/uvdata/ms.py index 76f2d38a53..2ca5266221 100644 --- a/pyuvdata/uvdata/ms.py +++ b/pyuvdata/uvdata/ms.py @@ -969,9 +969,7 @@ def read_ms( and self.telescope_name in self.known_telescopes() ): # get it from known telescopes - telescope_obj = Telescope.get_telescope_from_known_telescopes( - self.telescope_name - ) + telescope_obj = Telescope.from_known_telescopes(self.telescope_name) warnings.warn( "Setting telescope_location to value in known_telescopes for " f"{self.telescope_name}." diff --git a/pyuvdata/uvdata/mwa_corr_fits.py b/pyuvdata/uvdata/mwa_corr_fits.py index 51627f1d50..3d580fd8bd 100644 --- a/pyuvdata/uvdata/mwa_corr_fits.py +++ b/pyuvdata/uvdata/mwa_corr_fits.py @@ -72,7 +72,7 @@ def read_metafits( antenna_positions[:, 1] = meta_tbl["North"][1::2] antenna_positions[:, 2] = meta_tbl["Height"][1::2] - mwa_telescope_obj = Telescope.get_telescope_from_known_telescopes("mwa") + mwa_telescope_obj = Telescope.from_known_telescopes("mwa") # convert antenna positions from enu to ecef # antenna positions are "relative to diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index c8a6afe9c0..254fa3dd43 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -686,10 +686,22 @@ def __init__(self): self.telescope = Telescope() # set the appropriate telescope attributes as required - self.telescope._set_uvdata_requirements() + self._set_telescope_requirements() super(UVData, self).__init__() + def _set_telescope_requirements(self): + """Set the UVParameter required fields appropriately for UVData.""" + self.telescope._name.required = True + self.telescope._location.required = True + self.telescope._instrument.required = True + self.telescope._Nants.required = True + self.telescope._antenna_names.required = True + self.telescope._antenna_numbers.required = True + self.telescope._antenna_positions.required = True + self.telescope._x_orientation.required = False + self.telescope._antenna_diameters.required = False + @property def telescope_name(self): """The telescope name (stored on the Telescope object internally).""" diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 74a694e9f9..d6c88f51cd 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -545,7 +545,7 @@ def __init__( self.telescope = Telescope() # set the appropriate telescope attributes as required - self.telescope._set_uvflag_requirements() + self._set_telescope_requirements() # initialize the underlying UVBase properties super(UVFlag, self).__init__() @@ -634,6 +634,18 @@ def __init__( "list, tuple, string, pathlib.Path, UVData, or UVCal." ) + def _set_telescope_requirements(self): + """Set the UVParameter required fields appropriately for UVCal.""" + self.telescope._name.required = True + self.telescope._location.required = True + self.telescope._instrument.required = False + self.telescope._Nants.required = True + self.telescope._antenna_names.required = True + self.telescope._antenna_numbers.required = True + self.telescope._antenna_positions.required = True + self.telescope._antenna_diameters.required = False + self.telescope._x_orientation.required = False + @property def telescope_name(self): """The telescope name (stored on the Telescope object internally).""" @@ -4108,7 +4120,7 @@ def from_uvdata( self.channel_width = np.full(self.Nfreqs, indata.channel_width) self.telescope = indata.telescope.copy() - self.telescope._set_uvflag_requirements() + self._set_telescope_requirements() self.Nspws = indata.Nspws self.spw_array = copy.deepcopy(indata.spw_array) @@ -4303,7 +4315,7 @@ def from_uvcal( self.channel_width = np.full(self.Nfreqs, indata.channel_width) self.telescope = indata.telescope.copy() - self.telescope._set_uvflag_requirements() + self._set_telescope_requirements() self.Nspws = indata.Nspws self.spw_array = copy.deepcopy(indata.spw_array) From f53fc586f9701520f78445c906b2e4713aa0bfa5 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 19 Apr 2024 14:43:46 -0700 Subject: [PATCH 09/59] Change telescope.location to an Earth/MoonLocation object uvdata updates only --- pyuvdata/hdf5_utils.py | 50 +- pyuvdata/ms_utils.py | 25 +- pyuvdata/parameter.py | 222 +++++--- pyuvdata/telescopes.py | 72 +-- pyuvdata/utils.py | 615 ++++++++++++--------- pyuvdata/uvbase.py | 37 ++ pyuvdata/uvdata/fhd.py | 64 ++- pyuvdata/uvdata/initializers.py | 28 +- pyuvdata/uvdata/mir.py | 25 +- pyuvdata/uvdata/mir_parser.py | 11 +- pyuvdata/uvdata/miriad.py | 242 ++++---- pyuvdata/uvdata/ms.py | 81 ++- pyuvdata/uvdata/mwa_corr_fits.py | 32 +- pyuvdata/uvdata/tests/test_initializers.py | 7 +- pyuvdata/uvdata/tests/test_mir.py | 22 +- pyuvdata/uvdata/tests/test_miriad.py | 135 +++-- pyuvdata/uvdata/tests/test_ms.py | 56 +- pyuvdata/uvdata/tests/test_uvdata.py | 302 +++++----- pyuvdata/uvdata/tests/test_uvfits.py | 48 +- pyuvdata/uvdata/tests/test_uvh5.py | 75 +-- pyuvdata/uvdata/uvdata.py | 288 +++------- pyuvdata/uvdata/uvfits.py | 166 +++--- pyuvdata/uvdata/uvh5.py | 88 +-- 23 files changed, 1425 insertions(+), 1266 deletions(-) diff --git a/pyuvdata/hdf5_utils.py b/pyuvdata/hdf5_utils.py index 687de2f2e3..f84860950a 100644 --- a/pyuvdata/hdf5_utils.py +++ b/pyuvdata/hdf5_utils.py @@ -11,6 +11,15 @@ import h5py import numpy as np +from astropy import units +from astropy.coordinates import EarthLocation + +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False from . import utils as uvutils @@ -245,8 +254,6 @@ def __del__(self): def __getstate__(self): """Get the state of the object.""" - print(self.__dict__.keys()) - print(self.__class__.__name__) return { k: v for k, v in self.__dict__.items() @@ -373,15 +380,6 @@ def antpos_enu(self) -> np.ndarray: ellipsoid=self.ellipsoid, ) - @cached_property - def telescope_location(self): - """The telescope location in ECEF coordinates, in meters.""" - return uvutils.XYZ_from_LatLonAlt( - *self.telescope_location_lat_lon_alt, - frame=self.telescope_frame, - ellipsoid=self.ellipsoid, - ) - @property def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: """The telescope location in latitude, longitude, and altitude, in degrees.""" @@ -420,6 +418,36 @@ def ellipsoid(self) -> str: else: return None + @cached_property + def telescope_location_obj(self): + """The telescope location object.""" + if self.telescope_frame == "itrs": + return EarthLocation.from_geodetic( + lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, + lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, + height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, + ) + else: + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frames." + ) + return MoonLocation.from_selenodetic( + lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, + lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, + height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, + ellipsoid=self.ellipsoid, + ) + + @cached_property + def telescope_location(self): + """The telescope location in ECEF coordinates, in meters.""" + return uvutils.XYZ_from_LatLonAlt( + *self.telescope_location_lat_lon_alt, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + @cached_property def extra_keywords(self) -> dict: """The extra_keywords from the file.""" diff --git a/pyuvdata/ms_utils.py b/pyuvdata/ms_utils.py index 63c7825817..843bc8dea3 100644 --- a/pyuvdata/ms_utils.py +++ b/pyuvdata/ms_utils.py @@ -368,9 +368,10 @@ def write_ms_antenna( Path to MS (without ANTENNA suffix). uvobj : UVBase (with matching parameters) Optional parameter, can be used to automatically fill the other required - keywords for this function. Note that the UVBase object must have parameters - that match by name to the other keywords required here (with the exception of - telescope_frame, which is pulled from the telescope_location UVParameter). + keywords for this function. Note that the UVBase object must have a telescope + parameter with parameters that match by name to the other keywords + required here (with the exception of telescope_frame and telescope_ellipsoid, + which are derived from the telescope.location UVParameter). antenna_numbers : ndarray Required if uvobj not provided, antenna numbers for all antennas of the telescope, dtype int and shape (Nants_telescope,). @@ -403,11 +404,11 @@ def write_ms_antenna( filepath += "::ANTENNA" if uvobj is not None: - antenna_numbers = uvobj.antenna_numbers - antenna_names = uvobj.antenna_names - antenna_positions = uvobj.antenna_positions - antenna_diameters = uvobj.antenna_diameters - telescope_location = uvobj.telescope_location + antenna_numbers = uvobj.telescope.antenna_numbers + antenna_names = uvobj.telescope.antenna_names + antenna_positions = uvobj.telescope.antenna_positions + antenna_diameters = uvobj.telescope.antenna_diameters + telescope_location = uvobj.telescope._location.xyz() telescope_frame = uvobj.telescope._location.frame telescope_ellipsoid = uvobj.telescope._location.ellipsoid @@ -1160,8 +1161,8 @@ def write_ms_observation( filepath += "::OBSERVATION" if uvobj is not None: - telescope_name = uvobj.telescope_name - telescope_location = uvobj.telescope_location + telescope_name = uvobj.telescope.name + telescope_location = uvobj.telescope._location.xyz() observer = telescope_name for key in uvobj.extra_keywords: if key.upper() == "OBSERVER": @@ -1402,7 +1403,7 @@ def write_ms_feed( filepath += "::FEED" if uvobj is not None: - antenna_numbers = uvobj.antenna_numbers + antenna_numbers = uvobj.telescope.antenna_numbers polarization_array = uvobj.polarization_array flex_spw_polarization_array = uvobj.flex_spw_polarization_array nspws = uvobj.Nspws @@ -1698,7 +1699,7 @@ def write_ms_pointing( filepath += "::POINTING" if uvobj is not None: - max_ant = np.max(uvobj.antenna_numbers) + max_ant = np.max(uvobj.telescope.antenna_numbers) integration_time = uvobj.integration_time time_array = uvobj.time_array diff --git a/pyuvdata/parameter.py b/pyuvdata/parameter.py index a95d7b60d8..1a5ca25a1e 100644 --- a/pyuvdata/parameter.py +++ b/pyuvdata/parameter.py @@ -18,9 +18,18 @@ import astropy.units as units import numpy as np -from astropy.coordinates import SkyCoord +from astropy.coordinates import EarthLocation, SkyCoord + +allowed_location_types = [EarthLocation] +try: + from lunarsky import MoonLocation + + allowed_location_types.append(MoonLocation) + + hasmoon = True +except ImportError: + hasmoon = False -from . import utils __all__ = ["UVParameter", "AngleParameter", "LocationParameter"] @@ -843,6 +852,8 @@ class LocationParameter(UVParameter): """ Subclass of UVParameter for location type parameters. + Supports either :class:`astropy.coordinates.EarthLocation` objects or + :class:`lunarsky.MoonLocation` objects. Adds extra methods for conversion to & from lat/lon/alt in radians or degrees (used by UVBase objects for _lat_lon_alt and _lat_lon_alt_degrees properties associated with these parameters). @@ -857,30 +868,12 @@ class LocationParameter(UVParameter): the class with this UVParameter as an attribute. Default is True. value The value of the data or metadata. - spoof_val - A fake value that can be assigned to a non-required UVParameter if the - metadata is required for a particular file-type. - This is not an attribute of required UVParameters. description : str Description of the data or metadata in the object. - frame : str, optional - Coordinate frame. Valid options are "itrs" (default) or "mcmf". - ellipsoid : str, optional - Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", - "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). - Default is "SPHERE". Only used if frame is "mcmf". - acceptable_vals : list, optional - List giving allowed values for elements of value. - acceptable_range: 2-tuple, optional - Tuple giving a range of allowed magnitudes for elements of value. tols : float or 2-tuple of float Tolerances for testing the equality of UVParameters. Either a single absolute value or a tuple of relative and absolute values to be used by np.isclose() - strict_type_check : bool - When True, the input expected_type is used exactly, otherwise a more - generic type is found to allow changes in precicions or to/from numpy - dtypes to not break checks. Attributes ---------- @@ -893,25 +886,17 @@ class LocationParameter(UVParameter): value The value of the data or metadata. spoof_val - A fake value that can be assigned to a non-required UVParameter if the - metadata is required for a particular file-type. - This is not an attribute of required UVParameters. + Always set to None. form : int - Always set to 3. + Always set to None. description : str Description of the data or metadata in the object. - frame : str, optional - Coordinate frame. Valid options are "itrs" (default) or "mcmf". - ellipsoid : str, optional - Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", - "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default - is "SPHERE". Only used if frame is "mcmf". expected_type - Always set to float. + Set to EarthLocation or MoonLocation acceptable_vals : list, optional - List giving allowed values for elements of value. + Always set to None. acceptable_range: 2-tuple, optional - Tuple giving a range of allowed magnitudes for elements of value. + Always set to None. tols : 2-tuple of float Relative and absolute tolerances for testing the equality of UVParameters, to be used by np.isclose() @@ -930,9 +915,6 @@ def __init__( value=None, spoof_val=None, description="", - frame="itrs", - ellipsoid=None, - acceptable_range=None, tols=1e-3, ): super(LocationParameter, self).__init__( @@ -940,33 +922,71 @@ def __init__( required=required, value=value, spoof_val=spoof_val, - form=3, description=description, - expected_type=float, - acceptable_range=acceptable_range, + expected_type=tuple(allowed_location_types), + strict_type_check=True, + acceptable_range=None, tols=tols, ) - self.frame = frame - if frame == "mcmf" and ellipsoid is None: - ellipsoid = "SPHERE" + @property + def frame(self): + """Get the frame.""" + if isinstance(self.value, EarthLocation): + return "itrs" + elif hasmoon and isinstance(self.value, MoonLocation): + return "mcmf" + + @property + def ellipsoid(self): + """Get the ellipsoid.""" + if isinstance(self.value, EarthLocation): + return None + elif hasmoon and isinstance(self.value, MoonLocation): + return self.value.ellipsoid - self.ellipsoid = ellipsoid + def xyz(self): + """Get the body centric coordinates in meters.""" + if self.value is None: + return None + else: + if hasmoon and isinstance(self.value, MoonLocation): + centric_coords = self.value.selenocentric + else: + centric_coords = self.value.geocentric + return units.Quantity(centric_coords).to("m").value + + def set_xyz(self, xyz, frame="itrs", ellipsoid=None): + """Set the body centric coordinates in meters.""" + allowed_frames = ["itrs"] + if hasmoon: + allowed_frames += ["mcmf"] + if frame not in allowed_frames: + raise ValueError(f"frame must be one of {allowed_frames}") + if xyz is None: + self.value = None + else: + if frame == "itrs": + self.value = EarthLocation.from_geocentric(*(xyz * units.m)) + else: + if ellipsoid is None and isinstance(self.value, MoonLocation): + ellipsoid = self.value.ellipsoid + moonloc = MoonLocation.from_selenocentric(*(xyz * units.m)) + if ellipsoid is not None: + moonloc.ellipsoid = ellipsoid + self.value = moonloc def lat_lon_alt(self): """Get value in (latitude, longitude, altitude) tuple in radians.""" if self.value is None: return None else: - # check defaults to False b/c exposed check kwarg exists in UVData - return utils.LatLonAlt_from_XYZ( - self.value, - check_acceptability=False, - frame=self.frame, - ellipsoid=self.ellipsoid, - ) + lat = self.value.lat.rad + lon = self.value.lon.rad + alt = self.value.height.to("m").value + return lat, lon, alt - def set_lat_lon_alt(self, lat_lon_alt): + def set_lat_lon_alt(self, lat_lon_alt, ellipsoid=None): """ Set value from (latitude, longitude, altitude) tuple in radians. @@ -975,27 +995,40 @@ def set_lat_lon_alt(self, lat_lon_alt): lat_lon_alt : 3-tuple of float Tuple with the latitude (radians), longitude (radians) and altitude (meters) to use to set the value attribute. + ellipsoid : str or None + Ellipsoid to use to convert between lat/lon/alt and xyz. Only used + for MoonLocation objects. """ if lat_lon_alt is None: self.value = None else: - self.value = utils.XYZ_from_LatLonAlt( - latitude=lat_lon_alt[0], - longitude=lat_lon_alt[1], - altitude=lat_lon_alt[2], - frame=self.frame, - ellipsoid=self.ellipsoid, - ) + if hasmoon and isinstance(self.value, MoonLocation): + if ellipsoid is None: + ellipsoid = self.value.ellipsoid + self.value = MoonLocation.from_selenodetic( + lon=lat_lon_alt[1] * units.rad, + lat=lat_lon_alt[0] * units.rad, + height=lat_lon_alt[2] * units.m, + ellipsoid=ellipsoid, + ) + else: + self.value = EarthLocation.from_geodetic( + lon=lat_lon_alt[1] * units.rad, + lat=lat_lon_alt[0] * units.rad, + height=lat_lon_alt[2] * units.m, + ) def lat_lon_alt_degrees(self): """Get value in (latitude, longitude, altitude) tuple in degrees.""" if self.value is None: return None else: - latitude, longitude, altitude = self.lat_lon_alt() - return latitude * 180.0 / np.pi, longitude * 180.0 / np.pi, altitude + lat = self.value.lat.deg + lon = self.value.lon.deg + alt = self.value.height.to("m").value + return lat, lon, alt - def set_lat_lon_alt_degrees(self, lat_lon_alt_degree): + def set_lat_lon_alt_degrees(self, lat_lon_alt_degree, ellipsoid=None): """ Set value from (latitude, longitude, altitude) tuple in degrees. @@ -1004,39 +1037,60 @@ def set_lat_lon_alt_degrees(self, lat_lon_alt_degree): lat_lon_alt : 3-tuple of float Tuple with the latitude (degrees), longitude (degrees) and altitude (meters) to use to set the value attribute. + ellipsoid : str or None + Ellipsoid to use to convert between lat/lon/alt and xyz. Only used + for MoonLocation objects. """ if lat_lon_alt_degree is None: self.value = None else: - latitude, longitude, altitude = lat_lon_alt_degree - self.value = utils.XYZ_from_LatLonAlt( - latitude=latitude * np.pi / 180.0, - longitude=longitude * np.pi / 180.0, - altitude=altitude, - frame=self.frame, - ellipsoid=self.ellipsoid, + lat_lon_alt = ( + lat_lon_alt_degree[0] * np.pi / 180.0, + lat_lon_alt_degree[1] * np.pi / 180.0, + lat_lon_alt_degree[2], ) + self.set_lat_lon_alt(lat_lon_alt, ellipsoid=ellipsoid) def check_acceptability(self): """Check that vector magnitudes are in range.""" - if self.frame not in utils._range_dict.keys(): - return False, f"Frame must be one of {utils._range_dict.keys()}" + if not isinstance(self.value, allowed_location_types): + return ( + False, + f"Location must be an object of type: {allowed_location_types}", + ) - if self.acceptable_range is None: - return True, "No acceptability check" - else: - # acceptable_range is a tuple giving a range of allowed vector magnitudes - testval = np.sqrt(np.sum(np.abs(self.value) ** 2)) - if (testval >= self.acceptable_range[0]) and ( - testval <= self.acceptable_range[1] - ): - return True, "Value is acceptable" - else: - message = ( - f"Value {testval}, is not in allowed range: {self.acceptable_range}" - ) - return False, message + def __eq__(self, other, *, silent=False): + """Handle equality properly for Earth/Moon Location objects.""" + if not isinstance(self.value, tuple(allowed_location_types)) or not isinstance( + other.value, tuple(allowed_location_types) + ): + return super(LocationParameter, self).__eq__(other, silent=silent) + + if self.value.shape != other.value.shape: + if not silent: + print(f"{self.name} parameter shapes are different") + return False + + if not isinstance(other.value, self.value.__class__): + if not silent: + print("Classes do not match") + return False + + if hasmoon and isinstance(self.value, MoonLocation): + if self.value.ellipsoid != other.value.ellipsoid: + if not silent: + print(f"{self.name} parameter ellipsoid is not the same. ") + return False + + if not np.allclose( + self.xyz(), other.xyz(), rtol=self.tols[0], atol=self.tols[1] + ): + if not silent: + print(f"{self.name} parameter is not close. ") + return False + + return True class SkyCoordParameter(UVParameter): diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 928afcf0bb..fd121ddfbe 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -7,6 +7,7 @@ import warnings import numpy as np +from astropy import units from astropy.coordinates import Angle, EarthLocation from pyuvdata.data import DATA_PATH @@ -128,19 +129,14 @@ def known_telescopes(): class Telescope(uvbase.UVBase): """ - A class for defining a telescope for use with UVData objects. + A class for telescope metadata, used on UVData, UVCal and UVFlag objects. Attributes ---------- - citation : str - text giving source of telescope information - telescope_name : UVParameter of str - name of the telescope - telescope_location : UVParameter of array_like - telescope location xyz coordinates in ITRF (earth-centered frame). - antenna_diameters : UVParameter of float - Optional, antenna diameters in meters. Used by CASA to construct a - default beam if no beam is supplied. + UVParameter objects : + For full list see the documentation on ReadTheDocs: + http://pyuvdata.readthedocs.io/en/latest/. + """ def __init__(self): @@ -153,9 +149,8 @@ def __init__(self): "name", description="name of telescope (string)", form="str" ) desc = ( - "telescope location: xyz in ITRF (earth-centered frame). " - "Can also be set using telescope_location_lat_lon_alt or " - "telescope_location_lat_lon_alt_degrees properties" + "telescope location: Either an astropy.EarthLocation oject or a " + "lunarsky MoonLocation object." ) self._location = uvp.LocationParameter("location", description=desc, tols=1e-3) @@ -277,41 +272,11 @@ def check(self, *, check_extra=True, run_check_acceptability=True): uvutils.check_surface_based_positions( antenna_positions=self.antenna_positions, telescope_loc=self.location, - telescope_frame=self._location.frame, raise_error=False, ) return True - @property - def location_obj(self): - """The location as an EarthLocation or MoonLocation object.""" - if self.location is None: - return None - - if self._location.frame == "itrs": - return EarthLocation.from_geocentric(*self.location, unit="m") - elif uvutils.hasmoon and self._location.frame == "mcmf": - moon_loc = uvutils.MoonLocation.from_selenocentric(*self.location, unit="m") - moon_loc.ellipsoid = self._location.ellipsoid - return moon_loc - - @location_obj.setter - def location_obj(self, val): - if isinstance(val, EarthLocation): - self._location.frame = "itrs" - elif uvutils.hasmoon and isinstance(val, uvutils.MoonLocation): - self._location.frame = "mcmf" - self._location.ellipsoid = val.ellipsoid - else: - raise ValueError( - "location_obj is not a recognized location object. Must be an " - "EarthLocation or MoonLocation object." - ) - self.location = np.array( - [val.x.to("m").value, val.y.to("m").value, val.z.to("m").value] - ) - def update_params_from_known_telescopes( self, *, @@ -352,6 +317,11 @@ def update_params_from_known_telescopes( different dict to use in place of the KNOWN_TELESCOPES dict. """ + if self.name is None: + raise ValueError( + "The telescope name attribute must be set to update from " + "known_telescopes." + ) astropy_sites = EarthLocation.get_site_names() telescope_keys = list(known_telescope_dict.keys()) telescope_list = [tel.lower() for tel in telescope_keys] @@ -364,9 +334,7 @@ def update_params_from_known_telescopes( tel_loc = EarthLocation.of_site(self.name) self.citation = "astropy sites" - self.location = np.array( - [tel_loc.x.value, tel_loc.y.value, tel_loc.z.value] - ) + self.location = tel_loc astropy_sites_list.append("telescope_location") elif self.name.lower() in telescope_list: @@ -376,7 +344,9 @@ def update_params_from_known_telescopes( known_telescope_list.append("telescope_location") if telescope_dict["center_xyz"] is not None: - self.location = telescope_dict["center_xyz"] + self.location = EarthLocation.from_geocentric( + telescope_dict["center_xyz"], unit="m" + ) else: if ( telescope_dict["latitude"] is None @@ -389,10 +359,10 @@ def update_params_from_known_telescopes( "or the latitude, longitude and altitude of the " "telescope must be specified." ) - self.location_lat_lon_alt = ( - telescope_dict["latitude"], - telescope_dict["longitude"], - telescope_dict["altitude"], + self.location = EarthLocation.from_geodetic( + lat=telescope_dict["latitude"] * units.rad, + lon=telescope_dict["longitude"] * units.rad, + height=telescope_dict["altitude"] * units.m, ) else: # no telescope matching this name diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index ca0b4ac369..50df2cdd97 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -130,6 +130,7 @@ "mcmf": (1717100.0, 1757100.0, "Moon"), } +allowed_location_types = [EarthLocation] if hasmoon: selenoids = { "SPHERE": _utils.Body.Moon_sphere, @@ -137,6 +138,7 @@ "GRAIL23": _utils.Body.Moon_grail23, "CE-1-LAM-GEO": _utils.Body.Moon_ce1lamgeo, } + allowed_location_types.append(MoonLocation) allowed_cat_types = ["sidereal", "ephem", "unprojected", "driftscan"] @@ -1824,7 +1826,16 @@ def ECEF_from_rotECEF(xyz, longitude): return rot_matrix.dot(xyz.T).T -def ENU_from_ECEF(xyz, *, latitude, longitude, altitude, frame="ITRS", ellipsoid=None): +def ENU_from_ECEF( + xyz, + *, + center_loc=None, + latitude=None, + longitude=None, + altitude=None, + frame="ITRS", + ellipsoid=None, +): """ Calculate local ENU (east, north, up) coordinates from ECEF coordinates. @@ -1832,19 +1843,26 @@ def ENU_from_ECEF(xyz, *, latitude, longitude, altitude, frame="ITRS", ellipsoid ---------- xyz : ndarray of float numpy array, shape (Npts, 3), with ECEF x,y,z coordinates. + center_loc : EarthLocation or MoonLocation object + An EarthLocation or MoonLocation object giving the center of the ENU + coordinates. Either `center_loc` or all of `latitude`, `longitude`, + `altitude` must be passed. latitude : float Latitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. longitude : float Longitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. altitude : float Altitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. frame : str - Coordinate frame of xyz. - Valid options are ITRS (default) or MCMF. + Coordinate frame of xyz and center of ENU coordinates. Valid options are + ITRS (default) or MCMF. Not used if `center_loc` is passed. ellipsoid : str Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default - is "SPHERE". Only used if frame is MCMF. + is "SPHERE". Only used if frame is MCMF. Not used if `center_loc` is passed. Returns ------- @@ -1852,9 +1870,31 @@ def ENU_from_ECEF(xyz, *, latitude, longitude, altitude, frame="ITRS", ellipsoid numpy array, shape (Npts, 3), with local ENU coordinates """ - frame = frame.upper() - if not hasmoon and frame == "MCMF": - raise ValueError("Need to install `lunarsky` package to work with MCMF frame.") + if center_loc is not None: + if not isinstance(center_loc, tuple(allowed_location_types)): + raise ValueError( + "center_loc is not a supported type. It must be one of " + f"{allowed_location_types}" + ) + latitude = center_loc.lat.rad + longitude = center_loc.lon.rad + altitude = center_loc.height.to("m").value + if isinstance(center_loc, EarthLocation): + frame = "ITRS" + else: + frame = "MCMF" + ellipsoid = center_loc.ellipsoid + else: + if latitude is None or longitude is None or altitude is None: + raise ValueError( + "Either center_loc or all of latitude, longitude and altitude " + "must be passed." + ) + frame = frame.upper() + if not hasmoon and frame == "MCMF": + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frame." + ) if frame == "ITRS": sensible_radius_range = (6.35e6, 6.39e6) @@ -1864,7 +1904,6 @@ def ENU_from_ECEF(xyz, *, latitude, longitude, altitude, frame="ITRS", ellipsoid sensible_radius_range = (1.71e6, 1.75e6) if ellipsoid is None: ellipsoid = "SPHERE" - else: raise ValueError(f'No ENU_from_ECEF transform defined for frame "{frame}".') @@ -1908,7 +1947,15 @@ def ENU_from_ECEF(xyz, *, latitude, longitude, altitude, frame="ITRS", ellipsoid return enu -def ECEF_from_ENU(enu, *, latitude, longitude, altitude, frame="ITRS", ellipsoid=None): +def ECEF_from_ENU( + enu, + center_loc=None, + latitude=None, + longitude=None, + altitude=None, + frame="ITRS", + ellipsoid=None, +): """ Calculate ECEF coordinates from local ENU (east, north, up) coordinates. @@ -1916,19 +1963,26 @@ def ECEF_from_ENU(enu, *, latitude, longitude, altitude, frame="ITRS", ellipsoid ---------- enu : ndarray of float numpy array, shape (Npts, 3), with local ENU coordinates. + center_loc : EarthLocation or MoonLocation object + An EarthLocation or MoonLocation object giving the center of the ENU + coordinates. Either `center_loc` or all of `latitude`, `longitude`, + `altitude` must be passed. latitude : float Latitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. longitude : float Longitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. altitude : float - Altitude of center of ENU coordinates in meters. + Altitude of center of ENU coordinates in radians. + Not used if `center_loc` is passed. frame : str - Coordinate frame of xyz. - Valid options are ITRS (default) or MCMF. + Coordinate frame of xyz and center of ENU coordinates. Valid options are + ITRS (default) or MCMF. Not used if `center_loc` is passed. ellipsoid : str Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default - is "SPHERE". Only used if frame is MCMF. + is "SPHERE". Only used if frame is MCMF. Not used if `center_loc` is passed. Returns ------- @@ -1936,16 +1990,37 @@ def ECEF_from_ENU(enu, *, latitude, longitude, altitude, frame="ITRS", ellipsoid numpy array, shape (Npts, 3), with ECEF x,y,z coordinates. """ - frame = frame.upper() - - if frame not in ["ITRS", "MCMF"]: - raise ValueError(f'No ECEF_from_ENU transform defined for frame "{frame}".') + if center_loc is not None: + if not isinstance(center_loc, tuple(allowed_location_types)): + raise ValueError( + "center_loc is not a supported type. It must be one of " + f"{allowed_location_types}" + ) + latitude = center_loc.lat.rad + longitude = center_loc.lon.rad + altitude = center_loc.height.to("m").value + if isinstance(center_loc, EarthLocation): + frame = "ITRS" + else: + frame = "MCMF" + ellipsoid = center_loc.ellipsoid + else: + if latitude is None or longitude is None or altitude is None: + raise ValueError( + "Either center_loc or all of latitude, longitude and altitude " + "must be passed." + ) + frame = frame.upper() + if not hasmoon and frame == "MCMF": + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frame." + ) - if not hasmoon and frame == "MCMF": - raise ValueError("Need to install `lunarsky` package to work with MCMF frame.") + if frame not in ["ITRS", "MCMF"]: + raise ValueError(f'No ECEF_from_ENU transform defined for frame "{frame}".') - if frame == "MCMF" and ellipsoid is None: - ellipsoid = "SPHERE" + if frame == "MCMF" and ellipsoid is None: + ellipsoid = "SPHERE" enu = np.asarray(enu) if enu.ndim > 1 and enu.shape[1] != 3: @@ -2892,8 +2967,8 @@ def transform_icrs_to_app( ICRS Dec of the celestial target, expressed in units of radians. Can either be a single float or array of shape (Ntimes,), although this must be consistent with other parameters (with the exception of telescope location parameters). - telescope_loc : array-like of floats or EarthLocation - ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center + telescope_loc : array-like of floats or EarthLocation or MoonLocation + ITRS latitude, longitude, and altitude (rel to sea-level) of the phase center of the array. Can either be provided as an astropy EarthLocation, or a tuple of shape (3,) containing (in order) the latitude, longitude, and altitude, in units of radians, radians, and meters, respectively. @@ -3682,11 +3757,11 @@ def lookup_jplhorizons( in the JPL-Horizons database. time_array : array-like of float Times in UTC Julian days to gather an ephemeris for. - telescope_loc : array-like of float - ITRF latitude, longitude, and altitude (rel to sea-level) of the observer. Must - be an array-like of shape (3,) containing the latitude, longitude, and - altitude, in that order, with units of radians, radians, and meters, - respectively. + telescope_loc : tuple of floats or EarthLocation + ITRS latitude, longitude, and altitude (rel to sea-level) of the observer. + Can either be provided as an EarthLocation object, or an + array-like of shape (3,) containing the latitude, longitude, and altitude, + in that order, with units of radians, radians, and meters, respectively. high_cadence : bool If set to True, will calculate ephemeris points every 3 minutes in time, as opposed to the default of every 3 hours. @@ -3994,6 +4069,232 @@ def interpolate_ephem( return (ra_vals, dec_vals, dist_vals, vel_vals) +def get_lst_for_time( + jd_array=None, + *, + telescope_loc=None, + latitude=None, + longitude=None, + altitude=None, + astrometry_library=None, + frame="itrs", + ellipsoid=None, +): + """ + Get the local apparent sidereal time for a set of jd times at an earth location. + + This function calculates the local apparent sidereal time (LAST), given a UTC time + and a position on the Earth, using either the astropy or NOVAS libraries. It + is important to note that there is an apporoximate 20 microsecond difference + between the two methods, presumably due to small differences in the apparent + reference frame. These differences will cancel out when calculating coordinates + in the TOPO frame, so long as apparent coordinates are calculated using the + same library (i.e., astropy or NOVAS). Failing to do so can introduce errors + up to ~1 mas in the horizontal coordinate system (i.e., AltAz). + + Parameters + ---------- + jd_array : ndarray of float + JD times to get lsts for. + telescope_loc : tuple or EarthLocation or MoonLocation + Alternative way of specifying telescope lat/lon/alt, either as a 3-element tuple + or as an astropy EarthLocation (or lunarsky MoonLocation). Cannot supply both + `telescope_loc` and `latitute`, `longitude`, or `altitude`. + latitude : float + Latitude of location to get lst for in degrees. Cannot specify both `latitute` + and `telescope_loc`. + longitude : float + Longitude of location to get lst for in degrees. Cannot specify both `longitude` + and `telescope_loc`. + altitude : float + Altitude of location to get lst for in meters. Cannot specify both `altitude` + and `telescope_loc`. + astrometry_library : str + Library used for running the LST calculations. Allowed options are 'erfa' + (which uses the pyERFA), 'novas' (which uses the python-novas library), + and 'astropy' (which uses the astropy utilities). Default is erfa unless + the telescope_location is a MoonLocation object, in which case the default is + astropy. + frame : str + Reference frame for latitude/longitude/altitude. Options are itrs (default) + or mcmf. Not used if telescope_loc is an EarthLocation or MoonLocation object. + ellipsoid : str + Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", + "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default + is "SPHERE". Only used if frame is mcmf. Not used if telescope_loc is + an EarthLocation or MoonLocation object. + + Returns + ------- + ndarray of float + LASTs in radians corresponding to the jd_array. + + """ + site_loc = None + if telescope_loc is not None: + if not all(item is None for item in [latitude, longitude, altitude]): + raise ValueError( + "Cannot set both telescope_loc and latitude/longitude/altitude" + ) + if isinstance(telescope_loc, EarthLocation) or ( + hasmoon and isinstance(telescope_loc, MoonLocation) + ): + site_loc = telescope_loc + if isinstance(telescope_loc, EarthLocation): + frame = "ITRS" + else: + frame = "MCMF" + else: + latitude, longitude, altitude = telescope_loc + + if site_loc is None: + if frame.upper() == "MCMF": + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frame." + ) + if ellipsoid is None: + ellipsoid = "SPHERE" + + site_loc = MoonLocation.from_selenodetic( + Angle(longitude, unit="deg"), + Angle(latitude, unit="deg"), + altitude, + ellipsoid=ellipsoid, + ) + else: + site_loc = EarthLocation.from_geodetic( + Angle(longitude, unit="deg"), + Angle(latitude, unit="deg"), + height=altitude, + ) + if astrometry_library is None: + if frame == "itrs": + astrometry_library = "erfa" + else: + astrometry_library = "astropy" + + if astrometry_library not in ["erfa", "astropy", "novas"]: + raise ValueError( + "Requested coordinate transformation library is not supported, please " + "select either 'erfa' or 'astropy' for astrometry_library." + ) + + if isinstance(jd_array, np.ndarray): + lst_array = np.zeros_like(jd_array) + if lst_array.ndim == 0: + lst_array = lst_array.reshape(1) + else: + lst_array = np.zeros(1) + + jd, reverse_inds = np.unique(jd_array, return_inverse=True) + + if isinstance(site_loc, EarthLocation): + TimeClass = Time + else: + if not astrometry_library == "astropy": + raise NotImplementedError( + "The MCMF frame is only supported with the 'astropy' astrometry library" + ) + TimeClass = LTime + + times = TimeClass(jd, format="jd", scale="utc", location=site_loc) + + if iers.conf.auto_max_age is None: # pragma: no cover + delta, status = times.get_delta_ut1_utc(return_status=True) + if np.any( + np.isin(status, (iers.TIME_BEFORE_IERS_RANGE, iers.TIME_BEYOND_IERS_RANGE)) + ): + warnings.warn( + "time is out of IERS range, setting delta ut1 utc to extrapolated value" + ) + times.delta_ut1_utc = delta + if astrometry_library == "erfa": + # This appears to be what astropy is using under the hood, + # so it _should_ be totally consistent. + gast_array = erfa.gst06a( + times.ut1.jd1, times.ut1.jd2, times.tt.jd1, times.tt.jd2 + ) + + # Technically one should correct for the polar wobble here, but the differences + # along the equitorial are miniscule -- of order 10s of nanoradians, well below + # the promised accuracy of IERS -- and rotation matricies can be expensive. + # We do want to correct though for for secular polar drift (s'/TIO locator), + # which nudges the Earth rotation angle of order 47 uas per century. + sp = erfa.sp00(times.tt.jd1, times.tt.jd2) + + lst_array = np.mod(gast_array + sp + site_loc.lon.rad, 2.0 * np.pi)[ + reverse_inds + ] + elif astrometry_library == "astropy": + lst_array = times.sidereal_time("apparent").radian + if lst_array.ndim == 0: + lst_array = lst_array.reshape(1) + lst_array = lst_array[reverse_inds] + elif astrometry_library == "novas": + # Import the NOVAS library only if it's needed/available. + try: + import novas_de405 # noqa + from novas import compat as novas + from novas.compat import eph_manager + except ImportError as e: # pragma: no cover + raise ImportError( + "novas and/or novas_de405 are not installed but is required for " + "NOVAS functionality" + ) from e + + jd_start, jd_end, number = eph_manager.ephem_open() + + tt_time_array = times.tt.value + ut1_high_time_array = times.ut1.jd1 + ut1_low_time_array = times.ut1.jd2 + full_ut1_time_array = ut1_high_time_array + ut1_low_time_array + polar_motion_data = iers.earth_orientation_table.get() + + delta_x_array = np.interp( + times.mjd, + polar_motion_data["MJD"].value, + polar_motion_data["dX_2000A_B"].value, + left=0.0, + right=0.0, + ) + + delta_y_array = np.interp( + times.mjd, + polar_motion_data["MJD"].value, + polar_motion_data["dY_2000A_B"].value, + left=0.0, + right=0.0, + ) + + # Catch the case where we don't have CIP delta values yet (they don't typically + # have predictive values like the polar motion does) + delta_x_array[np.isnan(delta_x_array)] = 0.0 + delta_y_array[np.isnan(delta_y_array)] = 0.0 + + for idx in range(len(times)): + novas.cel_pole( + tt_time_array[idx], 2, delta_x_array[idx], delta_y_array[idx] + ) + # The NOVAS routine will return Greenwich Apparent Sidereal Time (GAST), + # in units of hours + lst_array[reverse_inds == idx] = novas.sidereal_time( + ut1_high_time_array[idx], + ut1_low_time_array[idx], + (tt_time_array[idx] - full_ut1_time_array[idx]) * 86400.0, + ) + + # Add the telescope lon to convert from GAST to LAST (local) + lst_array = np.mod(lst_array + (longitude / 15.0), 24.0) + + # Convert from hours back to rad + lst_array *= np.pi / 12.0 + + lst_array = np.reshape(lst_array, jd_array.shape) + + return lst_array + + def calc_app_coords( *, lon_coord, @@ -4113,11 +4414,6 @@ def calc_app_coords( height=telescope_loc[2], ) - if isinstance(site_loc, EarthLocation): - frame = "itrs" - else: - frame = "mcmf" - # Time objects and unique don't seem to play well together, so we break apart # their handling here if isinstance(time_array, Time): @@ -4127,14 +4423,7 @@ def calc_app_coords( if coord_type in ["driftscan", "unprojected"]: if lst_array is None: - unique_lst = get_lst_for_time( - unique_time_array, - latitude=site_loc.lat.deg, - longitude=site_loc.lon.deg, - altitude=site_loc.height.to_value("m"), - frame=frame, - ellipsoid=ellipsoid, - ) + unique_lst = get_lst_for_time(unique_time_array, telescope_loc=site_loc) else: unique_lst = lst_array[unique_mask] @@ -4157,7 +4446,6 @@ def calc_app_coords( ra=icrs_ra, dec=icrs_dec, telescope_loc=site_loc, - ellipsoid=ellipsoid, pm_ra=pm_ra, pm_dec=pm_dec, vrad=vrad, @@ -4200,7 +4488,6 @@ def calc_app_coords( ra=icrs_ra, dec=icrs_dec, telescope_loc=site_loc, - ellipsoid=ellipsoid, pm_ra=pm_ra, pm_dec=pm_dec, ) @@ -4323,238 +4610,17 @@ def calc_sidereal_coords( return ref_ra, ref_dec -def get_lst_for_time( - jd_array=None, - *, - latitude=None, - longitude=None, - altitude=None, - astrometry_library=None, - frame="itrs", - ellipsoid=None, - telescope_loc=None, -): - """ - Get the local apparent sidereal time for a set of jd times at an earth location. - - This function calculates the local apparent sidereal time (LAST), given a UTC time - and a position on the Earth, using either the astropy or NOVAS libraries. It - is important to note that there is an apporoximate 20 microsecond difference - between the two methods, presumably due to small differences in the apparent - reference frame. These differences will cancel out when calculating coordinates - in the TOPO frame, so long as apparent coordinates are calculated using the - same library (i.e., astropy or NOVAS). Failing to do so can introduce errors - up to ~1 mas in the horizontal coordinate system (i.e., AltAz). - - Parameters - ---------- - jd_array : ndarray of float - JD times to get lsts for. - latitude : float - Latitude of location to get lst for in degrees. Cannot specify both `latitute` - and `telescope_loc`. - longitude : float - Longitude of location to get lst for in degrees. Cannot specify both `longitude` - and `telescope_loc`. - altitude : float - Altitude of location to get lst for in meters. Cannot specify both `altitude` - and `telescope_loc`. - astrometry_library : str - Library used for running the LST calculations. Allowed options are 'erfa' - (which uses the pyERFA), 'novas' (which uses the python-novas library), - and 'astropy' (which uses the astropy utilities). Default is erfa unless - the telescope_location is a MoonLocation object, in which case the default is - astropy. - frame : str - Reference frame for latitude/longitude/altitude. - Options are itrs (default) or mcmf. - ellipsoid : str - Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", - "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default - is "SPHERE". Only used if frame is mcmf. - telescope_loc : tuple or EarthLocation or MoonLocation - Alternative way of specifying telescope lat/lon/alt, either as a 3-element tuple - or as an astropy EarthLocation (or lunarsky MoonLocation). Cannot supply both - `telescope_loc` and `latitute`, `longitude`, or `altitude`. - - Returns - ------- - ndarray of float - LASTs in radians corresponding to the jd_array. - - """ - if astrometry_library is None: - if frame == "itrs": - astrometry_library = "erfa" - else: - astrometry_library = "astropy" - - if astrometry_library not in ["erfa", "astropy", "novas"]: - raise ValueError( - "Requested coordinate transformation library is not supported, please " - "select either 'erfa' or 'astropy' for astrometry_library." - ) - - if isinstance(jd_array, np.ndarray): - lst_array = np.zeros_like(jd_array) - if lst_array.ndim == 0: - lst_array = lst_array.reshape(1) - else: - lst_array = np.zeros(1) - - jd, reverse_inds = np.unique(jd_array, return_inverse=True) - - site_loc = None - if telescope_loc is not None: - if not all(item is None for item in [latitude, longitude, altitude]): - raise ValueError( - "Cannot set both telescope_loc and latitude/longitude/altitude" - ) - if isinstance(telescope_loc, EarthLocation) or ( - hasmoon and isinstance(telescope_loc, MoonLocation) - ): - site_loc = telescope_loc - else: - latitude, longitude, altitude = telescope_loc - - if site_loc is None: - if frame.upper() == "MCMF": - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with MCMF frame." - ) - if ellipsoid is None: - ellipsoid = "SPHERE" - - site_loc = MoonLocation.from_selenodetic( - Angle(longitude, unit="deg"), - Angle(latitude, unit="deg"), - altitude, - ellipsoid=ellipsoid, - ) - else: - site_loc = EarthLocation.from_geodetic( - Angle(longitude, unit="deg"), - Angle(latitude, unit="deg"), - height=altitude, - ) - - if isinstance(site_loc, EarthLocation): - TimeClass = Time - else: - if not astrometry_library == "astropy": - raise NotImplementedError( - "The MCMF frame is only supported with the 'astropy' astrometry library" - ) - TimeClass = LTime - - times = TimeClass(jd, format="jd", scale="utc", location=site_loc) - - if iers.conf.auto_max_age is None: # pragma: no cover - delta, status = times.get_delta_ut1_utc(return_status=True) - if np.any( - np.isin(status, (iers.TIME_BEFORE_IERS_RANGE, iers.TIME_BEYOND_IERS_RANGE)) - ): - warnings.warn( - "time is out of IERS range, setting delta ut1 utc to extrapolated value" - ) - times.delta_ut1_utc = delta - if astrometry_library == "erfa": - # This appears to be what astropy is using under the hood, - # so it _should_ be totally consistent. - gast_array = erfa.gst06a( - times.ut1.jd1, times.ut1.jd2, times.tt.jd1, times.tt.jd2 - ) - - # Technically one should correct for the polar wobble here, but the differences - # along the equitorial are miniscule -- of order 10s of nanoradians, well below - # the promised accuracy of IERS -- and rotation matricies can be expensive. - # We do want to correct though for for secular polar drift (s'/TIO locator), - # which nudges the Earth rotation angle of order 47 uas per century. - sp = erfa.sp00(times.tt.jd1, times.tt.jd2) - - lst_array = np.mod(gast_array + sp + site_loc.lon.rad, 2.0 * np.pi)[ - reverse_inds - ] - elif astrometry_library == "astropy": - lst_array = times.sidereal_time("apparent").radian - if lst_array.ndim == 0: - lst_array = lst_array.reshape(1) - lst_array = lst_array[reverse_inds] - elif astrometry_library == "novas": - # Import the NOVAS library only if it's needed/available. - try: - import novas_de405 # noqa - from novas import compat as novas - from novas.compat import eph_manager - except ImportError as e: # pragma: no cover - raise ImportError( - "novas and/or novas_de405 are not installed but is required for " - "NOVAS functionality" - ) from e - - jd_start, jd_end, number = eph_manager.ephem_open() - - tt_time_array = times.tt.value - ut1_high_time_array = times.ut1.jd1 - ut1_low_time_array = times.ut1.jd2 - full_ut1_time_array = ut1_high_time_array + ut1_low_time_array - polar_motion_data = iers.earth_orientation_table.get() - - delta_x_array = np.interp( - times.mjd, - polar_motion_data["MJD"].value, - polar_motion_data["dX_2000A_B"].value, - left=0.0, - right=0.0, - ) - - delta_y_array = np.interp( - times.mjd, - polar_motion_data["MJD"].value, - polar_motion_data["dY_2000A_B"].value, - left=0.0, - right=0.0, - ) - - # Catch the case where we don't have CIP delta values yet (they don't typically - # have predictive values like the polar motion does) - delta_x_array[np.isnan(delta_x_array)] = 0.0 - delta_y_array[np.isnan(delta_y_array)] = 0.0 - - for idx in range(len(times)): - novas.cel_pole( - tt_time_array[idx], 2, delta_x_array[idx], delta_y_array[idx] - ) - # The NOVAS routine will return Greenwich Apparent Sidereal Time (GAST), - # in units of hours - lst_array[reverse_inds == idx] = novas.sidereal_time( - ut1_high_time_array[idx], - ut1_low_time_array[idx], - (tt_time_array[idx] - full_ut1_time_array[idx]) * 86400.0, - ) - - # Add the telescope lon to convert from GAST to LAST (local) - lst_array = np.mod(lst_array + (longitude / 15.0), 24.0) - - # Convert from hours back to rad - lst_array *= np.pi / 12.0 - - lst_array = np.reshape(lst_array, jd_array.shape) - - return lst_array - - def check_lsts_against_times( *, jd_array, lst_array, - latitude, - longitude, - altitude, lst_tols, + latitude=None, + longitude=None, + altitude=None, frame="itrs", ellipsoid=None, + telescope_loc=None, ): """ Check that LSTs are consistent with the time_array and telescope location. @@ -4584,6 +4650,10 @@ def check_lsts_against_times( Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default is "SPHERE". Only used if frame is mcmf. + telescope_loc : tuple or EarthLocation or MoonLocation + Alternative way of specifying telescope lat/lon/alt, either as a 3-element tuple + or as an astropy EarthLocation (or lunarsky MoonLocation). Cannot supply both + `telescope_loc` and `latitute`, `longitude`, or `altitude`. Returns ------- @@ -4601,6 +4671,7 @@ def check_lsts_against_times( # to better than our standard lst tolerances. lsts = get_lst_for_time( jd_array=jd_array, + telescope_loc=telescope_loc, latitude=latitude, longitude=longitude, altitude=altitude, @@ -4669,6 +4740,10 @@ def check_surface_based_positions( telescope_loc.y.to("m").value, telescope_loc.z.to("m").value, ) + if isinstance(telescope_loc, EarthLocation): + telescope_frame = "itrs" + else: + telescope_frame = "mcmf" elif telescope_loc is not None: antenna_positions = antenna_positions + telescope_loc @@ -4860,9 +4935,7 @@ def uvw_track_generator( time_array = np.repeat(time_array, Nbase) - lst_array = get_lst_for_time( - jd_array=time_array, telescope_loc=site_loc, frame=telescope_frame - ) + lst_array = get_lst_for_time(jd_array=time_array, telescope_loc=site_loc) app_ra, app_dec = calc_app_coords( lon_coord=lon_coord, lat_coord=lat_coord, diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index 2e02d5d2a0..a60d126a1c 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -20,6 +20,21 @@ __all__ = ["UVBase"] +# the old names of attributes as keys, values are the names on the telescope object +old_telescope_metadata_attrs = { + "telescope_name": "name", + "telescope_location": None, + "telescope_location_lat_lon_alt": None, + "telescope_location_lat_lon_alt_degrees": None, + "instrument": "instrument", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + "x_orientation": "x_orientation", + "antenna_diameters": "antenna_diameters", +} + def _warning(msg, *a, **kwargs): """ @@ -126,6 +141,28 @@ def __setstate__(self, state): self.__dict__ = state self._setup_parameters() + def __getattribute__(self, __name): + """Handle old names for telescope metadata.""" + if __name in old_telescope_metadata_attrs: + # _warn_old_phase_attr(__name) + + if hasattr(self, "telescope"): + tel_name = old_telescope_metadata_attrs[__name] + if tel_name is not None: + # if it's a simple remapping, just return the value + ret_val = getattr(self.telescope, tel_name) + else: + # handle location related stuff + if __name == "telescope_location": + ret_val = self.telescope._location.xyz() + elif __name == "telescope_location_lat_lon_alt": + ret_val = self.telescope.location_lat_lon_alt() + elif __name == "telescope_location_lat_lon_alt_degrees": + ret_val = self.telescope.location_lat_lon_alt_degrees() + return ret_val + + return super().__getattribute__(__name) + def prop_fget(self, param_name): """ Getter method for UVParameter properties. diff --git a/pyuvdata/uvdata/fhd.py b/pyuvdata/uvdata/fhd.py index 9d9c69fa67..90588dcea8 100644 --- a/pyuvdata/uvdata/fhd.py +++ b/pyuvdata/uvdata/fhd.py @@ -10,6 +10,8 @@ import numpy as np from astropy import constants as const +from astropy import units +from astropy.coordinates import EarthLocation from docstring_parser import DocstringStyle from scipy.io import readsav @@ -278,7 +280,7 @@ def get_fhd_layout_info( if _xyz_close(location_latlonalt, arr_center, loc_tols) or _latlonalt_close( (latitude, longitude, altitude), latlonalt_arr_center, radian_tol, loc_tols ): - telescope_location = arr_center + telescope_location = EarthLocation.from_geocentric(arr_center, unit="m") else: # values do not agree with each other to within the tolerances. # this is a known issue with FHD runs on cotter uvfits @@ -302,11 +304,17 @@ def get_fhd_layout_info( " Telescope is not in known_telescopes. " "Defaulting to using the obs derived values." ) - telescope_location = location_latlonalt + telescope_location = EarthLocation.from_geocentric( + *location_latlonalt, unit="m" + ) # issue warning warnings.warn(message) else: - telescope_location = uvutils.XYZ_from_LatLonAlt(latitude, longitude, altitude) + telescope_location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=altitude * units.m, + ) # The FHD positions derive directly from uvfits, so they are in the rotated # ECEF frame and must be converted to ECEF @@ -362,7 +370,7 @@ def get_fhd_layout_info( layout_fields.remove("time_system") if "diameters" in layout_fields: - diameters = np.asarray(layout["diameters"]) + diameters = np.asarray(layout["diameters"][0]) layout_fields.remove("diameters") else: diameters = None @@ -606,7 +614,7 @@ def read_fhd( np.ones_like(self.time_array, dtype=np.float64) * time_res[0] ) # # --- observation information --- - self.telescope_name = obs["INSTRUMENT"][0].decode("utf8") + self.telescope.name = obs["INSTRUMENT"][0].decode("utf8") # This is a bit of a kludge because nothing like a phase center name exists # in FHD files. @@ -620,13 +628,13 @@ def read_fhd( + str(obs["ORIG_PHASEDEC"][0]) ) # For the MWA, this can sometimes be converted to EoR fields - if self.telescope_name.lower() == "mwa": + if self.telescope.name.lower() == "mwa": if np.isclose(obs["ORIG_PHASERA"][0], 0) and np.isclose( obs["ORIG_PHASEDEC"][0], -27 ): cat_name = "EoR 0 Field" - self.instrument = self.telescope_name + self.telescope.instrument = self.telescope.name latitude = np.deg2rad(float(obs["LAT"][0])) longitude = np.deg2rad(float(obs["LON"][0])) altitude = float(obs["ALT"][0]) @@ -639,14 +647,14 @@ def read_fhd( obs_tile_names = [ ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist() ] - if self.telescope_name.lower() == "mwa" and obs_tile_names[0][0] != "T": + if self.telescope.name.lower() == "mwa" and obs_tile_names[0][0] != "T": obs_tile_names = [ "Tile" + "0" * (3 - len(ant.strip())) + ant.strip() for ant in obs_tile_names ] layout_param_dict = get_fhd_layout_info( layout_file=layout_file, - telescope_name=self.telescope_name, + telescope_name=self.telescope.name, latitude=latitude, longitude=longitude, altitude=altitude, @@ -656,24 +664,42 @@ def read_fhd( run_check_acceptability=True, ) + telescope_attrs = { + "telescope_location": "location", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + "diameters": "antenna_diameters", + } + for key, value in layout_param_dict.items(): - setattr(self, key, value) + if key in telescope_attrs: + setattr(self.telescope, telescope_attrs[key], value) + else: + setattr(self, key, value) else: - self.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=altitude * units.m, + ) # we don't have layout info, so go ahead and set the antenna_names, # antenna_numbers and Nants_telescope from the baseline info struct. - self.antenna_names = [ + self.telescope.antenna_names = [ ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist() ] - self.antenna_numbers = np.array([int(ant) for ant in self.antenna_names]) - if self.telescope_name.lower() == "mwa": - self.antenna_names = [ + self.telescope.antenna_numbers = np.array( + [int(ant) for ant in self.telescope.antenna_names] + ) + if self.telescope.name.lower() == "mwa": + self.telescope.antenna_names = [ "Tile" + "0" * (3 - len(ant.strip())) + ant.strip() - for ant in self.antenna_names + for ant in self.telescope.antenna_names ] - self.Nants_telescope = len(self.antenna_names) + self.telescope.Nants = len(self.telescope.antenna_names) # need to make sure telescope location is defined properly before this call proc = self.set_lsts_from_time_array( @@ -702,8 +728,8 @@ def read_fhd( # to get 0-indexed arrays ind_1_array = bl_info["TILE_A"][0] - 1 ind_2_array = bl_info["TILE_B"][0] - 1 - self.ant_1_array = self.antenna_numbers[ind_1_array] - self.ant_2_array = self.antenna_numbers[ind_2_array] + self.ant_1_array = self.telescope.antenna_numbers[ind_1_array] + self.ant_2_array = self.telescope.antenna_numbers[ind_2_array] self.Nants_data = int(np.union1d(self.ant_1_array, self.ant_2_array).size) self.baseline_array = self.antnums_to_baseline( diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index b8ed979c58..5d63c58ece 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -385,7 +385,6 @@ def new_uvdata( phase_center_id_array: np.ndarray | None = None, x_orientation: Literal["east", "north", "e", "n", "ew", "ns"] | None = None, astrometry_library: str | None = None, - ellipsoid: Literal["SPHERE", "GSFC", "GRAIL23", "CE-1-LAM-GEO"] | None = None, **kwargs, ): """Initialize a new UVData object from keyword arguments. @@ -501,10 +500,6 @@ def new_uvdata( (which uses the astropy utilities). Default is erfa unless the telescope_location frame is MCMF (on the moon), in which case the default is astropy. - ellipsoid : str - Ellipsoid to use for lunar coordinates. Must be one of "SPHERE", - "GSFC", "GRAIL23", "CE-1-LAM-GEO" (see lunarsky package for details). Default - is "SPHERE". Only used if telescope_location is a MoonLocation. Other Parameters ---------------- @@ -527,8 +522,11 @@ def new_uvdata( antname_format=antname_format, ) - if hasmoon and isinstance(telescope_location, MoonLocation): - telescope_location.ellipsoid = ellipsoid + if not isinstance(telescope_location, tuple(utils.allowed_location_types)): + raise ValueError( + "telescope_location has an unsupported type, it must be one of " + f"{utils.allowed_location_types}" + ) lst_array, integration_time = get_time_params( telescope_location=telescope_location, @@ -598,22 +596,22 @@ def new_uvdata( obj.freq_array = freq_array obj.polarization_array = polarization_array - obj.antenna_positions = antenna_positions - obj.telescope.location_obj = telescope_location - obj.telescope_name = telescope_name + obj.telescope.antenna_positions = antenna_positions + obj.telescope.location = telescope_location + obj.telescope.name = telescope_name obj.baseline_array = baseline_array obj.ant_1_array = ant_1_array obj.ant_2_array = ant_2_array obj.time_array = time_array obj.lst_array = lst_array obj.channel_width = channel_width - obj.antenna_names = antenna_names - obj.antenna_numbers = antenna_numbers + obj.telescope.antenna_names = antenna_names + obj.telescope.antenna_numbers = antenna_numbers obj.history = history - obj.instrument = instrument + obj.telescope.instrument = instrument obj.vis_units = vis_units obj.Nants_data = len(set(np.concatenate([ant_1_array, ant_2_array]))) - obj.Nants_telescope = len(antenna_numbers) + obj.telescope.Nants = len(antenna_numbers) obj.Nbls = nbls obj.Nblts = len(baseline_array) obj.Nfreqs = len(freq_array) @@ -623,7 +621,7 @@ def new_uvdata( obj.spw_array = spw_array obj.flex_spw_id_array = flex_spw_id_array obj.integration_time = integration_time - obj.x_orientation = x_orientation + obj.telescope.x_orientation = x_orientation set_phase_params( obj, diff --git a/pyuvdata/uvdata/mir.py b/pyuvdata/uvdata/mir.py index b1441e706e..8b8e575047 100644 --- a/pyuvdata/uvdata/mir.py +++ b/pyuvdata/uvdata/mir.py @@ -525,35 +525,36 @@ def _init_from_mir_parser( np.concatenate((mir_data.bl_data["iant1"], mir_data.bl_data["iant2"])) ) ) - self.Nants_telescope = 8 + self.telescope.Nants = 8 self.Nbls = int(self.Nants_data * (self.Nants_data - 1) / 2) self.Nblts = Nblts self.Npols = Npols self.Ntimes = len(mir_data.in_data) - self.antenna_names = ["Ant%i" % idx for idx in range(1, 9)] + self.telescope.antenna_names = ["Ant%i" % idx for idx in range(1, 9)] - self.antenna_numbers = np.arange(1, 9) + self.telescope.antenna_numbers = np.arange(1, 9) # Prepare the XYZ coordinates of the antenna positions. - antXYZ = np.zeros([self.Nants_telescope, 3]) - for idx in range(self.Nants_telescope): + antXYZ = np.zeros([self.telescope.Nants, 3]) + for idx in range(self.telescope.Nants): if (idx + 1) in mir_data.antpos_data["antenna"]: antXYZ[idx] = mir_data.antpos_data["xyz_pos"][ mir_data.antpos_data["antenna"] == (idx + 1) ] # Get the coordinates from the entry in telescope.py - lat, lon, alt = Telescope.from_known_telescopes("SMA").location_lat_lon_alt - self.telescope_location_lat_lon_alt = (lat, lon, alt) + self.telescope.location = Telescope.from_known_telescopes("SMA").location # Calculate antenna positions in ECEF frame. Note that since both # coordinate systems are in relative units, no subtraction from # telescope geocentric position is required , i.e we are going from # relRotECEF -> relECEF - self.antenna_positions = uvutils.ECEF_from_rotECEF(antXYZ, lon) + self.telescope.antenna_positions = uvutils.ECEF_from_rotECEF( + antXYZ, self.telescope.location.lon.rad + ) self.history = "Raw Data" - self.instrument = "SWARM" + self.telescope.instrument = "SWARM" # Before proceeding, we want to check that information that's stored on a # per-spectral record basis (sphid) is consistent across a given baseline-time @@ -691,7 +692,7 @@ def _init_from_mir_parser( self.polarization_array = polarization_array self.flex_spw_polarization_array = flex_pol self.spw_array = np.array(spw_array, dtype=int) - self.telescope_name = "SMA" + self.telescope.name = "SMA" # Need to flip the sign convention here on uvw, since we use a1-a2 versus the # standard a2-a1 that uvdata expects @@ -717,7 +718,7 @@ def _init_from_mir_parser( time_array=time_arr, app_ra=mir_data.in_data["ara"][source_mask], app_dec=mir_data.in_data["adec"][source_mask], - telescope_loc=self.telescope_location_lat_lon_alt, + telescope_loc=self.telescope.location, ) self._add_phase_center( source_name, @@ -763,7 +764,7 @@ def _init_from_mir_parser( use_ant_pos=False, ) - self.antenna_diameters = np.zeros(self.Nants_telescope) + 6.0 + self.telescope.antenna_diameters = np.zeros(self.telescope.Nants) + 6.0 self.blt_order = ("time", "baseline") # set filename attribute diff --git a/pyuvdata/uvdata/mir_parser.py b/pyuvdata/uvdata/mir_parser.py index 8e72cd0bd4..face74f9ab 100644 --- a/pyuvdata/uvdata/mir_parser.py +++ b/pyuvdata/uvdata/mir_parser.py @@ -4139,9 +4139,7 @@ def _make_v3_compliant(self): # if swarm_only: # self.select(where=("correlator", "eq", 1)) # Get SMA coordinates for various data-filling stuff - sma_lat, sma_lon, sma_alt = Telescope.from_known_telescopes( - "SMA" - ).location_lat_lon_alt + telescope_location = Telescope.from_known_telescopes("SMA").location # in_data updates: mjd, lst, ara, adec # First sort out the time stamps using the day reference inside codes_data, and @@ -4165,10 +4163,7 @@ def _make_v3_compliant(self): # Calculate the LST at the time of obs lst_arr = (12.0 / np.pi) * uvutils.get_lst_for_time( - jd_array=jd_arr, - latitude=np.rad2deg(sma_lat), - longitude=np.rad2deg(sma_lon), - altitude=sma_alt, + jd_array=jd_arr, telescope_loc=telescope_location ) # Finally, calculate the apparent coordinates based on what we have in the data @@ -4176,7 +4171,7 @@ def _make_v3_compliant(self): lon_coord=self.in_data["rar"], lat_coord=self.in_data["decr"], time_array=jd_arr, - telescope_loc=(sma_lat, sma_lon, sma_alt), + telescope_loc=telescope_location, ) # Update the fields accordingly diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index f9fa2280cf..09ea20f742 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -11,7 +11,8 @@ import numpy as np import scipy from astropy import constants as const -from astropy.coordinates import Angle, SkyCoord +from astropy import units +from astropy.coordinates import Angle, EarthLocation, SkyCoord from astropy.time import Time from docstring_parser import DocstringStyle @@ -172,15 +173,13 @@ def _load_miriad_variables(self, uv): "Nspws": "nspect", "Npols": "npol", "channel_width": "sdf", # in Ghz! - "telescope_name": "telescop", } for item in miriad_header_data: - if isinstance(uv[miriad_header_data[item]], str): - header_value = uv[miriad_header_data[item]].replace("\x00", "") - else: - header_value = uv[miriad_header_data[item]] + header_value = uv[miriad_header_data[item]] setattr(self, item, header_value) + self.telescope.name = uv["telescop"].replace("\x00", "") + # Do the units and potential sign conversion for channel_width self.channel_width = np.abs(self.channel_width * 1e9) # change from GHz to Hz @@ -242,9 +241,10 @@ def _load_miriad_variables(self, uv): else: self.vis_units = "uncalib" # assume no calibration if "instrume" in uv.vartable.keys(): - self.instrument = uv["instrume"].replace("\x00", "") + self.telescope.instrument = uv["instrume"].replace("\x00", "") else: - self.instrument = self.telescope_name # set instrument = telescope + # set instrument = telescope name + self.telescope.instrument = self.telescope.name if "dut1" in uv.vartable.keys(): self.dut1 = uv["dut1"] @@ -257,7 +257,7 @@ def _load_miriad_variables(self, uv): if "timesys" in uv.vartable.keys(): self.timesys = uv["timesys"].replace("\x00", "") if "xorient" in uv.vartable.keys(): - self.x_orientation = uv["xorient"].replace("\x00", "") + self.telescope.x_orientation = uv["xorient"].replace("\x00", "") if "bltorder" in uv.vartable.keys(): blt_order_str = uv["bltorder"].replace("\x00", "") self.blt_order = tuple(blt_order_str.split(", ")) @@ -280,7 +280,7 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): """ # check if telescope name is present - if self.telescope_name is None: + if self.telescope.name is None: self._load_miriad_variables(uv) latitude = uv["latitud"] # in units of radians @@ -292,31 +292,33 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): longitude -= 2 * np.pi try: altitude = uv["altitude"] - self.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=altitude * units.m, + ) except KeyError: # get info from known telescopes. # Check to make sure the lat/lon values match reasonably well try: - telescope_obj = Telescope.from_known_telescopes(self.telescope_name) + telescope_obj = Telescope.from_known_telescopes(self.telescope.name) except ValueError: telescope_obj = None if telescope_obj is not None: tol = 2 * np.pi * 1e-3 / (60.0 * 60.0 * 24.0) # 1mas in radians lat_close = np.isclose( - telescope_obj.location_lat_lon_alt[0], latitude, rtol=0, atol=tol + telescope_obj.location.lat.rad, latitude, rtol=0, atol=tol ) lon_close = np.isclose( - telescope_obj.location_lat_lon_alt[1], longitude, rtol=0, atol=tol + telescope_obj.location.lon.rad, longitude, rtol=0, atol=tol ) if correct_lat_lon: - self.telescope_location_lat_lon_alt = ( - telescope_obj.location_lat_lon_alt - ) + self.telescope.location = telescope_obj.location else: - self.telescope_location_lat_lon_alt = ( - latitude, - longitude, - telescope_obj.location_lat_lon_alt[2], + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=telescope_obj.location.height, ) if lat_close and lon_close: if correct_lat_lon: @@ -365,7 +367,7 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): else: warnings.warn( "Altitude is not present in Miriad file, and " - f"telescope {self.telescope_name} is not in " + f"telescope {self.telescope.name} is not in " "known_telescopes. Telescope location will be " "set using antenna positions." ) @@ -386,7 +388,7 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): """ # check if telescope coords exist - if self.telescope_location_lat_lon_alt is None: + if self.telescope.location is None: self._load_telescope_coords(uv, correct_lat_lon=correct_lat_lon) latitude = uv["latitud"] # in units of radians @@ -407,11 +409,11 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): # pyuvdata. # For some reason Miriad doesn't handle an array of integers properly, # so we convert to floats on write and back here - self.antenna_numbers = uv["antnums"].astype(int) - self.Nants_telescope = len(self.antenna_numbers) + self.telescope.antenna_numbers = uv["antnums"].astype(int) + self.telescope.Nants = len(self.telescope.antenna_numbers) except KeyError: - self.antenna_numbers = None - self.Nants_telescope = None + self.telescope.antenna_numbers = None + self.telescope.Nants = None nants = uv["nants"] try: @@ -435,9 +437,9 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): # relative to the array center. ecef_antpos = uvutils.ECEF_from_rotECEF(antpos, longitude) - if self.telescope_location is not None: + if self.telescope.location is not None: if absolute_positions: - rel_ecef_antpos = ecef_antpos - self.telescope_location + rel_ecef_antpos = ecef_antpos - self.telescope._location.xyz() # maintain zeros because they mark missing data rel_ecef_antpos[np.where(antpos_length == 0)[0]] = ecef_antpos[ np.where(antpos_length == 0)[0] @@ -445,25 +447,29 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): else: rel_ecef_antpos = ecef_antpos else: - self.telescope_location = np.mean(ecef_antpos[good_antpos, :], axis=0) + self.telescope.location = EarthLocation.from_geocentric( + *np.mean(ecef_antpos[good_antpos, :], axis=0) * units.m + ) valid_location = uvutils.check_surface_based_positions( - telescope_loc=self.telescope_location, - telescope_frame=self.telescope._location.frame, + telescope_loc=self.telescope.location, raise_error=False, raise_warning=False, ) - # check to see if this could be a valid telescope_location + # check to see if this could be a valid telescope location if valid_location: - mean_lat, mean_lon, mean_alt = self.telescope_location_lat_lon_alt + mean_lon, mean_lat, mean_alt = self.telescope.location.geodetic + mean_lat = mean_lat.rad + mean_lon = mean_lon.rad + mean_alt = mean_alt.to("m").value tol = 2 * np.pi / (60.0 * 60.0 * 24.0) # 1 arcsecond in radians mean_lat_close = np.isclose(mean_lat, latitude, rtol=0, atol=tol) mean_lon_close = np.isclose(mean_lon, longitude, rtol=0, atol=tol) if mean_lat_close and mean_lon_close: - # this looks like a valid telescope_location, and the + # this looks like a valid telescope location, and the # mean antenna lat & lon values are close. Set the - # telescope_location using the file lat/lons and the mean alt. + # telescope location using the file lat/lons and the mean alt. # Then subtract it off of the antenna positions warnings.warn( "Telescope location is not set, but antenna " @@ -472,26 +478,28 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): "telescope_position will be set using the " "mean of the antenna altitudes" ) - self.telescope_location_lat_lon_alt = ( - latitude, - longitude, - mean_alt, + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=mean_alt * units.m, ) - rel_ecef_antpos = ecef_antpos - self.telescope_location + rel_ecef_antpos = ecef_antpos - self.telescope._location.xyz() else: - # this looks like a valid telescope_location, but the + # this looks like a valid telescope location, but the # mean antenna lat & lon values are not close. Set the - # telescope_location using the file lat/lons at sea level. + # telescope location using the file lat/lons at sea level. # Then subtract it off of the antenna positions - self.telescope_location_lat_lon_alt = (latitude, longitude, 0) + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, lon=longitude * units.rad + ) warn_string = ( "Telescope location is set at sealevel at " "the file lat/lon coordinates. Antenna " "positions are present, but the mean " "antenna " ) - rel_ecef_antpos = ecef_antpos - self.telescope_location + rel_ecef_antpos = ecef_antpos - self.telescope._location.xyz() if not mean_lat_close and not mean_lon_close: warn_string += ( @@ -514,23 +522,19 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): warnings.warn(warn_string) else: - # This does not give a valid telescope_location. Instead + # This does not give a valid telescope location. Instead # calculate it from the file lat/lon and sea level for altitude - self.telescope_location_lat_lon_alt = (latitude, longitude, 0) - warn_string = ( - "Telescope location is set at sealevel at " - "the file lat/lon coordinates. Antenna " - "positions are present, but the mean " - "antenna " + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, lon=longitude * units.rad ) - - warn_string += ( - "position does not give a " - "telescope_location on the surface of the " - "earth." + warn_string = ( + "Telescope location is set at sealevel at the file lat/lon " + "coordinates. Antenna positions are present, but the mean " + "antenna position does not give a telescope location on the " + "surface of the earth." ) if absolute_positions: - rel_ecef_antpos = ecef_antpos - self.telescope_location + rel_ecef_antpos = ecef_antpos - self.telescope._location.xyz() else: warn_string += ( " Antenna positions do not appear to be " @@ -541,19 +545,21 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): warnings.warn(warn_string) - if self.Nants_telescope is not None: + if self.telescope.Nants is not None: # in this case there is an antnums variable # (meaning that the file was written with pyuvdata), so we'll use it - if nants == self.Nants_telescope: + if nants == self.telescope.Nants: # no inflation, so just use the positions - self.antenna_positions = rel_ecef_antpos + self.telescope.antenna_positions = rel_ecef_antpos else: # there is some inflation, just use the ones that appear in antnums - self.antenna_positions = np.zeros( - (self.Nants_telescope, 3), dtype=antpos.dtype + self.telescope.antenna_positions = np.zeros( + (self.telescope.Nants, 3), dtype=antpos.dtype ) - for ai, num in enumerate(self.antenna_numbers): - self.antenna_positions[ai, :] = rel_ecef_antpos[num, :] + for ai, num in enumerate(self.telescope.antenna_numbers): + self.telescope.antenna_positions[ai, :] = rel_ecef_antpos[ + num, : + ] else: # there is no antnums variable (meaning that this file was not # written by pyuvdata), so we test for antennas with non-zero @@ -571,12 +577,12 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): ants_use = set(good_antpos) # ants_use are the antennas we'll keep track of in the UVData # object, so they dictate Nants_telescope - self.Nants_telescope = len(ants_use) - self.antenna_numbers = np.array(list(ants_use)) - self.antenna_positions = np.zeros( - (self.Nants_telescope, 3), dtype=rel_ecef_antpos.dtype + self.telescope.Nants = len(ants_use) + self.telescope.antenna_numbers = np.array(list(ants_use)) + self.telescope.antenna_positions = np.zeros( + (self.telescope.Nants, 3), dtype=rel_ecef_antpos.dtype ) - for ai, num in enumerate(self.antenna_numbers): + for ai, num in enumerate(self.telescope.antenna_numbers): if antpos_length[num] == 0: warnings.warn( "antenna number {n} has visibilities " @@ -585,20 +591,22 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): ) else: # leave bad locations as zeros to make them obvious - self.antenna_positions[ai, :] = rel_ecef_antpos[num, :] + self.telescope.antenna_positions[ai, :] = rel_ecef_antpos[ + num, : + ] except KeyError: # there is no antpos variable warnings.warn("Antenna positions are not present in the file.") - self.antenna_positions = None + self.telescope.antenna_positions = None - if self.antenna_numbers is None: + if self.telescope.antenna_numbers is None: # there are no antenna_numbers or antenna_positions, so just use # the antennas present in the visibilities # (Nants_data will therefore match Nants_telescope) if sorted_unique_ants is not None: - self.antenna_numbers = np.array(sorted_unique_ants) - self.Nants_telescope = len(self.antenna_numbers) + self.telescope.antenna_numbers = np.array(sorted_unique_ants) + self.telescope.Nants = len(self.telescope.antenna_numbers) # antenna names is a foreign concept in miriad but required in other formats. try: @@ -608,25 +616,28 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): ant_name_var = uv["antnames"] ant_name_str = ant_name_var.replace("\x00", "") ant_name_list = ant_name_str[1:-1].split(", ") - self.antenna_names = ant_name_list + self.telescope.antenna_names = ant_name_list except KeyError: - self.antenna_names = self.antenna_numbers.astype(str).tolist() + self.telescope.antenna_names = self.telescope.antenna_numbers.astype( + str + ).tolist() # check for antenna diameters try: - self.antenna_diameters = uv["antdiam"] + self.telescope.antenna_diameters = uv["antdiam"] except KeyError: # backwards compatibility for when keyword was 'diameter' try: - self.antenna_diameters = uv["diameter"] + self.telescope.antenna_diameters = uv["diameter"] # if we find it, we need to remove it from extra_keywords to # keep from writing it out self.extra_keywords.pop("diameter") except KeyError: pass - if self.antenna_diameters is not None: - self.antenna_diameters = self.antenna_diameters * np.ones( - self.Nants_telescope, dtype=np.float64 + if self.telescope.antenna_diameters is not None: + self.telescope.antenna_diameters = ( + self.telescope.antenna_diameters + * np.ones(self.telescope.Nants, dtype=np.float64) ) def _read_miriad_metadata(self, uv, *, correct_lat_lon=True): @@ -925,7 +936,9 @@ def read_miriad( ( p if isinstance(p, (int, np.integer)) - else uvutils.polstr2num(p, x_orientation=self.x_orientation) + else uvutils.polstr2num( + p, x_orientation=self.telescope.x_orientation + ) ) for p in polarizations ] @@ -1220,7 +1233,7 @@ def read_miriad( # instead of _miriad values which come from pyephem. # The differences are of order 5 seconds. proc = None - if (self.telescope_location is not None) and calc_lst: + if (self.telescope.location is not None) and calc_lst: proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library ) @@ -1365,7 +1378,7 @@ def read_miriad( if proc is not None: proc.join() - if (self.telescope_location is None) or not calc_lst: + if (self.telescope.location is None) or not calc_lst: # The float below is the number of sidereal days per solar day, and the # formula below adjusts for the fact that in MIRIAD, the lst is for the # start of the integration, as opposed to pyuvdata, which evaluates these @@ -1537,7 +1550,7 @@ def read_miriad( ) zenith_coord = SkyCoord( ra=self.lst_array, - dec=self.telescope_location_lat_lon_alt[0], + dec=self.telescope.location.lat, unit="radian", frame="icrs", ) @@ -1673,7 +1686,7 @@ def write_miriad( """ from . import aipy_extracts - if self.telescope._location.frame != "itrs": + if not isinstance(self.telescope.location, EarthLocation): raise ValueError( "Only ITRS telescope locations are supported in Miriad files." ) @@ -1681,13 +1694,9 @@ def write_miriad( # change time_array and lst_array to mark beginning of integration, # per Miriad format miriad_time_array = self.time_array - self.integration_time / (24 * 3600.0) / 2 - if (self.telescope_location is not None) and calc_lst: - latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees + if (self.telescope.location is not None) and calc_lst: miriad_lsts = uvutils.get_lst_for_time( - miriad_time_array, - latitude=latitude, - longitude=longitude, - altitude=altitude, + miriad_time_array, telescope_loc=self.telescope.location ) else: # The long float below is the number of sidereal days per day. The below @@ -1786,11 +1795,11 @@ def write_miriad( uv["sdf"] = np.median(self.channel_width) * freq_dir / 1e9 # Hz -> GHz uv.add_var("telescop", "a") - uv["telescop"] = self.telescope_name + uv["telescop"] = self.telescope.name uv.add_var("latitud", "d") - uv["latitud"] = self.telescope_location_lat_lon_alt[0].astype(np.double) + uv["latitud"] = self.telescope.location.lat.rad.astype(np.double) uv.add_var("longitu", "d") - uv["longitu"] = self.telescope_location_lat_lon_alt[1].astype(np.double) + uv["longitu"] = self.telescope.location.lon.rad.astype(np.double) uv.add_var("nants", "i") # DCP 2024.01.12 - Adding defaults required for basic imaging @@ -1812,15 +1821,17 @@ def write_miriad( "veldop, jyperk, and systemp" ) - if self.antenna_diameters is not None: - if not np.allclose(self.antenna_diameters, self.antenna_diameters[0]): + if self.telescope.antenna_diameters is not None: + if not np.allclose( + self.telescope.antenna_diameters, self.telescope.antenna_diameters[0] + ): warnings.warn( "Antenna diameters are not uniform, but miriad only " "supports a single diameter. Skipping." ) else: uv.add_var("antdiam", "d") - uv["antdiam"] = float(self.antenna_diameters[0]) + uv["antdiam"] = float(self.telescope.antenna_diameters[0]) # Miriad has no way to keep track of antenna numbers, so the antenna # numbers are simply the index for each antenna in any array that @@ -1832,22 +1843,25 @@ def write_miriad( # read. If the file was written by pyuvdata, then the variable antnums # will be present and we can use it, otherwise we need to test for zeros # in the antpos array and/or antennas with no visibilities. - nants = np.max(self.antenna_numbers) + 1 + nants = np.max(self.telescope.antenna_numbers) + 1 uv["nants"] = nants - if self.antenna_positions is not None: + if self.telescope.antenna_positions is not None: # Miriad wants antenna_positions to be in absolute coordinates # (not relative to array center) in a rotated ECEF frame where the # x-axis goes through the local meridian. - rel_ecef_antpos = np.zeros((nants, 3), dtype=self.antenna_positions.dtype) - for ai, num in enumerate(self.antenna_numbers): - rel_ecef_antpos[num, :] = self.antenna_positions[ai, :] + rel_ecef_antpos = np.zeros( + (nants, 3), dtype=self.telescope.antenna_positions.dtype + ) + for ai, num in enumerate(self.telescope.antenna_numbers): + rel_ecef_antpos[num, :] = self.telescope.antenna_positions[ai, :] # find zeros so antpos can be zeroed there too antpos_length = np.sqrt(np.sum(np.abs(rel_ecef_antpos) ** 2, axis=1)) - ecef_antpos = rel_ecef_antpos + self.telescope_location - longitude = self.telescope_location_lat_lon_alt[1] - antpos = uvutils.rotECEF_from_ECEF(ecef_antpos, longitude) + ecef_antpos = rel_ecef_antpos + self.telescope._location.xyz() + antpos = uvutils.rotECEF_from_ECEF( + ecef_antpos, self.telescope.location.lon.rad + ) # zero out bad locations (these are checked on read) antpos[np.where(antpos_length == 0), :] = [0, 0, 0] @@ -1868,9 +1882,9 @@ def write_miriad( uv.add_var("visunits", "a") uv["visunits"] = self.vis_units uv.add_var("instrume", "a") - uv["instrume"] = self.instrument + uv["instrume"] = self.telescope.instrument uv.add_var("altitude", "d") - uv["altitude"] = self.telescope_location_lat_lon_alt[2].astype(np.double) + uv["altitude"] = self.telescope.location.height.to("m").value.astype(np.double) # optional pyuvdata variables that are not recognized miriad variables if self.dut1 is not None: @@ -1888,9 +1902,9 @@ def write_miriad( if self.timesys is not None: uv.add_var("timesys", "a") uv["timesys"] = self.timesys - if self.x_orientation is not None: + if self.telescope.x_orientation is not None: uv.add_var("xorient", "a") - uv["xorient"] = self.x_orientation + uv["xorient"] = self.telescope.x_orientation if self.blt_order is not None: blt_order_str = ", ".join(self.blt_order) uv.add_var("bltorder", "a") @@ -1946,12 +1960,12 @@ def write_miriad( # For some reason Miriad doesn't handle an array of integers properly, # so convert to floats here and integers on read. uv.add_var("antnums", "d") - uv["antnums"] = self.antenna_numbers.astype(np.float64) + uv["antnums"] = self.telescope.antenna_numbers.astype(np.float64) # antenna names is a foreign concept in miriad but required in other formats. # Miriad can't handle arrays of strings, so we make it into one long # comma-separated string and convert back on read. - ant_name_str = "[" + ", ".join(self.antenna_names) + "]" + ant_name_str = "[" + ", ".join(self.telescope.antenna_names) + "]" uv.add_var("antnames", "a") uv["antnames"] = ant_name_str @@ -1997,7 +2011,7 @@ def write_miriad( az=np.zeros_like(times) + phase_dict["cat_lon"], frame="altaz", unit="rad", - location=self.telescope.location_obj, + location=self.telescope.location, obstime=Time(times, format="jd"), ) driftscan_coords[cat_id] = { diff --git a/pyuvdata/uvdata/ms.py b/pyuvdata/uvdata/ms.py index 2ca5266221..33a33ca613 100644 --- a/pyuvdata/uvdata/ms.py +++ b/pyuvdata/uvdata/ms.py @@ -11,9 +11,17 @@ import warnings import numpy as np +from astropy.coordinates import EarthLocation from astropy.time import Time from docstring_parser import DocstringStyle +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False + from .. import Telescope, ms_utils from .. import utils as uvutils from ..docstrings import copy_replace_short_description @@ -364,8 +372,8 @@ def write_ms( if len(self.extra_keywords) != 0: ms.putkeyword("pyuvdata_extra", self.extra_keywords) - if self.x_orientation is not None: - ms.putkeyword("pyuvdata_xorient", self.x_orientation) + if self.telescope.x_orientation is not None: + ms.putkeyword("pyuvdata_xorient", self.telescope.x_orientation) ms.done() @@ -458,7 +466,7 @@ def _read_ms_main( self.extra_keywords = main_keywords["pyuvdata_extra"] if "pyuvdata_xorient" in main_keywords.keys(): - self.x_orientation = main_keywords["pyuvdata_xorient"] + self.telescope.x_orientation = main_keywords["pyuvdata_xorient"] default_vis_units = { "DATA": "uncalib", @@ -954,45 +962,72 @@ def read_ms( # open table with antenna location information tb_ant_dict = ms_utils.read_ms_antenna(filepath) obs_dict = ms_utils.read_ms_observation(filepath) - self.telescope_name = obs_dict["telescope_name"] - self.instrument = obs_dict["telescope_name"] + self.telescope.name = obs_dict["telescope_name"] + self.telescope.instrument = obs_dict["telescope_name"] self.extra_keywords["observer"] = obs_dict["observer"] full_antenna_positions = tb_ant_dict["antenna_positions"] xyz_telescope_frame = tb_ant_dict["telescope_frame"] xyz_telescope_ellipsoid = tb_ant_dict["telescope_ellipsoid"] - self.antenna_numbers = tb_ant_dict["antenna_numbers"] + self.telescope.antenna_numbers = tb_ant_dict["antenna_numbers"] # check to see if a TELESCOPE_LOCATION column is present in the observation # table. This is non-standard, but inserted by pyuvdata if ( "telescope_location" not in obs_dict - and self.telescope_name in self.known_telescopes() + and self.telescope.name in self.known_telescopes() ): # get it from known telescopes - telescope_obj = Telescope.from_known_telescopes(self.telescope_name) + telescope_obj = Telescope.from_known_telescopes(self.telescope.name) warnings.warn( "Setting telescope_location to value in known_telescopes for " - f"{self.telescope_name}." + f"{self.telescope.name}." ) - self.telescope_location = telescope_obj.location + self.telescope.location = telescope_obj.location else: - self.telescope._location.frame = xyz_telescope_frame - self.telescope._location.ellipsoid = xyz_telescope_ellipsoid + if xyz_telescope_frame not in ["itrs", "mcmf"]: + raise ValueError( + f"Telescope frame in file is {xyz_telescope_frame}. " + "Only 'itrs' and 'mcmf' are currently supported." + ) + if xyz_telescope_frame == "mcmf": + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with " + "MCMF frames." + ) + + if xyz_telescope_ellipsoid is None: + xyz_telescope_ellipsoid = "SPHERE" if "telescope_location" in obs_dict: - self.telescope_location = np.squeeze(obs_dict["telescope_location"]) + if xyz_telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geocentric( + *np.squeeze(obs_dict["telescope_location"]), unit="m" + ) + else: + self.telescope.location = MoonLocation.from_selenocentric( + *np.squeeze(obs_dict["telescope_location"]), unit="m" + ) + self.telescope.location.ellipsoid = xyz_telescope_ellipsoid + else: # Set it to be the mean of the antenna positions (this is not ideal!) - self.telescope_location = np.array( - np.mean(full_antenna_positions, axis=0) - ) + if xyz_telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geocentric( + *np.array(np.mean(full_antenna_positions, axis=0)), unit="m" + ) + else: + self.telescope.location = MoonLocation.from_selenocentric( + *np.array(np.mean(full_antenna_positions, axis=0)), unit="m" + ) + self.telescope.location.ellipsoid = xyz_telescope_ellipsoid # antenna names ant_names = tb_ant_dict["antenna_names"] station_names = tb_ant_dict["station_names"] antenna_diameters = tb_ant_dict["antenna_diameters"] if np.any(antenna_diameters > 0): - self.antenna_diameters = antenna_diameters + self.telescope.antenna_diameters = antenna_diameters # importuvfits measurement sets store antenna names in the STATION column. # cotter measurement sets store antenna names in the NAME column, which is @@ -1002,17 +1037,17 @@ def read_ms( if ("importuvfits" not in self.history) and ( len(ant_names) == len(np.unique(ant_names)) and ("" not in ant_names) ): - self.antenna_names = ant_names + self.telescope.antenna_names = ant_names else: - self.antenna_names = station_names + self.telescope.antenna_names = station_names - self.Nants_telescope = len(self.antenna_names) + self.telescope.Nants = len(self.telescope.antenna_names) relative_positions = np.zeros_like(full_antenna_positions) - relative_positions = full_antenna_positions - self.telescope_location.reshape( - 1, 3 + relative_positions = ( + full_antenna_positions - self.telescope._location.xyz().reshape(1, 3) ) - self.antenna_positions = relative_positions + self.telescope.antenna_positions = relative_positions # set LST array from times and itrf proc = self.set_lsts_from_time_array( diff --git a/pyuvdata/uvdata/mwa_corr_fits.py b/pyuvdata/uvdata/mwa_corr_fits.py index 3d580fd8bd..b04a470ce5 100644 --- a/pyuvdata/uvdata/mwa_corr_fits.py +++ b/pyuvdata/uvdata/mwa_corr_fits.py @@ -83,7 +83,7 @@ def read_metafits( antenna_positions, latitude=latitude, longitude=longitude, altitude=altitude ) # make antenna positions relative to telescope location - antenna_positions = antenna_positions_ecef - mwa_telescope_obj.location + antenna_positions = antenna_positions_ecef - mwa_telescope_obj._location.xyz() # reorder antenna parameters from metafits ordering reordered_inds = antenna_inds.argsort() @@ -1507,7 +1507,7 @@ def read_mwa_corr_fits( self.spw_array = np.array([0]) self.vis_units = "uncalib" self.Npols = 4 - self.xorientation = "east" + self.telescope.x_orientation = "east" meta_dict = read_metafits( metafits_file, @@ -1518,12 +1518,12 @@ def read_mwa_corr_fits( telescope_info_only=False, ) - self.telescope_name = meta_dict["telescope_name"] - self.telescope_location = meta_dict["telescope_location"] - self.instrument = meta_dict["instrument"] - self.antenna_numbers = meta_dict["antenna_numbers"] - self.antenna_names = meta_dict["antenna_names"] - self.antenna_positions = meta_dict["antenna_positions"] + self.telescope.name = meta_dict["telescope_name"] + self.telescope.location = meta_dict["telescope_location"] + self.telescope.instrument = meta_dict["instrument"] + self.telescope.antenna_numbers = meta_dict["antenna_numbers"] + self.telescope.antenna_names = meta_dict["antenna_names"] + self.telescope.antenna_positions = meta_dict["antenna_positions"] self.history = meta_dict["history"] if not uvutils._check_history_version(self.history, self.pyuvdata_version_str): self.history += self.pyuvdata_version_str @@ -1536,10 +1536,12 @@ def read_mwa_corr_fits( self.extra_keywords[key] = value # set parameters from other parameters - self.Nants_telescope = len(self.antenna_numbers) - self.Nants_data = len(self.antenna_numbers) + self.telescope.Nants = len(self.telescope.antenna_numbers) + self.Nants_data = len(self.telescope.antenna_numbers) self.Nbls = int( - len(self.antenna_numbers) * (len(self.antenna_numbers) + 1) / 2.0 + len(self.telescope.antenna_numbers) + * (len(self.telescope.antenna_numbers) + 1) + / 2.0 ) if phase_to_pointing_center: # use another name to prevent name collision in phase call below @@ -1579,7 +1581,11 @@ def read_mwa_corr_fits( # this is a little faster than having nested for-loops moving over the # upper triangle of antenna-pair combinations matrix. ant_1_array, ant_2_array = np.transpose( - list(itertools.combinations_with_replacement(self.antenna_numbers, 2)) + list( + itertools.combinations_with_replacement( + self.telescope.antenna_numbers, 2 + ) + ) ) self.ant_1_array = np.tile(np.array(ant_1_array), self.Ntimes) @@ -1838,7 +1844,7 @@ def read_mwa_corr_fits( # select must be called after lst thread is re-joined if remove_flagged_ants: good_ants = np.delete( - np.array(self.antenna_numbers), meta_dict["flagged_ant_inds"] + np.array(self.telescope.antenna_numbers), meta_dict["flagged_ant_inds"] ) self.select(antenna_nums=good_ants, run_check=False) diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index 6b12df1314..42f06235df 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -73,7 +73,8 @@ def test_simplest_new_uvdata(simplest_working_params: dict[str, Any]): @pytest.mark.parametrize("selenoid", selenoids) def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: str): - uvd = UVData.new(**lunar_simple_params, ellipsoid=selenoid) + lunar_simple_params["telescope_location"].ellipsoid = selenoid + uvd = UVData.new(**lunar_simple_params) assert uvd.telescope._location.frame == "mcmf" assert uvd.telescope._location.ellipsoid == selenoid @@ -568,9 +569,9 @@ def test_get_spw_params(): def test_passing_xorient(simplest_working_params, xorient): uvd = UVData.new(x_orientation=xorient, **simplest_working_params) if xorient.lower().startswith("e"): - assert uvd.x_orientation == "east" + assert uvd.telescope.x_orientation == "east" else: - assert uvd.x_orientation == "north" + assert uvd.telescope.x_orientation == "north" def test_passing_directional_pols(simplest_working_params): diff --git a/pyuvdata/uvdata/tests/test_mir.py b/pyuvdata/uvdata/tests/test_mir.py index bb25fc5d91..0df134cfb2 100644 --- a/pyuvdata/uvdata/tests/test_mir.py +++ b/pyuvdata/uvdata/tests/test_mir.py @@ -74,9 +74,9 @@ def test_read_mir_write_uvfits(sma_mir, tmp_path, future_shapes): sma_mir.use_current_array_shapes() sma_mir.write_uvfits(testfile) uvfits_uv.read_uvfits(testfile, use_future_array_shapes=future_shapes) - print("sma_mir instrument", sma_mir.instrument) - print("uvfits_uv instrument", uvfits_uv.instrument) - assert sma_mir.instrument == uvfits_uv.instrument + print("sma_mir instrument", sma_mir.telescope.instrument) + print("uvfits_uv instrument", uvfits_uv.telescope.instrument) + assert sma_mir.telescope.instrument == uvfits_uv.telescope.instrument for item in ["dut1", "earth_omega", "gst0", "rdate", "timesys"]: # Check to make sure that the UVFITS-specific paramters are set on the # UVFITS-based obj, and not on our original object. Then set it to None for the @@ -206,9 +206,9 @@ def test_read_mir_write_ms(sma_mir, tmp_path, future_shapes): # MS doesn't have the concept of an "instrument" name like FITS does, and instead # defaults to the telescope name. Make sure that checks out here. - assert sma_mir.instrument == "SWARM" - assert ms_uv.instrument == "SMA" - sma_mir.instrument = ms_uv.instrument + assert sma_mir.telescope.instrument == "SWARM" + assert ms_uv.telescope.instrument == "SMA" + sma_mir.telescope.instrument = ms_uv.telescope.instrument # Quick check for history here assert ms_uv.history != sma_mir.history @@ -372,9 +372,9 @@ def test_read_mir_write_ms_flex_pol(mir_data, tmp_path): # MS doesn't have the concept of an "instrument" name like FITS does, and instead # defaults to the telescope name. Make sure that checks out here. - assert mir_uv.instrument == "SWARM" - assert ms_uv.instrument == "SMA" - mir_uv.instrument = ms_uv.instrument + assert mir_uv.telescope.instrument == "SWARM" + assert ms_uv.telescope.instrument == "SMA" + mir_uv.telescope.instrument = ms_uv.telescope.instrument # Quick check for history here assert ms_uv.history != mir_uv.history @@ -818,7 +818,9 @@ def test_generate_sma_antpos_dict(use_file, sma_mir): filepath = os.path.join(filepath, "antennas") ant_dict = generate_sma_antpos_dict(filepath) - for ant_num, xyz_pos in zip(sma_mir.antenna_numbers, sma_mir.antenna_positions): + for ant_num, xyz_pos in zip( + sma_mir.telescope.antenna_numbers, sma_mir.telescope.antenna_positions + ): assert np.allclose(ant_dict[ant_num], xyz_pos) diff --git a/pyuvdata/uvdata/tests/test_miriad.py b/pyuvdata/uvdata/tests/test_miriad.py index a948a39197..dac7b66ce6 100644 --- a/pyuvdata/uvdata/tests/test_miriad.py +++ b/pyuvdata/uvdata/tests/test_miriad.py @@ -26,6 +26,7 @@ import numpy as np import pytest from astropy import constants as const +from astropy import units from astropy.coordinates import Angle from astropy.time import Time, TimeDelta @@ -90,7 +91,7 @@ "telescope_at_sealevel": ( "Telescope location is set at sealevel at the file lat/lon " "coordinates. Antenna positions are present, but the mean antenna " - "position does not give a telescope_location on the surface of the " + "position does not give a telescope location on the surface of the " "earth. Antenna positions do not appear to be on the surface of the " "earth and will be treated as relative." ), @@ -348,9 +349,9 @@ def test_read_carma_miriad_write_ms(tmp_path): uv_in.read(carma_file, use_future_array_shapes=True) # MIRIAD is missing these in the file, so we'll fill it in here. - uv_in.antenna_diameters = np.zeros(uv_in.Nants_telescope) - uv_in.antenna_diameters[:6] = 10.0 - uv_in.antenna_diameters[15:] = 3.5 + uv_in.telescope.antenna_diameters = np.zeros(uv_in.telescope.Nants) + uv_in.telescope.antenna_diameters[:6] = 10.0 + uv_in.telescope.antenna_diameters[15:] = 3.5 # We need to recalculate app coords here for one source ("NOISE"), which was # not actually correctly calculated in the online CARMA system (long story). Since @@ -590,16 +591,17 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): # Test for using antenna positions to get telescope position # extract antenna positions and rotate them for miriad nants = aipy_uv["nants"] - rel_ecef_antpos = np.zeros((nants, 3), dtype=uv_in.antenna_positions.dtype) - for ai, num in enumerate(uv_in.antenna_numbers): - rel_ecef_antpos[num, :] = uv_in.antenna_positions[ai, :] + rel_ecef_antpos = np.zeros( + (nants, 3), dtype=uv_in.telescope.antenna_positions.dtype + ) + for ai, num in enumerate(uv_in.telescope.antenna_numbers): + rel_ecef_antpos[num, :] = uv_in.telescope.antenna_positions[ai, :] # find zeros so antpos can be zeroed there too antpos_length = np.sqrt(np.sum(np.abs(rel_ecef_antpos) ** 2, axis=1)) - ecef_antpos = rel_ecef_antpos + uv_in.telescope_location - longitude = uv_in.telescope_location_lat_lon_alt[1] - antpos = uvutils.rotECEF_from_ECEF(ecef_antpos, longitude) + ecef_antpos = rel_ecef_antpos + uv_in.telescope._location.xyz() + antpos = uvutils.rotECEF_from_ECEF(ecef_antpos, uv_in.telescope.location.lon.rad) # zero out bad locations (these are checked on read) antpos[np.where(antpos_length == 0), :] = [0, 0, 0] @@ -732,8 +734,12 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): good_antpos = np.where(antpos_length > 0)[0] rot_ants = good_antpos[: len(good_antpos) // 2] - rot_antpos = uvutils.rotECEF_from_ECEF(ecef_antpos[rot_ants, :], longitude + np.pi) - modified_antpos = uvutils.rotECEF_from_ECEF(ecef_antpos, longitude) + rot_antpos = uvutils.rotECEF_from_ECEF( + ecef_antpos[rot_ants, :], uv_in.telescope.location.lon.rad + np.pi + ) + modified_antpos = uvutils.rotECEF_from_ECEF( + ecef_antpos, uv_in.telescope.location.lon.rad + ) modified_antpos[rot_ants, :] = rot_antpos # zero out bad locations (these are checked on read) @@ -758,17 +764,13 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): with uvtest.check_warnings( UserWarning, [ - ( - "Altitude is not present in Miriad file, and " - "telescope foo is not in known_telescopes. " - "Telescope location will be set using antenna positions." - ), - "Altitude is not present ", + warn_dict["altitude_missing_foo"], + warn_dict["altitude_missing_foo"], ( "Telescope location is set at sealevel at the " "file lat/lon coordinates. Antenna positions " "are present, but the mean antenna position " - "does not give a telescope_location on the " + "does not give a telescope location on the " "surface of the earth." ), warn_dict["uvw_mismatch"], @@ -901,22 +903,23 @@ def test_miriad_multi_phase_error(tmp_path, paper_miriad): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_miriad_only_itrs(tmp_path, paper_miriad): pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + uv_in = paper_miriad testfile = os.path.join(tmp_path, "outtest_miriad.uv") enu_antpos, _ = uv_in.get_ENU_antpos() - latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in.telescope._location.frame = "mcmf" - uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + latitude, longitude, altitude = uv_in.telescope.location_lat_lon_alt + uv_in.telescope.location = MoonLocation.from_selenodetic( + lat=latitude * units.rad, lon=longitude * units.rad, height=altitude * units.m + ) new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", + enu=enu_antpos, center_loc=uv_in.telescope.location ) - uv_in.antenna_positions = new_full_antpos - uv_in.telescope_location + uv_in.telescope.antenna_positions = ( + new_full_antpos - uv_in.telescope._location.xyz() + ) uv_in.set_lsts_from_time_array() uv_in.check() @@ -988,10 +991,6 @@ def test_miriad_ephem(tmp_path, casa_uvfits, cut_ephem_pts, extrapolate): "info_source" ] - uv_in.print_phase_center_info() - print("") - uv2.print_phase_center_info() - if cut_ephem_pts: # Only one ephem points results in only one ra/dec, so it is interpretted as # a sidereal rather than ephem phase center @@ -1186,7 +1185,7 @@ def test_miriad_extra_keywords(uv_in_paper, tmp_path, kwd_names, kwd_values): def test_roundtrip_optional_params(uv_in_paper, tmp_path): uv_in, uv_out, testfile = uv_in_paper - uv_in.x_orientation = "east" + uv_in.telescope.x_orientation = "east" uv_in.reorder_blts() _write_miriad(uv_in, testfile, clobber=True) @@ -1297,7 +1296,7 @@ def test_read_write_read_miriad(uv_in_paper): assert str(cm.value).startswith("File exists: skipping") # check that if x_orientation is set, it's read back out properly - uv_in.x_orientation = "east" + uv_in.telescope.x_orientation = "east" _write_miriad(uv_in, write_file, clobber=True) uv_out.read(write_file, use_future_array_shapes=True) uv_out._consolidate_phase_center_catalogs(other=uv_in) @@ -1308,8 +1307,8 @@ def test_read_write_read_miriad(uv_in_paper): def test_miriad_antenna_diameters(uv_in_paper): # check that if antenna_diameters is set, it's read back out properly uv_in, uv_out, write_file = uv_in_paper - uv_in.antenna_diameters = ( - np.zeros((uv_in.Nants_telescope,), dtype=np.float32) + 14.0 + uv_in.telescope.antenna_diameters = ( + np.zeros((uv_in.telescope.Nants,), dtype=np.float32) + 14.0 ) _write_miriad(uv_in, write_file, clobber=True) uv_out.read(write_file, use_future_array_shapes=True) @@ -1323,8 +1322,8 @@ def test_miriad_antenna_diameters(uv_in_paper): assert uv_in == uv_out # check that antenna diameters get written if not exactly float - uv_in.antenna_diameters = ( - np.zeros((uv_in.Nants_telescope,), dtype=np.float32) + 14.0 + uv_in.telescope.antenna_diameters = ( + np.zeros((uv_in.telescope.Nants,), dtype=np.float32) + 14.0 ) _write_miriad(uv_in, write_file, clobber=True) uv_out.read(write_file, use_future_array_shapes=True) @@ -1332,10 +1331,10 @@ def test_miriad_antenna_diameters(uv_in_paper): assert uv_in == uv_out # check warning when antenna diameters vary - uv_in.antenna_diameters = ( - np.zeros((uv_in.Nants_telescope,), dtype=np.float32) + 14.0 + uv_in.telescope.antenna_diameters = ( + np.zeros((uv_in.telescope.Nants,), dtype=np.float32) + 14.0 ) - uv_in.antenna_diameters[1] = 15.0 + uv_in.telescope.antenna_diameters[1] = 15.0 _write_miriad( uv_in, write_file, @@ -1347,8 +1346,8 @@ def test_miriad_antenna_diameters(uv_in_paper): ], ) uv_out.read(write_file, use_future_array_shapes=True) - assert uv_out.antenna_diameters is None - uv_out.antenna_diameters = uv_in.antenna_diameters + assert uv_out.telescope.antenna_diameters is None + uv_out.telescope.antenna_diameters = uv_in.telescope.antenna_diameters uv_out._consolidate_phase_center_catalogs(other=uv_in) assert uv_in == uv_out @@ -1397,13 +1396,13 @@ def test_miriad_telescope_locations(): uv_in = Miriad() uv = aipy_extracts.UV(paper_miriad_file) uv_in._load_telescope_coords(uv) - assert uv_in.telescope_location_lat_lon_alt is not None + assert uv_in.telescope.location is not None uv.close() # test load_antpos w/ blank Miriad uv_in = Miriad() uv = aipy_extracts.UV(paper_miriad_file) uv_in._load_antpos(uv) - assert uv_in.antenna_positions is not None + assert uv_in.telescope.antenna_positions is not None @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @@ -1773,17 +1772,18 @@ def test_read_write_read_miriad_partial_metadata_only(uv_in_paper, tmp_path): assert uv_in_meta.time_array is None assert uv_in_meta.data_array is None assert uv_in_meta.integration_time is None - metadata = [ + metadata = ["channel_width", "history", "vis_units"] + for par in metadata: + assert getattr(uv_in_meta, par) is not None + + telescope_metadata = [ "antenna_positions", "antenna_names", "antenna_positions", - "channel_width", - "history", - "vis_units", - "telescope_location", + "location", ] - for m in metadata: - assert getattr(uv_in_meta, m) is not None + for par in telescope_metadata: + assert getattr(uv_in_meta.telescope, par) is not None # metadata only multiple file read-in del uv_in_meta @@ -1850,7 +1850,7 @@ def test_rwr_miriad_antpos_issues(uv_in_paper, tmp_path): uv_in, uv_out, write_file = uv_in_paper uv_in_copy = uv_in.copy() - uv_in_copy.antenna_positions = None + uv_in_copy.telescope.antenna_positions = None _write_miriad( uv_in_copy, write_file, @@ -1873,8 +1873,8 @@ def test_rwr_miriad_antpos_issues(uv_in_paper, tmp_path): uv_in_copy = uv_in.copy() ants_with_data = list(set(uv_in_copy.ant_1_array).union(uv_in_copy.ant_2_array)) - ant_ind = np.where(uv_in_copy.antenna_numbers == ants_with_data[0])[0] - uv_in_copy.antenna_positions[ant_ind, :] = [0, 0, 0] + ant_ind = np.where(uv_in_copy.telescope.antenna_numbers == ants_with_data[0])[0] + uv_in_copy.telescope.antenna_positions[ant_ind, :] = [0, 0, 0] _write_miriad( uv_in_copy, write_file, @@ -1895,17 +1895,17 @@ def test_rwr_miriad_antpos_issues(uv_in_paper, tmp_path): uv_out._consolidate_phase_center_catalogs(other=uv_in_copy) assert uv_in_copy == uv_out - uv_in.antenna_positions = None + uv_in.telescope.antenna_positions = None ants_with_data = sorted(set(uv_in.ant_1_array).union(uv_in.ant_2_array)) new_nums = [] new_names = [] for a in ants_with_data: new_nums.append(a) - ind = np.where(uv_in.antenna_numbers == a)[0][0] - new_names.append(uv_in.antenna_names[ind]) - uv_in.antenna_numbers = np.array(new_nums) - uv_in.antenna_names = new_names - uv_in.Nants_telescope = len(uv_in.antenna_numbers) + ind = np.where(uv_in.telescope.antenna_numbers == a)[0][0] + new_names.append(uv_in.telescope.antenna_names[ind]) + uv_in.telescope.antenna_numbers = np.array(new_nums) + uv_in.telescope.antenna_names = new_names + uv_in.telescope.Nants = len(uv_in.telescope.antenna_numbers) _write_miriad( uv_in, write_file, @@ -2011,12 +2011,12 @@ def test_antpos_units(casa_uvfits, tmp_path): _write_miriad(uv, testfile, clobber=True) auv = aipy_extracts.UV(testfile) aantpos = auv["antpos"].reshape(3, -1).T * const.c.to("m/ns").value - aantpos = aantpos[uv.antenna_numbers, :] + aantpos = aantpos[uv.telescope.antenna_numbers, :] aantpos = ( - uvutils.ECEF_from_rotECEF(aantpos, uv.telescope_location_lat_lon_alt[1]) - - uv.telescope_location + uvutils.ECEF_from_rotECEF(aantpos, uv.telescope.location.lon.rad) + - uv.telescope._location.xyz() ) - assert np.allclose(aantpos, uv.antenna_positions) + assert np.allclose(aantpos, uv.telescope.antenna_positions) @pytest.mark.filterwarnings("ignore:Fixing auto-correlations to be be real-only,") @@ -2035,12 +2035,11 @@ def test_readmiriad_write_miriad_check_time_format(tmp_path): uv = aipy_extracts.UV(fname) uv_t = uv["time"] + uv["inttime"] / (24 * 3600.0) / 2 - lat, lon, alt = uvd.telescope_location_lat_lon_alt - t1 = Time(uv["time"], format="jd", location=(lon, lat)) + t1 = Time(uv["time"], format="jd", location=uvd.telescope.location) dt = TimeDelta(uv["inttime"] / 2, format="sec") t2 = t1 + dt lsts = uvutils.get_lst_for_time( - np.array([t1.jd, t2.jd]), latitude=lat, longitude=lon, altitude=alt + np.array([t1.jd, t2.jd]), telescope_loc=uvd.telescope.location ) delta_lst = lsts[1] - lsts[0] uv_l = uv["lst"] + delta_lst diff --git a/pyuvdata/uvdata/tests/test_ms.py b/pyuvdata/uvdata/tests/test_ms.py index 53c1e53b7f..95e5543eff 100644 --- a/pyuvdata/uvdata/tests/test_ms.py +++ b/pyuvdata/uvdata/tests/test_ms.py @@ -123,19 +123,21 @@ def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): if telescope_frame == "mcmf": pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + enu_antpos, _ = uvobj.get_ENU_antpos() - latitude, longitude, altitude = uvobj.telescope_location_lat_lon_alt - uvobj.telescope._location.frame = "mcmf" - uvobj.telescope._location.ellipsoid = selenoid - uvobj.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + uvobj.telescope.location = MoonLocation.from_selenodetic( + lat=uvobj.telescope.location.lat, + lon=uvobj.telescope.location.lon, + height=uvobj.telescope.location.height, + ellipsoid=selenoid, + ) new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", + enu=enu_antpos, center_loc=uvobj.telescope.location + ) + uvobj.telescope.antenna_positions = ( + new_full_antpos - uvobj.telescope._location.xyz() ) - uvobj.antenna_positions = new_full_antpos - uvobj.telescope_location uvobj.set_lsts_from_time_array() uvobj.set_uvws_from_antenna_positions() uvobj._set_app_coords_helper() @@ -265,7 +267,7 @@ def test_read_ms_read_uvfits(nrao_uv, casa_uvfits): # The uvfits was written by CASA, which adds one to all the antenna numbers relative # to the measurement set. Adjust those: - uvfits_uv.antenna_numbers = uvfits_uv.antenna_numbers - 1 + uvfits_uv.telescope.antenna_numbers = uvfits_uv.telescope.antenna_numbers - 1 uvfits_uv.ant_1_array = uvfits_uv.ant_1_array - 1 uvfits_uv.ant_2_array = uvfits_uv.ant_2_array - 1 uvfits_uv.baseline_array = uvfits_uv.antnums_to_baseline( @@ -282,7 +284,7 @@ def test_read_ms_read_uvfits(nrao_uv, casa_uvfits): assert uvfits_uv.__eq__(ms_uv, check_extra=False) # set those parameters to none to check that the rest of the objects match - ms_uv.antenna_diameters = None + ms_uv.telescope.antenna_diameters = None uvfits_required_extra = ["dut1", "earth_omega", "gst0", "rdate", "timesys"] for p in uvfits_uv.extra(): fits_param = getattr(uvfits_uv, p) @@ -435,7 +437,7 @@ def test_multi_files(casa_uvfits, axis, tmp_path): assert uv_multi.__eq__(uv_full, check_extra=False) # set those parameters to none to check that the rest of the objects match - uv_multi.antenna_diameters = None + uv_multi.telescope.antenna_diameters = None uvfits_required_extra = ["dut1", "earth_omega", "gst0", "rdate", "timesys"] for p in uv_full.extra(): @@ -567,9 +569,7 @@ def test_ms_phasing(sma_mir, future_shapes, tmp_path): ms_uv.read(testfile, use_future_array_shapes=True) assert np.allclose(ms_uv.phase_center_app_ra, ms_uv.lst_array) - assert np.allclose( - ms_uv.phase_center_app_dec, ms_uv.telescope_location_lat_lon_alt[0] - ) + assert np.allclose(ms_uv.phase_center_app_dec, ms_uv.telescope.location.lat.rad) @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @@ -619,7 +619,7 @@ def test_ms_single_chan(sma_mir, future_shapes, tmp_path): ms_uv.extra_keywords = {} ms_uv.history = sma_mir.history ms_uv.filename = sma_mir.filename - ms_uv.instrument = sma_mir.instrument + ms_uv.telescope.instrument = sma_mir.telescope.instrument ms_uv.reorder_blts() # propagate scan numbers to the uvfits, ONLY for comparison @@ -668,9 +668,9 @@ def test_ms_scannumber_multiphasecenter(tmp_path, multi_frame): miriad_uv.read(carma_file, use_future_array_shapes=True) # MIRIAD is missing these in the file, so we'll fill it in here. - miriad_uv.antenna_diameters = np.zeros(miriad_uv.Nants_telescope) - miriad_uv.antenna_diameters[:6] = 10.0 - miriad_uv.antenna_diameters[15:] = 3.5 + miriad_uv.telescope.antenna_diameters = np.zeros(miriad_uv.telescope.Nants) + miriad_uv.telescope.antenna_diameters[:6] = 10.0 + miriad_uv.telescope.antenna_diameters[15:] = 3.5 # We need to recalculate app coords here for one source ("NOISE"), which was # not actually correctly calculated in the online CARMA system (long story). Since @@ -779,7 +779,7 @@ def test_ms_extra_data_descrip(sma_mir, tmp_path): ms_uv.reorder_blts() # Fix the remaining differences between the two objects, all of which are expected - sma_mir.instrument = sma_mir.telescope_name + sma_mir.telescope.instrument = sma_mir.telescope.name ms_uv.history = sma_mir.history sma_mir.extra_keywords = ms_uv.extra_keywords sma_mir.filename = ms_uv.filename = None @@ -903,7 +903,9 @@ def test_ms_reader_errs(sma_mir, tmp_path, badcol, badval, errtype, msg): def test_antenna_diameter_handling(hera_uvh5, tmp_path): uv_obj = hera_uvh5 - uv_obj.antenna_diameters = np.asarray(uv_obj.antenna_diameters, dtype=">f4") + uv_obj.telescope.antenna_diameters = np.asarray( + uv_obj.telescope.antenna_diameters, dtype=">f4" + ) test_file = os.path.join(tmp_path, "dish_diameter_out.ms") with uvtest.check_warnings( @@ -1031,7 +1033,7 @@ def test_importuvfits_flip_conj(sma_mir, tmp_path): uv.read(filename, use_future_array_shapes=True) uv.history = sma_mir.history uv.extra_keywords = sma_mir.extra_keywords - uv.instrument = sma_mir.instrument + uv.telescope.instrument = sma_mir.telescope.instrument assert sma_mir == uv @@ -1049,9 +1051,9 @@ def test_flip_conj_multispw(sma_mir, tmp_path): # MS doesn't have the concept of an "instrument" name like FITS does, and instead # defaults to the telescope name. Make sure that checks out here. - assert sma_mir.instrument == "SWARM" - assert ms_uv.instrument == "SMA" - sma_mir.instrument = ms_uv.instrument + assert sma_mir.telescope.instrument == "SWARM" + assert ms_uv.telescope.instrument == "SMA" + sma_mir.telescope.instrument = ms_uv.telescope.instrument # Quick check for history here assert ms_uv.history != sma_mir.history @@ -1108,7 +1110,7 @@ def test_read_ms_write_ms_alt_data_colums(sma_mir, tmp_path, data_column): assert uvd.extra_keywords["DATA_COL"] == data_column uvd.extra_keywords = sma_mir.extra_keywords - uvd.instrument = sma_mir.instrument + uvd.telescope.instrument = sma_mir.telescope.instrument assert sma_mir.history in uvd.history uvd.history = sma_mir.history assert uvd == sma_mir diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index 490ec1247d..dd0e614ee4 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -300,7 +300,11 @@ def pyuvsim_redundant_main(): # read in test file for the compress/inflate redundancy functions uv_object = UVData() testfile = os.path.join(DATA_PATH, "fewant_randsrc_airybeam_Nsrc100_10MHz.uvfits") - uv_object.read(testfile, use_future_array_shapes=True) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "antenna_diameters are not set or are being overwritten." + ) + uv_object.read(testfile, use_future_array_shapes=True) yield uv_object @@ -326,11 +330,11 @@ def pyuvsim_redundant(pyuvsim_redundant_main): @pytest.fixture(scope="function") def uvdata_baseline(): uv_object = UVData() - uv_object.Nants_telescope = 128 + uv_object.telescope.Nants = 128 uv_object2 = UVData() - uv_object2.Nants_telescope = 2147483649 + uv_object2.telescope.Nants = 2147483649 uv_object3 = UVData() - uv_object3.Nants_telescope = 2050 + uv_object3.telescope.Nants = 2050 DataHolder = namedtuple("DataHolder", ["uv_object", "uv_object2", "uv_object3"]) @@ -715,12 +719,16 @@ def test_future_array_shape(casa_uvfits): def test_nants_data_telescope_larger(casa_uvfits): uvobj = casa_uvfits # make sure it's okay for Nants_telescope to be strictly greater than Nants_data - uvobj.Nants_telescope += 1 + uvobj.telescope.Nants += 1 # add dummy information for "new antenna" to pass object check - uvobj.antenna_names = np.concatenate((uvobj.antenna_names, ["dummy_ant"])) - uvobj.antenna_numbers = np.concatenate((uvobj.antenna_numbers, [20])) - uvobj.antenna_positions = np.concatenate( - (uvobj.antenna_positions, np.zeros((1, 3))), axis=0 + uvobj.telescope.antenna_names = np.concatenate( + (uvobj.telescope.antenna_names, ["dummy_ant"]) + ) + uvobj.telescope.antenna_numbers = np.concatenate( + (uvobj.telescope.antenna_numbers, [20]) + ) + uvobj.telescope.antenna_positions = np.concatenate( + (uvobj.telescope.antenna_positions, np.zeros((1, 3))), axis=0 ) assert uvobj.check() @@ -730,10 +738,10 @@ def test_ant1_array_not_in_antnums(casa_uvfits): uvobj = casa_uvfits # make sure an error is raised if antennas in ant_1_array not in antenna_numbers # remove antennas from antenna_names & antenna_numbers by hand - uvobj.antenna_names = uvobj.antenna_names[1:] - uvobj.antenna_numbers = uvobj.antenna_numbers[1:] - uvobj.antenna_positions = uvobj.antenna_positions[1:, :] - uvobj.Nants_telescope = uvobj.antenna_numbers.size + uvobj.telescope.antenna_names = uvobj.telescope.antenna_names[1:] + uvobj.telescope.antenna_numbers = uvobj.telescope.antenna_numbers[1:] + uvobj.telescope.antenna_positions = uvobj.telescope.antenna_positions[1:, :] + uvobj.telescope.Nants = uvobj.telescope.antenna_numbers.size with pytest.raises( ValueError, match="All antennas in ant_1_array must be in antenna_numbers" ): @@ -746,10 +754,10 @@ def test_ant2_array_not_in_antnums(casa_uvfits): # make sure an error is raised if antennas in ant_2_array not in antenna_numbers # remove antennas from antenna_names & antenna_numbers by hand uvobj = uvobj - uvobj.antenna_names = uvobj.antenna_names[:-1] - uvobj.antenna_numbers = uvobj.antenna_numbers[:-1] - uvobj.antenna_positions = uvobj.antenna_positions[:-1] - uvobj.Nants_telescope = uvobj.antenna_numbers.size + uvobj.telescope.antenna_names = uvobj.telescope.antenna_names[:-1] + uvobj.telescope.antenna_numbers = uvobj.telescope.antenna_numbers[:-1] + uvobj.telescope.antenna_positions = uvobj.telescope.antenna_positions[:-1] + uvobj.telescope.Nants = uvobj.telescope.antenna_numbers.size with pytest.raises( ValueError, match="All antennas in ant_2_array must be in antenna_numbers" ): @@ -778,7 +786,7 @@ def test_baseline_to_antnums(uvdata_baseline): with pytest.raises( Exception, match=( - f"error Nants={uvdata_baseline.uv_object2.Nants_telescope}>2147483648" + f"error Nants={uvdata_baseline.uv_object2.telescope.Nants}>2147483648" " not supported" ), ): @@ -835,7 +843,7 @@ def test_antnums_to_baselines(uvdata_baseline): ValueError, match=( "cannot convert ant1, ant2 to a baseline index with Nants={Nants}" - ">2147483648.".format(Nants=uvdata_baseline.uv_object2.Nants_telescope) + ">2147483648.".format(Nants=uvdata_baseline.uv_object2.telescope.Nants) ), ): uvdata_baseline.uv_object2.antnums_to_baseline(0, 0) @@ -887,7 +895,7 @@ def test_known_telescopes(): def test_hera_diameters(paper_uvh5): uv_in = paper_uvh5 - uv_in.telescope_name = "HERA" + uv_in.telescope.name = "HERA" with uvtest.check_warnings( UserWarning, match=( @@ -897,8 +905,8 @@ def test_hera_diameters(paper_uvh5): ): uv_in.set_telescope_params() - assert uv_in.telescope_name == "HERA" - assert uv_in.antenna_diameters is not None + assert uv_in.telescope.name == "HERA" + assert uv_in.telescope.antenna_diameters is not None uv_in.check() @@ -926,8 +934,8 @@ def test_generic_read(): ): uv_in.read( uvfits_file, - antenna_nums=uv_in.antenna_numbers[0], - antenna_names=uv_in.antenna_names[1], + antenna_nums=uv_in.telescope.antenna_numbers[0], + antenna_names=uv_in.telescope.antenna_names[1], use_future_array_shapes=True, ) @@ -1013,21 +1021,23 @@ def test_phase_unphase_hera_antpos(hera_uvh5): uv_raw = hera_uvh5 # check that they match if you phase & unphase using antenna locations # first replace the uvws with the right values - lat, lon, alt = uv_raw.telescope_location_lat_lon_alt + lat, lon, alt = uv_raw.telescope.location_lat_lon_alt antenna_enu = uvutils.ENU_from_ECEF( - (uv_raw.antenna_positions + uv_raw.telescope_location), - latitude=lat, - longitude=lon, - altitude=alt, + (uv_raw.telescope.antenna_positions + uv_raw.telescope._location.xyz()), + center_loc=uv_raw.telescope.location, ) uvw_calc = np.zeros_like(uv_raw.uvw_array) - unique_times, unique_inds = np.unique(uv_raw.time_array, return_index=True) + unique_times = np.unique(uv_raw.time_array) for jd in unique_times: inds = np.where(uv_raw.time_array == jd)[0] for bl_ind in inds: - wh_ant1 = np.where(uv_raw.antenna_numbers == uv_raw.ant_1_array[bl_ind]) + wh_ant1 = np.where( + uv_raw.telescope.antenna_numbers == uv_raw.ant_1_array[bl_ind] + ) ant1_index = wh_ant1[0][0] - wh_ant2 = np.where(uv_raw.antenna_numbers == uv_raw.ant_2_array[bl_ind]) + wh_ant2 = np.where( + uv_raw.telescope.antenna_numbers == uv_raw.ant_2_array[bl_ind] + ) ant2_index = wh_ant2[0][0] uvw_calc[bl_ind, :] = ( antenna_enu[ant2_index, :] - antenna_enu[ant1_index, :] @@ -1442,6 +1452,7 @@ def test_select_blts(paper_uvh5, future_shapes): def test_select_phase_center_id(tmp_path, carma_miriad): uv_obj = carma_miriad testfile = os.path.join(tmp_path, "outtest.uvh5") + assert uv_obj.telescope.instrument is not None uv1 = uv_obj.select(phase_center_ids=0, inplace=False) uv2 = uv_obj.select(phase_center_ids=[1, 2], inplace=False) @@ -1557,8 +1568,8 @@ def test_select_antennas(casa_uvfits): ants_to_keep = np.array(sorted(ants_to_keep)) ant_names = [] for a in ants_to_keep: - ind = np.where(uv_object3.antenna_numbers == a)[0][0] - ant_names.append(uv_object3.antenna_names[ind]) + ind = np.where(uv_object3.telescope.antenna_numbers == a)[0][0] + ant_names.append(uv_object3.telescope.antenna_names[ind]) uv_object3.select(antenna_names=ant_names) @@ -1569,8 +1580,8 @@ def test_select_antennas(casa_uvfits): ants_to_keep = np.array(sorted(ants_to_keep)) ant_names = [] for a in ants_to_keep: - ind = np.where(uv_object3.antenna_numbers == a)[0][0] - ant_names.append(uv_object3.antenna_names[ind]) + ind = np.where(uv_object3.telescope.antenna_numbers == a)[0][0] + ant_names.append(uv_object3.telescope.antenna_names[ind]) uv_object3.select(antenna_names=[ant_names]) @@ -1578,26 +1589,32 @@ def test_select_antennas(casa_uvfits): # test removing metadata associated with antennas that are no longer present # also add (different) antenna_diameters to test downselection - uv_object.antenna_diameters = 1.0 * np.ones( - (uv_object.Nants_telescope,), dtype=np.float64 + uv_object.telescope.antenna_diameters = 1.0 * np.ones( + (uv_object.telescope.Nants,), dtype=np.float64 ) - for i in range(uv_object.Nants_telescope): - uv_object.antenna_diameters += i + for i in range(uv_object.telescope.Nants): + uv_object.telescope.antenna_diameters += i uv_object4 = uv_object.copy() uv_object4.select(antenna_nums=ants_to_keep, keep_all_metadata=False) - assert uv_object4.Nants_telescope == 9 - assert set(uv_object4.antenna_numbers) == set(ants_to_keep) + assert uv_object4.telescope.Nants == 9 + assert set(uv_object4.telescope.antenna_numbers) == set(ants_to_keep) for a in ants_to_keep: - idx1 = uv_object.antenna_numbers.tolist().index(a) - idx2 = uv_object4.antenna_numbers.tolist().index(a) - assert uv_object.antenna_names[idx1] == uv_object4.antenna_names[idx2] + idx1 = uv_object.telescope.antenna_numbers.tolist().index(a) + idx2 = uv_object4.telescope.antenna_numbers.tolist().index(a) + assert ( + uv_object.telescope.antenna_names[idx1] + == uv_object4.telescope.antenna_names[idx2] + ) assert np.allclose( - uv_object.antenna_positions[idx1, :], uv_object4.antenna_positions[idx2, :] + uv_object.telescope.antenna_positions[idx1, :], + uv_object4.telescope.antenna_positions[idx2, :], ) - assert uv_object.antenna_diameters[idx1], uv_object4.antenna_diameters[idx2] + assert uv_object.telescope.antenna_diameters[ + idx1 + ], uv_object4.telescope.antenna_diameters[idx2] # remove antenna_diameters from object - uv_object.antenna_diameters = None + uv_object.telescope.antenna_diameters = None # check for errors associated with antennas not included in data, bad names # or providing numbers and names @@ -1778,7 +1795,14 @@ def test_select_bls(casa_uvfits): with pytest.raises( ValueError, match="bls must be a list of tuples of antenna numbers" ): - uv_object.select(bls=[(uv_object.antenna_names[0], uv_object.antenna_names[1])]) + uv_object.select( + bls=[ + ( + uv_object.telescope.antenna_names[0], + uv_object.telescope.antenna_names[1], + ) + ] + ) with pytest.raises( ValueError, match=re.escape("Antenna pair (5, 1) does not have any data") @@ -2598,7 +2622,7 @@ def test_select_polarizations(hera_uvh5, future_shapes, pols_to_keep): assert p in uv_object2.polarization_array else: assert ( - uvutils.polstr2num(p, x_orientation=uv_object2.x_orientation) + uvutils.polstr2num(p, x_orientation=uv_object2.telescope.x_orientation) in uv_object2.polarization_array ) for p in np.unique(uv_object2.polarization_array): @@ -2606,7 +2630,7 @@ def test_select_polarizations(hera_uvh5, future_shapes, pols_to_keep): assert p in pols_to_keep else: assert p in uvutils.polstr2num( - pols_to_keep, x_orientation=uv_object2.x_orientation + pols_to_keep, x_orientation=uv_object2.telescope.x_orientation ) assert uvutils._check_histories( @@ -3456,7 +3480,7 @@ def test_reorder_freqs_eq_coeffs(casa_uvfits): # with a pre-determined order that we can flip casa_uvfits.reorder_freqs(channel_order="-freq") casa_uvfits.eq_coeffs = np.tile( - np.arange(casa_uvfits.Nfreqs, dtype=float), (casa_uvfits.Nants_telescope, 1) + np.arange(casa_uvfits.Nfreqs, dtype=float), (casa_uvfits.telescope.Nants, 1) ) # modify the channel widths so we can check them too casa_uvfits.channel_width += np.arange(casa_uvfits.Nfreqs, dtype=float) @@ -3553,21 +3577,18 @@ def test_sum_vis(casa_uvfits, future_shapes): # check override_params uv_overrides = uv_full.copy() uv_overrides.timesys = "foo" - uv_overrides.telescope_location = [ - -1601183.15377712, - -5042003.74810822, - 3554841.17192104, - ] + uv_overrides.telescope.location = EarthLocation.from_geocentric( + -1601183.15377712, -5042003.74810822, 3554841.17192104, unit="m" + ) uv_overrides_2 = uv_overrides.sum_vis( uv_full, override_params=["timesys", "telescope"] ) assert uv_overrides_2.timesys == "foo" - assert uv_overrides_2.telescope_location == [ - -1601183.15377712, - -5042003.74810822, - 3554841.17192104, - ] + assert np.allclose( + uv_overrides_2.telescope._location.xyz(), + np.array([-1601183.15377712, -5042003.74810822, 3554841.17192104]), + ) @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @@ -5010,9 +5031,9 @@ def test_key2inds_conj_all_pols_bl_fringe(casa_uvfits): uv.ant_1_array[0] = ant2 uv.ant_2_array[0] = ant1 uv.baseline_array[0] = uvutils.antnums_to_baseline( - ant2, ant1, Nants_telescope=uv.Nants_telescope + ant2, ant1, Nants_telescope=uv.telescope.Nants ) - bl = uvutils.antnums_to_baseline(ant1, ant2, Nants_telescope=uv.Nants_telescope) + bl = uvutils.antnums_to_baseline(ant1, ant2, Nants_telescope=uv.telescope.Nants) bltind = np.where((uv.ant_1_array == ant1) & (uv.ant_2_array == ant2))[0] ind1, ind2, indp = uv._key2inds(bl) @@ -5041,7 +5062,7 @@ def test_key2inds_conj_all_pols_bls(casa_uvfits): ant1 = uv.ant_1_array[0] ant2 = uv.ant_2_array[0] - bl = uvutils.antnums_to_baseline(ant2, ant1, Nants_telescope=uv.Nants_telescope) + bl = uvutils.antnums_to_baseline(ant2, ant1, Nants_telescope=uv.telescope.Nants) bltind = np.where((uv.ant_1_array == ant1) & (uv.ant_2_array == ant2))[0] ind1, ind2, indp = uv._key2inds(bl) @@ -5059,7 +5080,7 @@ def test_key2inds_conj_all_pols_missing_data_bls(casa_uvfits): uv.select(polarizations=["rl"]) ant1 = uv.ant_1_array[0] ant2 = uv.ant_2_array[0] - bl = uvutils.antnums_to_baseline(ant2, ant1, Nants_telescope=uv.Nants_telescope) + bl = uvutils.antnums_to_baseline(ant2, ant1, Nants_telescope=uv.telescope.Nants) with pytest.raises( KeyError, match="Baseline 81924 not found for polarization array in data." @@ -5244,14 +5265,11 @@ def test_get_data(casa_uvfits, kind): def test_antpair2ind(paper_uvh5): paper_uvh5.set_rectangularity() - print(paper_uvh5.blts_are_rectangular, paper_uvh5.time_axis_faster_than_bls) - # Test for baseline-time axis indexer uv = paper_uvh5 # get indices inds = uv.antpair2ind(0, 1, ordered=False) - print(inds) assert inds == slice(1, None, 21) @@ -5362,7 +5380,9 @@ def test_get_enu_antpos(hera_uvh5_xx): def test_telescope_loc_xyz_check(paper_uvh5, tmp_path): # test that improper telescope locations can still be read uv = paper_uvh5 - uv.telescope_location = uvutils.XYZ_from_LatLonAlt(*uv.telescope_location) + uv.telescope.location = EarthLocation.from_geocentric( + *uvutils.XYZ_from_LatLonAlt(*uv.telescope._location.xyz()), unit="m" + ) # fix LST values uv.set_lsts_from_time_array() fname = str(tmp_path / "test.uvh5") @@ -5396,13 +5416,13 @@ def test_get_pols(casa_uvfits): def test_get_pols_x_orientation(paper_uvh5): uv_in = paper_uvh5 - uv_in.x_orientation = "east" + uv_in.telescope.x_orientation = "east" pols = uv_in.get_pols() pols_data = ["en"] assert pols == pols_data - uv_in.x_orientation = "north" + uv_in.telescope.x_orientation = "north" pols = uv_in.get_pols() pols_data = ["ne"] @@ -6591,6 +6611,7 @@ def test_redundancy_contract_expand_nblts_not_nbls_times_ntimes( assert uv3 == uv1 +@pytest.mark.filterwarnings("ignore:antenna_diameters are not set or are being") @pytest.mark.parametrize("grid_alg", [True, False]) def test_compress_redundancy_variable_inttime(grid_alg): uv0 = UVData() @@ -6676,6 +6697,7 @@ def test_compress_redundancy_wrong_method(pyuvsim_redundant): uv0.compress_by_redundancy(method="foo", tol=tol, inplace=True) +@pytest.mark.filterwarnings("ignore:antenna_diameters are not set or are being") @pytest.mark.parametrize("grid_alg", [True, False]) @pytest.mark.parametrize("method", ("select", "average")) def test_redundancy_missing_groups(method, grid_alg, pyuvsim_redundant, tmp_path): @@ -6705,7 +6727,6 @@ def test_redundancy_missing_groups(method, grid_alg, pyuvsim_redundant, tmp_path uv1._consolidate_phase_center_catalogs( reference_catalog=uv0.phase_center_catalog, ignore_name=True ) - print(uv0.flag_array.shape, uv1.flag_array.shape) assert uv0 == uv1 # Check that writing compressed files causes no issues. with uvtest.check_warnings( @@ -6922,10 +6943,9 @@ def test_lsts_from_time_with_only_unique(paper_uvh5): Test `set_lsts_from_time_array` with only unique values is identical to full array. """ uv = paper_uvh5 - lat, lon, alt = uv.telescope_location_lat_lon_alt_degrees # calculate the lsts for all elements in time array full_lsts = uvutils.get_lst_for_time( - uv.time_array, latitude=lat, longitude=lon, altitude=alt + uv.time_array, telescope_loc=uv.telescope.location ) # use `set_lst_from_time_array` to set the uv.lst_array using only unique values uv.set_lsts_from_time_array() @@ -6938,10 +6958,9 @@ def test_lsts_from_time_with_only_unique_background(paper_uvh5): Test `set_lsts_from_time_array` with only unique values is identical to full array. """ uv = paper_uvh5 - lat, lon, alt = uv.telescope_location_lat_lon_alt_degrees # calculate the lsts for all elements in time array full_lsts = uvutils.get_lst_for_time( - uv.time_array, latitude=lat, longitude=lon, altitude=alt + uv.time_array, telescope_loc=uv.telescope.location ) # use `set_lst_from_time_array` to set the uv.lst_array using only unique values proc = uv.set_lsts_from_time_array(background=True) @@ -8853,7 +8872,7 @@ def test_frequency_average(casa_uvfits, future_shapes, flex_spw, sum_corr): uvobj2 = uvobj.copy() eq_coeffs = np.tile( - np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.Nants_telescope, 1) + np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.telescope.Nants, 1) ) uvobj.eq_coeffs = eq_coeffs @@ -8892,7 +8911,7 @@ def test_frequency_average(casa_uvfits, future_shapes, flex_spw, sum_corr): assert np.max(np.abs(uvobj.channel_width - expected_chan_widths)) == 0 expected_coeffs = eq_coeffs.reshape( - uvobj2.Nants_telescope, int(uvobj2.Nfreqs / 2), 2 + uvobj2.telescope.Nants, int(uvobj2.Nfreqs / 2), 2 ).mean(axis=2) assert np.max(np.abs(uvobj.eq_coeffs - expected_coeffs)) == 0 @@ -8932,7 +8951,7 @@ def test_frequency_average_uneven( uvobj.use_current_array_shapes() eq_coeffs = np.tile( - np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.Nants_telescope, 1) + np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.telescope.Nants, 1) ) uvobj.eq_coeffs = eq_coeffs @@ -9452,7 +9471,7 @@ def test_frequency_average_nsample_precision(casa_uvfits): uvobj = casa_uvfits uvobj2 = uvobj.copy() eq_coeffs = np.tile( - np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.Nants_telescope, 1) + np.arange(uvobj.Nfreqs, dtype=np.float64), (uvobj.telescope.Nants, 1) ) uvobj.eq_coeffs = eq_coeffs uvobj.check() @@ -9475,7 +9494,7 @@ def test_frequency_average_nsample_precision(casa_uvfits): assert np.max(np.abs(uvobj.freq_array - expected_freqs)) == 0 expected_coeffs = eq_coeffs.reshape( - uvobj2.Nants_telescope, int(uvobj2.Nfreqs / 2), 2 + uvobj2.telescope.Nants, int(uvobj2.Nfreqs / 2), 2 ).mean(axis=2) assert np.max(np.abs(uvobj.eq_coeffs - expected_coeffs)) == 0 @@ -9560,8 +9579,8 @@ def test_remove_eq_coeffs_divide(casa_uvfits, future_shapes): uvobj2 = uvobj.copy() # give eq_coeffs to the object - eq_coeffs = np.empty((uvobj.Nants_telescope, uvobj.Nfreqs), dtype=np.float64) - for i, ant in enumerate(uvobj.antenna_numbers): + eq_coeffs = np.empty((uvobj.telescope.Nants, uvobj.Nfreqs), dtype=np.float64) + for i, ant in enumerate(uvobj.telescope.antenna_numbers): eq_coeffs[i, :] = ant + 1 uvobj.eq_coeffs = eq_coeffs uvobj.eq_coeffs_convention = "divide" @@ -9591,8 +9610,8 @@ def test_remove_eq_coeffs_multiply(casa_uvfits, future_shapes): uvobj2 = uvobj.copy() # give eq_coeffs to the object - eq_coeffs = np.empty((uvobj.Nants_telescope, uvobj.Nfreqs), dtype=np.float64) - for i, ant in enumerate(uvobj.antenna_numbers): + eq_coeffs = np.empty((uvobj.telescope.Nants, uvobj.Nfreqs), dtype=np.float64) + for i, ant in enumerate(uvobj.telescope.antenna_numbers): eq_coeffs[i, :] = ant + 1 uvobj.eq_coeffs = eq_coeffs uvobj.eq_coeffs_convention = "multiply" @@ -9618,7 +9637,7 @@ def test_remove_eq_coeffs_errors(casa_uvfits): uvobj.remove_eq_coeffs() # raise error when eq_coeffs are defined but not eq_coeffs_convention - uvobj.eq_coeffs = np.ones((uvobj.Nants_telescope, uvobj.Nfreqs)) + uvobj.eq_coeffs = np.ones((uvobj.telescope.Nants, uvobj.Nfreqs)) with pytest.raises( ValueError, match="The eq_coeffs_convention attribute must be defined" ): @@ -10012,16 +10031,12 @@ def test_rephase_to_time(): time = Time(phase_time, format="jd") # Generate ra/dec of zenith at time in the phase_frame coordinate # system to use for phasing - telescope_location = EarthLocation.from_geocentric( - *uvd.telescope_location, unit="m" - ) - zenith_coord = SkyCoord( alt=Angle(90 * units.deg), az=Angle(0 * units.deg), obstime=time, frame="altaz", - location=telescope_location, + location=uvd.telescope.location, ) obs_zenith_coord = zenith_coord.transform_to("icrs") @@ -10139,7 +10154,6 @@ def test_print_object_full(sma_mir, frame, epoch): epoch_str = epoch else: epoch_str = "B" + str(epoch) - print(epoch_str) check_str = ( " ID Cat Entry Type Az/Lon/RA" " El/Lat/Dec Frame Epoch PM-Ra PM-Dec Dist V_rad \n" @@ -11128,7 +11142,7 @@ def test_fix_phase(hera_uvh5, tmp_path, future_shapes, use_ant_pos, phase_frame) # These values could be anything -- we're just picking something that we know should # be visible from the telescope at the time of obs (ignoring horizon limits). phase_ra = uv_in.lst_array[-1] - phase_dec = uv_in.telescope_location_lat_lon_alt[0] * 0.333 + phase_dec = uv_in.telescope.location.lat.rad * 0.333 # Do the improved phasing on the data set. uv_in.phase(lon=phase_ra, lat=phase_dec, phase_frame=phase_frame, cat_name="foo") @@ -11187,6 +11201,9 @@ def test_fix_phase(hera_uvh5, tmp_path, future_shapes, use_ant_pos, phase_frame) warnings.filterwarnings( "ignore", "Fixing auto-correlations to be be real-only" ) + warnings.filterwarnings( + "ignore", "antenna_diameters are not set or are being overwritten." + ) uv_in_bad2 = UVData.from_file( outfile, fix_old_proj=True, @@ -11559,10 +11576,9 @@ def test_set_data(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] data = 2 * uv.get_data(ant1, ant2, squeeze="none", force_copy=True) - inds1, inds2, indp = uv._key2inds((ant1, ant2)) uv.set_data(data, ant1, ant2) data2 = uv.get_data(ant1, ant2, squeeze="none") @@ -11584,8 +11600,8 @@ def test_set_data_evla(future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] data = 2 * uv.get_data(ant1, ant2, squeeze="none", force_copy=True) inds1, inds2, indp = uv._key2inds((ant1, ant2)) uv.set_data(data, ant1, ant2) @@ -11607,8 +11623,8 @@ def test_set_data_polkey(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] pol = "xx" data = 2 * uv.get_data(ant1, ant2, pol, squeeze="none", force_copy=True) inds1, inds2, indp = uv._key2inds((ant1, ant2, pol)) @@ -11630,8 +11646,8 @@ def test_set_flags(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] flags = uv.get_flags(ant1, ant2, squeeze="none", force_copy=True) if future_shapes: flags[:, :, :] = True @@ -11657,8 +11673,8 @@ def test_set_flags_polkey(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] pol = "xx" flags = uv.get_flags(ant1, ant2, pol, squeeze="none", force_copy=True) if future_shapes: @@ -11685,8 +11701,8 @@ def test_set_nsamples(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] nsamples = uv.get_nsamples(ant1, ant2, squeeze="none", force_copy=True) if future_shapes: nsamples[:, :, :] = np.pi @@ -11712,8 +11728,8 @@ def test_set_nsamples_polkey(hera_uvh5, future_shapes): if not future_shapes: uv.use_current_array_shapes() - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] pol = "xx" nsamples = uv.get_nsamples(ant1, ant2, pol, squeeze="none", force_copy=True) if future_shapes: @@ -11736,8 +11752,8 @@ def test_set_data_bad_key_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] data = uv.get_data(ant1, ant2, squeeze="none", force_copy=True) match = "no more than 3 key values can be passed" with pytest.raises(ValueError, match=match): @@ -11753,8 +11769,8 @@ def test_set_data_conj_data_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] data = uv.get_data(ant1, ant2, squeeze="none", force_copy=True) match = "the requested key is present on the object, but conjugated" with pytest.raises(ValueError, match=match): @@ -11770,8 +11786,8 @@ def test_set_data_wrong_shape_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] data = uv.get_data(ant1, ant2, squeeze="none", force_copy=True) # make data the wrong rank data = data[0] @@ -11789,8 +11805,8 @@ def test_set_flags_bad_key_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] flags = uv.get_data(ant1, ant2, squeeze="none", force_copy=True) match = "no more than 3 key values can be passed" with pytest.raises(ValueError, match=match): @@ -11806,8 +11822,8 @@ def test_set_flags_conj_data_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] flags = uv.get_flags(ant1, ant2, squeeze="none", force_copy=True) match = "the requested key is present on the object, but conjugated" with pytest.raises(ValueError, match=match): @@ -11823,8 +11839,8 @@ def test_set_flags_wrong_shape_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] flags = uv.get_flags(ant1, ant2, squeeze="none", force_copy=True) # make data the wrong rank flags = flags[0] @@ -11842,8 +11858,8 @@ def test_set_nsamples_bad_key_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] nsamples = uv.get_data(ant1, ant2, squeeze="none", force_copy=True) match = "no more than 3 key values can be passed" with pytest.raises(ValueError, match=match): @@ -11859,8 +11875,8 @@ def test_set_nsamples_conj_data_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] nsamples = uv.get_nsamples(ant1, ant2, squeeze="none", force_copy=True) match = "the requested key is present on the object, but conjugated" with pytest.raises(ValueError, match=match): @@ -11876,8 +11892,8 @@ def test_set_nsamples_wrong_shape_error(hera_uvh5): """ uv = hera_uvh5 - ant1 = np.unique(uv.antenna_numbers)[0] - ant2 = np.unique(uv.antenna_numbers)[1] + ant1 = np.unique(uv.telescope.antenna_numbers)[0] + ant2 = np.unique(uv.telescope.antenna_numbers)[1] nsamples = uv.get_nsamples(ant1, ant2, squeeze="none", force_copy=True) # make data the wrong rank nsamples = nsamples[0] @@ -12480,7 +12496,6 @@ def test_init_like_hera_cal( params = [ "Nants_data", - "Nants_telescope", "Nbls", "Nblts", "Nfreqs", @@ -12489,16 +12504,12 @@ def test_init_like_hera_cal( "Ntimes", "ant_1_array", "ant_2_array", - "antenna_names", - "antenna_numbers", "baseline_array", "channel_width", "data_array", "flag_array", "freq_array", "history", - "x_orientation", - "instrument", "integration_time", "lst_array", "nsample_array", @@ -12506,11 +12517,19 @@ def test_init_like_hera_cal( "phase_type", "polarization_array", "spw_array", - "telescope_location", - "telescope_name", "time_array", "uvw_array", "vis_units", + ] + + tel_params = [ + "name", + "location", + "instrument", + "x_orientation", + "Nants", + "antenna_names", + "antenna_numbers", "antenna_positions", ] @@ -12549,6 +12568,9 @@ def test_init_like_hera_cal( with uvtest.check_warnings(warn_type, match=msg): uvd.__setattr__(par, param_dict[par]) + for par in tel_params: + setattr(uvd.telescope, par, getattr(hera_uvh5.telescope, par)) + assert uvd.phase_center_catalog is None if projected: @@ -12600,7 +12622,7 @@ def test_init_like_hera_cal( uvd.check() return - uvd.antenna_diameters = hera_uvh5.antenna_diameters + uvd.telescope.antenna_diameters = hera_uvh5.telescope.antenna_diameters uvd.extra_keywords = hera_uvh5.extra_keywords if check_before_write: @@ -12744,12 +12766,14 @@ def test_update_antenna_positions(sma_mir, delta_antpos, flip_antpos): # Flip the coords and antpos to see if we get back to where we are supposed to # be in the uvws (though we'll ignore the data in this case). sma_mir.uvw_array *= -1 - sma_mir.antenna_positions *= -1 + sma_mir.telescope.antenna_positions *= -1 else: # Introduce a small delta to all ants so that the positions are different - sma_mir.antenna_positions += 1 + sma_mir.telescope.antenna_positions += 1 - new_positions = dict(zip(sma_copy.antenna_numbers, sma_copy.antenna_positions)) + new_positions = dict( + zip(sma_copy.telescope.antenna_numbers, sma_copy.telescope.antenna_positions) + ) sma_mir.update_antenna_positions( new_positions=new_positions, delta_antpos=delta_antpos @@ -12766,8 +12790,6 @@ def test_antpair2ind_rect_not_ordered(hera_uvh5): assert hera_uvh5.blts_are_rectangular - print(hera_uvh5.get_antpairs()) - inds = hera_uvh5.antpair2ind((0, 1), ordered=True) assert np.all(inds == hera_uvh5.antpair2ind((1, 0), ordered=False)) diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index bd74bc6290..572cb0e0b9 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -158,10 +158,9 @@ def test_time_precision(tmp_path): uvd2 = UVData() uvd2.read(testfile, use_future_array_shapes=True) - latitude, longitude, altitude = uvd2.telescope_location_lat_lon_alt_degrees unique_times, inverse_inds = np.unique(uvd2.time_array, return_inverse=True) unique_lst_array = uvutils.get_lst_for_time( - unique_times, latitude=latitude, longitude=longitude, altitude=altitude + unique_times, telescope_loc=uvd.telescope.location ) calc_lst_array = unique_lst_array[inverse_inds] @@ -506,7 +505,7 @@ def test_casa_nonascii_bytes_antenna_names(): 'H124', 'H124', 'H124', 'H124', 'HH136', 'HH137', 'HH138', 'HH139', 'HH140', 'HH141', 'HH142', 'HH143'] # fmt: on - assert uv1.antenna_names == expected_ant_names + assert uv1.telescope.antenna_names == expected_ant_names @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @@ -528,20 +527,21 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se if telescope_frame == "mcmf": pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + enu_antpos, _ = uv_in.get_ENU_antpos() - latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in.telescope._location.frame = "mcmf" - uv_in.telescope._location.ellipsoid = selenoid - uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) - new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", + uv_in.telescope.location = MoonLocation.from_selenodetic( + lat=uv_in.telescope.location.lat, + lon=uv_in.telescope.location.lon, + height=uv_in.telescope.location.height, ellipsoid=selenoid, ) - uv_in.antenna_positions = new_full_antpos - uv_in.telescope_location + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, center_loc=uv_in.telescope.location + ) + uv_in.telescope.antenna_positions = ( + new_full_antpos - uv_in.telescope._location.xyz() + ) uv_in.set_lsts_from_time_array() uv_in.set_uvws_from_antenna_positions() uv_in._set_app_coords_helper() @@ -698,7 +698,7 @@ def test_readwriteread_x_orientation(tmp_path, casa_uvfits): write_file = str(tmp_path / "outtest_casa.uvfits") # check that if x_orientation is set, it's read back out properly - uv_in.x_orientation = "east" + uv_in.telescope.x_orientation = "east" uv_in.write_uvfits(write_file) uv_out.read(write_file, use_future_array_shapes=True) @@ -722,8 +722,8 @@ def test_readwriteread_antenna_diameters(tmp_path, casa_uvfits): write_file = str(tmp_path / "outtest_casa.uvfits") # check that if antenna_diameters is set, it's read back out properly - uv_in.antenna_diameters = ( - np.zeros((uv_in.Nants_telescope,), dtype=np.float64) + 14.0 + uv_in.telescope.antenna_diameters = ( + np.zeros((uv_in.telescope.Nants,), dtype=np.float64) + 14.0 ) uv_in.write_uvfits(write_file) uv_out.read(write_file, use_future_array_shapes=True) @@ -748,7 +748,7 @@ def test_readwriteread_large_antnums(tmp_path, casa_uvfits): write_file = str(tmp_path / "outtest_casa.uvfits") # check that if antenna_numbers are > 256 everything works - uv_in.antenna_numbers = uv_in.antenna_numbers + 256 + uv_in.telescope.antenna_numbers = uv_in.telescope.antenna_numbers + 256 uv_in.ant_1_array = uv_in.ant_1_array + 256 uv_in.ant_2_array = uv_in.ant_2_array + 256 uv_in.baseline_array = uv_in.antnums_to_baseline( @@ -838,7 +838,7 @@ def test_readwriteread_missing_info(tmp_path, casa_uvfits, lat_lon_alt): ], ): uv_out.read(write_file2, use_future_array_shapes=True) - assert uv_out.telescope_name == "EVLA" + assert uv_out.telescope.name == "EVLA" assert uv_out.timesys == time_sys return @@ -1682,10 +1682,10 @@ def test_miriad_convention(tmp_path): uv.read(casa_tutorial_uvfits) # Change an antenna ID to 512 - old_idx = uv.antenna_numbers[10] # This is antenna 19 + old_idx = uv.telescope.antenna_numbers[10] # This is antenna 19 new_idx = 512 - uv.antenna_numbers[10] = new_idx + uv.telescope.antenna_numbers[10] = new_idx uv.ant_1_array[uv.ant_1_array == old_idx] = new_idx uv.ant_2_array[uv.ant_2_array == old_idx] = new_idx uv.baseline_array = uv.antnums_to_baseline(uv.ant_1_array, uv.ant_2_array) @@ -1720,9 +1720,9 @@ def test_miriad_convention(tmp_path): assert uv2 == uv # Test that antennas get +1 if there is a 0-indexed antennas - old_idx = uv.antenna_numbers[0] + old_idx = uv.telescope.antenna_numbers[0] new_idx = 0 - uv.antenna_numbers[0] = new_idx + uv.telescope.antenna_numbers[0] = new_idx uv.ant_1_array[uv.ant_1_array == old_idx] = new_idx uv.ant_2_array[uv.ant_2_array == old_idx] = new_idx uv.baseline_array = uv.antnums_to_baseline(uv.ant_1_array, uv.ant_2_array) @@ -1746,7 +1746,7 @@ def test_miriad_convention(tmp_path): ] # adjust for expected antenna number changes: - uv2.antenna_numbers -= 1 + uv2.telescope.antenna_numbers -= 1 uv2.ant_1_array -= 1 uv2.ant_2_array -= 1 uv2.baseline_array = uv2.antnums_to_baseline(uv2.ant_1_array, uv2.ant_2_array) diff --git a/pyuvdata/uvdata/tests/test_uvh5.py b/pyuvdata/uvdata/tests/test_uvh5.py index 435debc559..da4e27e1d0 100644 --- a/pyuvdata/uvdata/tests/test_uvh5.py +++ b/pyuvdata/uvdata/tests/test_uvh5.py @@ -55,9 +55,8 @@ def uv_partial_write(casa_uvfits, tmp_path): # convert a uvfits file to uvh5, cutting down the amount of data uv_uvfits = casa_uvfits uv_uvfits.select(antenna_nums=[3, 7, 24]) - lat, lon, alt = uv_uvfits.telescope_location_lat_lon_alt_degrees uv_uvfits.lst_array = uvutils.get_lst_for_time( - uv_uvfits.time_array, latitude=lat, longitude=lon, altitude=alt + uv_uvfits.time_array, telescope_loc=uv_uvfits.telescope.location ) testfile = str(tmp_path / "outtest.uvh5") @@ -203,20 +202,21 @@ def test_read_uvfits_write_uvh5_read_uvh5( if telescope_frame == "mcmf": pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + enu_antpos, _ = uv_in.get_ENU_antpos() - latitude, longitude, altitude = uv_in.telescope_location_lat_lon_alt - uv_in.telescope._location.frame = "mcmf" - uv_in.telescope._location.ellipsoid = selenoid - uv_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) - new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", + uv_in.telescope.location = MoonLocation.from_selenodetic( + lat=uv_in.telescope.location.lat, + lon=uv_in.telescope.location.lon, + height=uv_in.telescope.location.height, ellipsoid=selenoid, ) - uv_in.antenna_positions = new_full_antpos - uv_in.telescope_location + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, center_loc=uv_in.telescope.location + ) + uv_in.telescope.antenna_positions = ( + new_full_antpos - uv_in.telescope._location.xyz() + ) uv_in.set_lsts_from_time_array() uv_in.check() @@ -346,8 +346,10 @@ def test_uvh5_optional_parameters(casa_uvfits, tmp_path): testfile = str(tmp_path / "outtest_uvfits.uvh5") # set optional parameters - uv_in.x_orientation = "east" - uv_in.antenna_diameters = np.ones_like(uv_in.antenna_numbers) * 1.0 + uv_in.telescope.x_orientation = "east" + uv_in.telescope.antenna_diameters = ( + np.ones_like(uv_in.telescope.antenna_numbers) * 1.0 + ) uv_in.uvplane_reference_time = 0 # reorder_blts @@ -571,7 +573,7 @@ def test_uvh5_partial_read_antennas(casa_uvfits, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv.read(testfile, use_future_array_shapes=True) @@ -599,7 +601,7 @@ def test_uvh5_partial_read_freqs(casa_uvfits, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv.read(testfile, use_future_array_shapes=True) @@ -629,7 +631,7 @@ def test_uvh5_partial_read_pols(casa_uvfits, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv.read(testfile, use_future_array_shapes=True) @@ -667,7 +669,7 @@ def test_uvh5_partial_read_times(casa_uvfits, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv.read(testfile, use_future_array_shapes=True) @@ -699,7 +701,7 @@ def test_uvh5_partial_read_lsts(casa_uvfits, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv.read(testfile, use_future_array_shapes=True) @@ -737,7 +739,7 @@ def test_uvh5_partial_read_multi1(casa_uvfits, future_shapes, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) if not future_shapes: @@ -812,7 +814,7 @@ def test_uvh5_partial_read_multi2(casa_uvfits, future_shapes, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) if not future_shapes: @@ -886,7 +888,7 @@ def test_uvh5_partial_read_multi3(casa_uvfits, future_shapes, tmp_path): uvh5_uv2 = UVData() testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) if not future_shapes: @@ -959,7 +961,7 @@ def test_uvh5_read_multdim_index(tmp_path, future_shapes, casa_uvfits): testfile = str(tmp_path / "outtest.uvh5") # change telescope name to avoid errors - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) uvh5_uv = UVData() uvh5_uv.read(testfile, use_future_array_shapes=future_shapes) @@ -1447,7 +1449,7 @@ def test_uvh5_partial_write_irregular_multi1(uv_partial_write, future_shapes, tm Test writing a uvh5 file using irregular intervals for blts and freqs. """ full_uvh5 = uv_partial_write - full_uvh5.telescope_name = "PAPER" + full_uvh5.telescope.name = "PAPER" if not future_shapes: full_uvh5.use_current_array_shapes() @@ -1548,7 +1550,7 @@ def test_uvh5_partial_write_irregular_multi2(uv_partial_write, future_shapes, tm Test writing a uvh5 file using irregular intervals for freqs and pols. """ full_uvh5 = uv_partial_write - full_uvh5.telescope_name = "PAPER" + full_uvh5.telescope.name = "PAPER" if not future_shapes: full_uvh5.use_current_array_shapes() @@ -1653,7 +1655,7 @@ def test_uvh5_partial_write_irregular_multi3(uv_partial_write, future_shapes, tm Test writing a uvh5 file using irregular intervals for blts and pols. """ full_uvh5 = uv_partial_write - full_uvh5.telescope_name = "PAPER" + full_uvh5.telescope.name = "PAPER" if not future_shapes: full_uvh5.use_current_array_shapes() @@ -1749,7 +1751,7 @@ def test_uvh5_partial_write_irregular_multi4(uv_partial_write, future_shapes, tm Test writing a uvh5 file using irregular intervals for all axes. """ full_uvh5 = uv_partial_write - full_uvh5.telescope_name = "PAPER" + full_uvh5.telescope.name = "PAPER" if not future_shapes: full_uvh5.use_current_array_shapes() @@ -2068,7 +2070,7 @@ def test_uvh5_lst_array(casa_uvfits, tmp_path): uv_in = casa_uvfits uv_out = UVData() testfile = str(tmp_path / "outtest_uvfits.uvh5") - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) # remove lst_array from file; check that it's correctly computed on read @@ -2119,7 +2121,7 @@ def test_uvh5_read_header_special_cases(casa_uvfits, tmp_path): uv_in = casa_uvfits uv_out = UVData() testfile = str(tmp_path / "outtest_uvfits.uvh5") - uv_in.telescope_name = "PAPER" + uv_in.telescope.name = "PAPER" uv_in.write_uvh5(testfile, clobber=True) # change some of the metadata to trip certain if/else clauses with h5py.File(testfile, "r+") as h5f: @@ -3371,14 +3373,14 @@ def test_antenna_names_not_list(casa_uvfits, tmp_path): testfile = str(tmp_path / "outtest_uvfits_ant_names.uvh5") # simulate a user defining antenna names as an array of unicode - uv_in.antenna_names = np.array(uv_in.antenna_names, dtype="U") + uv_in.telescope.antenna_names = np.array(uv_in.telescope.antenna_names, dtype="U") uv_in.write_uvh5(testfile, clobber=True) uv_out.read(testfile, use_future_array_shapes=True) # recast as list since antenna names should be a list and will be cast as # list on read - uv_in.antenna_names = uv_in.antenna_names.tolist() + uv_in.telescope.antenna_names = uv_in.telescope.antenna_names.tolist() # make sure filenames are what we expect assert uv_in.filename == ["day2_TDEM0003_10s_norx_1src_1spw.uvfits"] @@ -3399,7 +3401,7 @@ def test_eq_coeffs_roundtrip(casa_uvfits, tmp_path): uv_in = casa_uvfits uv_out = UVData() testfile = str(tmp_path / "outtest_eq_coeffs.uvh5") - uv_in.eq_coeffs = np.ones((uv_in.Nants_telescope, uv_in.Nfreqs)) + uv_in.eq_coeffs = np.ones((uv_in.telescope.Nants, uv_in.Nfreqs)) uv_in.eq_coeffs_convention = "divide" uv_in.write_uvh5(testfile, clobber=True) uv_out.read(testfile, use_future_array_shapes=True) @@ -3432,7 +3434,7 @@ def test_read_metadata(casa_uvfits, tmp_path): # now read uv_out = UVData() uv_out.read(testfile, use_future_array_shapes=True) - assert isinstance(uv_out.telescope_name, str) + assert isinstance(uv_out.telescope.name, str) # clean up when done os.remove(testfile) @@ -3732,6 +3734,7 @@ def test_getting_lsts(self): del f["/Header/lst_array"] meta1 = uvh5.FastUVH5Meta(os.path.join(self.tmp_path.name, "no_lsts.uvh5")) + assert np.allclose(meta1.lsts, meta.lsts) assert np.allclose(meta1.lst_array, meta.lst_array) # Now test a different ordering. @@ -3831,7 +3834,7 @@ def test_recompute_nbls(self): uvd = meta.to_uvdata() uvd.Nbls = uvd.Nblts uvd.Ntimes = uvd.Nblts // nbls - uvd.telescope_name = "HERA" + uvd.telescope.name = "HERA" uvd.initialize_uvh5_file(newfl, clobber=True) meta.close() @@ -3841,7 +3844,7 @@ def test_recompute_nbls(self): newfl = os.path.join(self.tmp_path.name, "not_hera.uvh5") uvd = meta.to_uvdata() - uvd.telescope_name = "not-HERA" + uvd.telescope.name = "not-HERA" uvd.initialize_uvh5_file(newfl, clobber=True) meta.close() diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 254fa3dd43..f9c441ba0a 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -702,128 +702,6 @@ def _set_telescope_requirements(self): self.telescope._x_orientation.required = False self.telescope._antenna_diameters.required = False - @property - def telescope_name(self): - """The telescope name (stored on the Telescope object internally).""" - return self.telescope.name - - @telescope_name.setter - def telescope_name(self, val): - self.telescope.name = val - - @property - def instrument(self): - """The instrument name (stored on the Telescope object internally).""" - return self.telescope.instrument - - @instrument.setter - def instrument(self, val): - self.telescope.instrument = val - - @property - def telescope_location(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location - - @telescope_location.setter - def telescope_location(self, val): - self.telescope.location = val - - @property - def telescope_location_lat_lon_alt(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt - - @telescope_location_lat_lon_alt.setter - def telescope_location_lat_lon_alt(self, val): - self.telescope.location_lat_lon_alt = val - - @property - def telescope_location_lat_lon_alt_degrees(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt_degrees - - @telescope_location_lat_lon_alt_degrees.setter - def telescope_location_lat_lon_alt_degrees(self, val): - self.telescope.location_lat_lon_alt_degrees = val - - @property - def Nants_telescope(self): - """ - The number of antennas in the telescope. - - This property is stored on the Telescope object internally. - """ - return self.telescope.Nants - - @Nants_telescope.setter - def Nants_telescope(self, val): - self.telescope.Nants = val - - @property - def antenna_names(self): - """The antenna names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_names - - @antenna_names.setter - def antenna_names(self, val): - self.telescope.antenna_names = val - - @property - def antenna_numbers(self): - """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_numbers - - @antenna_numbers.setter - def antenna_numbers(self, val): - self.telescope.antenna_numbers = val - - @property - def antenna_positions(self): - """The antenna positions coordinates of antennas relative to telescope_location. - - The coordinates are in the ITRF frame, shape (Nants_telescope, 3). - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_positions - - @antenna_positions.setter - def antenna_positions(self, val): - self.telescope.antenna_positions = val - - @property - def x_orientation(self): - """Orientation of the physical dipole corresponding to the x label. - - Options are 'east' (indicating east/west orientation) and 'north (indicating - north/south orientation). - This property is stored on the Telescope object internally. - """ - return self.telescope.x_orientation - - @x_orientation.setter - def x_orientation(self, val): - self.telescope.x_orientation = val - - @property - def antenna_diameters(self): - """The antenna diameters in meters. - - Used by CASA to construct a default beam if no beam is supplied. - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_diameters - - @antenna_diameters.setter - def antenna_diameters(self, val): - self.telescope.antenna_diameters = val - @staticmethod def _clear_antpair2ind_cache(obj): """Clear the antpair2ind cache.""" @@ -1871,13 +1749,10 @@ def _calc_single_integration_time(self): return np.diff(np.sort(list(set(self.time_array))))[0] * 86400 def _set_lsts_helper(self, *, astrometry_library=None): - latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees # the utility function is efficient -- it only calculates unique times self.lst_array = uvutils.get_lst_for_time( jd_array=self.time_array, - latitude=latitude, - longitude=longitude, - altitude=altitude, + telescope_loc=self.telescope.location, frame=self.telescope._location.frame, ellipsoid=self.telescope._location.ellipsoid, astrometry_library=astrometry_library, @@ -1931,9 +1806,7 @@ def _set_app_coords_helper(self, *, pa_only=False): dist=dist, time_array=self.time_array[select_mask], lst_array=self.lst_array[select_mask], - telescope_loc=self.telescope_location_lat_lon_alt, - telescope_frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + telescope_loc=self.telescope.location, coord_type=cat_type, ) @@ -1952,11 +1825,9 @@ def _set_app_coords_helper(self, *, pa_only=False): time_array=self.time_array[select_mask], app_ra=app_ra[select_mask], app_dec=app_dec[select_mask], - telescope_loc=self.telescope_location_lat_lon_alt, + telescope_loc=self.telescope.location, ref_frame=frame, ref_epoch=epoch, - telescope_frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) self.phase_center_app_ra = app_ra self.phase_center_app_dec = app_dec @@ -2803,9 +2674,13 @@ def check( # require that all entries in ant_1_array and ant_2_array exist in # antenna_numbers logger.debug("Doing Antenna Uniqueness Check...") - if not set(np.unique(self.ant_1_array)).issubset(self.antenna_numbers): + if not set(np.unique(self.ant_1_array)).issubset( + self.telescope.antenna_numbers + ): raise ValueError("All antennas in ant_1_array must be in antenna_numbers.") - if not set(np.unique(self.ant_2_array)).issubset(self.antenna_numbers): + if not set(np.unique(self.ant_2_array)).issubset( + self.telescope.antenna_numbers + ): raise ValueError("All antennas in ant_2_array must be in antenna_numbers.") logger.debug("... Done Antenna Uniqueness Check") @@ -2830,23 +2705,17 @@ def check( if run_check_acceptability: # Check antenna positions uvutils.check_surface_based_positions( - antenna_positions=self.antenna_positions, - telescope_loc=self.telescope_location, - telescope_frame=self.telescope._location.frame, + antenna_positions=self.telescope.antenna_positions, + telescope_loc=self.telescope.location, raise_error=False, ) - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees # Check the LSTs against what we expect given up-to-date IERS data uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + telescope_loc=self.telescope.location, ) # create a metadata copy to do operations on @@ -3055,7 +2924,7 @@ def baseline_to_antnums(self, baseline): second antenna number(s) """ return uvutils.baseline_to_antnums( - baseline, Nants_telescope=self.Nants_telescope + baseline, Nants_telescope=self.telescope.Nants ) def antnums_to_baseline( @@ -3089,7 +2958,7 @@ def antnums_to_baseline( return uvutils.antnums_to_baseline( ant1, ant2, - Nants_telescope=self.Nants_telescope, + Nants_telescope=self.telescope.Nants, attempt256=attempt256, use_miriad_convention=use_miriad_convention, ) @@ -3251,7 +3120,7 @@ def _key2inds(self, key: str | tuple[int] | tuple[int, int] | tuple[int, int, st # Single string given, assume it is polarization pol_ind1 = np.where( self.polarization_array - == uvutils.polstr2num(key, x_orientation=self.x_orientation) + == uvutils.polstr2num(key, x_orientation=self.telescope.x_orientation) )[0] if len(pol_ind1) > 0: blt_ind1 = slice(None) @@ -3298,7 +3167,9 @@ def _key2inds(self, key: str | tuple[int] | tuple[int, int] | tuple[int, int, st if len(key) == 3: orig_pol = key[2] if isinstance(key[2], str): - pol = uvutils.polstr2num(key[2], x_orientation=self.x_orientation) + pol = uvutils.polstr2num( + key[2], x_orientation=self.telescope.x_orientation + ) else: pol = key[2] @@ -3481,7 +3352,7 @@ def get_pols(self): list of polarizations (as strings) in the data. """ return uvutils.polnum2str( - self.polarization_array, x_orientation=self.x_orientation + self.polarization_array, x_orientation=self.telescope.x_orientation ) def get_antpairpols(self): @@ -3795,20 +3666,13 @@ def get_ENU_antpos(self, *, center=False, pick_data_ants=False): Antenna numbers matching ordering of antpos, shape=(Nants,) """ - latitude, longitude, altitude = self.telescope_location_lat_lon_alt - antpos = uvutils.ENU_from_ECEF( - (self.antenna_positions + self.telescope_location), - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, - ) - ants = self.antenna_numbers + antenna_xyz = self.telescope.antenna_positions + self.telescope._location.xyz() + antpos = uvutils.ENU_from_ECEF(antenna_xyz, center_loc=self.telescope.location) + ants = self.telescope.antenna_numbers if pick_data_ants: data_ants = np.unique(np.concatenate([self.ant_1_array, self.ant_2_array])) - telescope_ants = self.antenna_numbers + telescope_ants = self.telescope.antenna_numbers select = np.in1d(telescope_ants, data_ants) antpos = antpos[select, :] ants = telescope_ants[select] @@ -4731,8 +4595,12 @@ def remove_eq_coeffs(self): # get indices for this key blt_inds = self.antpair2ind(key) - ant1_index = np.asarray(self.antenna_numbers == key[0]).nonzero()[0][0] - ant2_index = np.asarray(self.antenna_numbers == key[1]).nonzero()[0][0] + ant1_index = np.asarray(self.telescope.antenna_numbers == key[0]).nonzero()[ + 0 + ][0] + ant2_index = np.asarray(self.telescope.antenna_numbers == key[1]).nonzero()[ + 0 + ][0] eq_coeff1 = self.eq_coeffs[ant1_index, :] eq_coeff2 = self.eq_coeffs[ant2_index, :] @@ -4875,15 +4743,15 @@ def unproject_phase( lst_array=self.lst_array, use_ant_pos=use_ant_pos, uvw_array=self.uvw_array, - antenna_positions=self.antenna_positions, - antenna_numbers=self.antenna_numbers, + antenna_positions=self.telescope.antenna_positions, + antenna_numbers=self.telescope.antenna_numbers, ant_1_array=self.ant_1_array, ant_2_array=self.ant_2_array, old_app_ra=self.phase_center_app_ra, old_app_dec=self.phase_center_app_dec, old_frame_pa=self.phase_center_frame_pa, - telescope_lat=self.telescope_location_lat_lon_alt[0], - telescope_lon=self.telescope_location_lat_lon_alt[1], + telescope_lat=self.telescope.location.lat.rad, + telescope_lon=self.telescope.location.lon.rad, to_enu=True, ) @@ -4906,9 +4774,7 @@ def unproject_phase( self.phase_center_app_ra[select_mask_use] = self.lst_array[ select_mask_use ].copy() - self.phase_center_app_dec[select_mask_use] = ( - self.telescope_location_lat_lon_alt[0] - ) + self.phase_center_app_dec[select_mask_use] = self.telescope.location.lat.rad self.phase_center_frame_pa[select_mask_use] = 0 return @@ -4960,9 +4826,7 @@ def _phase_dict_helper( if (cat_type is None) or (cat_type == "ephem"): [cat_times, cat_lon, cat_lat, cat_dist, cat_vrad] = ( uvutils.lookup_jplhorizons( - cat_name, - time_array, - telescope_loc=self.telescope_location_lat_lon_alt, + cat_name, time_array, telescope_loc=self.telescope.location ) ) cat_type = "ephem" @@ -5071,7 +4935,7 @@ def _phase_dict_helper( uvutils.lookup_jplhorizons( cat_name, np.concatenate((np.reshape(time_array, -1), cat_times)), - telescope_loc=self.telescope_location_lat_lon_alt, + telescope_loc=self.telescope.location, ) ) elif check_ephem: @@ -5307,9 +5171,7 @@ def phase( pm_dec=phase_dict["cat_pm_dec"], vrad=phase_dict["cat_vrad"], dist=phase_dict["cat_dist"], - telescope_loc=self.telescope_location_lat_lon_alt, - telescope_frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + telescope_loc=self.telescope.location, ) # Now calculate position angles. @@ -5318,11 +5180,9 @@ def phase( time_array=time_array, app_ra=new_app_ra, app_dec=new_app_dec, - telescope_loc=self.telescope_location_lat_lon_alt, + telescope_loc=self.telescope.location, ref_frame=phase_frame, ref_epoch=epoch, - telescope_frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) else: new_frame_pa = np.zeros(time_array.shape, dtype=float) @@ -5335,15 +5195,15 @@ def phase( lst_array=lst_array, use_ant_pos=use_ant_pos, uvw_array=uvw_array, - antenna_positions=self.antenna_positions, - antenna_numbers=self.antenna_numbers, + antenna_positions=self.telescope.antenna_positions, + antenna_numbers=self.telescope.antenna_numbers, ant_1_array=ant_1_array, ant_2_array=ant_2_array, old_app_ra=old_app_ra, old_app_dec=old_app_dec, old_frame_pa=old_frame_pa, - telescope_lat=self.telescope_location_lat_lon_alt[0], - telescope_lon=self.telescope_location_lat_lon_alt[1], + telescope_lat=self.telescope.location.lat.rad, + telescope_lon=self.telescope.location.lon.rad, ) # With all operations complete, we now start manipulating the UVData object @@ -5430,14 +5290,12 @@ def phase_to_time( # Generate ra/dec of zenith at time in the phase_frame coordinate # system to use for phasing - telescope_location = self.telescope.location_obj - zenith_coord = SkyCoord( alt=Angle(90 * units.deg), az=Angle(0 * units.deg), obstime=time, frame="altaz", - location=telescope_location, + location=self.telescope.location, ) obs_zenith_coord = zenith_coord.transform_to(phase_frame) @@ -5467,7 +5325,6 @@ def set_uvws_from_antenna_positions(self, *, update_vis=True): trusted), as misuse can significantly corrupt data. """ - telescope_location = self.telescope_location_lat_lon_alt unprojected_blts = self._check_for_cat_type("unprojected") new_uvw = uvutils.calc_uvw( @@ -5476,12 +5333,12 @@ def set_uvws_from_antenna_positions(self, *, update_vis=True): frame_pa=self.phase_center_frame_pa, lst_array=self.lst_array, use_ant_pos=True, - antenna_positions=self.antenna_positions, - antenna_numbers=self.antenna_numbers, + antenna_positions=self.telescope.antenna_positions, + antenna_numbers=self.telescope.antenna_numbers, ant_1_array=self.ant_1_array, ant_2_array=self.ant_2_array, - telescope_lat=telescope_location[0], - telescope_lon=telescope_location[1], + telescope_lat=self.telescope.location.lat.rad, + telescope_lon=self.telescope.location.lon.rad, ) if np.any(~unprojected_blts): # At least some are phased @@ -5530,15 +5387,15 @@ def update_antenna_positions( circumstances (e.g., when certain metadata like exact times are not trusted), as misuse can significantly corrupt data. """ - new_antpos = self.antenna_positions.copy() - for idx, ant in enumerate(self.antenna_numbers): + new_antpos = self.telescope.antenna_positions.copy() + for idx, ant in enumerate(self.telescope.antenna_numbers): try: new_antpos[idx] = new_positions[ant] except KeyError: # If no updated position is found, then just keep going pass - if np.array_equal(new_antpos, self.antenna_positions): + if np.array_equal(new_antpos, self.telescope.antenna_positions): warnings.warn("No antenna positions appear to have changed, returning.") return @@ -5555,12 +5412,12 @@ def update_antenna_positions( frame_pa=self.phase_center_frame_pa, lst_array=self.lst_array, use_ant_pos=True, - antenna_positions=new_antpos - self.antenna_positions, - antenna_numbers=self.antenna_numbers, + antenna_positions=new_antpos - self.telescope.antenna_positions, + antenna_numbers=self.telescope.antenna_numbers, ant_1_array=self.ant_1_array, ant_2_array=self.ant_2_array, - telescope_lat=self.telescope_location_lat_lon_alt[0], - telescope_lon=self.telescope_location_lat_lon_alt[1], + telescope_lat=self.telescope.location.lat.rad, + telescope_lon=self.telescope.location.lon.rad, ) # Calculate the new uvw values, relate to the old ones, and add that to @@ -5577,14 +5434,14 @@ def update_antenna_positions( ) # Assign the new antenna position values. - self.antenna_positions = new_antpos + self.telescope.antenna_positions = new_antpos # Finally, add the deltas to the original uvw array. self.uvw_array += delta_uvw else: # Otherwise under "normal" circumstances, just plug in the new values and # update the uvws accordingly. - self.antenna_positions = new_antpos + self.telescope.antenna_positions = new_antpos self.set_uvws_from_antenna_positions(update_vis=update_vis) def fix_phase(self, *, use_ant_pos=True): @@ -5674,9 +5531,10 @@ def fix_phase(self, *, use_ant_pos=True): unique_times, _ = np.unique(self.time_array, return_index=True) - telescope_location = self.telescope.location_obj obs_times = Time(unique_times, format="jd") - itrs_telescope_locations = telescope_location.get_itrs(obstime=obs_times) + itrs_telescope_locations = self.telescope.location.get_itrs( + obstime=obs_times + ) itrs_telescope_locations = SkyCoord(itrs_telescope_locations) # just calling transform_to(coord.GCRS) will delete the obstime information # need to re-add obstimes for a GCRS transformation @@ -5698,7 +5556,6 @@ def fix_phase(self, *, use_ant_pos=True): obs_time = obs_times[ind] frame_telescope_location = frame_telescope_locations[ind] - itrs_lat_lon_alt = self.telescope_location_lat_lon_alt uvws_use = self.uvw_array[inds, :] @@ -5716,13 +5573,10 @@ def fix_phase(self, *, use_ant_pos=True): ) itrs_uvw_coord = frame_uvw_coord.transform_to("itrs") - lat, lon, alt = itrs_lat_lon_alt # now convert them to ENU, which is the space uvws are in self.uvw_array[inds, :] = uvutils.ENU_from_ECEF( itrs_uvw_coord.cartesian.get_xyz().value.T, - latitude=lat, - longitude=lon, - altitude=alt, + center_loc=self.telescope.location, ) # remove/add phase center @@ -5733,7 +5587,7 @@ def fix_phase(self, *, use_ant_pos=True): self.phase_center_app_ra = self.lst_array.copy() self.phase_center_app_dec[:] = ( - np.zeros(self.Nblts) + self.telescope_location_lat_lon_alt[0] + np.zeros(self.Nblts) + self.telescope.location.lat.rad ) self.phase_center_frame_pa = np.zeros(self.Nblts) @@ -7257,7 +7111,7 @@ def parse_ants(self, ant_str, *, print_toggle=False): uv=self, ant_str=ant_str, print_toggle=print_toggle, - x_orientation=self.x_orientation, + x_orientation=self.telescope.x_orientation, ) def _select_preprocess( @@ -7442,13 +7296,15 @@ def _select_preprocess( antenna_names = np.array(antenna_names).flatten() antenna_nums = [] for s in antenna_names: - if s not in self.antenna_names: + if s not in self.telescope.antenna_names: raise ValueError( "Antenna name {a} is not present in the antenna_names" " array".format(a=s) ) antenna_nums.append( - self.antenna_numbers[np.where(np.array(self.antenna_names) == s)][0] + self.telescope.antenna_numbers[ + np.where(np.array(self.telescope.antenna_names) == s) + ][0] ) if antenna_nums is not None: @@ -7707,7 +7563,9 @@ def _select_preprocess( spw_inds = np.zeros(0, dtype=np.int64) for p in polarizations: if isinstance(p, str): - p_num = uvutils.polstr2num(p, x_orientation=self.x_orientation) + p_num = uvutils.polstr2num( + p, x_orientation=self.telescope.x_orientation + ) else: p_num = p if p_num in self.polarization_array: @@ -7869,7 +7727,7 @@ def _select_by_index( # evaluate the antenna axis of all parameters ind_dict["Nants_telescope"] = np.where( np.isin( - self.antenna_numbers, + self.telescope.antenna_numbers, list(set(self.ant_1_array).union(self.ant_2_array)), ) )[0] @@ -7889,7 +7747,7 @@ def _select_by_index( self.Nspws = len(ind_arr) elif key == "Nants_telescope": # Count the number of unique ants after ant-based selection - self.Nants_telescope = len(ind_arr) + self.telescope.Nants = len(ind_arr) # Update the history string self.history += history_update_string @@ -9166,7 +9024,7 @@ def frequency_average( final_channel_width = np.zeros(final_nchan, dtype=float) final_flex_spw_id_array = np.zeros(final_nchan, dtype=int) if self.eq_coeffs is not None: - final_eq_coeffs = np.zeros((self.Nants_telescope, final_nchan), dtype=float) + final_eq_coeffs = np.zeros((self.telescope.Nants, final_nchan), dtype=float) if not self.metadata_only: final_shape_tuple = (self.Nblts, final_nchan, self.Npols) @@ -9230,7 +9088,7 @@ def frequency_average( if self.eq_coeffs is not None: final_eq_coeffs[:, this_final_reg_inds] = ( self.eq_coeffs[:, regular_inds] - .reshape((self.Nants_telescope, n_final_chan_reg, n_chan_to_avg)) + .reshape((self.telescope.Nants, n_final_chan_reg, n_chan_to_avg)) .mean(axis=2) ) if this_ragged: diff --git a/pyuvdata/uvdata/uvfits.py b/pyuvdata/uvdata/uvfits.py index dc10e42680..998c14eba7 100644 --- a/pyuvdata/uvdata/uvfits.py +++ b/pyuvdata/uvdata/uvfits.py @@ -9,10 +9,19 @@ import numpy as np from astropy import constants as const +from astropy import units +from astropy.coordinates import EarthLocation from astropy.io import fits from astropy.time import Time from docstring_parser import DocstringStyle +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False + from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning @@ -60,20 +69,12 @@ def _get_parameter_data( # angles in uvfits files are stored in degrees, so convert to radians self.lst_array = np.deg2rad(vis_hdu.data.par("lst")) if run_check_acceptability: - (latitude, longitude, altitude) = ( - self.telescope_location_lat_lon_alt_degrees + uvutils.check_lsts_against_times( + jd_array=self.time_array, + lst_array=self.lst_array, + telescope_loc=self.telescope.location, + lst_tols=(0, uvutils.LST_RAD_TOL), ) - uvutils.check_lsts_against_times( - jd_array=self.time_array, - lst_array=self.lst_array, - latitude=latitude, - longitude=longitude, - altitude=altitude, - lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, - ) - else: proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library @@ -468,12 +469,12 @@ def read_uvfits( self.polarization_array = np.int32(uvutils._fits_gethduaxis(vis_hdu, 3)) # other info -- not required but frequently used - self.telescope_name = vis_hdr.pop("TELESCOP", None) - self.instrument = vis_hdr.pop("INSTRUME", None) + self.telescope.name = vis_hdr.pop("TELESCOP", None) + self.telescope.instrument = vis_hdr.pop("INSTRUME", None) latitude_degrees = vis_hdr.pop("LAT", None) longitude_degrees = vis_hdr.pop("LON", None) altitude = vis_hdr.pop("ALT", None) - self.x_orientation = vis_hdr.pop("XORIENT", None) + self.telescope.x_orientation = vis_hdr.pop("XORIENT", None) blt_order_str = vis_hdr.pop("BLTORDER", None) if blt_order_str is not None: self.blt_order = tuple(blt_order_str.split(", ")) @@ -553,8 +554,8 @@ def read_uvfits( ant_hdu = hdu_list[hdunames["AIPS AN"]] # stuff in the header - if self.telescope_name is None: - self.telescope_name = ant_hdu.header["ARRNAM"] + if self.telescope.name is None: + self.telescope.name = ant_hdu.header["ARRNAM"] self.gst0 = ant_hdu.header["GSTIA0"] self.rdate = ant_hdu.header["RDATE"] @@ -567,10 +568,11 @@ def read_uvfits( self.timesys = ant_hdu.header["TIMSYS"] prefer_lat_lon_alt = False + ellipsoid = None if "FRAME" in ant_hdu.header.keys(): if ant_hdu.header["FRAME"] == "ITRF": # uvfits uses ITRF, astropy uses itrs. They are the same. - self.telescope._location.frame = "itrs" + telescope_frame = "itrs" elif ant_hdu.header["FRAME"] == "????": # default to itrs, but use the lat/lon/alt to set the location # if they are available. @@ -580,7 +582,7 @@ def read_uvfits( "may lead to other warnings or errors." ) prefer_lat_lon_alt = True - self.telescope._location.frame = "itrs" + telescope_frame = "itrs" else: telescope_frame = ant_hdu.header["FRAME"].lower() if telescope_frame not in ["itrs", "mcmf"]: @@ -588,19 +590,24 @@ def read_uvfits( f"Telescope frame in file is {telescope_frame}. " "Only 'itrs' and 'mcmf' are currently supported." ) - self.telescope._location.frame = telescope_frame - if ( - telescope_frame != "itrs" - and "ELLIPSOI" in ant_hdu.header.keys() - ): - self.telescope._location.ellipsoid = ant_hdu.header["ELLIPSOI"] + if telescope_frame == "mcmf": + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with " + "MCMF frames." + ) + + if "ELLIPSOI" in ant_hdu.header.keys(): + ellipsoid = ant_hdu.header["ELLIPSOI"] + else: + ellipsoid = "SPHERE" else: warnings.warn( "Required Antenna keyword 'FRAME' not set; " "Assuming frame is 'ITRF'." ) - self.telescope._location.frame = "itrs" + telescope_frame = "itrs" # get telescope location and antenna positions. # VLA incorrectly sets ARRAYX/ARRAYY/ARRAYZ to 0, and puts array center @@ -613,9 +620,9 @@ def read_uvfits( x_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 0]) y_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 1]) z_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 2]) - self.antenna_positions = ant_hdu.data.field("STABXYZ") - np.array( - [x_telescope, y_telescope, z_telescope] - ) + self.telescope.antenna_positions = ant_hdu.data.field( + "STABXYZ" + ) - np.array([x_telescope, y_telescope, z_telescope]) else: x_telescope = ant_hdu.header["ARRAYX"] @@ -627,11 +634,11 @@ def read_uvfits( rot_ecef_positions = ant_hdu.data.field("STABXYZ") _, longitude, altitude = uvutils.LatLonAlt_from_XYZ( np.array([x_telescope, y_telescope, z_telescope]), - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + frame=telescope_frame, + ellipsoid=ellipsoid, check_acceptability=run_check_acceptability, ) - self.antenna_positions = uvutils.ECEF_from_rotECEF( + self.telescope.antenna_positions = uvutils.ECEF_from_rotECEF( rot_ecef_positions, longitude ) @@ -640,19 +647,37 @@ def read_uvfits( and longitude_degrees is not None and altitude is not None ): - self.telescope_location_lat_lon_alt_degrees = ( - latitude_degrees, - longitude_degrees, - altitude, - ) + if telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude_degrees * units.deg, + lon=longitude_degrees * units.deg, + height=altitude * units.m, + ) + else: + self.telescope.location = MoonLocation.from_selenodetic( + lat=latitude_degrees * units.deg, + lon=longitude_degrees * units.deg, + height=altitude * units.m, + ellipsoid=self.ellipsoid, + ) else: - self.telescope_location = np.array( - [x_telescope, y_telescope, z_telescope] - ) + if telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geocentric( + x=x_telescope * units.m, + y=y_telescope * units.m, + z=z_telescope * units.m, + ) + else: + self.telescope.location = MoonLocation.from_selenocentric( + x=x_telescope * units.m, + y=y_telescope * units.m, + z=z_telescope * units.m, + ) + self.telescope.location.ellipsoid = ellipsoid # stuff in columns ant_names = ant_hdu.data.field("ANNAME").tolist() - self.antenna_names = [] + self.telescope.antenna_names = [] for name in ant_names: # Sometimes CASA writes antnames as bytes not strings. # If the ant name is shorter than 8 characters, the trailing @@ -669,17 +694,17 @@ def read_uvfits( .replace("\x07", "") .replace("!", "") ) - self.antenna_names.append(ant_name_str) + self.telescope.antenna_names.append(ant_name_str) # Note: we no longer subtract one to get to 0-indexed values # rather than 1-indexed values. Antenna numbers are not indices # but are unique to each antenna. - self.antenna_numbers = ant_hdu.data.field("NOSTA") + self.telescope.antenna_numbers = ant_hdu.data.field("NOSTA") - self.Nants_telescope = len(self.antenna_numbers) + self.telescope.Nants = len(self.telescope.antenna_numbers) if "DIAMETER" in ant_hdu.columns.names: - self.antenna_diameters = ant_hdu.data.field("DIAMETER") + self.telescope.antenna_diameters = ant_hdu.data.field("DIAMETER") try: self.set_telescope_params( @@ -1031,7 +1056,7 @@ def write_uvfits( int_time_array = self.integration_time # If using MIRIAD convention, we need 1-indexed data - ant_nums_use = copy.copy(self.antenna_numbers) + ant_nums_use = copy.copy(self.telescope.antenna_numbers) ant1_array_use = copy.copy(self.ant_1_array) ant2_array_use = copy.copy(self.ant_2_array) if use_miriad_convention: @@ -1219,11 +1244,12 @@ def write_uvfits( hdu.header["BZERO "] = 0.0 hdu.header["OBJECT "] = name_use - hdu.header["TELESCOP"] = self.telescope_name - hdu.header["LAT "] = self.telescope_location_lat_lon_alt_degrees[0] - hdu.header["LON "] = self.telescope_location_lat_lon_alt_degrees[1] - hdu.header["ALT "] = self.telescope_location_lat_lon_alt[2] - hdu.header["INSTRUME"] = self.instrument + hdu.header["TELESCOP"] = self.telescope.name + lat, lon, alt = self.telescope.location_lat_lon_alt_degrees + hdu.header["LAT "] = lat + hdu.header["LON "] = lon + hdu.header["ALT "] = alt + hdu.header["INSTRUME"] = self.telescope.instrument if self.Nphase == 1: hdu.header["EPOCH "] = float(phase_dict["cat_epoch"]) # TODO: This is a keyword that should at some point get added for velocity @@ -1253,8 +1279,8 @@ def write_uvfits( hdu.header["RADESYS"] = frame break - if self.x_orientation is not None: - hdu.header["XORIENT"] = self.x_orientation + if self.telescope.x_orientation is not None: + hdu.header["XORIENT"] = self.telescope.x_orientation if self.blt_order is not None: blt_order_str = ", ".join(self.blt_order) @@ -1287,24 +1313,25 @@ def write_uvfits( hdu.header[keyword] = value # ADD the ANTENNA table - staxof = np.zeros(self.Nants_telescope) + staxof = np.zeros(self.telescope.Nants) # 0 specifies alt-az, 6 would specify a phased array - mntsta = np.zeros(self.Nants_telescope) + mntsta = np.zeros(self.telescope.Nants) # beware, X can mean just about anything - poltya = np.full((self.Nants_telescope), "X", dtype=np.object_) - polaa = [90.0] + np.zeros(self.Nants_telescope) - poltyb = np.full((self.Nants_telescope), "Y", dtype=np.object_) - polab = [0.0] + np.zeros(self.Nants_telescope) + poltya = np.full((self.telescope.Nants), "X", dtype=np.object_) + polaa = [90.0] + np.zeros(self.telescope.Nants) + poltyb = np.full((self.telescope.Nants), "Y", dtype=np.object_) + polab = [0.0] + np.zeros(self.telescope.Nants) - col1 = fits.Column(name="ANNAME", format="8A", array=self.antenna_names) + col1 = fits.Column( + name="ANNAME", format="8A", array=self.telescope.antenna_names + ) # AIPS memo #117 says that antenna_positions should be relative to # the array center, but in a rotated ECEF frame so that the x-axis # goes through the local meridian. - longitude = self.telescope_location_lat_lon_alt[1] rot_ecef_positions = uvutils.rotECEF_from_ECEF( - self.antenna_positions, longitude + self.telescope.antenna_positions, self.telescope.location.lon.rad ) col2 = fits.Column(name="STABXYZ", format="3D", array=rot_ecef_positions) # col3 = fits.Column(name="ORBPARAM", format="0D", array=Norb) @@ -1321,9 +1348,9 @@ def write_uvfits( # The commented out entires are up above to help check for consistency with the # UVFITS format. ORBPARAM, POLCALA, and POLCALB are all technically required, # but are all of zero length. Added here to help with debugging. - if self.antenna_diameters is not None: + if self.telescope.antenna_diameters is not None: col12 = fits.Column( - name="DIAMETER", format="1E", array=self.antenna_diameters + name="DIAMETER", format="1E", array=self.telescope.antenna_diameters ) col_list.append(col12) @@ -1335,9 +1362,10 @@ def write_uvfits( ant_hdu.header["EXTVER"] = 1 # write XYZ coordinates - ant_hdu.header["ARRAYX"] = self.telescope_location[0] - ant_hdu.header["ARRAYY"] = self.telescope_location[1] - ant_hdu.header["ARRAYZ"] = self.telescope_location[2] + tel_x, tel_y, tel_z = self.telescope._location.xyz() + ant_hdu.header["ARRAYX"] = tel_x + ant_hdu.header["ARRAYY"] = tel_y + ant_hdu.header["ARRAYZ"] = tel_z if self.telescope._location.frame == "itrs": # uvfits uses "ITRF" rather than "ITRS". They are the same thing. ant_hdu.header["FRAME"] = "ITRF" @@ -1381,7 +1409,7 @@ def write_uvfits( '"UTC" time system files are supported'.format(tsys=self.timesys) ) ant_hdu.header["TIMESYS"] = "UTC" - ant_hdu.header["ARRNAM"] = self.telescope_name + ant_hdu.header["ARRNAM"] = self.telescope.name ant_hdu.header["NO_IF"] = self.Nspws # Note the value below is basically 360 deg x num of sidereal days in a year / # num of soalr days in a year. diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index 2d4dbadaad..899d8b8295 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -86,7 +86,7 @@ class FastUVH5Meta(hdf5_utils.HDF5Meta): Library used for calculating the LSTs. Allowed options are 'erfa' (which uses the pyERFA), 'novas' (which uses the python-novas library), and 'astropy' (which uses the astropy utilities). Default is erfa - unless the telescope_location frame is MCMF (on the moon), in which case the + unless the telescope location frame is MCMF (on the moon), in which case the default is astropy. Notes @@ -337,14 +337,10 @@ def lst_array(self) -> np.ndarray: if "lst_array" in h: return h["lst_array"][:] else: - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees lst_array = uvutils.get_lst_for_time( jd_array=self.time_array, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope_location_obj, astrometry_library=self._astrometry_library, - frame=self.telescope_frame, ) return lst_array @@ -476,7 +472,7 @@ def to_uvdata( Library used for calculating the LSTs. Allowed options are 'erfa' (which uses the pyERFA), 'novas' (which uses the python-novas library), and 'astropy' (which uses the astropy utilities). Default is erfa - unless the telescope_location frame is MCMF (on the moon), in which case the + unless the telescope location frame is MCMF (on the moon), in which case the default is astropy. """ @@ -529,29 +525,18 @@ def _read_header_with_fast_meta( # First, get the things relevant for setting LSTs, so that can be run in the # background if desired. self.time_array = obj.time_array - # must set the frame before setting the location using lat/lon/alt - self.telescope._location.frame = obj.telescope_frame - if self.telescope._location.frame == "mcmf": - self.telescope._location.ellipsoid = obj.ellipsoid - self.telescope_location_lat_lon_alt_degrees = ( - obj.telescope_location_lat_lon_alt_degrees - ) + self.telescope.location = obj.telescope_location_obj if "lst_array" in obj.header: self.lst_array = obj.header["lst_array"][:] proc = None if run_check_acceptability: - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) else: proc = self.set_lsts_from_time_array( @@ -560,8 +545,6 @@ def _read_header_with_fast_meta( # Required parameters for attr in [ - "instrument", - "telescope_name", "history", "vis_units", "Nfreqs", @@ -571,10 +554,6 @@ def _read_header_with_fast_meta( "Nblts", "Nbls", "Nants_data", - "Nants_telescope", - "antenna_names", - "antenna_numbers", - "antenna_positions", "ant_1_array", "ant_2_array", "phase_center_id_array", @@ -592,6 +571,21 @@ def _read_header_with_fast_meta( except AttributeError as e: raise KeyError(str(e)) from e + # Required telescope parameters + telescope_attrs = { + "instrument": "instrument", + "telescope_name": "name", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + } + for attr, tel_attr in telescope_attrs.items(): + try: + setattr(self.telescope, tel_attr, getattr(obj, attr)) + except AttributeError as e: + raise KeyError(str(e)) from e + # check this as soon as we have the inputs if self.freq_array.ndim == 1: arr_shape_msg = ( @@ -622,9 +616,7 @@ def _read_header_with_fast_meta( "gst0", "rdate", "timesys", - "x_orientation", "blt_order", - "antenna_diameters", "uvplane_reference_time", "eq_coeffs", "eq_coeffs_convention", @@ -640,6 +632,17 @@ def _read_header_with_fast_meta( except AttributeError: pass + # Optional telescope parameters + telescope_attrs = { + "x_orientation": "x_orientation", + "antenna_diameters": "antenna_diameters", + } + for attr, tel_attr in telescope_attrs.items(): + try: + setattr(self.telescope, tel_attr, getattr(obj, attr)) + except AttributeError: + pass + if self.blt_order is not None: self._blt_order.form = (len(self.blt_order),) @@ -1217,22 +1220,23 @@ def _write_header(self, header): header["telescope_frame"] = np.string_(self.telescope._location.frame) if self.telescope._location.frame == "mcmf": header["ellipsoid"] = self.telescope._location.ellipsoid - header["latitude"] = self.telescope_location_lat_lon_alt_degrees[0] - header["longitude"] = self.telescope_location_lat_lon_alt_degrees[1] - header["altitude"] = self.telescope_location_lat_lon_alt_degrees[2] - header["telescope_name"] = np.string_(self.telescope_name) - header["instrument"] = np.string_(self.instrument) + lat, lon, alt = self.telescope.location_lat_lon_alt_degrees + header["latitude"] = lat + header["longitude"] = lon + header["altitude"] = alt + header["telescope_name"] = np.string_(self.telescope.name) + header["instrument"] = np.string_(self.telescope.instrument) # write out required UVParameters header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.Nants_telescope + header["Nants_telescope"] = self.telescope.Nants header["Nbls"] = self.Nbls header["Nblts"] = self.Nblts header["Nfreqs"] = self.Nfreqs header["Npols"] = self.Npols header["Nspws"] = self.Nspws header["Ntimes"] = self.Ntimes - header["antenna_numbers"] = self.antenna_numbers + header["antenna_numbers"] = self.telescope.antenna_numbers header["uvw_array"] = self.uvw_array header["vis_units"] = np.string_(self.vis_units) header["channel_width"] = self.channel_width @@ -1244,10 +1248,12 @@ def _write_header(self, header): header["spw_array"] = self.spw_array header["ant_1_array"] = self.ant_1_array header["ant_2_array"] = self.ant_2_array - header["antenna_positions"] = self.antenna_positions + header["antenna_positions"] = self.telescope.antenna_positions header["flex_spw"] = self.flex_spw # handle antenna_names; works for lists or arrays - header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes") + header["antenna_names"] = np.asarray( + self.telescope.antenna_names, dtype="bytes" + ) # write out phasing information # Write out the catalog, if available @@ -1280,12 +1286,12 @@ def _write_header(self, header): header["rdate"] = np.string_(self.rdate) if self.timesys is not None: header["timesys"] = np.string_(self.timesys) - if self.x_orientation is not None: - header["x_orientation"] = np.string_(self.x_orientation) + if self.telescope.x_orientation is not None: + header["x_orientation"] = np.string_(self.telescope.x_orientation) if self.blt_order is not None: header["blt_order"] = np.string_(", ".join(self.blt_order)) - if self.antenna_diameters is not None: - header["antenna_diameters"] = self.antenna_diameters + if self.telescope.antenna_diameters is not None: + header["antenna_diameters"] = self.telescope.antenna_diameters if self.uvplane_reference_time is not None: header["uvplane_reference_time"] = self.uvplane_reference_time if self.eq_coeffs is not None: From 9746f1f83e0e20a4e63760e919ac3590b75c60bd Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 19 Apr 2024 16:55:30 -0700 Subject: [PATCH 10/59] make new telescope.location work on uvcal --- pyuvdata/ms_utils.py | 82 ++++++++- pyuvdata/uvcal/calfits.py | 106 +++++++---- pyuvdata/uvcal/calh5.py | 85 ++++----- pyuvdata/uvcal/fhd_cal.py | 58 ++++-- pyuvdata/uvcal/initializers.py | 8 +- pyuvdata/uvcal/ms_cal.py | 70 ++++---- pyuvdata/uvcal/tests/test_calfits.py | 37 ++-- pyuvdata/uvcal/tests/test_calh5.py | 39 ++-- pyuvdata/uvcal/tests/test_fhd_cal.py | 2 +- pyuvdata/uvcal/tests/test_initializers.py | 30 ++-- pyuvdata/uvcal/tests/test_ms_cal.py | 17 +- pyuvdata/uvcal/tests/test_uvcal.py | 210 ++++++++++++---------- pyuvdata/uvcal/uvcal.py | 194 +++----------------- pyuvdata/uvdata/ms.py | 68 +------ 14 files changed, 484 insertions(+), 522 deletions(-) diff --git a/pyuvdata/ms_utils.py b/pyuvdata/ms_utils.py index 843bc8dea3..b234bab2cb 100644 --- a/pyuvdata/ms_utils.py +++ b/pyuvdata/ms_utils.py @@ -7,12 +7,20 @@ import warnings import numpy as np +from astropy.coordinates import EarthLocation from astropy.time import Time -from . import __version__ +from . import __version__, telescopes from . import utils as uvutils from .uvdata.uvdata import reporting_request +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False + no_casa_message = ( "casacore is not installed but is required for measurement set functionality" ) @@ -2098,3 +2106,75 @@ def init_ms_cal_file(filename, delay_table=False): ms.putkeyword("VisCal", "unknown") ms.putkeyword("PolBasis", "unknown") ms.putkeyword("CASA_Version", "unknown") + + +def get_ms_telescope_location(*, tb_ant_dict, obs_dict): + """ + Get the telescope location object. + + Parameters + ---------- + tb_ant_dict : dict + dict returned by `read_ms_antenna` + obs_dict : dict + dict returned by `read_ms_observation` + + """ + xyz_telescope_frame = tb_ant_dict["telescope_frame"] + xyz_telescope_ellipsoid = tb_ant_dict["telescope_ellipsoid"] + + # check to see if a TELESCOPE_LOCATION column is present in the observation + # table. This is non-standard, but inserted by pyuvdata + if ( + "telescope_location" not in obs_dict + and obs_dict["telescope_name"] in telescopes.known_telescopes() + ): + # get it from known telescopes + telescope_obj = telescopes.Telescope.from_known_telescopes( + obs_dict["telescope_name"] + ) + warnings.warn( + "Setting telescope_location to value in known_telescopes for " + f"{obs_dict['telescope_name']}." + ) + return telescope_obj.location + else: + if xyz_telescope_frame not in ["itrs", "mcmf"]: + raise ValueError( + f"Telescope frame in file is {xyz_telescope_frame}. " + "Only 'itrs' and 'mcmf' are currently supported." + ) + if xyz_telescope_frame == "mcmf": + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with " "MCMF frames." + ) + + if xyz_telescope_ellipsoid is None: + xyz_telescope_ellipsoid = "SPHERE" + + if "telescope_location" in obs_dict: + if xyz_telescope_frame == "itrs": + return EarthLocation.from_geocentric( + *np.squeeze(obs_dict["telescope_location"]), unit="m" + ) + else: + loc = MoonLocation.from_selenocentric( + *np.squeeze(obs_dict["telescope_location"]), unit="m" + ) + loc.ellipsoid = xyz_telescope_ellipsoid + return loc + else: + # Set it to be the mean of the antenna positions (this is not ideal!) + if xyz_telescope_frame == "itrs": + return EarthLocation.from_geocentric( + *np.array(np.mean(tb_ant_dict["antenna_positions"], axis=0)), + unit="m", + ) + else: + loc = MoonLocation.from_selenocentric( + *np.array(np.mean(tb_ant_dict["antenna_positions"], axis=0)), + unit="m", + ) + loc.ellipsoid = xyz_telescope_ellipsoid + return loc diff --git a/pyuvdata/uvcal/calfits.py b/pyuvdata/uvcal/calfits.py index 112cab62af..ed8b3693ab 100644 --- a/pyuvdata/uvcal/calfits.py +++ b/pyuvdata/uvcal/calfits.py @@ -6,9 +6,18 @@ import warnings import numpy as np +from astropy import units +from astropy.coordinates import EarthLocation from astropy.io import fits from docstring_parser import DocstringStyle +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False + from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvcal import UVCal, _future_array_shapes_warning @@ -204,17 +213,18 @@ def write_calfits( sechdr["EXTNAME"] = "FLAGS" # Conforming to fits format prihdr["SIMPLE"] = True - prihdr["TELESCOP"] = self.telescope_name - prihdr["ARRAYX"] = self.telescope_location[0] - prihdr["ARRAYY"] = self.telescope_location[1] - prihdr["ARRAYZ"] = self.telescope_location[2] + prihdr["TELESCOP"] = self.telescope.name + tel_x, tel_y, tel_z = self.telescope._location.xyz() + prihdr["ARRAYX"] = tel_x + prihdr["ARRAYY"] = tel_y + prihdr["ARRAYZ"] = tel_z prihdr["FRAME"] = self.telescope._location.frame if self.telescope._location.ellipsoid is not None: # use ELLIPSOI because of FITS 8 character limit for header items prihdr["ELLIPSOI"] = self.telescope._location.ellipsoid - prihdr["LAT"] = self.telescope_location_lat_lon_alt_degrees[0] - prihdr["LON"] = self.telescope_location_lat_lon_alt_degrees[1] - prihdr["ALT"] = self.telescope_location_lat_lon_alt[2] + prihdr["LAT"] = self.telescope.location.lat.rad + prihdr["LON"] = self.telescope.location.lon.rad + prihdr["ALT"] = self.telescope.location.height.to("m").value prihdr["GNCONVEN"] = self.gain_convention prihdr["CALTYPE"] = self.cal_type prihdr["CALSTYLE"] = self.cal_style @@ -250,7 +260,7 @@ def write_calfits( else: prihdr["CHWIDTH"] = self.channel_width - prihdr["XORIENT"] = self.x_orientation + prihdr["XORIENT"] = self.telescope.x_orientation if self.future_array_shapes and self.freq_range is not None: freq_range_use = self.freq_range[0, :] else: @@ -266,8 +276,8 @@ def write_calfits( if self.observer: prihdr["OBSERVER"] = self.observer - if self.instrument: - prihdr["INSTRUME"] = self.instrument + if self.telescope.instrument: + prihdr["INSTRUME"] = self.telescope.instrument if self.git_origin_cal: prihdr["ORIGCAL"] = self.git_origin_cal @@ -512,23 +522,31 @@ def write_calfits( prihdu = fits.PrimaryHDU(data=pridata, header=prihdr) # ant HDU - col1 = fits.Column(name="ANTNAME", format="8A", array=self.antenna_names) - col2 = fits.Column(name="ANTINDEX", format="D", array=self.antenna_numbers) - if self.Nants_data == self.Nants_telescope: + col1 = fits.Column( + name="ANTNAME", format="8A", array=self.telescope.antenna_names + ) + col2 = fits.Column( + name="ANTINDEX", format="D", array=self.telescope.antenna_numbers + ) + if self.Nants_data == self.telescope.Nants: col3 = fits.Column(name="ANTARR", format="D", array=self.ant_array) else: # ant_array is shorter than the other columns. # Pad the extra rows with -1s. Need to undo on read. - nants_add = self.Nants_telescope - self.Nants_data + nants_add = self.telescope.Nants - self.Nants_data ant_array_use = np.append( self.ant_array, np.zeros(nants_add, dtype=np.int64) - 1 ) col3 = fits.Column(name="ANTARR", format="D", array=ant_array_use) - col4 = fits.Column(name="ANTXYZ", format="3D", array=self.antenna_positions) + col4 = fits.Column( + name="ANTXYZ", format="3D", array=self.telescope.antenna_positions + ) collist = [col1, col2, col3, col4] - if self.antenna_diameters is not None: + if self.telescope.antenna_diameters is not None: collist.append( - fits.Column(name="ANTDIAM", format="D", array=self.antenna_diameters) + fits.Column( + name="ANTDIAM", format="D", array=self.telescope.antenna_diameters + ) ) cols = fits.ColDefs(collist) ant_hdu = fits.BinTableHDU.from_columns(cols) @@ -571,10 +589,12 @@ def read_calfits( hdunames = uvutils._fits_indexhdus(fname) anthdu = fname[hdunames["ANTENNAS"]] - self.Nants_telescope = anthdu.header["NAXIS2"] + self.telescope.Nants = anthdu.header["NAXIS2"] antdata = anthdu.data - self.antenna_names = np.array(list(map(str, antdata["ANTNAME"]))) - self.antenna_numbers = np.array(list(map(int, antdata["ANTINDEX"]))) + self.telescope.antenna_names = np.array(list(map(str, antdata["ANTNAME"]))) + self.telescope.antenna_numbers = np.array( + list(map(int, antdata["ANTINDEX"])) + ) self.ant_array = np.array(list(map(int, antdata["ANTARR"]))) if np.min(self.ant_array) < 0: # ant_array was shorter than the other columns, so it was @@ -583,16 +603,16 @@ def read_calfits( self.ant_array = self.ant_array[np.where(self.ant_array >= 0)[0]] if "ANTXYZ" in antdata.names: - self.antenna_positions = antdata["ANTXYZ"] + self.telescope.antenna_positions = antdata["ANTXYZ"] if "ANTDIAM" in antdata.names: - self.antenna_diameters = antdata["ANTDIAM"] + self.telescope.antenna_diameters = antdata["ANTDIAM"] self.channel_width = hdr.pop("CHWIDTH") self.integration_time = hdr.pop("INTTIME") - self.telescope_name = hdr.pop("TELESCOP") + self.telescope.name = hdr.pop("TELESCOP") - self.x_orientation = hdr.pop("XORIENT") + self.telescope.x_orientation = hdr.pop("XORIENT") x_telescope = hdr.pop("ARRAYX", None) y_telescope = hdr.pop("ARRAYY", None) @@ -603,7 +623,12 @@ def read_calfits( telescope_frame = hdr.pop("FRAME", "itrs") ellipsoid = None - if telescope_frame != "itrs": + if telescope_frame == "mcmf": + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with " + "MCMF frames." + ) ellipsoid = hdr.pop("ELLIPSOI", "SPHERE") if ( @@ -611,14 +636,29 @@ def read_calfits( and y_telescope is not None and z_telescope is not None ): - self.telescope_location = np.array( - [x_telescope, y_telescope, z_telescope] - ) + if telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geocentric( + x_telescope, y_telescope, z_telescope, unit="m" + ) + else: + self.telescope.location = MoonLocation.from_selenocentric( + x_telescope, y_telescope, z_telescope, unit="m" + ) + self.telescope.location.ellipsoid = ellipsoid + elif lat is not None and lon is not None and alt is not None: - self.telescope_location_lat_lon_alt_degrees = (lat, lon, alt) + if telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geodetic( + lat=lat * units.rad, lon=lon * units.rad, height=alt * units.m + ) + else: + self.telescope.location = MoonLocation.from_selenodetic( + lat=lat * units.rad, + lon=lon * units.rad, + height=alt * units.m, + ellipsoid=ellipsoid, + ) - self.telescope._location.frame = telescope_frame - self.telescope._location.ellipsoid = ellipsoid try: self.set_telescope_params() except ValueError as ve: @@ -660,7 +700,7 @@ def read_calfits( self.diffuse_model = hdr.pop("DIFFUSE", None) self.observer = hdr.pop("OBSERVER", None) - self.instrument = hdr.pop("INSTRUME", None) + self.telescope.instrument = hdr.pop("INSTRUME", None) self.git_origin_cal = hdr.pop("ORIGCAL", None) self.git_hash_cal = hdr.pop("HASHCAL", None) @@ -681,7 +721,7 @@ def read_calfits( else: self.time_array = main_hdr_time_array - if self.telescope_location is not None: + if self.telescope.location is not None: proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library ) diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index bcf82ab103..c11bf15846 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -181,14 +181,7 @@ def _read_header( # First, get the things relevant for setting LSTs, so that can be run in the # background if desired. - # must set the frame before setting the location using lat/lon/alt - self.telescope._location.frame = meta.telescope_frame - if self.telescope._location.frame == "mcmf": - self.telescope._location.ellipsoid = meta.ellipsoid - - self.telescope_location_lat_lon_alt_degrees = ( - meta.telescope_location_lat_lon_alt_degrees - ) + self.telescope.location = meta.telescope_location_obj if "time_array" in meta.header: self.time_array = meta.time_array @@ -211,17 +204,12 @@ def _read_header( # Required parameters for attr in [ - "telescope_name", "history", "Nfreqs", "Njones", "Nspws", "Ntimes", "Nants_data", - "Nants_telescope", - "antenna_names", - "antenna_numbers", - "antenna_positions", "ant_array", "integration_time", "spw_array", @@ -230,13 +218,27 @@ def _read_header( "cal_type", "gain_convention", "wide_band", - "x_orientation", ]: try: setattr(self, attr, getattr(meta, attr)) except AttributeError as e: raise KeyError(str(e)) from e + # Required telescope parameters + telescope_attrs = { + "x_orientation": "x_orientation", + "telescope_name": "name", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + } + for attr, tel_attr in telescope_attrs.items(): + try: + setattr(self.telescope, tel_attr, getattr(meta, attr)) + except AttributeError as e: + raise KeyError(str(e)) from e + self._set_future_array_shapes() if self.wide_band: self._set_wide_band() @@ -248,7 +250,6 @@ def _read_header( # Optional parameters for attr in [ - "antenna_diameters", "channel_width", "flex_spw_id_array", "flex_jones_array", @@ -270,13 +271,23 @@ def _read_header( "scan_number_array", "sky_catalog", "sky_field", - "instrument", ]: try: setattr(self, attr, getattr(meta, attr)) except AttributeError: pass + # Optional telescope parameters + telescope_attrs = { + "instrument": "instrument", + "antenna_diameters": "antenna_diameters", + } + for attr, tel_attr in telescope_attrs.items(): + try: + setattr(self.telescope, tel_attr, getattr(meta, attr)) + except AttributeError: + pass + # set telescope params try: self.set_telescope_params() @@ -288,28 +299,19 @@ def _read_header( proc.join() if run_check_acceptability: - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees if self.time_array is not None: uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) if self.time_range is not None: uvutils.check_lsts_against_times( jd_array=self.time_range, lst_array=self.lst_range, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=(0, uvutils.LST_RAD_TOL), - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) def _get_data( @@ -732,27 +734,30 @@ def _write_header(self, header): header["telescope_frame"] = np.string_(self.telescope._location.frame) if self.telescope._location.frame == "mcmf": header["ellipsoid"] = self.telescope._location.ellipsoid - header["latitude"] = self.telescope_location_lat_lon_alt_degrees[0] - header["longitude"] = self.telescope_location_lat_lon_alt_degrees[1] - header["altitude"] = self.telescope_location_lat_lon_alt_degrees[2] - header["telescope_name"] = np.string_(self.telescope_name) + lat, lon, alt = self.telescope.location_lat_lon_alt_degrees + header["latitude"] = lat + header["longitude"] = lon + header["altitude"] = alt + header["telescope_name"] = np.string_(self.telescope.name) # write out required UVParameters header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.Nants_telescope + header["Nants_telescope"] = self.telescope.Nants header["Nfreqs"] = self.Nfreqs header["Njones"] = self.Njones header["Nspws"] = self.Nspws header["Ntimes"] = self.Ntimes - header["antenna_numbers"] = self.antenna_numbers + header["antenna_numbers"] = self.telescope.antenna_numbers header["integration_time"] = self.integration_time header["jones_array"] = self.jones_array header["spw_array"] = self.spw_array header["ant_array"] = self.ant_array - header["antenna_positions"] = self.antenna_positions + header["antenna_positions"] = self.telescope.antenna_positions # handle antenna_names; works for lists or arrays - header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes") - header["x_orientation"] = np.string_(self.x_orientation) + header["antenna_names"] = np.asarray( + self.telescope.antenna_names, dtype="bytes" + ) + header["x_orientation"] = np.string_(self.telescope.x_orientation) header["cal_type"] = np.string_(self.cal_type) header["cal_style"] = np.string_(self.cal_style) header["gain_convention"] = np.string_(self.gain_convention) @@ -777,8 +782,8 @@ def _write_header(self, header): if self.scan_number_array is not None: header["scan_number_array"] = self.scan_number_array - if self.antenna_diameters is not None: - header["antenna_diameters"] = self.antenna_diameters + if self.telescope.antenna_diameters is not None: + header["antenna_diameters"] = self.telescope.antenna_diameters if self.ref_antenna_array is not None: header["ref_antenna_array"] = self.ref_antenna_array @@ -823,8 +828,8 @@ def _write_header(self, header): this_group[key] = value # extra telescope-related parameters - if self.instrument is not None: - header["instrument"] = np.string_(self.instrument) + if self.telescope.instrument is not None: + header["instrument"] = np.string_(self.telescope.instrument) # write out extra keywords if it exists and has elements if self.extra_keywords: diff --git a/pyuvdata/uvcal/fhd_cal.py b/pyuvdata/uvcal/fhd_cal.py index ee1b58659a..d3c0cc7284 100644 --- a/pyuvdata/uvcal/fhd_cal.py +++ b/pyuvdata/uvcal/fhd_cal.py @@ -7,6 +7,8 @@ import warnings import numpy as np +from astropy import units +from astropy.coordinates import EarthLocation from docstring_parser import DocstringStyle from scipy.io import readsav @@ -112,7 +114,7 @@ def read_fhd_cal( (1, 2), ) - self.telescope_name = obs_data["instrument"][0].decode("utf8") + self.telescope.name = obs_data["instrument"][0].decode("utf8") latitude = np.deg2rad(float(obs_data["LAT"][0])) longitude = np.deg2rad(float(obs_data["LON"][0])) altitude = float(obs_data["ALT"][0]) @@ -129,7 +131,7 @@ def read_fhd_cal( + str(obs_data["ORIG_PHASEDEC"][0]) ) # For the MWA, this can sometimes be converted to EoR fields - if self.telescope_name.lower() == "mwa": + if self.telescope.name.lower() == "mwa": if np.isclose(obs_data["ORIG_PHASERA"][0], 0) and np.isclose( obs_data["ORIG_PHASEDEC"][0], -27 ): @@ -151,7 +153,7 @@ def read_fhd_cal( obs_tile_names = [ ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist() ] - if self.telescope_name.lower() == "mwa": + if self.telescope.name.lower() == "mwa": obs_tile_names = [ "Tile" + "0" * (3 - len(ant.strip())) + ant.strip() for ant in obs_tile_names @@ -159,7 +161,7 @@ def read_fhd_cal( layout_param_dict = get_fhd_layout_info( layout_file=layout_file, - telescope_name=self.telescope_name, + telescope_name=self.telescope.name, latitude=latitude, longitude=longitude, altitude=altitude, @@ -177,8 +179,22 @@ def read_fhd_cal( "timesys", "diameters", ] + + telescope_attrs = { + "telescope_location": "location", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + "diameters": "antenna_diameters", + } + for key, value in layout_param_dict.items(): - if key not in layout_params_to_ignore: + if key in layout_params_to_ignore: + continue + if key in telescope_attrs: + setattr(self.telescope, telescope_attrs[key], value) + else: setattr(self, key, value) else: @@ -187,22 +203,28 @@ def read_fhd_cal( "and antenna_names might be incorrect." ) - self.telescope_location_lat_lon_alt = (latitude, longitude, altitude) + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, + lon=longitude * units.rad, + height=altitude * units.m, + ) # FHD stores antenna numbers, not names, in the "TILE_NAMES" field - self.antenna_names = [ + self.telescope.antenna_names = [ ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist() ] - self.antenna_numbers = np.array([int(ant) for ant in self.antenna_names]) - if self.telescope_name.lower() == "mwa": - self.antenna_names = [ + self.telescope.antenna_numbers = np.array( + [int(ant) for ant in self.telescope.antenna_names] + ) + if self.telescope.name.lower() == "mwa": + self.telescope.antenna_names = [ "Tile" + "0" * (3 - len(ant.strip())) + ant.strip() - for ant in self.antenna_names + for ant in self.telescope.antenna_names ] - self.Nants_telescope = len(self.antenna_names) + self.telescope.Nants = len(self.telescope.antenna_names) - self.antenna_names = np.asarray(self.antenna_names) + self.telescope.antenna_names = np.asarray(self.telescope.antenna_names) - self.x_orientation = "east" + self.telescope.x_orientation = "east" try: self.set_telescope_params() @@ -210,7 +232,7 @@ def read_fhd_cal( warnings.warn(str(ve)) # need to make sure telescope location is defined properly before this call - if self.telescope_location is not None: + if self.telescope.location is not None: proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library ) @@ -255,7 +277,7 @@ def read_fhd_cal( # for calibration FHD includes all antennas in the antenna table, # regardless of whether or not they have data - self.Nants_data = len(self.antenna_names) + self.Nants_data = len(self.telescope.antenna_names) # get details from settings file keywords = [ @@ -397,7 +419,7 @@ def read_fhd_cal( for freq in flagged_freqs: self.flag_array[:, :, freq] = 1 - if self.telescope_name.lower() == "mwa": + if self.telescope.name.lower() == "mwa": self.ref_antenna_name = ( "Tile" + "0" * (3 - len(self.ref_antenna_name)) + self.ref_antenna_name ) @@ -436,7 +458,7 @@ def read_fhd_cal( # rather than (Nfreqs, Nants_data). This means the antenna array will # contain all antennas in the antenna table instead of only those # which had data in the original uvfits file - self.ant_array = self.antenna_numbers + self.ant_array = self.telescope.antenna_numbers self.extra_keywords["autoscal".upper()] = ( "[" + ", ".join(str(d) for d in auto_scale) + "]" diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index c8677cae63..3de740ff05 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -102,7 +102,7 @@ def new_uvcal( Telescope object containing the telescope-related metadata including telescope name and location, x_orientation and antenna names, numbers and positions. - telescope_location : ndarray of float or str + telescope_location : EarthLocation or MoonLocation object Telescope location as an astropy EarthLocation object or MoonLocation object. Not required or used if a Telescope object is passed to `telescope`. telescope_name : str @@ -197,7 +197,7 @@ def new_uvcal( if telescope is not None: antenna_numbers = telescope.antenna_numbers - telescope_location = telescope.location_obj + telescope_location = telescope.location else: antenna_positions, antenna_names, antenna_numbers = get_antenna_params( antenna_positions=antenna_positions, @@ -298,9 +298,10 @@ def new_uvcal( new_telescope = Telescope() new_telescope.name = telescope_name - new_telescope.location_obj = telescope_location + new_telescope.location = telescope_location new_telescope.antenna_names = antenna_names new_telescope.antenna_numbers = antenna_numbers + new_telescope.Nants = len(antenna_numbers) new_telescope.antenna_positions = antenna_positions new_telescope.x_orientation = x_orientation @@ -345,7 +346,6 @@ def new_uvcal( uvc._set_wide_band(wide_band) uvc.Nants_data = len(ant_array) - uvc.Nants_telescope = len(antenna_numbers) uvc.Nfreqs = len(freq_array) if freq_array is not None else 1 uvc.Nspws = len(spw_array) diff --git a/pyuvdata/uvcal/ms_cal.py b/pyuvdata/uvcal/ms_cal.py index 0c2e28f69a..0e3ef781a4 100644 --- a/pyuvdata/uvcal/ms_cal.py +++ b/pyuvdata/uvcal/ms_cal.py @@ -135,36 +135,24 @@ def read_ms_cal( obs_info = ms_utils.read_ms_observation(filepath) self.observer = obs_info["observer"] - self.telescope_name = obs_info["telescope_name"] - self.telescope._location.frame = ant_info["telescope_frame"] - self.telescope._location.ellipsoid = ant_info["telescope_ellipsoid"] - - # check to see if a TELESCOPE_LOCATION column is present in the observation - # table. This is non-standard, but inserted by pyuvdata - if "telescope_location" in obs_info: - self.telescope_location = obs_info["telescope_location"] - else: - # get it from known telescopes - try: - self.set_telescope_params() - except ValueError: - # If no telescope is found, the we will set the telescope position to be - # the mean of the antenna positions (this is not ideal!) - self.telescope_location = np.mean(ant_info["antenna_positions"], axis=0) - - self.antenna_names = ant_info["antenna_names"] - self.Nants_telescope = len(self.antenna_names) - self.antenna_numbers = ant_info["antenna_numbers"] + self.telescope.name = obs_info["telescope_name"] + self.telescope.location = ms_utils.get_ms_telescope_location( + tb_ant_dict=ant_info, obs_dict=obs_info + ) + + self.telescope.antenna_names = ant_info["antenna_names"] + self.telescope.Nants = len(self.telescope.antenna_names) + self.telescope.antenna_numbers = ant_info["antenna_numbers"] if all(ant_info["antenna_diameters"] > 0): - self.antenna_diameters = ant_info["antenna_diameters"] + self.telescope.antenna_diameters = ant_info["antenna_diameters"] # MS-format seems to want to preserve the blank entries in the gains tables # This looks to be the same for MS files. - self.ant_array = self.antenna_numbers - self.Nants_data = self.Nants_telescope + self.ant_array = self.telescope.antenna_numbers + self.Nants_data = self.telescope.Nants - self.antenna_positions = ant_info["antenna_positions"] + self.telescope.antenna_positions = ant_info["antenna_positions"] # Subtract off telescope location to get relative ECEF - self.antenna_positions -= self.telescope_location.reshape(1, 3) + self.telescope.antenna_positions -= self.telescope._location.xyz().reshape(1, 3) self.phase_center_catalog, field_id_map = ms_utils.read_ms_field( filepath, return_phase_center_catalog=True ) @@ -180,10 +168,10 @@ def read_ms_cal( # inline with the MS definition doc. In that case all the station names are # the same. Default to using what the MS definition doc specifies, unless # we read importuvfits in the history, or if the antenna column is not filled. - if self.Nants_telescope != len(np.unique(self.antenna_names)) or ( - "" in self.antenna_names + if self.telescope.Nants != len(np.unique(self.telescope.antenna_names)) or ( + "" in self.telescope.antenna_names ): - self.antenna_names = ant_info["station_names"] + self.telescope.antenna_names = ant_info["station_names"] spw_info = ms_utils.read_ms_spectral_window(filepath) @@ -291,7 +279,7 @@ def read_ms_cal( self.Ntimes = time_count # Make a map to things. - ant_dict = {ant: idx for idx, ant in enumerate(self.antenna_numbers)} + ant_dict = {ant: idx for idx, ant in enumerate(self.telescope.antenna_numbers)} cal_arr_shape = (self.Nants_data, nchan, self.Ntimes, self.Njones) ms_cal_soln = np.zeros( @@ -352,8 +340,8 @@ def read_ms_cal( refant = self.ref_antenna_array[0] self.ref_antenna_array = None try: - self.ref_antenna_name = self.antenna_names[ - np.where(self.antenna_numbers == refant)[0][0] + self.ref_antenna_name = self.telescope.antenna_names[ + np.where(self.telescope.antenna_numbers == refant)[0][0] ] except IndexError: if self.cal_style == "sky": @@ -385,15 +373,15 @@ def read_ms_cal( [field_id_map[idx] for idx in self.phase_center_id_array] ) - self.x_orientation = main_keywords.get("pyuvdata_xorient", None) - if self.x_orientation is None: + self.telescope.x_orientation = main_keywords.get("pyuvdata_xorient", None) + if self.telescope.x_orientation is None: if default_x_orientation is None: - self.x_orientation = "east" + self.telescope.x_orientation = "east" warnings.warn( 'Unknown x_orientation basis for solutions, assuming "east".' ) else: - self.x_orientation = default_x_orientation + self.telescope.x_orientation = default_x_orientation # Use if this is a delay soln if self.cal_type == "gain": @@ -501,8 +489,8 @@ def write_ms_cal(self, filename, clobber=False): if len(extra_copy) != 0: ms.putkeyword("pyuvdata_extra", extra_copy) - if self.x_orientation is not None: - ms.putkeyword("pyuvdata_xorient", self.x_orientation) + if self.telescope.x_orientation is not None: + ms.putkeyword("pyuvdata_xorient", self.telescope.x_orientation) if self.jones_array is not None: ms.putkeyword("pyuvdata_jones", self.jones_array) @@ -540,7 +528,7 @@ def write_ms_cal(self, filename, clobber=False): # For some reason, CASA seems to want to pad the main table with zero # entries for the "blank" antennas, similar to what's seen in the ANTENNA # table. So we'll calculate this up front for convenience. - Nants_casa = np.max(self.antenna_numbers) + 1 + Nants_casa = np.max(self.telescope.antenna_numbers) + 1 # Add all the rows we need up front, which will allow us to fill the # columns all in one shot. @@ -571,8 +559,10 @@ def write_ms_cal(self, filename, clobber=False): try: # Cast list here to deal w/ ndarrays refant = str( - self.antenna_numbers[ - list(self.antenna_names).index(self.ref_antenna_name) + self.telescope.antenna_numbers[ + list(self.telescope.antenna_names).index( + self.ref_antenna_name + ) ] ) except ValueError: diff --git a/pyuvdata/uvcal/tests/test_calfits.py b/pyuvdata/uvcal/tests/test_calfits.py index c5ecd379e6..4efd26dcde 100644 --- a/pyuvdata/uvcal/tests/test_calfits.py +++ b/pyuvdata/uvcal/tests/test_calfits.py @@ -65,8 +65,10 @@ def test_readwriteread( cal_in._total_quality_array.expected_shape(cal_in) ) # add instrument and antenna_diameters - cal_in.instrument = cal_in.telescope_name - cal_in.antenna_diameters = np.zeros((cal_in.Nants_telescope,), dtype=float) + 5.0 + cal_in.telescope.instrument = cal_in.telescope.name + cal_in.telescope.antenna_diameters = ( + np.zeros((cal_in.telescope.Nants,), dtype=float) + 5.0 + ) write_file = str(tmp_path / "outtest.fits") cal_in.write_calfits(write_file, clobber=True) @@ -103,30 +105,27 @@ def test_readwriteread( @pytest.mark.parametrize("selenoid", selenoids) def test_moon_loopback(tmp_path, gain_data, selenoid): pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + cal_in = gain_data - latitude, longitude, altitude = cal_in.telescope_location_lat_lon_alt enu_antpos = uvutils.ENU_from_ECEF( - (cal_in.antenna_positions + cal_in.telescope_location), - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame=cal_in.telescope._location.frame, - ellipsoid=cal_in.telescope._location.ellipsoid, + (cal_in.telescope.antenna_positions + cal_in.telescope._location.xyz()), + center_loc=cal_in.telescope.location, + ) + cal_in.telescope.location = MoonLocation.from_selenodetic( + lat=cal_in.telescope.location.lat, + lon=cal_in.telescope.location.lon, + height=cal_in.telescope.location.height, + ellipsoid=selenoid, ) - cal_in.telescope._location.frame = "mcmf" - cal_in.telescope._location.ellipsoid = selenoid - cal_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", - ellipsoid=selenoid, + enu=enu_antpos, center_loc=cal_in.telescope.location + ) + cal_in.telescope.antenna_positions = ( + new_full_antpos - cal_in.telescope._location.xyz() ) - cal_in.antenna_positions = new_full_antpos - cal_in.telescope_location cal_in.set_lsts_from_time_array() cal_in.check() diff --git a/pyuvdata/uvcal/tests/test_calh5.py b/pyuvdata/uvcal/tests/test_calh5.py index c0326a6bf3..1a70eb71ca 100644 --- a/pyuvdata/uvcal/tests/test_calh5.py +++ b/pyuvdata/uvcal/tests/test_calh5.py @@ -8,6 +8,7 @@ import h5py import numpy as np import pytest +from astropy.units import Quantity import pyuvdata.tests as uvtest import pyuvdata.utils as uvutils @@ -36,7 +37,7 @@ def test_calh5_write_read_loop_gain(gain_data, tmp_path, time_range, future_shap calobj._total_quality_array.expected_shape(calobj) ) # add instrument - calobj.instrument = calobj.telescope_name + calobj.telescope.instrument = calobj.telescope.name write_file = str(tmp_path / "outtest.calh5") calobj.write_calh5(write_file, clobber=True) @@ -116,30 +117,27 @@ def test_calh5_loop_bitshuffle(gain_data, tmp_path): @pytest.mark.parametrize("selenoid", selenoids) def test_calh5_loop_moon(tmp_path, gain_data, selenoid): pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + cal_in = gain_data - latitude, longitude, altitude = cal_in.telescope_location_lat_lon_alt enu_antpos = uvutils.ENU_from_ECEF( - (cal_in.antenna_positions + cal_in.telescope_location), - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame=cal_in.telescope._location.frame, - ellipsoid=cal_in.telescope._location.ellipsoid, + (cal_in.telescope.antenna_positions + cal_in.telescope._location.xyz()), + center_loc=cal_in.telescope.location, + ) + cal_in.telescope.location = MoonLocation.from_selenodetic( + lat=cal_in.telescope.location.lat, + lon=cal_in.telescope.location.lon, + height=cal_in.telescope.location.height, + ellipsoid=selenoid, ) - cal_in.telescope._location.frame = "mcmf" - cal_in.telescope._location.ellipsoid = selenoid - cal_in.telescope_location_lat_lon_alt = (latitude, longitude, altitude) new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", - ellipsoid=selenoid, + enu=enu_antpos, center_loc=cal_in.telescope.location + ) + cal_in.telescope.antenna_positions = ( + new_full_antpos - cal_in.telescope._location.xyz() ) - cal_in.antenna_positions = new_full_antpos - cal_in.telescope_location cal_in.set_lsts_from_time_array() cal_in.check() @@ -184,7 +182,10 @@ def test_calh5_meta(gain_data, tmp_path): assert not cal_meta.has_key(600) assert not cal_meta.has_key(ant_nums[5], "ll") - assert np.allclose(cal_meta.telescope_location, calobj.telescope_location) + assert np.allclose( + Quantity(list(cal_meta.telescope_location_obj.geocentric)), + Quantity(list(calobj.telescope.location.geocentric)), + ) # remove history to test adding pyuvdata version cal_meta.history = "" diff --git a/pyuvdata/uvcal/tests/test_fhd_cal.py b/pyuvdata/uvcal/tests/test_fhd_cal.py index b47c142383..45cec00f6b 100644 --- a/pyuvdata/uvcal/tests/test_fhd_cal.py +++ b/pyuvdata/uvcal/tests/test_fhd_cal.py @@ -311,7 +311,7 @@ def test_unknown_telescope(): settings_file=settings_testfile, use_future_array_shapes=True, ) - assert fhd_cal.telescope_name == "foo" + assert fhd_cal.telescope.name == "foo" @pytest.mark.parametrize( diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index 6a29c6a772..fa25dd18d9 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -52,7 +52,7 @@ def uvc_kw(uvd_kw, uvc_only_kw): def test_new_uvcal_simplest(uvc_kw): uvc = UVCal.new(**uvc_kw) assert uvc.Nants_data == 3 - assert uvc.Nants_telescope == 3 + assert uvc.telescope.Nants == 3 assert uvc.Nfreqs == 10 assert uvc.Ntimes == 12 @@ -68,8 +68,8 @@ def test_new_uvcal_simple_moon(uvc_kw, selenoid): uvc = UVCal.new(**uvc_kw) assert uvc.telescope._location.frame == "mcmf" assert uvc.telescope._location.ellipsoid == selenoid - assert uvc.telescope.location_obj == uvc_kw["telescope_location"] - assert uvc.telescope.location_obj.ellipsoid == selenoid + assert uvc.telescope.location == uvc_kw["telescope_location"] + assert uvc.telescope.location.ellipsoid == selenoid def test_new_uvcal_time_range(uvc_kw): @@ -202,7 +202,7 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): assert np.all(uvc.time_array == uvd_kw["times"]) assert np.all(uvc.freq_array == uvd_kw["freq_array"]) - assert uvc.telescope_name == uvd_kw["telescope_name"] + assert uvc.telescope.name == uvd_kw["telescope_name"] uvc = new_uvcal_from_uvdata(uvd, time_array=uvd_kw["times"][:-1], **uvc_only_kw) assert np.all(uvc.time_array == uvd_kw["times"][:-1]) @@ -222,12 +222,12 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): **uvc_only_kw ) - assert np.all(uvc.antenna_positions[0] == uvd_kw["antenna_positions"][0]) - assert len(uvc.antenna_positions) == 2 + assert np.all(uvc.telescope.antenna_positions[0] == uvd_kw["antenna_positions"][0]) + assert len(uvc.telescope.antenna_positions) == 2 - uvd.antenna_diameters = np.zeros(uvd.Nants_telescope, dtype=float) + 5.0 + uvd.telescope.antenna_diameters = np.zeros(uvd.telescope.Nants, dtype=float) + 5.0 uvc = new_uvcal_from_uvdata(uvd, **uvc_only_kw) - assert np.all(uvc.antenna_diameters == uvd.antenna_diameters) + assert np.all(uvc.telescope.antenna_diameters == uvd.telescope.antenna_diameters) def test_new_uvcal_set_freq_range_for_gain_type(uvd_kw, uvc_only_kw): @@ -267,7 +267,9 @@ def test_new_uvcal_from_uvdata_specify_numbers_names(uvd_kw, uvc_only_kw, diamet uvd = new_uvdata(**uvd_kw) if diameters in ["uvdata", "both"]: - uvd.antenna_diameters = np.zeros(uvd.Nants_telescope, dtype=float) + 5.0 + uvd.telescope.antenna_diameters = ( + np.zeros(uvd.telescope.Nants, dtype=float) + 5.0 + ) elif diameters in ["kwargs", "both"]: uvc_only_kw["antenna_diameters"] = np.zeros(1, dtype=float) + 5.0 @@ -276,19 +278,19 @@ def test_new_uvcal_from_uvdata_specify_numbers_names(uvd_kw, uvc_only_kw, diamet ): new_uvcal_from_uvdata( uvd, - antenna_numbers=uvd.antenna_numbers, - antenna_names=uvd.antenna_names, + antenna_numbers=uvd.telescope.antenna_numbers, + antenna_names=uvd.telescope.antenna_names, **uvc_only_kw ) uvc = new_uvcal_from_uvdata( - uvd, antenna_numbers=uvd.antenna_numbers[:1], **uvc_only_kw + uvd, antenna_numbers=uvd.telescope.antenna_numbers[:1], **uvc_only_kw ) uvc2 = new_uvcal_from_uvdata( - uvd, antenna_names=uvd.antenna_names[:1], **uvc_only_kw + uvd, antenna_names=uvd.telescope.antenna_names[:1], **uvc_only_kw ) if diameters is not None: - assert np.all(uvc.antenna_diameters == 5.0) + assert np.all(uvc.telescope.antenna_diameters == 5.0) uvc.history = uvc2.history assert uvc == uvc2 diff --git a/pyuvdata/uvcal/tests/test_ms_cal.py b/pyuvdata/uvcal/tests/test_ms_cal.py index 2dd472ac08..989ef58a4a 100644 --- a/pyuvdata/uvcal/tests/test_ms_cal.py +++ b/pyuvdata/uvcal/tests/test_ms_cal.py @@ -7,6 +7,7 @@ import numpy as np import pytest +from astropy.units import Quantity from ... import tests as uvtest from ...data import DATA_PATH @@ -29,8 +30,7 @@ "Unknown x_orientation basis for solutions, assuming", "key CASA_Version in extra_keywords is longer than 8 characters. " "It will be truncated to 8 if written to a calfits file format.", - "telescope_location are not set or are being overwritten. " - "telescope_location are set using values from known telescopes for SMA.", + "Setting telescope_location to value in known_telescopes for SMA.", ] @@ -216,8 +216,8 @@ def test_ms_default_setting(): with uvtest.check_warnings(UserWarning, match=sma_warnings): uvc2.read(testfile) - assert uvc1.x_orientation == "north" - assert uvc2.x_orientation == "east" + assert uvc1.telescope.x_orientation == "north" + assert uvc2.telescope.x_orientation == "east" assert np.array_equal(uvc1.jones_array, [-5, -6]) assert np.array_equal(uvc2.jones_array, [0, 0]) @@ -244,9 +244,12 @@ def test_ms_muck_ants(sma_pcal, tmp_path): uvc.read(testfile) - assert uvc.telescope_name == "FOO" - assert uvc.antenna_names == sma_pcal.antenna_names - assert np.allclose(uvc.telescope_location, sma_pcal.telescope_location) + assert uvc.telescope.name == "FOO" + assert uvc.telescope.antenna_names == sma_pcal.telescope.antenna_names + assert np.allclose( + Quantity(list(uvc.telescope.location.geocentric)), + Quantity(list(sma_pcal.telescope.location.geocentric)), + ) def test_ms_total_quality(sma_pcal, tmp_path): diff --git a/pyuvdata/uvcal/tests/test_uvcal.py b/pyuvdata/uvcal/tests/test_uvcal.py index 5a44621d13..855bdb2680 100644 --- a/pyuvdata/uvcal/tests/test_uvcal.py +++ b/pyuvdata/uvcal/tests/test_uvcal.py @@ -31,7 +31,7 @@ def uvcal_phase_center_main(gain_data_main): gain_copy = gain_data_main.copy() gain_copy._set_sky() - gain_copy.ref_antenna_name = gain_copy.antenna_names[0] + gain_copy.ref_antenna_name = gain_copy.telescope.antenna_names[0] gain_copy.sky_catalog = "unknown" # Copying the catalog from sma_test.mir @@ -265,7 +265,6 @@ def test_check_time_range_errors(gain_data): original_range = copy.copy(calobj.time_range) calobj.time_range[1, 1] = calobj.time_range[0, 0] - print(calobj.time_range[:, 1] - calobj.time_range[:, 0]) with pytest.raises( ValueError, match="The time ranges are not well-formed, some stop times are after start " @@ -535,16 +534,20 @@ def test_unknown_telescopes(gain_data, tmp_path): def test_nants_data_telescope_larger(gain_data): # make sure it's okay for Nants_telescope to be strictly greater than Nants_data - gain_data.Nants_telescope += 1 + gain_data.telescope.Nants += 1 # add dummy information for "new antenna" to pass object check - gain_data.antenna_names = np.concatenate((gain_data.antenna_names, ["dummy_ant"])) - gain_data.antenna_numbers = np.concatenate((gain_data.antenna_numbers, [20])) - gain_data.antenna_positions = np.concatenate( - (gain_data.antenna_positions, np.zeros((1, 3), dtype=float)) + gain_data.telescope.antenna_names = np.concatenate( + (gain_data.telescope.antenna_names, ["dummy_ant"]) ) - if gain_data.antenna_diameters is not None: - gain_data.antenna_diameters = np.concatenate( - (gain_data.antenna_diameters, np.ones((1,), dtype=float)) + gain_data.telescope.antenna_numbers = np.concatenate( + (gain_data.telescope.antenna_numbers, [20]) + ) + gain_data.telescope.antenna_positions = np.concatenate( + (gain_data.telescope.antenna_positions, np.zeros((1, 3), dtype=float)) + ) + if gain_data.telescope.antenna_diameters is not None: + gain_data.telescope.antenna_diameters = np.concatenate( + (gain_data.telescope.antenna_diameters, np.ones((1,), dtype=float)) ) assert gain_data.check() @@ -553,12 +556,14 @@ def test_nants_data_telescope_larger(gain_data): def test_ant_array_not_in_antnums(gain_data): # make sure an error is raised if antennas with data not in antenna_numbers # remove antennas from antenna_names & antenna_numbers by hand - gain_data.antenna_names = gain_data.antenna_names[1:] - gain_data.antenna_numbers = gain_data.antenna_numbers[1:] - gain_data.antenna_positions = gain_data.antenna_positions[1:, :] - if gain_data.antenna_diameters is not None: - gain_data.antenna_diameters = gain_data.antenna_diameters[1:] - gain_data.Nants_telescope = gain_data.antenna_numbers.size + gain_data.telescope.antenna_names = gain_data.telescope.antenna_names[1:] + gain_data.telescope.antenna_numbers = gain_data.telescope.antenna_numbers[1:] + gain_data.telescope.antenna_positions = gain_data.telescope.antenna_positions[1:, :] + if gain_data.telescope.antenna_diameters is not None: + gain_data.telescope.antenna_diameters = gain_data.telescope.antenna_diameters[ + 1: + ] + gain_data.telescope.Nants = gain_data.telescope.antenna_numbers.size with pytest.raises(ValueError) as cm: gain_data.check() assert str(cm.value).startswith( @@ -958,8 +963,8 @@ def test_select_antennas( ants_to_keep = np.array(sorted(ants_to_keep)) ant_names = [] for a in ants_to_keep: - ind = np.where(calobj.antenna_numbers == a)[0][0] - ant_names.append(calobj.antenna_names[ind]) + ind = np.where(calobj.telescope.antenna_numbers == a)[0][0] + ant_names.append(calobj.telescope.antenna_names[ind]) calobj3 = calobj.select(antenna_names=ant_names, inplace=False) @@ -1524,7 +1529,7 @@ def test_select_polarizations( assert j in calobj2.jones_array else: assert ( - uvutils.jstr2num(j, x_orientation=calobj2.x_orientation) + uvutils.jstr2num(j, x_orientation=calobj2.telescope.x_orientation) in calobj2.jones_array ) for j in np.unique(calobj2.jones_array): @@ -1532,7 +1537,7 @@ def test_select_polarizations( assert j in jones_to_keep else: assert j in uvutils.jstr2num( - jones_to_keep, x_orientation=calobj2.x_orientation + jones_to_keep, x_orientation=calobj2.telescope.x_orientation ) assert uvutils._check_histories( @@ -1856,14 +1861,14 @@ def test_reorder_ants( ant_num_diff = np.diff(calobj2.ant_array) assert np.all(ant_num_diff < 0) - sorted_names = np.sort(calobj.antenna_names) + sorted_names = np.sort(calobj.telescope.antenna_names) calobj.reorder_antennas("name") - temp = np.asarray(calobj.antenna_names) + temp = np.asarray(calobj.telescope.antenna_names) dtype_use = temp.dtype name_array = np.zeros_like(calobj.ant_array, dtype=dtype_use) for ind, ant in enumerate(calobj.ant_array): - name_array[ind] = calobj.antenna_names[ - np.nonzero(calobj.antenna_numbers == ant)[0][0] + name_array[ind] = calobj.telescope.antenna_names[ + np.nonzero(calobj.telescope.antenna_numbers == ant)[0][0] ] assert np.all(sorted_names == name_array) @@ -1889,14 +1894,14 @@ def test_reorder_ants_errors(gain_data): match="If order is an index array, it must contain all indicies for the" "ant_array, without duplicates.", ): - gain_data.reorder_antennas(gain_data.antenna_numbers.astype(float)) + gain_data.reorder_antennas(gain_data.telescope.antenna_numbers.astype(float)) with pytest.raises( ValueError, match="If order is an index array, it must contain all indicies for the" "ant_array, without duplicates.", ): - gain_data.reorder_antennas(gain_data.antenna_numbers[:8]) + gain_data.reorder_antennas(gain_data.telescope.antenna_numbers[:8]) @pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated") @@ -2187,13 +2192,17 @@ def test_reorder_jones( # the default order is "name" calobj2.reorder_jones() name_array = np.asarray( - uvutils.jnum2str(calobj2.jones_array, x_orientation=calobj2.x_orientation) + uvutils.jnum2str( + calobj2.jones_array, x_orientation=calobj2.telescope.x_orientation + ) ) sorted_names = np.sort(name_array) assert np.all(sorted_names == name_array) # test sorting with an index array. Sort back to number first so indexing works - sorted_nums = uvutils.jstr2num(sorted_names, x_orientation=calobj.x_orientation) + sorted_nums = uvutils.jstr2num( + sorted_names, x_orientation=calobj.telescope.x_orientation + ) index_array = [np.nonzero(calobj.jones_array == num)[0][0] for num in sorted_nums] calobj.reorder_jones(index_array) assert calobj2 == calobj @@ -2558,8 +2567,6 @@ def test_add_frequencies(future_shapes, gain_data, method): calobj2.quality_array = None getattr(calobj, method)(calobj2, **kwargs) assert np.allclose(calobj.input_flag_array, tot_ifa) - print(calobj.quality_array[2:4, :, 0, 0]) - print(tot_qa[2:4, :, 0, 0]) assert np.allclose(calobj.quality_array, tot_qa) # test for when input_flag_array is present in second file but not first @@ -3557,7 +3564,7 @@ def test_add_errors( getattr(calobj, method)("foo", **kwargs) # test compatibility param mismatch - calobj2.telescope_name = "PAPER" + calobj2.telescope.name = "PAPER" with pytest.raises(ValueError, match="Parameter telescope does not match"): getattr(calobj, method)(calobj2, **kwargs) @@ -4065,14 +4072,14 @@ def test_copy(future_shapes, gain_data, delay_data_inputflag, caltype): def test_match_antpos_antname(gain_data, antnamefix, tmp_path): # fix the antenna names in the uvcal object to match telescope object new_names = np.array( - [name.replace("ant", "HH") for name in gain_data.antenna_names] + [name.replace("ant", "HH") for name in gain_data.telescope.antenna_names] ) if antnamefix == "all": - gain_data.antenna_names = new_names + gain_data.telescope.antenna_names = new_names else: - gain_data.antenna_names[0 : gain_data.Nants_telescope // 2] = new_names[ - 0 : gain_data.Nants_telescope // 2 - ] + gain_data.telescope.antenna_names[0 : gain_data.telescope.Nants // 2] = ( + new_names[0 : gain_data.telescope.Nants // 2] + ) # remove the antenna_positions to test matching them on read write_file = str(tmp_path / "test.calfits") @@ -4100,7 +4107,7 @@ def test_match_antpos_antname(gain_data, antnamefix, tmp_path): ): gain_data2 = UVCal.from_file(write_file2, use_future_array_shapes=True) - assert gain_data2.antenna_positions is not None + assert gain_data2.telescope.antenna_positions is not None assert gain_data == gain_data2 @@ -4109,22 +4116,22 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): """Test that setting antenna positions doesn't happen if ants don't match.""" # fix the antenna names in the uvcal object to match telescope object new_names = np.array( - [name.replace("ant", "HH") for name in gain_data.antenna_names] + [name.replace("ant", "HH") for name in gain_data.telescope.antenna_names] ) - gain_data.antenna_names = new_names + gain_data.telescope.antenna_names = new_names if modtype == "rename": # change the name & number of one of the antennas - orig_num = gain_data.antenna_numbers[0] - gain_data.antenna_names[0] = "HH400" - gain_data.antenna_numbers[0] = 400 + orig_num = gain_data.telescope.antenna_numbers[0] + gain_data.telescope.antenna_names[0] = "HH400" + gain_data.telescope.antenna_numbers[0] = 400 gain_data.ant_array[np.where(gain_data.ant_array == orig_num)[0]] = 400 else: # change the name of one antenna and swap the number with a different antenna - orig_num = gain_data.antenna_numbers[0] - gain_data.antenna_names[0] = "HH400" - gain_data.antenna_numbers[0] = gain_data.antenna_numbers[1] - gain_data.antenna_numbers[1] = orig_num + orig_num = gain_data.telescope.antenna_numbers[0] + gain_data.telescope.antenna_names[0] = "HH400" + gain_data.telescope.antenna_numbers[0] = gain_data.telescope.antenna_numbers[1] + gain_data.telescope.antenna_numbers[1] = orig_num # remove the antenna_positions to test matching them on read write_file = str(tmp_path / "test.calfits") @@ -4170,7 +4177,7 @@ def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path): write_file2, use_future_array_shapes=True, run_check=False ) - assert gain_data2.antenna_positions is None + assert gain_data2.telescope.antenna_positions is None def test_read_errors(): @@ -4240,8 +4247,10 @@ def test_init_from_uvdata( future_array_shapes=uvcal_future_shapes, ) - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4254,9 +4263,9 @@ def test_init_from_uvdata( uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4291,9 +4300,6 @@ def test_init_from_uvdata_setfreqs( if not uvcal_future_shapes: uvc.use_current_array_shapes() - print(uvc.cal_type) - print(uvc.freq_range) - uvc2 = uvc.copy(metadata_only=True) uvc2.select(frequencies=freqs_use) @@ -4345,8 +4351,10 @@ def test_init_from_uvdata_setfreqs( # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4357,9 +4365,9 @@ def test_init_from_uvdata_setfreqs( ) # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4430,8 +4438,10 @@ def test_init_from_uvdata_settimes( # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4444,9 +4454,9 @@ def test_init_from_uvdata_settimes( uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None if not metadata_only: uvc2.gain_array[:] = 1.0 @@ -4495,8 +4505,10 @@ def test_init_from_uvdata_setjones(uvcalibrate_data): # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4509,9 +4521,9 @@ def test_init_from_uvdata_setjones(uvcalibrate_data): uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4550,8 +4562,10 @@ def test_init_single_pol(uvcalibrate_data, pol): # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4564,9 +4578,9 @@ def test_init_single_pol(uvcalibrate_data, pol): uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4599,8 +4613,10 @@ def test_init_from_uvdata_circular_pol(uvcalibrate_data): # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4613,9 +4629,9 @@ def test_init_from_uvdata_circular_pol(uvcalibrate_data): uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4691,8 +4707,10 @@ def test_init_from_uvdata_sky( # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4705,9 +4723,9 @@ def test_init_from_uvdata_sky( uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4803,8 +4821,10 @@ def test_init_from_uvdata_delay( # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4817,9 +4837,9 @@ def test_init_from_uvdata_delay( uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -4907,8 +4927,10 @@ def test_init_from_uvdata_wideband( # derive from info on our telescope object while the ones in the uvdata file # derive from the HERA correlator. I'm not sure why they're different, but it may be # because the data are a little old - assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1) - uvc_new.antenna_positions = uvc2.antenna_positions + assert np.allclose( + uvc2.telescope.antenna_positions, uvc_new.telescope.antenna_positions, atol=0.1 + ) + uvc_new.telescope.antenna_positions = uvc2.telescope.antenna_positions assert uvutils._check_histories( uvc_new.history[:200], @@ -4921,9 +4943,9 @@ def test_init_from_uvdata_wideband( uvc_new.history = uvc2.history # the new one has an instrument set because UVData requires it - assert uvc_new.instrument == uvd.instrument + assert uvc_new.telescope.instrument == uvd.telescope.instrument # remove it to match uvc2 - uvc_new.instrument = None + uvc_new.telescope.instrument = None # The times are different by 9.31322575e-10, which is below than our tolerance on # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences @@ -5155,7 +5177,7 @@ def test_flex_jones_write(multi_spw_gain, func, suffix, tmp_path): uvc_copy.jones_array[0] = -6 multi_spw_gain += uvc_copy multi_spw_gain.convert_to_flex_jones() - multi_spw_gain.ref_antenna_name = multi_spw_gain.antenna_names[0] + multi_spw_gain.ref_antenna_name = multi_spw_gain.telescope.antenna_names[0] filename = os.path.join(tmp_path, "flex_jones_write." + suffix) getattr(multi_spw_gain, func)(filename) @@ -5347,8 +5369,6 @@ def test_flex_jones_shuffle(multi_spw_gain, multi_spw_delay, mode): uvc_comb = uvc2.fast_concat(uvc1, axis=("spw" if (mode == "delay") else "freq")) uvc_comb.history = uvc.history - print(uvc_comb.flex_jones_array) - print(uvc.flex_jones_array) assert uvc_comb != uvc uvc_comb.reorder_freqs(spw_order="number") assert uvc_comb == uvc @@ -5458,9 +5478,13 @@ def test_refant_array_write_roundtrip(uvcal_phase_center, func, suffix, tmp_path if suffix == "ms": pytest.importorskip("casacore") uvcal_phase_center.ref_antenna_array = np.full( - uvcal_phase_center.Ntimes, uvcal_phase_center.antenna_numbers[0], dtype=int + uvcal_phase_center.Ntimes, + uvcal_phase_center.telescope.antenna_numbers[0], + dtype=int, + ) + uvcal_phase_center.ref_antenna_array[-1] = ( + uvcal_phase_center.telescope.antenna_numbers[1] ) - uvcal_phase_center.ref_antenna_array[-1] = uvcal_phase_center.antenna_numbers[1] uvcal_phase_center.ref_antenna_name = "various" filename = os.path.join(tmp_path, "refantarr_roundtrip." + suffix) @@ -5493,8 +5517,8 @@ def test_antdiam_write_roundtrip(uvcal_phase_center, func, suffix, tmp_path): pytest.importorskip("casacore") filename = os.path.join(tmp_path, "pc_roundtrip." + suffix) - uvcal_phase_center.antenna_diameters = np.full( - uvcal_phase_center.Nants_telescope, 10.0 + uvcal_phase_center.telescope.antenna_diameters = np.full( + uvcal_phase_center.telescope.Nants, 10.0 ) getattr(uvcal_phase_center, func)(filename) diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index 2502c0aff2..ab05151429 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -678,128 +678,6 @@ def _set_telescope_requirements(self): self.telescope._antenna_diameters.required = False self.telescope._x_orientation.required = True - @property - def telescope_name(self): - """The telescope name (stored on the Telescope object internally).""" - return self.telescope.name - - @telescope_name.setter - def telescope_name(self, val): - self.telescope.name = val - - @property - def instrument(self): - """The instrument name (stored on the Telescope object internally).""" - return self.telescope.instrument - - @instrument.setter - def instrument(self, val): - self.telescope.instrument = val - - @property - def telescope_location(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location - - @telescope_location.setter - def telescope_location(self, val): - self.telescope.location = val - - @property - def telescope_location_lat_lon_alt(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt - - @telescope_location_lat_lon_alt.setter - def telescope_location_lat_lon_alt(self, val): - self.telescope.location_lat_lon_alt = val - - @property - def telescope_location_lat_lon_alt_degrees(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt_degrees - - @telescope_location_lat_lon_alt_degrees.setter - def telescope_location_lat_lon_alt_degrees(self, val): - self.telescope.location_lat_lon_alt_degrees = val - - @property - def Nants_telescope(self): # noqa - """ - The number of antennas in the telescope. - - This property is stored on the Telescope object internally. - """ - return self.telescope.Nants - - @Nants_telescope.setter - def Nants_telescope(self, val): # noqa - self.telescope.Nants = val - - @property - def antenna_names(self): - """The antenna names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_names - - @antenna_names.setter - def antenna_names(self, val): - self.telescope.antenna_names = val - - @property - def antenna_numbers(self): - """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_numbers - - @antenna_numbers.setter - def antenna_numbers(self, val): - self.telescope.antenna_numbers = val - - @property - def antenna_positions(self): - """The antenna positions coordinates of antennas relative to telescope_location. - - The coordinates are in the ITRF frame, shape (Nants_telescope, 3). - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_positions - - @antenna_positions.setter - def antenna_positions(self, val): - self.telescope.antenna_positions = val - - @property - def x_orientation(self): - """Orientation of the physical dipole corresponding to the x label. - - Options are 'east' (indicating east/west orientation) and 'north (indicating - north/south orientation). - This property is stored on the Telescope object internally. - """ - return self.telescope.x_orientation - - @x_orientation.setter - def x_orientation(self, val): - self.telescope.x_orientation = val - - @property - def antenna_diameters(self): - """The antenna diameters in meters. - - Used by CASA to construct a default beam if no beam is supplied. - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_diameters - - @antenna_diameters.setter - def antenna_diameters(self, val): - self.telescope.antenna_diameters = val - @staticmethod @combine_docstrings(initializers.new_uvcal, style=DocstringStyle.NUMPYDOC) def new(**kwargs): @@ -1608,27 +1486,14 @@ def set_telescope_params( ) def _set_lsts_helper(self, *, astrometry_library=None): - latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees if self.time_array is not None: self.lst_array = uvutils.get_lst_for_time( - jd_array=self.time_array, - latitude=latitude, - longitude=longitude, - altitude=altitude, - astrometry_library=astrometry_library, - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + jd_array=self.time_array, telescope_loc=self.telescope.location ) if self.time_range is not None: self.lst_range = uvutils.get_lst_for_time( - jd_array=self.time_range, - latitude=latitude, - longitude=longitude, - altitude=altitude, - astrometry_library=astrometry_library, - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + jd_array=self.time_range, telescope_loc=self.telescope.location ) return @@ -2198,7 +2063,7 @@ def check( # Assume they are ok if time_ranges are ok. # require that all entries in ant_array exist in antenna_numbers - if not all(ant in self.antenna_numbers for ant in self.ant_array): + if not all(ant in self.telescope.antenna_numbers for ant in self.ant_array): raise ValueError("All antennas in ant_array must be in antenna_numbers.") # issue warning if extra_keywords keys are longer than 8 characters @@ -2272,34 +2137,24 @@ def check( if run_check_acceptability: # Check antenna positions uvutils.check_surface_based_positions( - antenna_positions=self.antenna_positions, - telescope_loc=self.telescope_location, - telescope_frame=self.telescope._location.frame, + antenna_positions=self.telescope.antenna_positions, + telescope_loc=self.telescope.location, raise_error=False, ) - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees if self.time_array is not None: uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) if self.time_range is not None: uvutils.check_lsts_against_times( jd_array=self.time_range, lst_array=self.lst_range, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) return True @@ -2357,7 +2212,9 @@ def _key_exists(self, *, antnum=None, jpol=None): return False if jpol is not None: if isinstance(jpol, (str, np.str_)): - jpol = uvutils.jstr2num(jpol, x_orientation=self.x_orientation) + jpol = uvutils.jstr2num( + jpol, x_orientation=self.telescope.x_orientation + ) if jpol not in self.jones_array: return False @@ -2397,7 +2254,7 @@ def jpol2ind(self, jpol): Antenna polarization index in data arrays """ if isinstance(jpol, (str, np.str_)): - jpol = uvutils.jstr2num(jpol, x_orientation=self.x_orientation) + jpol = uvutils.jstr2num(jpol, x_orientation=self.telescope.x_orientation) if not self._key_exists(jpol=jpol): raise ValueError("{} not found in jones_array".format(jpol)) @@ -2588,15 +2445,8 @@ def get_lst_array(self, *, astrometry_library=None): """ if self.lst_range is not None: - latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees return uvutils.get_lst_for_time( - jd_array=self.get_time_array(), - latitude=latitude, - longitude=longitude, - altitude=altitude, - astrometry_library=astrometry_library, - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, + jd_array=self.get_time_array(), telescope_loc=self.telescope.location ) else: return self.lst_array @@ -2653,13 +2503,13 @@ def reorder_antennas( if "number" in order: index_array = np.argsort(self.ant_array) elif "name" in order: - temp = np.asarray(self.antenna_names) + temp = np.asarray(self.telescope.antenna_names) dtype_use = temp.dtype name_array = np.zeros_like(self.ant_array, dtype=dtype_use) # there has to be a better way to do this without a loop... for ind, ant in enumerate(self.ant_array): - name_array[ind] = self.antenna_names[ - np.nonzero(self.antenna_numbers == ant)[0][0] + name_array[ind] = self.telescope.antenna_names[ + np.nonzero(self.telescope.antenna_numbers == ant)[0][0] ] index_array = np.argsort(name_array) @@ -3016,7 +2866,9 @@ def reorder_jones( index_array = np.argsort(self.jones_array) elif "name" in order: name_array = np.asarray( - uvutils.jnum2str(self.jones_array, x_orientation=self.x_orientation) + uvutils.jnum2str( + self.jones_array, x_orientation=self.telescope.x_orientation + ) ) index_array = np.argsort(name_array) @@ -5297,12 +5149,12 @@ def _select_preprocess( antenna_names = uvutils._get_iterable(antenna_names) antenna_nums = [] for s in antenna_names: - if s not in self.antenna_names: + if s not in self.telescope.antenna_names: raise ValueError( f"Antenna name {s} is not present in the antenna_names array" ) - ind = np.where(np.array(self.antenna_names) == s)[0][0] - antenna_nums.append(self.antenna_numbers[ind]) + ind = np.where(np.array(self.telescope.antenna_names) == s)[0][0] + antenna_nums.append(self.telescope.antenna_numbers[ind]) if antenna_nums is not None: antenna_nums = uvutils._get_iterable(antenna_nums) @@ -5520,7 +5372,9 @@ def _select_preprocess( jones_spws = np.zeros(0, dtype=np.int64) for j in jones: if isinstance(j, str): - j_num = uvutils.jstr2num(j, x_orientation=self.x_orientation) + j_num = uvutils.jstr2num( + j, x_orientation=self.telescope.x_orientation + ) else: j_num = j if j_num in self.jones_array: diff --git a/pyuvdata/uvdata/ms.py b/pyuvdata/uvdata/ms.py index 33a33ca613..6dd3dadb0b 100644 --- a/pyuvdata/uvdata/ms.py +++ b/pyuvdata/uvdata/ms.py @@ -11,18 +11,10 @@ import warnings import numpy as np -from astropy.coordinates import EarthLocation from astropy.time import Time from docstring_parser import DocstringStyle -try: - from lunarsky import MoonLocation - - hasmoon = True -except ImportError: - hasmoon = False - -from .. import Telescope, ms_utils +from .. import ms_utils from .. import utils as uvutils from ..docstrings import copy_replace_short_description from .uvdata import UVData, _future_array_shapes_warning @@ -965,62 +957,12 @@ def read_ms( self.telescope.name = obs_dict["telescope_name"] self.telescope.instrument = obs_dict["telescope_name"] self.extra_keywords["observer"] = obs_dict["observer"] - full_antenna_positions = tb_ant_dict["antenna_positions"] - xyz_telescope_frame = tb_ant_dict["telescope_frame"] - xyz_telescope_ellipsoid = tb_ant_dict["telescope_ellipsoid"] self.telescope.antenna_numbers = tb_ant_dict["antenna_numbers"] - # check to see if a TELESCOPE_LOCATION column is present in the observation - # table. This is non-standard, but inserted by pyuvdata - if ( - "telescope_location" not in obs_dict - and self.telescope.name in self.known_telescopes() - ): - # get it from known telescopes - telescope_obj = Telescope.from_known_telescopes(self.telescope.name) - warnings.warn( - "Setting telescope_location to value in known_telescopes for " - f"{self.telescope.name}." - ) - self.telescope.location = telescope_obj.location - else: - if xyz_telescope_frame not in ["itrs", "mcmf"]: - raise ValueError( - f"Telescope frame in file is {xyz_telescope_frame}. " - "Only 'itrs' and 'mcmf' are currently supported." - ) - if xyz_telescope_frame == "mcmf": - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with " - "MCMF frames." - ) - - if xyz_telescope_ellipsoid is None: - xyz_telescope_ellipsoid = "SPHERE" - - if "telescope_location" in obs_dict: - if xyz_telescope_frame == "itrs": - self.telescope.location = EarthLocation.from_geocentric( - *np.squeeze(obs_dict["telescope_location"]), unit="m" - ) - else: - self.telescope.location = MoonLocation.from_selenocentric( - *np.squeeze(obs_dict["telescope_location"]), unit="m" - ) - self.telescope.location.ellipsoid = xyz_telescope_ellipsoid - - else: - # Set it to be the mean of the antenna positions (this is not ideal!) - if xyz_telescope_frame == "itrs": - self.telescope.location = EarthLocation.from_geocentric( - *np.array(np.mean(full_antenna_positions, axis=0)), unit="m" - ) - else: - self.telescope.location = MoonLocation.from_selenocentric( - *np.array(np.mean(full_antenna_positions, axis=0)), unit="m" - ) - self.telescope.location.ellipsoid = xyz_telescope_ellipsoid + self.telescope.location = ms_utils.get_ms_telescope_location( + tb_ant_dict=tb_ant_dict, obs_dict=obs_dict + ) + full_antenna_positions = tb_ant_dict["antenna_positions"] # antenna names ant_names = tb_ant_dict["antenna_names"] From 1e410200e6b63c03d984364a8afb5ba721e08a86 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Sun, 21 Apr 2024 20:03:31 -0700 Subject: [PATCH 11/59] make new telescope location work on uvflag --- pyuvdata/uvflag/tests/test_uvflag.py | 185 +++++++------ pyuvdata/uvflag/uvflag.py | 387 +++++++++++---------------- 2 files changed, 258 insertions(+), 314 deletions(-) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 83f6e9dfc8..79634a0e12 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -20,6 +20,7 @@ from pyuvdata.tests.test_utils import frame_selenoid from pyuvdata.uvflag.uvflag import _future_array_shapes_warning +from ...uvbase import old_telescope_metadata_attrs from ..uvflag import and_rows_cols, flags2waterfall test_d_file = os.path.join(DATA_PATH, "zen.2457698.40355.xx.HH.uvcAA.uvh5") @@ -160,9 +161,9 @@ def uvf_from_file_future_main(): match=["channel_width not available in file, computing it from the freq_array"], ): uvf = UVFlag(test_f_file, use_future_array_shapes=True, telescope_name="HERA") - uvf.telescope_name = "HERA" - uvf.antenna_numbers = None - uvf.antenna_names = None + uvf.telescope.name = "HERA" + uvf.telescope.antenna_numbers = None + uvf.telescope.antenna_names = None uvf.set_telescope_params() yield uvf @@ -412,11 +413,11 @@ def test_read_extra_keywords(uvdata_obj): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_init_uvdata_x_orientation(uvdata_obj): uv = uvdata_obj - uv.x_orientation = "east" + uv.telescope.x_orientation = "east" uvf = UVFlag( uv, history="I made a UVFlag object", label="test", use_future_array_shapes=True ) - assert uvf.x_orientation == uv.x_orientation + assert uvf.telescope.x_orientation == uv.telescope.x_orientation @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @@ -541,7 +542,7 @@ def test_init_uvcal(): assert uvf.type == "antenna" assert uvf.mode == "metric" assert np.all(uvf.time_array == uvc.time_array) - assert uvf.x_orientation == uvc.x_orientation + assert uvf.telescope.x_orientation == uvc.telescope.x_orientation assert np.all(uvf.lst_array == uvc.lst_array) assert np.all(uvf.freq_array == uvc.freq_array) assert np.all(uvf.polarization_array == uvc.jones_array) @@ -870,20 +871,19 @@ def test_read_write_loop_spw(uvdata_obj, test_outfile, telescope_frame, selenoid if telescope_frame == "mcmf": pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + enu_antpos, _ = uv.get_ENU_antpos() - latitude, longitude, altitude = uv.telescope_location_lat_lon_alt - uv.telescope._location.frame = "mcmf" - uv.telescope._location.ellipsoid = selenoid - uv.telescope_location_lat_lon_alt = (latitude, longitude, altitude) - new_full_antpos = uvutils.ECEF_from_ENU( - enu=enu_antpos, - latitude=latitude, - longitude=longitude, - altitude=altitude, - frame="mcmf", + uv.telescope.location = MoonLocation.from_selenodetic( + lat=uv.telescope.location.lat, + lon=uv.telescope.location.lon, + height=uv.telescope.location.height, ellipsoid=selenoid, ) - uv.antenna_positions = new_full_antpos - uv.telescope_location + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, center_loc=uv.telescope.location + ) + uv.telescope.antenna_positions = new_full_antpos - uv.telescope._location.xyz() uv.set_lsts_from_time_array() uv.check() @@ -1144,13 +1144,19 @@ def test_read_write_loop_missing_telescope_info( uv.set_telescope_params(overwrite=True) elif uv_mod == "remove_extra_metadata": if uvf_type == "antenna": - ant_inds_keep = np.nonzero(np.isin(uv.antenna_numbers, uv.ant_array))[0] - uv.antenna_names = uv.antenna_names[ant_inds_keep] - uv.antenna_numbers = uv.antenna_numbers[ant_inds_keep] - uv.antenna_positions = uv.antenna_positions[ant_inds_keep] - if uv.antenna_diameters is not None: - uv.antenna_diameters = uv.antenna_diameters[ant_inds_keep] - uv.Nants_telescope = ant_inds_keep.size + ant_inds_keep = np.nonzero( + np.isin(uv.telescope.antenna_numbers, uv.ant_array) + )[0] + uv.telescope.antenna_names = uv.telescope.antenna_names[ant_inds_keep] + uv.telescope.antenna_numbers = uv.telescope.antenna_numbers[ant_inds_keep] + uv.telescope.antenna_positions = uv.telescope.antenna_positions[ + ant_inds_keep + ] + if uv.telescope.antenna_diameters is not None: + uv.telescope.antenna_diameters = uv.telescope.antenna_diameters[ + ant_inds_keep + ] + uv.telescope.Nants = ant_inds_keep.size uv.check() else: uv.select( @@ -1163,13 +1169,17 @@ def test_read_write_loop_missing_telescope_info( max_ant = np.max(uv.ant_array) new_max = max_ant + 300 uv.ant_array[np.nonzero(uv.ant_array == max_ant)[0]] = new_max - uv.antenna_numbers[np.nonzero(uv.antenna_numbers == max_ant)[0]] = new_max + uv.telescope.antenna_numbers[ + np.nonzero(uv.telescope.antenna_numbers == max_ant)[0] + ] = new_max else: max_ant = np.max(np.union1d(uv.ant_1_array, uv.ant_2_array)) new_max = max_ant + 300 uv.ant_1_array[np.nonzero(uv.ant_1_array == max_ant)[0]] = new_max uv.ant_2_array[np.nonzero(uv.ant_2_array == max_ant)[0]] = new_max - uv.antenna_numbers[np.nonzero(uv.antenna_numbers == max_ant)[0]] = new_max + uv.telescope.antenna_numbers[ + np.nonzero(uv.telescope.antenna_numbers == max_ant)[0] + ] = new_max else: run_check = False @@ -1190,16 +1200,19 @@ def test_read_write_loop_missing_telescope_info( if uv_mod is None: if param_list == ["antenna_names"]: - assert not np.array_equal(uvf2.antenna_names, uvf.antenna_numbers) - uvf2.antenna_names = uvf.antenna_names + assert not np.array_equal( + uvf2.telescope.antenna_names, uvf.telescope.antenna_numbers + ) + uvf2.telescope.antenna_names = uvf.telescope.antenna_names else: for param in param_list: + tel_param = old_telescope_metadata_attrs[param] if param != "Nants_telescope": - assert getattr(uvf2, param) is None - setattr(uvf2, param, getattr(uv, param)) + assert getattr(uvf2.telescope, tel_param) is None + setattr(uvf2.telescope, tel_param, getattr(uv.telescope, tel_param)) elif "telescope_name" in param_list: - assert uvf2.telescope_name is None - uvf2.telescope_name = uvf.telescope_name + assert uvf2.telescope.name is None + uvf2.telescope.name = uvf.telescope.name if uv_mod != "change_ant_numbers": assert uvf.__eq__(uvf2, check_history=True) assert uvf2.filename == [os.path.basename(test_outfile)] @@ -1280,7 +1293,7 @@ def test_missing_telescope_info_mwa(test_outfile): test_outfile, use_future_array_shapes=True, mwa_metafits_file=metafits ) - assert uvf2.Nants_telescope > uvf3.Nants_telescope + assert uvf2.telescope.Nants > uvf3.telescope.Nants def test_read_write_loop_wrong_nants_data(uvdata_obj, test_outfile): @@ -1348,7 +1361,7 @@ def test_read_write_loop_missing_spw_array(uvdata_obj, test_outfile): def test_read_write_loop_with_optional_x_orientation(uvdata_obj, test_outfile): uv = uvdata_obj uvf = UVFlag(uv, label="test", use_future_array_shapes=True) - uvf.x_orientation = "east" + uvf.telescope.x_orientation = "east" uvf.write(test_outfile, clobber=True) uvf2 = UVFlag(test_outfile, use_future_array_shapes=True) assert uvf.__eq__(uvf2, check_history=True) @@ -1551,8 +1564,8 @@ def test_init_list(uvdata_obj): uvf1 = UVFlag(uv, use_future_array_shapes=True) uvf2 = UVFlag(test_f_file, use_future_array_shapes=True) - uv.telescope_location = uvf2.telescope_location - uv.antenna_names = uvf2.antenna_names + uv.telescope.location = uvf2.telescope.location + uv.telescope.antenna_names = uvf2.telescope.antenna_names uvf = UVFlag([uv, test_f_file], use_future_array_shapes=True) assert np.array_equal( @@ -1720,7 +1733,7 @@ def test_set_telescope_params(uvdata_obj): assert uvf.telescope._antenna_positions == uvd.telescope._antenna_positions uvf = UVFlag(uvd2, use_future_array_shapes=True) - uvf.antenna_positions = None + uvf.telescope.antenna_positions = None with uvtest.check_warnings( UserWarning, match="antenna_positions are not set or are being overwritten. " @@ -1729,8 +1742,8 @@ def test_set_telescope_params(uvdata_obj): uvf.set_telescope_params() uvf = UVFlag(uvd2, use_future_array_shapes=True) - uvf.telescope_name = "foo" - uvf.telescope_location = None + uvf.telescope.name = "foo" + uvf.telescope.location = None with pytest.raises( ValueError, match="Telescope foo is not in astropy_sites or known_telescopes_dict.", @@ -1850,8 +1863,10 @@ def test_add_antenna(uvcal_obj): uv1 = UVFlag(uvc, use_future_array_shapes=True) uv2 = uv1.copy() uv2.ant_array += 100 # Arbitrary - uv2.antenna_numbers += 100 - uv2.antenna_names = np.array([name + "_new" for name in uv2.antenna_names]) + uv2.telescope.antenna_numbers += 100 + uv2.telescope.antenna_names = np.array( + [name + "_new" for name in uv2.telescope.antenna_names] + ) uv3 = uv1.__add__(uv2, axis="antenna") assert np.array_equal(np.concatenate((uv1.ant_array, uv2.ant_array)), uv3.ant_array) assert np.array_equal( @@ -2074,7 +2089,7 @@ def test_add_errors(uvdata_obj, uvcal_obj): uv1.__add__(uv3) uv3 = uv1.copy() - uv3.telescope_name = "foo" + uv3.telescope.name = "foo" with pytest.raises( ValueError, match="telescope_name is not the same the two objects. The value on this " @@ -2109,7 +2124,7 @@ def test_clear_unused_attributes(): assert hasattr(uv, "baseline_array") assert hasattr(uv, "ant_1_array") assert hasattr(uv, "ant_2_array") - assert hasattr(uv, "Nants_telescope") + assert hasattr(uv.telescope, "Nants") uv._set_type_antenna() uv.clear_unused_attributes() # clear_unused_attributes now sets these to None @@ -2442,15 +2457,19 @@ def test_to_baseline_flags(uvdata_obj, uvd_future_shapes, uvf_future_shapes, res if resort: rng = np.random.default_rng() - new_order = rng.permutation(uvf.Nants_telescope) + new_order = rng.permutation(uvf.telescope.Nants) if uvf_future_shapes: - uvf.antenna_numbers = uvf.antenna_numbers[new_order] - uvf.antenna_names = uvf.antenna_names[new_order] - uvf.antenna_positions = uvf.antenna_positions[new_order, :] + uvf.telescope.antenna_numbers = uvf.telescope.antenna_numbers[new_order] + uvf.telescope.antenna_names = uvf.telescope.antenna_names[new_order] + uvf.telescope.antenna_positions = uvf.telescope.antenna_positions[ + new_order, : + ] else: - uv.antenna_numbers = uvf.antenna_numbers[new_order] - uv.antenna_names = uvf.antenna_names[new_order] - uv.antenna_positions = uvf.antenna_positions[new_order, :] + uv.telescope.antenna_numbers = uvf.telescope.antenna_numbers[new_order] + uv.telescope.antenna_names = uvf.telescope.antenna_names[new_order] + uv.telescope.antenna_positions = uvf.telescope.antenna_positions[ + new_order, : + ] uvf.to_baseline(uv) assert uvf.type == "baseline" @@ -2487,23 +2506,23 @@ def test_to_baseline_metric(uvdata_obj, uvd_future_shapes, uvf_future_shapes): uvf = UVFlag(uv, use_future_array_shapes=uvf_future_shapes) uvf.to_waterfall() # remove telescope info to check that it's set properly - uvf.telescope_name = None - uvf.telescope_location = None + uvf.telescope.name = None + uvf.telescope.location = None # remove antenna info to check that it's set properly - uvf.antenna_names = None - uvf.antenna_numbers = None - uvf.antenna_positions = None + uvf.telescope.antenna_names = None + uvf.telescope.antenna_numbers = None + uvf.telescope.antenna_positions = None uvf.metric_array[0, 10, 0] = 3.2 # Fill in time0, chan10 uvf.metric_array[1, 15, 0] = 2.1 # Fill in time1, chan15 uvf.to_baseline(uv) - assert uvf.telescope_name == uv.telescope_name - assert np.all(uvf.telescope_location == uv.telescope_location) - assert np.all(uvf.antenna_names == uv.antenna_names) - assert np.all(uvf.antenna_numbers == uv.antenna_numbers) - assert np.all(uvf.antenna_positions == uv.antenna_positions) + assert uvf.telescope.name == uv.telescope.name + assert np.all(uvf.telescope._location.xyz() == uv.telescope._location.xyz()) + assert np.all(uvf.telescope.antenna_names == uv.telescope.antenna_names) + assert np.all(uvf.telescope.antenna_numbers == uv.telescope.antenna_numbers) + assert np.all(uvf.telescope.antenna_positions == uv.telescope.antenna_positions) assert np.all(uvf.baseline_array == uv.baseline_array) assert np.all(uvf.time_array == uv.time_array) @@ -2828,15 +2847,19 @@ def test_to_antenna_flags(uvcal_obj, uvf_future_shapes, uvc_future_shapes, resor if resort: rng = np.random.default_rng() - new_order = rng.permutation(uvf.Nants_telescope) + new_order = rng.permutation(uvf.telescope.Nants) if uvf_future_shapes: - uvf.antenna_numbers = uvf.antenna_numbers[new_order] - uvf.antenna_names = uvf.antenna_names[new_order] - uvf.antenna_positions = uvf.antenna_positions[new_order, :] + uvf.telescope.antenna_numbers = uvf.telescope.antenna_numbers[new_order] + uvf.telescope.antenna_names = uvf.telescope.antenna_names[new_order] + uvf.telescope.antenna_positions = uvf.telescope.antenna_positions[ + new_order, : + ] else: - uvc.antenna_numbers = uvf.antenna_numbers[new_order] - uvc.antenna_names = uvf.antenna_names[new_order] - uvc.antenna_positions = uvf.antenna_positions[new_order, :] + uvc.telescope.antenna_numbers = uvf.telescope.antenna_numbers[new_order] + uvc.telescope.antenna_names = uvf.telescope.antenna_names[new_order] + uvc.telescope.antenna_positions = uvf.telescope.antenna_positions[ + new_order, : + ] uvf.to_antenna(uvc) assert uvf.type == "antenna" @@ -2878,22 +2901,22 @@ def test_to_antenna_metric(uvcal_obj, future_shapes): uvf = UVFlag(uvc, use_future_array_shapes=future_shapes) uvf.to_waterfall() # remove telescope info to check that it's set properly - uvf.telescope_name = None - uvf.telescope_location = None + uvf.telescope.name = None + uvf.telescope.location = None # remove antenna info to check that it's set properly - uvf.antenna_names = None - uvf.antenna_numbers = None - uvf.antenna_positions = None + uvf.telescope.antenna_names = None + uvf.telescope.antenna_numbers = None + uvf.telescope.antenna_positions = None uvf.metric_array[0, 10, 0] = 3.2 # Fill in time0, chan10 uvf.metric_array[1, 15, 0] = 2.1 # Fill in time1, chan15 uvf.to_antenna(uvc) - assert uvf.telescope_name == uvc.telescope_name - assert np.all(uvf.telescope_location == uvc.telescope_location) - assert np.all(uvf.antenna_names == uvc.antenna_names) - assert np.all(uvf.antenna_numbers == uvc.antenna_numbers) - assert np.all(uvf.antenna_positions == uvc.antenna_positions) + assert uvf.telescope.name == uvc.telescope.name + assert np.all(uvf.telescope._location.xyz() == uvc.telescope._location.xyz()) + assert np.all(uvf.telescope.antenna_names == uvc.telescope.antenna_names) + assert np.all(uvf.telescope.antenna_numbers == uvc.telescope.antenna_numbers) + assert np.all(uvf.telescope.antenna_positions == uvc.telescope.antenna_positions) assert np.all(uvf.ant_array == uvc.ant_array) assert np.all(uvf.time_array == uvc.time_array) @@ -3978,7 +4001,7 @@ def test_select_polarizations(uvf_mode, pols_to_keep, input_uvf, future_shapes): np.random.seed(0) old_history = uvf.history - uvf.x_orientation = "north" + uvf.telescope.x_orientation = "north" uvf2 = uvf.copy() uvf2.select(polarizations=pols_to_keep) @@ -3991,7 +4014,7 @@ def test_select_polarizations(uvf_mode, pols_to_keep, input_uvf, future_shapes): assert p in uvf2.polarization_array else: assert ( - uvutils.polstr2num(p, x_orientation=uvf2.x_orientation) + uvutils.polstr2num(p, x_orientation=uvf2.telescope.x_orientation) in uvf2.polarization_array ) for p in np.unique(uvf2.polarization_array): @@ -3999,7 +4022,7 @@ def test_select_polarizations(uvf_mode, pols_to_keep, input_uvf, future_shapes): assert p in pols_to_keep else: assert p in uvutils.polstr2num( - pols_to_keep, x_orientation=uvf2.x_orientation + pols_to_keep, x_orientation=uvf2.telescope.x_orientation ) assert uvutils._check_histories( @@ -4222,7 +4245,7 @@ def test_select_parse_ants(uvf_from_data, uvf_mode): np.unique(uvf.baseline_array), uvutils.antnums_to_baseline( *np.transpose([(88, 97), (97, 104), (97, 105)]), - Nants_telescope=uvf.Nants_telescope, + Nants_telescope=uvf.telescope.Nants, ), ) diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index d6c88f51cd..1ca38016ac 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -11,6 +11,14 @@ import h5py import numpy as np +from astropy.coordinates import EarthLocation + +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False from .. import Telescope from .. import parameter as uvp @@ -646,128 +654,6 @@ def _set_telescope_requirements(self): self.telescope._antenna_diameters.required = False self.telescope._x_orientation.required = False - @property - def telescope_name(self): - """The telescope name (stored on the Telescope object internally).""" - return self.telescope.name - - @telescope_name.setter - def telescope_name(self, val): - self.telescope.name = val - - @property - def instrument(self): - """The instrument name (stored on the Telescope object internally).""" - return self.telescope.instrument - - @instrument.setter - def instrument(self, val): - self.telescope.instrument = val - - @property - def telescope_location(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location - - @telescope_location.setter - def telescope_location(self, val): - self.telescope.location = val - - @property - def telescope_location_lat_lon_alt(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt - - @telescope_location_lat_lon_alt.setter - def telescope_location_lat_lon_alt(self, val): - self.telescope.location_lat_lon_alt = val - - @property - def telescope_location_lat_lon_alt_degrees(self): - """The telescope location (stored on the Telescope object internally).""" - return self.telescope.location_lat_lon_alt_degrees - - @telescope_location_lat_lon_alt_degrees.setter - def telescope_location_lat_lon_alt_degrees(self, val): - self.telescope.location_lat_lon_alt_degrees = val - - @property - def Nants_telescope(self): # noqa - """ - The number of antennas in the telescope. - - This property is stored on the Telescope object internally. - """ - return self.telescope.Nants - - @Nants_telescope.setter - def Nants_telescope(self, val): # noqa - self.telescope.Nants = val - - @property - def antenna_names(self): - """The antenna names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_names - - @antenna_names.setter - def antenna_names(self, val): - self.telescope.antenna_names = val - - @property - def antenna_numbers(self): - """The antenna numbers corresponding to antenna_names, shape (Nants_telescope,). - - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_numbers - - @antenna_numbers.setter - def antenna_numbers(self, val): - self.telescope.antenna_numbers = val - - @property - def antenna_positions(self): - """The antenna positions coordinates of antennas relative to telescope_location. - - The coordinates are in the ITRF frame, shape (Nants_telescope, 3). - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_positions - - @antenna_positions.setter - def antenna_positions(self, val): - self.telescope.antenna_positions = val - - @property - def x_orientation(self): - """Orientation of the physical dipole corresponding to the x label. - - Options are 'east' (indicating east/west orientation) and 'north (indicating - north/south orientation). - This property is stored on the Telescope object internally. - """ - return self.telescope.x_orientation - - @x_orientation.setter - def x_orientation(self, val): - self.telescope.x_orientation = val - - @property - def antenna_diameters(self): - """The antenna diameters in meters. - - Used by CASA to construct a default beam if no beam is supplied. - This property is stored on the Telescope object internally. - """ - return self.telescope.antenna_diameters - - @antenna_diameters.setter - def antenna_diameters(self, val): - self.telescope.antenna_diameters = val - @property def _data_params(self): """List of strings giving the data-like parameters.""" @@ -1084,19 +970,23 @@ def check( "times in the time_array" ) - if self.antenna_numbers is not None: - if not set(np.unique(self.ant_1_array)).issubset(self.antenna_numbers): + if self.telescope.antenna_numbers is not None: + if not set(np.unique(self.ant_1_array)).issubset( + self.telescope.antenna_numbers + ): raise ValueError( "All antennas in ant_1_array must be in antenna_numbers." ) - if not set(np.unique(self.ant_2_array)).issubset(self.antenna_numbers): + if not set(np.unique(self.ant_2_array)).issubset( + self.telescope.antenna_numbers + ): raise ValueError( "All antennas in ant_2_array must be in antenna_numbers." ) elif self.type == "antenna": - if self.antenna_numbers is not None: + if self.telescope.antenna_numbers is not None: missing_ants = self.ant_array[ - ~np.isin(self.ant_array, self.antenna_numbers) + ~np.isin(self.ant_array, self.telescope.antenna_numbers) ] if missing_ants.size > 0: raise ValueError( @@ -1121,22 +1011,16 @@ def check( if run_check_acceptability: # Check antenna positions uvutils.check_surface_based_positions( - antenna_positions=self.antenna_positions, - telescope_loc=self.telescope_location, - telescope_frame=self.telescope._location.frame, + antenna_positions=self.telescope.antenna_positions, + telescope_loc=self.telescope.location, raise_error=False, ) - lat, lon, alt = self.telescope_location_lat_lon_alt_degrees uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, - latitude=lat, - longitude=lon, - altitude=alt, + telescope_loc=self.telescope.location, lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) return True @@ -1218,15 +1102,10 @@ def __ne__(self, other, *, check_history=True, check_extra=True): ) def _set_lsts_helper(self, astrometry_library=None): - latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees self.lst_array = uvutils.get_lst_for_time( jd_array=self.time_array, - latitude=latitude, - longitude=longitude, - altitude=altitude, + telescope_loc=self.telescope.location, astrometry_library=astrometry_library, - frame=self.telescope._location.frame, - ellipsoid=self.telescope._location.ellipsoid, ) return @@ -1331,7 +1210,7 @@ def baseline_to_antnums(self, baseline): """ assert self.type == "baseline", "Must be 'baseline' type UVFlag object." return uvutils.baseline_to_antnums( - baseline, Nants_telescope=self.Nants_telescope + baseline, Nants_telescope=self.telescope.Nants ) def antnums_to_baseline(self, ant1, ant2, *, attempt256=False): @@ -1355,7 +1234,7 @@ def antnums_to_baseline(self, ant1, ant2, *, attempt256=False): """ assert self.type == "baseline", "Must be 'baseline' type UVFlag object." return uvutils.antnums_to_baseline( - ant1, ant2, Nants_telescope=self.Nants_telescope, attempt256=attempt256 + ant1, ant2, Nants_telescope=self.telescope.Nants, attempt256=attempt256 ) def get_baseline_nums(self): @@ -1394,7 +1273,7 @@ def get_pols(self): list of polarizations (as strings) in the data. """ return uvutils.polnum2str( - self.polarization_array, x_orientation=self.x_orientation + self.polarization_array, x_orientation=self.telescope.x_orientation ) def parse_ants(self, ant_str, *, print_toggle=False): @@ -1440,7 +1319,7 @@ def parse_ants(self, ant_str, *, print_toggle=False): self, ant_str=ant_str, print_toggle=print_toggle, - x_orientation=self.x_orientation, + x_orientation=self.telescope.x_orientation, ) def collapse_pol( @@ -1658,29 +1537,39 @@ def sort_ant_metadata_like(self, uv): """ if ( - self.antenna_numbers is not None - and uv.antenna_numbers is not None - and np.intersect1d(self.antenna_numbers, uv.antenna_numbers).size - == self.Nants_telescope - and not np.allclose(self.antenna_numbers, uv.antenna_numbers) + self.telescope.antenna_numbers is not None + and uv.telescope.antenna_numbers is not None + and np.intersect1d( + self.telescope.antenna_numbers, uv.telescope.antenna_numbers + ).size + == self.telescope.Nants + and not np.allclose( + self.telescope.antenna_numbers, uv.telescope.antenna_numbers + ) ): # first get sort order for each - this_order = np.argsort(self.antenna_numbers) - uv_order = np.argsort(uv.antenna_numbers) + this_order = np.argsort(self.telescope.antenna_numbers) + uv_order = np.argsort(uv.telescope.antenna_numbers) # now get array to invert the uv sort inv_uv_order = np.empty_like(uv_order) - inv_uv_order[uv_order] = np.arange(uv.Nants_telescope) + inv_uv_order[uv_order] = np.arange(uv.telescope.Nants) # generate the array to sort self like uv this_uv_sort = this_order[inv_uv_order] # do the sorting - self.antenna_numbers = self.antenna_numbers[this_uv_sort] - if self.antenna_names is not None: - self.antenna_names = self.antenna_names[this_uv_sort] - if self.antenna_positions is not None: - self.antenna_positions = self.antenna_positions[this_uv_sort] + self.telescope.antenna_numbers = self.telescope.antenna_numbers[ + this_uv_sort + ] + if self.telescope.antenna_names is not None: + self.telescope.antenna_names = self.telescope.antenna_names[ + this_uv_sort + ] + if self.telescope.antenna_positions is not None: + self.telescope.antenna_positions = self.telescope.antenna_positions[ + this_uv_sort + ] def to_baseline( self, @@ -2499,18 +2388,21 @@ def __add__( this.ant_array = np.concatenate([this.ant_array, other.ant_array]) this.Nants_data = len(this.ant_array) temp_ant_nums = np.concatenate( - [this.antenna_numbers, other.antenna_numbers] + [this.telescope.antenna_numbers, other.telescope.antenna_numbers] + ) + temp_ant_names = np.concatenate( + [this.telescope.antenna_names, other.telescope.antenna_names] ) - temp_ant_names = np.concatenate([this.antenna_names, other.antenna_names]) temp_ant_pos = np.concatenate( - [this.antenna_positions, other.antenna_positions], axis=0 + [this.telescope.antenna_positions, other.telescope.antenna_positions], + axis=0, ) - this.antenna_numbers, unique_inds = np.unique( + this.telescope.antenna_numbers, unique_inds = np.unique( temp_ant_nums, return_index=True ) - this.antenna_names = temp_ant_names[unique_inds] - this.antenna_positions = temp_ant_pos[unique_inds] - this.Nants_telescope = len(this.antenna_numbers) + this.telescope.antenna_names = temp_ant_names[unique_inds] + this.telescope.antenna_positions = temp_ant_pos[unique_inds] + this.Nants_telescope = len(this.telescope.antenna_numbers) elif axis == "frequency": this.freq_array = np.concatenate( @@ -3140,7 +3032,9 @@ def _select_preprocess( pol_inds = np.zeros(0, dtype=np.int64) for p in polarizations: if isinstance(p, str): - p_num = uvutils.polstr2num(p, x_orientation=self.x_orientation) + p_num = uvutils.polstr2num( + p, x_orientation=self.telescope.x_orientation + ) else: p_num = p if p_num in self.polarization_array: @@ -3557,9 +3451,11 @@ def read( ) if "x_orientation" in header.keys(): - self.x_orientation = header["x_orientation"][()].decode("utf8") + self.telescope.x_orientation = header["x_orientation"][()].decode( + "utf8" + ) if "instrument" in header.keys(): - self.instrument = header["instrument"][()].decode("utf8") + self.telescope.instrument = header["instrument"][()].decode("utf8") self.time_array = header["time_array"][()] if "Ntimes" in header.keys(): @@ -3651,11 +3547,11 @@ def read( mwa_metafits_file, telescope_info_only=True ) - self.telescope_name = meta_dict["telescope_name"] - self.telescope_location = meta_dict["telescope_location"] - self.antenna_numbers = meta_dict["antenna_numbers"] - self.antenna_names = meta_dict["antenna_names"] - self.antenna_positions = meta_dict["antenna_positions"] + self.telescope.name = meta_dict["telescope_name"] + self.telescope.location = meta_dict["telescope_location"] + self.telescope.antenna_numbers = meta_dict["antenna_numbers"] + self.telescope.antenna_names = meta_dict["antenna_names"] + self.telescope.antenna_positions = meta_dict["antenna_positions"] override_params = [] params_to_check = [ @@ -3677,7 +3573,7 @@ def read( ) else: if telescope_name is not None: - self.telescope_name = telescope_name + self.telescope.name = telescope_name if "telescope_name" in header.keys(): file_telescope_name = header["telescope_name"][()].decode( @@ -3691,24 +3587,36 @@ def read( f"name in the file ({file_telescope_name})." ) else: - self.telescope_name = file_telescope_name + self.telescope.name = file_telescope_name if "telescope_location" in header.keys(): - self.telescope_location = header["telescope_location"][()] if "telescope_frame" in header.keys(): - self.telescope._location.frame = header["telescope_frame"][ - () - ].decode("utf8") - if self.telescope._location.frame != "itrs": - self.telescope._location.ellipsoid = header["ellipsoid"][ - () - ].decode("utf8") + telescope_frame = header["telescope_frame"][()].decode( + "utf8" + ) + else: + telescope_frame = "itrs" + if telescope_frame == "itrs": + self.telescope.location = EarthLocation.from_geocentric( + *header["telescope_location"][()], unit="m" + ) + else: + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with " + "MCMF frames." + ) + ellipsoid = header["ellipsoid"][()].decode("utf8") + self.telescope.location = MoonLocation.from_selenocentric( + *header["telescope_location"][()], unit="m" + ) + self.telescope.location.ellipsoid = ellipsoid if "antenna_numbers" in header.keys(): - self.antenna_numbers = header["antenna_numbers"][()] + self.telescope.antenna_numbers = header["antenna_numbers"][()] if "antenna_names" in header.keys(): - self.antenna_names = np.array( + self.telescope.antenna_names = np.array( [ bytes(n).decode("utf8") for n in header["antenna_names"][:] @@ -3716,9 +3624,13 @@ def read( ) if "antenna_positions" in header.keys(): - self.antenna_positions = header["antenna_positions"][()] + self.telescope.antenna_positions = header["antenna_positions"][ + () + ] if "antenna_diameters" in header.keys(): - self.antenna_diameters = header["antenna_diameters"][()] + self.telescope.antenna_diameters = header["antenna_diameters"][ + () + ] self.history = header["history"][()].decode("utf8") @@ -3799,9 +3711,9 @@ def read( self.Nants_data = len(self.ant_array) if "Nants_telescope" in header.keys(): - self.Nants_telescope = int(header["Nants_telescope"][()]) + self.telescope.Nants = int(header["Nants_telescope"][()]) - if self.telescope_name is None: + if self.telescope.name is None: warnings.warn( "telescope_name not available in file, so telescope related " "parameters cannot be set. This will result in errors when the " @@ -3810,22 +3722,22 @@ def read( "to turn off the check." ) elif ( - self.telescope_location is None - or self.antenna_numbers is None - or self.antenna_names is None - or self.antenna_positions is None + self.telescope.location is None + or self.telescope.antenna_numbers is None + or self.telescope.antenna_names is None + or self.telescope.antenna_positions is None ): if ( - self.antenna_numbers is None - and self.antenna_names is None - and self.antenna_positions is None + self.telescope.antenna_numbers is None + and self.telescope.antenna_names is None + and self.telescope.antenna_positions is None ): - self.Nants_telescope = None + self.telescope.Nants = None - if "mwa" in self.telescope_name.lower() and ( - self.antenna_numbers is None - or self.antenna_names is None - or self.antenna_positions is None + if "mwa" in self.telescope.name.lower() and ( + self.telescope.antenna_numbers is None + or self.telescope.antenna_names is None + or self.telescope.antenna_positions is None ): warnings.warn( "Antenna metadata are missing for this file. Since this " @@ -3838,23 +3750,23 @@ def read( ) self.set_telescope_params(run_check=False) - if self.antenna_numbers is None and self.type in [ + if self.telescope.antenna_numbers is None and self.type in [ "baseline", "antenna", ]: msg = "antenna_numbers not in file" if ( - self.Nants_telescope is None - or self.Nants_telescope == self.Nants_data + self.telescope.Nants is None + or self.telescope.Nants == self.Nants_data ): if self.type == "baseline": msg += ", setting based on ant_1_array and ant_2_array." - self.antenna_numbers = np.unique( + self.telescope.antenna_numbers = np.unique( np.union1d(self.ant_1_array, self.ant_2_array) ) else: msg += ", setting based on ant_array." - self.antenna_numbers = np.unique(self.ant_array) + self.telescope.antenna_numbers = np.unique(self.ant_array) else: msg += ", cannot be set based on " if self.type == "baseline": @@ -3869,19 +3781,26 @@ def read( ) warnings.warn(msg) - if self.antenna_names is None and self.antenna_numbers is not None: + if ( + self.telescope.antenna_names is None + and self.telescope.antenna_numbers is not None + ): warnings.warn( "antenna_names not in file, setting based on antenna_numbers" ) - self.antenna_names = self.antenna_numbers.astype(str) + self.telescope.antenna_names = ( + self.telescope.antenna_numbers.astype(str) + ) - if self.Nants_telescope is None: - if self.antenna_numbers is not None: - self.Nants_telescope = self.antenna_numbers.size - elif self.antenna_names is not None: - self.Nants_telescope = self.antenna_names.size - elif self.antenna_positions is not None: - self.Nants_telescope = (self.antenna_positions.shape)[0] + if self.telescope.Nants is None: + if self.telescope.antenna_numbers is not None: + self.telescope.Nants = self.telescope.antenna_numbers.size + elif self.telescope.antenna_names is not None: + self.telescope.Nants = self.telescope.antenna_names.size + elif self.telescope.antenna_positions is not None: + self.telescope.Nants = (self.telescope.antenna_positions.shape)[ + 0 + ] self.clear_unused_attributes() @@ -3939,10 +3858,10 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["type"] = np.string_(self.type) header["mode"] = np.string_(self.mode) - if self.telescope_name is not None: - header["telescope_name"] = np.string_(self.telescope_name) - if self.telescope_location is not None: - header["telescope_location"] = self.telescope_location + if self.telescope.name is not None: + header["telescope_name"] = np.string_(self.telescope.name) + if self.telescope.location is not None: + header["telescope_location"] = self.telescope._location.xyz() header["telescope_frame"] = np.string_(self.telescope._location.frame) if self.telescope._location.frame == "mcmf": header["ellipsoid"] = np.string_(self.telescope._location.ellipsoid) @@ -3962,12 +3881,12 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["Npols"] = self.Npols - if self.x_orientation is not None: - header["x_orientation"] = np.string_(self.x_orientation) - if self.instrument is not None: - header["instrument"] = np.string_(self.instrument) - if self.antenna_diameters is not None: - header["antenna_diameters"] = self.antenna_diameters + if self.telescope.x_orientation is not None: + header["x_orientation"] = np.string_(self.telescope.x_orientation) + if self.telescope.instrument is not None: + header["instrument"] = np.string_(self.telescope.instrument) + if self.telescope.antenna_diameters is not None: + header["antenna_diameters"] = self.telescope.antenna_diameters if isinstance(self.polarization_array.item(0), str): polarization_array = np.asarray( @@ -4007,13 +3926,15 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["ant_array"] = self.ant_array header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.Nants_telescope - if self.antenna_names is not None: - header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes") - if self.antenna_numbers is not None: - header["antenna_numbers"] = self.antenna_numbers - if self.antenna_positions is not None: - header["antenna_positions"] = self.antenna_positions + header["Nants_telescope"] = self.telescope.Nants + if self.telescope.antenna_names is not None: + header["antenna_names"] = np.asarray( + self.telescope.antenna_names, dtype="bytes" + ) + if self.telescope.antenna_numbers is not None: + header["antenna_numbers"] = self.telescope.antenna_numbers + if self.telescope.antenna_positions is not None: + header["antenna_positions"] = self.telescope.antenna_positions dgrp = f.create_group("Data") if self.mode == "metric": From a07bce898f4070dc7a29e392a4eb9834770c2353 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Sun, 21 Apr 2024 21:00:07 -0700 Subject: [PATCH 12/59] Make everything else work --- pyuvdata/parameter.py | 13 ++++-- pyuvdata/telescopes.py | 2 +- pyuvdata/tests/test_parameter.py | 78 +++++++++++++++++++++++-------- pyuvdata/tests/test_telescopes.py | 18 ++----- pyuvdata/tests/test_utils.py | 33 +++++++------ pyuvdata/tests/test_uvbase.py | 6 ++- pyuvdata/utils.py | 37 +++++++++------ 7 files changed, 121 insertions(+), 66 deletions(-) diff --git a/pyuvdata/parameter.py b/pyuvdata/parameter.py index 1a5ca25a1e..28668641c3 100644 --- a/pyuvdata/parameter.py +++ b/pyuvdata/parameter.py @@ -956,8 +956,13 @@ def xyz(self): centric_coords = self.value.geocentric return units.Quantity(centric_coords).to("m").value - def set_xyz(self, xyz, frame="itrs", ellipsoid=None): + def set_xyz(self, xyz, frame=None, ellipsoid=None): """Set the body centric coordinates in meters.""" + if frame is None and hasmoon and isinstance(self.value, MoonLocation): + frame = "mcmf" + else: + frame = "itrs" + allowed_frames = ["itrs"] if hasmoon: allowed_frames += ["mcmf"] @@ -1053,12 +1058,14 @@ def set_lat_lon_alt_degrees(self, lat_lon_alt_degree, ellipsoid=None): self.set_lat_lon_alt(lat_lon_alt, ellipsoid=ellipsoid) def check_acceptability(self): - """Check that vector magnitudes are in range.""" - if not isinstance(self.value, allowed_location_types): + """Check that value is an allowed object type.""" + if not isinstance(self.value, tuple(allowed_location_types)): return ( False, f"Location must be an object of type: {allowed_location_types}", ) + else: + return True, "" def __eq__(self, other, *, silent=False): """Handle equality properly for Earth/Moon Location objects.""" diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index fd121ddfbe..0da48f4254 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -345,7 +345,7 @@ def update_params_from_known_telescopes( known_telescope_list.append("telescope_location") if telescope_dict["center_xyz"] is not None: self.location = EarthLocation.from_geocentric( - telescope_dict["center_xyz"], unit="m" + *telescope_dict["center_xyz"], unit="m" ) else: if ( diff --git a/pyuvdata/tests/test_parameter.py b/pyuvdata/tests/test_parameter.py index 938f6086e4..3d06da897a 100644 --- a/pyuvdata/tests/test_parameter.py +++ b/pyuvdata/tests/test_parameter.py @@ -6,10 +6,23 @@ import astropy.units as units import numpy as np import pytest -from astropy.coordinates import CartesianRepresentation, Latitude, Longitude, SkyCoord +from astropy.coordinates import ( + CartesianRepresentation, + EarthLocation, + Latitude, + Longitude, + SkyCoord, +) + +try: + from lunarsky import MoonLocation + + hasmoon = True +except ImportError: + hasmoon = False from pyuvdata import parameter as uvp -from pyuvdata import utils +from pyuvdata.parameter import allowed_location_types from pyuvdata.tests.test_utils import ( frame_selenoid, ref_latlonalt, @@ -410,25 +423,54 @@ def test_location_xyz_latlonalt_match(frame, selenoid): if frame == "itrs": xyz_val = ref_xyz latlonalt_val = ref_latlonalt + loc_centric = EarthLocation.from_geocentric(*ref_xyz, unit="m") + loc_detic = EarthLocation.from_geodetic( + lat=ref_latlonalt[0] * units.rad, + lon=ref_latlonalt[1] * units.rad, + height=ref_latlonalt[2] * units.m, + ) + wrong_obj = EarthLocation.of_site("mwa") else: xyz_val = ref_xyz_moon[selenoid] latlonalt_val = ref_latlonalt_moon + loc_centric = MoonLocation.from_selenocentric(*ref_xyz_moon[selenoid], unit="m") + loc_centric.ellipsoid = selenoid + loc_detic = MoonLocation.from_selenodetic( + lat=ref_latlonalt_moon[0] * units.rad, + lon=ref_latlonalt_moon[1] * units.rad, + height=ref_latlonalt_moon[2] * units.m, + ellipsoid=selenoid, + ) + wrong_obj = MoonLocation.from_selenocentric(0, 0, 0, unit="m") + wrong_obj.ellipsoid = selenoid - param1 = uvp.LocationParameter( - name="p1", value=xyz_val, frame=frame, ellipsoid=selenoid - ) + param1 = uvp.LocationParameter(name="p1", value=loc_centric) np.testing.assert_allclose(latlonalt_val, param1.lat_lon_alt()) + param4 = uvp.LocationParameter(name="p1", value=wrong_obj) + param4.set_xyz(xyz_val) + assert param1 == param4 + if selenoid == "SPHERE": - param1 = uvp.LocationParameter(name="p1", value=xyz_val, frame=frame) + param1 = uvp.LocationParameter( + name="p1", + value=MoonLocation.from_selenodetic( + lat=ref_latlonalt_moon[0] * units.rad, + lon=ref_latlonalt_moon[1] * units.rad, + height=ref_latlonalt_moon[2] * units.m, + ), + ) np.testing.assert_allclose(latlonalt_val, param1.lat_lon_alt()) - param2 = uvp.LocationParameter(name="p2", frame=frame, ellipsoid=selenoid) - param2.set_lat_lon_alt(latlonalt_val) + param2 = uvp.LocationParameter(name="p2", value=loc_detic) + np.testing.assert_allclose(xyz_val, param2.xyz()) + + param5 = uvp.LocationParameter(name="p2", value=wrong_obj) + param5.set_lat_lon_alt(latlonalt_val, ellipsoid=selenoid) - np.testing.assert_allclose(xyz_val, param2.value) + assert param2 == param5 - param3 = uvp.LocationParameter(name="p2", frame=frame, ellipsoid=selenoid) + param3 = uvp.LocationParameter(name="p2", value=wrong_obj) latlonalt_deg_val = np.array( [ latlonalt_val[0] * 180 / np.pi, @@ -438,23 +480,21 @@ def test_location_xyz_latlonalt_match(frame, selenoid): ) param3.set_lat_lon_alt_degrees(latlonalt_deg_val) - np.testing.assert_allclose(xyz_val, param3.value) + np.testing.assert_allclose(xyz_val, param3.xyz()) def test_location_acceptability(): """Test check_acceptability with LocationParameters""" - val = np.array([0.5, 0.5, 0.5]) - param1 = uvp.LocationParameter("p1", value=val, acceptable_range=[0, 1]) + param1 = uvp.LocationParameter( + "p1", value=EarthLocation.from_geocentric(*ref_xyz, unit="m") + ) assert param1.check_acceptability()[0] - val += 0.5 - param1 = uvp.LocationParameter("p1", value=val, acceptable_range=[0, 1]) - assert not param1.check_acceptability()[0] - - param1 = uvp.LocationParameter("p1", value=val, frame="foo") + val = np.array([0.5, 0.5, 0.5]) + param1 = uvp.LocationParameter("p1", value=val) acceptable, reason = param1.check_acceptability() assert not acceptable - assert reason == f"Frame must be one of {utils._range_dict.keys()}" + assert reason == f"Location must be an object of type: {allowed_location_types}" @pytest.mark.parametrize( diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 28831a7cf2..7588b8fb1b 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -159,7 +159,7 @@ def test_get_telescope_center_xyz(): telescope_obj_ext = Telescope() telescope_obj_ext.citation = "" telescope_obj_ext.name = "test" - telescope_obj_ext.location = ref_xyz + telescope_obj_ext.location = EarthLocation(*ref_xyz, unit="m") assert telescope_obj == telescope_obj_ext @@ -191,18 +191,6 @@ def test_get_telescope_no_loc(): ) -def test_bad_location_obj(): - tel = Telescope() - tel.name = "foo" - - with pytest.raises( - ValueError, - match="location_obj is not a recognized location object. Must be an " - "EarthLocation or MoonLocation object.", - ): - tel.location_obj = (-2562123.42683, 5094215.40141, -2848728.58869) - - def test_hera_loc(): hera_file = os.path.join(DATA_PATH, "zen.2458098.45361.HH.uvh5_downselected") hera_data = UVData() @@ -213,8 +201,8 @@ def test_hera_loc(): telescope_obj = Telescope.from_known_telescopes("HERA") assert np.allclose( - telescope_obj.location, - hera_data.telescope_location, + telescope_obj._location.xyz(), + hera_data.telescope._location.xyz(), rtol=hera_data.telescope._location.tols[0], atol=hera_data.telescope._location.tols[1], ) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 9f1841036e..2610ae3199 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -3628,7 +3628,7 @@ def test_uvcalibrate_apply_gains_oldfiles(): use_future_array_shapes=True, ) # give it an x_orientation - uvd.x_orientation = "east" + uvd.telescope.x_orientation = "east" uvc = UVCal() uvc.read_calfits( os.path.join(DATA_PATH, "zen.2457698.40355.xx.gain.calfits"), @@ -3887,12 +3887,14 @@ def test_uvcalibrate_flag_propagation( uvc_sub = uvc.select(antenna_nums=[1, 12], inplace=False) uvdata_unique_nums = np.unique(np.append(uvd.ant_1_array, uvd.ant_2_array)) - uvd.antenna_names = np.array(uvd.antenna_names) + uvd.telescope.antenna_names = np.array(uvd.telescope.antenna_names) missing_ants = uvdata_unique_nums.tolist() missing_ants.remove(1) missing_ants.remove(12) missing_ant_names = [ - uvd.antenna_names[np.where(uvd.antenna_numbers == antnum)[0][0]] + uvd.telescope.antenna_names[ + np.where(uvd.telescope.antenna_numbers == antnum)[0][0] + ] for antnum in missing_ants ] @@ -4195,7 +4197,7 @@ def test_uvcalibrate_feedpol_mismatch(uvcalibrate_data): uvd, uvc = uvcalibrate_data # downselect the feed polarization to get warnings - uvc.select(jones=uvutils.jstr2num("Jnn", x_orientation=uvc.x_orientation)) + uvc.select(jones=uvutils.jstr2num("Jnn", x_orientation=uvc.telescope.x_orientation)) with pytest.raises( ValueError, match=("Feed polarization e exists on UVData but not on UVCal.") ): @@ -4206,8 +4208,8 @@ def test_uvcalibrate_x_orientation_mismatch(uvcalibrate_data): uvd, uvc = uvcalibrate_data # next check None uvd_x - uvd.x_orientation = None - uvc.x_orientation = "east" + uvd.telescope.x_orientation = None + uvc.telescope.x_orientation = "east" with pytest.warns( UserWarning, match=r"UVData object does not have `x_orientation` specified but UVCal does", @@ -4741,16 +4743,18 @@ def test_uvw_track_generator(flip_u, use_uvw, use_earthloc): sma_mir.set_uvws_from_antenna_positions() if not use_uvw: # Just subselect the antennas in the dataset - sma_mir.antenna_positions = sma_mir.antenna_positions[[0, 3], :] + sma_mir.telescope.antenna_positions = sma_mir.telescope.antenna_positions[ + [0, 3], : + ] if use_earthloc: telescope_loc = EarthLocation.from_geodetic( - lon=sma_mir.telescope_location_lat_lon_alt_degrees[1], - lat=sma_mir.telescope_location_lat_lon_alt_degrees[0], - height=sma_mir.telescope_location_lat_lon_alt_degrees[2], + lon=sma_mir.telescope.location_lat_lon_alt_degrees[1], + lat=sma_mir.telescope.location_lat_lon_alt_degrees[0], + height=sma_mir.telescope.location_lat_lon_alt_degrees[2], ) else: - telescope_loc = sma_mir.telescope_location_lat_lon_alt_degrees + telescope_loc = sma_mir.telescope.location_lat_lon_alt_degrees if use_uvw: sma_copy = sma_mir.copy() @@ -4767,7 +4771,9 @@ def test_uvw_track_generator(flip_u, use_uvw, use_earthloc): coord_epoch=cat_dict["cat_epoch"], telescope_loc=telescope_loc, time_array=sma_mir.time_array if use_uvw else sma_mir.time_array[0], - antenna_positions=sma_mir.antenna_positions if uvw_array is None else None, + antenna_positions=( + sma_mir.telescope.antenna_positions if uvw_array is None else None + ), force_postive_u=flip_u, uvw_array=uvw_array, ) @@ -4892,7 +4898,8 @@ def test_check_surface_based_positions_earthmoonloc(tel_loc, check_frame): else: with pytest.raises(ValueError, match=(f"{frame} position vector")): uvutils.check_surface_based_positions( - telescope_loc=loc, telescope_frame=frame + telescope_loc=[loc.x.value, loc.y.value, loc.z.value], + telescope_frame=frame, ) diff --git a/pyuvdata/tests/test_uvbase.py b/pyuvdata/tests/test_uvbase.py index 4359f82a30..96b3bfc679 100644 --- a/pyuvdata/tests/test_uvbase.py +++ b/pyuvdata/tests/test_uvbase.py @@ -10,7 +10,7 @@ import numpy as np import pytest from astropy import units -from astropy.coordinates import Distance, Latitude, Longitude, SkyCoord +from astropy.coordinates import Distance, EarthLocation, Latitude, Longitude, SkyCoord from astropy.time import Time from pyuvdata import parameter as uvp @@ -83,7 +83,9 @@ def __init__(self): ) self._location = uvp.LocationParameter( - "location", description="location", value=np.array(ref_xyz) + "location", + description="location", + value=EarthLocation.from_geocentric(*ref_xyz, unit="m"), ) self._time = uvp.UVParameter( diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 50df2cdd97..6992a04551 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -5658,18 +5658,22 @@ def uvcalibrate( # Check whether the UVData antennas *that have data associated with them* # have associated data in the UVCal object uvdata_unique_nums = np.unique(np.append(uvdata.ant_1_array, uvdata.ant_2_array)) - uvdata.antenna_names = np.asarray(uvdata.antenna_names) + uvdata.telescope.antenna_names = np.asarray(uvdata.telescope.antenna_names) uvdata_used_antnames = np.array( [ - uvdata.antenna_names[np.where(uvdata.antenna_numbers == antnum)][0] + uvdata.telescope.antenna_names[ + np.where(uvdata.telescope.antenna_numbers == antnum) + ][0] for antnum in uvdata_unique_nums ] ) uvcal_unique_nums = np.unique(uvcal.ant_array) - uvcal.antenna_names = np.asarray(uvcal.antenna_names) + uvcal.telescope.antenna_names = np.asarray(uvcal.telescope.antenna_names) uvcal_used_antnames = np.array( [ - uvcal.antenna_names[np.where(uvcal.antenna_numbers == antnum)][0] + uvcal.telescope.antenna_names[ + np.where(uvcal.telescope.antenna_numbers == antnum) + ][0] for antnum in uvcal_unique_nums ] ) @@ -5842,24 +5846,26 @@ def uvcalibrate( if len(uvcal_freqs_to_keep) < uvcal.Nfreqs: downselect_cal_freq = True - # check if uvdata.x_orientation isn't set (it's required for uvcal) - uvd_x = uvdata.x_orientation + # check if uvdata.telescope.x_orientation isn't set (it's required for uvcal) + uvd_x = uvdata.telescope.x_orientation if uvd_x is None: # use the uvcal x_orientation throughout - uvd_x = uvcal.x_orientation + uvd_x = uvcal.telescope.x_orientation warnings.warn( "UVData object does not have `x_orientation` specified but UVCal does. " "Matching based on `x` and `y` only " ) uvdata_pol_strs = polnum2str(uvdata.polarization_array, x_orientation=uvd_x) - uvcal_pol_strs = jnum2str(uvcal.jones_array, x_orientation=uvcal.x_orientation) + uvcal_pol_strs = jnum2str( + uvcal.jones_array, x_orientation=uvcal.telescope.x_orientation + ) uvdata_feed_pols = { feed for pol in uvdata_pol_strs for feed in POL_TO_FEED_DICT[pol] } for feed in uvdata_feed_pols: # get diagonal jones str - jones_str = parse_jpolstr(feed, x_orientation=uvcal.x_orientation) + jones_str = parse_jpolstr(feed, x_orientation=uvcal.telescope.x_orientation) if jones_str not in uvcal_pol_strs: raise ValueError( f"Feed polarization {feed} exists on UVData but not on UVCal. " @@ -5916,9 +5922,13 @@ def uvcalibrate( # No D-term calibration else: # key is number, value is name - uvdata_ant_dict = dict(zip(uvdata.antenna_numbers, uvdata.antenna_names)) + uvdata_ant_dict = dict( + zip(uvdata.telescope.antenna_numbers, uvdata.telescope.antenna_names) + ) # opposite: key is name, value is number - uvcal_ant_dict = dict(zip(uvcal.antenna_names, uvcal.antenna_numbers)) + uvcal_ant_dict = dict( + zip(uvcal.telescope.antenna_names, uvcal.telescope.antenna_numbers) + ) # iterate over keys for key in uvdata.get_antpairpols(): @@ -6181,9 +6191,10 @@ def parse_ants(uv, ant_str, *, print_toggle=False, x_orientation=None): ) if x_orientation is None and ( - hasattr(uv, "x_orientation") and uv.x_orientation is not None + hasattr(uv.telescope, "x_orientation") + and uv.telescope.x_orientation is not None ): - x_orientation = uv.x_orientation + x_orientation = uv.telescope.x_orientation ant_re = r"(\(((-?\d+[lrxy]?,?)+)\)|-?\d+[lrxy]?)" bl_re = "(^(%s_%s|%s),?)" % (ant_re, ant_re, ant_re) From 92cdfe8cb1547c47dc22037e13695ea408aa9e57 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 22 Apr 2024 09:18:35 -0700 Subject: [PATCH 13/59] add testing for get/set attr --- pyuvdata/tests/test_uvbase.py | 66 ++++++++++++++++++++++++++++++++++- pyuvdata/uvbase.py | 28 +++++++++++++-- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/pyuvdata/tests/test_uvbase.py b/pyuvdata/tests/test_uvbase.py index 96b3bfc679..5e734a84c9 100644 --- a/pyuvdata/tests/test_uvbase.py +++ b/pyuvdata/tests/test_uvbase.py @@ -15,7 +15,8 @@ from pyuvdata import parameter as uvp from pyuvdata import tests as uvtest -from pyuvdata.uvbase import UVBase, _warning +from pyuvdata.telescopes import Telescope +from pyuvdata.uvbase import UVBase, _warning, old_telescope_metadata_attrs ref_latlonalt = (-26.7 * np.pi / 180.0, 116.7 * np.pi / 180.0, 377.8) ref_xyz = (-2562123.42683, 5094215.40141, -2848728.58869) @@ -176,6 +177,8 @@ def __init__(self): form=(), ) + self.telescope = Telescope.from_known_telescopes("mwa") + super(UVTest, self).__init__() @@ -459,3 +462,64 @@ def test_name_error(): test_obj._location.name = "place" with pytest.raises(ValueError, match="UVParameter _location does not follow the"): test_obj.check() + + +def test_getattr_old_telescope(): + test_obj = UVTest() + + for param, tel_param in old_telescope_metadata_attrs.items(): + param_val = getattr(test_obj, param) + if tel_param is not None: + tel_param_val = getattr(test_obj.telescope, tel_param) + if not isinstance(param_val, np.ndarray): + assert param_val == tel_param_val + else: + if not isinstance(param_val.flat[0], str): + np.testing.assert_allclose(param_val, tel_param_val) + else: + assert param_val.tolist() == tel_param_val.tolist() + elif param == "telescope_location": + np.testing.assert_allclose(param_val, test_obj.telescope._location.xyz()) + elif param == "telescope_location_lat_lon_alt": + np.testing.assert_allclose( + param_val, test_obj.telescope._location.lat_lon_alt() + ) + elif param == "telescope_location_lat_lon_alt_degrees": + np.testing.assert_allclose( + param_val, test_obj.telescope._location.lat_lon_alt_degrees() + ) + + +def test_setattr_old_telescope(): + test_obj = UVTest() + + new_telescope = Telescope.from_known_telescopes("hera") + + for param, tel_param in old_telescope_metadata_attrs.items(): + if tel_param is not None: + tel_val = getattr(new_telescope, tel_param) + setattr(test_obj, param, tel_val) + param_val = getattr(test_obj, param) + if not isinstance(param_val, np.ndarray): + assert param_val == tel_val + else: + if not isinstance(param_val.flat[0], str): + np.testing.assert_allclose(param_val, tel_val) + else: + assert param_val.tolist() == tel_val.tolist() + elif param == "telescope_location": + tel_val = new_telescope._location.xyz() + test_obj.telescope_location = tel_val + assert new_telescope.location == test_obj.telescope.location + elif param == "telescope_location_lat_lon_alt": + tel_val = new_telescope._location.lat_lon_alt() + test_obj.telescope_location_lat_lon_alt = tel_val + np.testing.assert_allclose( + new_telescope._location.xyz(), test_obj.telescope._location.xyz() + ) + elif param == "telescope_location_lat_lon_alt_degrees": + tel_val = new_telescope._location.lat_lon_alt_degrees() + test_obj.telescope_location_lat_lon_alt_degrees = tel_val + np.testing.assert_allclose( + new_telescope._location.xyz(), test_obj.telescope._location.xyz() + ) diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index a60d126a1c..8ce047ff7a 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -141,7 +141,7 @@ def __setstate__(self, state): self.__dict__ = state self._setup_parameters() - def __getattribute__(self, __name): + def __getattr__(self, __name): """Handle old names for telescope metadata.""" if __name in old_telescope_metadata_attrs: # _warn_old_phase_attr(__name) @@ -156,13 +156,35 @@ def __getattribute__(self, __name): if __name == "telescope_location": ret_val = self.telescope._location.xyz() elif __name == "telescope_location_lat_lon_alt": - ret_val = self.telescope.location_lat_lon_alt() + ret_val = self.telescope._location.lat_lon_alt() elif __name == "telescope_location_lat_lon_alt_degrees": - ret_val = self.telescope.location_lat_lon_alt_degrees() + ret_val = self.telescope._location.lat_lon_alt_degrees() return ret_val return super().__getattribute__(__name) + def __setattr__(self, __name, __value): + """Handle old names for telescope metadata.""" + if __name in old_telescope_metadata_attrs: + # _warn_old_phase_attr(__name) + + if hasattr(self, "telescope"): + tel_name = old_telescope_metadata_attrs[__name] + if tel_name is not None: + # if it's a simple remapping, just set the value + setattr(self.telescope, tel_name, __value) + else: + # handle location related stuff + if __name == "telescope_location": + self.telescope._location.set_xyz(__value) + elif __name == "telescope_location_lat_lon_alt": + self.telescope._location.set_lat_lon_alt(__value) + elif __name == "telescope_location_lat_lon_alt_degrees": + self.telescope._location.set_lat_lon_alt_degrees(__value) + return + + return super().__setattr__(__name, __value) + def prop_fget(self, param_name): """ Getter method for UVParameter properties. From c867a70c79330e6680d5972b1d1c3de455f1e177 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 24 Apr 2024 09:16:52 -0700 Subject: [PATCH 14/59] consolidate initialization code for telescope metadata in telescopes Also make antenna metadata always required. Pull location from known telescopes out to a function. Deprecate initializing UVCal or UVData with telescope metadata rather than a telescope object. --- pyuvdata/ms_utils.py | 6 +- pyuvdata/telescopes.py | 309 ++++++++++++++++----- pyuvdata/tests/test_telescopes.py | 157 ++++++++++- pyuvdata/utils.py | 23 +- pyuvdata/uvbeam/initializers.py | 3 +- pyuvdata/uvcal/initializers.py | 133 +++++---- pyuvdata/uvcal/tests/test_initializers.py | 180 ++++++++---- pyuvdata/uvcal/uvcal.py | 9 +- pyuvdata/uvdata/initializers.py | 241 +++++++--------- pyuvdata/uvdata/mir.py | 14 +- pyuvdata/uvdata/mir_parser.py | 4 +- pyuvdata/uvdata/miriad.py | 24 +- pyuvdata/uvdata/tests/test_initializers.py | 175 +++--------- pyuvdata/uvdata/uvdata.py | 8 +- pyuvdata/uvflag/uvflag.py | 9 +- 15 files changed, 791 insertions(+), 504 deletions(-) diff --git a/pyuvdata/ms_utils.py b/pyuvdata/ms_utils.py index b234bab2cb..0d914eb07f 100644 --- a/pyuvdata/ms_utils.py +++ b/pyuvdata/ms_utils.py @@ -2130,14 +2130,12 @@ def get_ms_telescope_location(*, tb_ant_dict, obs_dict): and obs_dict["telescope_name"] in telescopes.known_telescopes() ): # get it from known telescopes - telescope_obj = telescopes.Telescope.from_known_telescopes( - obs_dict["telescope_name"] - ) + telescope_loc = telescopes.known_telescope_location(obs_dict["telescope_name"]) warnings.warn( "Setting telescope_location to value in known_telescopes for " f"{obs_dict['telescope_name']}." ) - return telescope_obj.location + return telescope_loc else: if xyz_telescope_frame not in ["itrs", "mcmf"]: raise ValueError( diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 0da48f4254..4294b46f73 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -5,6 +5,7 @@ """Telescope information and known telescope list.""" import os import warnings +from typing import Literal, Union import numpy as np from astropy import units @@ -18,6 +19,8 @@ __all__ = ["Telescope", "known_telescopes"] +Locations = Union[uvutils.allowed_location_types] + # We use astropy sites for telescope locations. The dict below is for # telescopes not in astropy sites, or to include extra information for a telescope. @@ -127,6 +130,139 @@ def known_telescopes(): return known_telescopes +def known_telescope_location( + name: str, + return_citation: bool = False, + known_telescope_dict: dict = KNOWN_TELESCOPES, +): + """ + Get the location for a known telescope. + + Parameters + ---------- + name : str + Name of the telescope + return_citation : bool + Option to return the citation. + known_telescope_dict: dict + This should only be used for testing. This allows passing in a + different dict to use in place of the KNOWN_TELESCOPES dict. + + Returns + ------- + location : EarthLocation + Telescope location as an EarthLocation object. + citation : str, optional + Citation string. + + """ + astropy_sites = EarthLocation.get_site_names() + telescope_keys = list(known_telescope_dict.keys()) + telescope_list = [tel.lower() for tel in telescope_keys] + + # first deal with location. + if name in astropy_sites: + location = EarthLocation.of_site(name) + + citation = "astropy sites" + elif name.lower() in telescope_list: + telescope_index = telescope_list.index(name.lower()) + telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] + citation = telescope_dict["citation"] + + if telescope_dict["center_xyz"] is not None: + location = EarthLocation.from_geocentric( + *telescope_dict["center_xyz"], unit="m" + ) + else: + if ( + telescope_dict["latitude"] is None + or telescope_dict["longitude"] is None + or telescope_dict["altitude"] is None + ): + raise ValueError( + "Bad location information in known_telescopes_dict " + f"for telescope {name}. Either the center_xyz " + "or the latitude, longitude and altitude of the " + "telescope must be specified." + ) + location = EarthLocation.from_geodetic( + lat=telescope_dict["latitude"] * units.rad, + lon=telescope_dict["longitude"] * units.rad, + height=telescope_dict["altitude"] * units.m, + ) + else: + # no telescope matching this name + raise ValueError( + f"Telescope {name} is not in astropy_sites or " "known_telescopes_dict." + ) + + if not return_citation: + return location + else: + return location, citation + + +def get_antenna_params( + *, + antenna_positions: np.ndarray | dict[str | int, np.ndarray], + antenna_names: list[str] | None = None, + antenna_numbers: list[int] | None = None, + antname_format: str = "{0:03d}", +) -> tuple[np.ndarray, list[str], list[int]]: + """Configure antenna parameters for new UVData object.""" + # Get Antenna Parameters + + if isinstance(antenna_positions, dict): + keys = list(antenna_positions.keys()) + if all(isinstance(key, int) for key in keys): + antenna_numbers = list(antenna_positions.keys()) + elif all(isinstance(key, str) for key in keys): + antenna_names = list(antenna_positions.keys()) + else: + raise ValueError( + "antenna_positions must be a dictionary with keys that are all type " + "int or all type str." + ) + antenna_positions = np.array(list(antenna_positions.values())) + + if antenna_numbers is None and antenna_names is None: + raise ValueError( + "Either antenna_numbers or antenna_names must be provided unless " + "antenna_positions is a dict." + ) + + if antenna_names is None: + antenna_names = [antname_format.format(i) for i in antenna_numbers] + elif antenna_numbers is None: + try: + antenna_numbers = [int(name) for name in antenna_names] + except ValueError as e: + raise ValueError( + "Antenna names must be integers if antenna_numbers is not provided." + ) from e + + if not isinstance(antenna_positions, np.ndarray): + raise ValueError("antenna_positions must be a numpy array or a dictionary.") + + if antenna_positions.shape != (len(antenna_numbers), 3): + raise ValueError( + "antenna_positions must be a 2D array with shape (N_antennas, 3), " + f"got {antenna_positions.shape}" + ) + + if len(antenna_names) != len(set(antenna_names)): + raise ValueError("Duplicate antenna names found.") + + if len(antenna_numbers) != len(set(antenna_numbers)): + raise ValueError("Duplicate antenna numbers found.") + + if len(antenna_numbers) != len(antenna_names): + raise ValueError("antenna_numbers and antenna_names must have the same length.") + + return antenna_positions, np.asarray(antenna_names), np.asarray(antenna_numbers) + + class Telescope(uvbase.UVBase): """ A class for telescope metadata, used on UVData, UVCal and UVFlag objects. @@ -154,29 +290,15 @@ def __init__(self): ) self._location = uvp.LocationParameter("location", description=desc, tols=1e-3) - self._instrument = uvp.UVParameter( - "instrument", - description="Receiver or backend. Sometimes identical to telescope_name.", - required=False, - form="str", - expected_type=str, - ) - desc = "Number of antennas in the array." - self._Nants = uvp.UVParameter( - "Nants", required=False, description=desc, expected_type=int - ) + self._Nants = uvp.UVParameter("Nants", description=desc, expected_type=int) desc = ( "Array of antenna names, shape (Nants), " "with numbers given by antenna_numbers." ) self._antenna_names = uvp.UVParameter( - "antenna_names", - description=desc, - required=False, - form=("Nants",), - expected_type=str, + "antenna_names", description=desc, form=("Nants",), expected_type=str ) desc = ( @@ -184,28 +306,31 @@ def __init__(self): "shape (Nants)." ) self._antenna_numbers = uvp.UVParameter( - "antenna_numbers", - description=desc, - required=False, - form=("Nants",), - expected_type=int, + "antenna_numbers", description=desc, form=("Nants",), expected_type=int ) desc = ( "Array giving coordinates of antennas relative to " - "telescope_location (ITRF frame), shape (Nants, 3), " + "location (ITRF frame), shape (Nants, 3), " "units meters. See the tutorial page in the documentation " "for an example of how to convert this to topocentric frame." ) self._antenna_positions = uvp.UVParameter( "antenna_positions", description=desc, - required=False, form=("Nants", 3), expected_type=float, tols=1e-3, # 1 mm ) + self._instrument = uvp.UVParameter( + "instrument", + description="Receiver or backend. Sometimes identical to name.", + required=False, + form="str", + expected_type=str, + ) + desc = ( "Orientation of the physical dipole corresponding to what is " "labelled as the x polarization. Options are 'east' " @@ -322,7 +447,6 @@ def update_params_from_known_telescopes( "The telescope name attribute must be set to update from " "known_telescopes." ) - astropy_sites = EarthLocation.get_site_names() telescope_keys = list(known_telescope_dict.keys()) telescope_list = [tel.lower() for tel in telescope_keys] @@ -330,46 +454,16 @@ def update_params_from_known_telescopes( known_telescope_list = [] # first deal with location. if overwrite or self.location is None: - if self.name in astropy_sites: - tel_loc = EarthLocation.of_site(self.name) - - self.citation = "astropy sites" - self.location = tel_loc + location, citation = known_telescope_location( + self.name, + return_citation=True, + known_telescope_dict=known_telescope_dict, + ) + self.location = location + if "astropy sites" in citation: astropy_sites_list.append("telescope_location") - - elif self.name.lower() in telescope_list: - telescope_index = telescope_list.index(self.name.lower()) - telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] - self.citation = telescope_dict["citation"] - - known_telescope_list.append("telescope_location") - if telescope_dict["center_xyz"] is not None: - self.location = EarthLocation.from_geocentric( - *telescope_dict["center_xyz"], unit="m" - ) - else: - if ( - telescope_dict["latitude"] is None - or telescope_dict["longitude"] is None - or telescope_dict["altitude"] is None - ): - raise ValueError( - "Bad location information in known_telescopes_dict " - f"for telescope {self.name}. Either the center_xyz " - "or the latitude, longitude and altitude of the " - "telescope must be specified." - ) - self.location = EarthLocation.from_geodetic( - lat=telescope_dict["latitude"] * units.rad, - lon=telescope_dict["longitude"] * units.rad, - height=telescope_dict["altitude"] * units.m, - ) else: - # no telescope matching this name - raise ValueError( - f"Telescope {self.name} is not in astropy_sites or " - "known_telescopes_dict." - ) + known_telescope_list.append("telescope_location") # check for extra info if self.name.lower() in telescope_list: @@ -545,3 +639,94 @@ def from_known_telescopes( known_telescope_dict=known_telescope_dict, ) return tel_obj + + @classmethod + def from_params( + cls, + name: str, + location: Locations, + antenna_positions: np.ndarray | dict[str | int, np.ndarray] | None = None, + antenna_names: list[str] | np.ndarray | None = None, + antenna_numbers: list[int] | np.ndarray | None = None, + antname_format: str = "{0:03d}", + instrument: str | None = None, + x_orientation: Literal["east", "north", "e", "n", "ew", "ns"] | None = None, + antenna_diameters: list[float] | np.ndarray | None = None, + ): + """ + Initialize a new Telescope object from keyword arguments. + + Parameters + ---------- + name : str + Telescope name. + location : EarthLocation or MoonLocation object + Telescope location as an astropy EarthLocation object or MoonLocation + object. + antenna_positions : ndarray of float or dict of ndarray of float + Array of antenna positions in ECEF coordinates in meters. + If a dict, keys can either be antenna numbers or antenna names, and + values are position arrays. Keys are interpreted as antenna numbers + if they are integers, otherwise they are interpreted as antenna names + if strings. You cannot provide a mix of different types of keys. + antenna_names : list or np.ndarray of str, optional + List or array of antenna names. Not used if antenna_positions is a + dict with string keys. Otherwise, if not provided, antenna numbers + will be used to form the antenna_names, according to the antname_format. + antenna_numbers : list or np.ndarray of int, optional + List or array of antenna numbers. Not used if antenna_positions is a + dict with integer keys. Otherwise, if not provided, antenna names + will be used to form the antenna_numbers, but in this case the + antenna_names must be strings that can be converted to integers. + antname_format : str, optional + Format string for antenna names. Default is '{0:03d}'. + instrument : str, optional + Instrument name. + x_orientation : str + Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', + 'ew', 'ns'. + antenna_diameters : list or np.ndarray of float, optional + List or array of antenna diameters. + + Returns + ------- + Telescope object + A Telescope object with the specified metadata. + + """ + tel_obj = cls() + + if not isinstance(location, tuple(uvutils.allowed_location_types)): + raise ValueError( + "telescope_location has an unsupported type, it must be one of " + f"{uvutils.allowed_location_types}" + ) + + tel_obj.name = name + tel_obj.location = location + + antenna_positions, antenna_names, antenna_numbers = get_antenna_params( + antenna_positions=antenna_positions, + antenna_names=antenna_names, + antenna_numbers=antenna_numbers, + antname_format=antname_format, + ) + + tel_obj.antenna_positions = antenna_positions + tel_obj.antenna_names = antenna_names + tel_obj.antenna_numbers = antenna_numbers + tel_obj.Nants = len(antenna_numbers) + + if instrument is not None: + tel_obj.instrument = instrument + + if x_orientation is not None: + x_orientation = uvutils.XORIENTMAP[x_orientation.lower()] + tel_obj.x_orientation = x_orientation + + if antenna_diameters is not None: + tel_obj.antenna_diameters = np.asarray(antenna_diameters) + + tel_obj.check() + + return tel_obj diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 7588b8fb1b..0e8b41a1c5 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -14,27 +14,26 @@ import pyuvdata from pyuvdata import Telescope, UVData from pyuvdata.data import DATA_PATH +from pyuvdata.telescopes import get_antenna_params -required_parameters = ["_name", "_location"] -required_properties = ["name", "location"] -extra_parameters = [ - "_antenna_diameters", +required_parameters = [ + "_name", + "_location", "_Nants", "_antenna_names", "_antenna_numbers", "_antenna_positions", - "_x_orientation", - "_instrument", ] -extra_properties = [ - "antenna_diameters", +required_properties = [ + "name", + "location", "Nants", "antenna_names", "antenna_numbers", "antenna_positions", - "x_orientation", - "instrument", ] +extra_parameters = ["_antenna_diameters", "_x_orientation", "_instrument"] +extra_properties = ["antenna_diameters", "x_orientation", "instrument"] other_attributes = [ "citation", "telescope_location_lat_lon_alt", @@ -51,6 +50,19 @@ ) +@pytest.fixture(scope="function") +def simplest_working_params(): + return { + "antenna_positions": { + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + } + + # Tests for Telescope object def test_parameter_iter(): "Test expected parameters." @@ -130,7 +142,8 @@ def test_known_telescopes(): def test_from_known(): for inst in pyuvdata.known_telescopes(): - telescope_obj = Telescope.from_known_telescopes(inst) + # don't run check b/c some telescopes won't have antenna info defined + telescope_obj = Telescope.from_known_telescopes(inst, run_check=False) assert telescope_obj.name == inst @@ -154,7 +167,7 @@ def test_get_telescope_center_xyz(): }, } telescope_obj = Telescope.from_known_telescopes( - "test", known_telescope_dict=test_telescope_dict + "test", known_telescope_dict=test_telescope_dict, run_check=False ) telescope_obj_ext = Telescope() telescope_obj_ext.citation = "" @@ -165,7 +178,7 @@ def test_get_telescope_center_xyz(): telescope_obj_ext.name = "test2" telescope_obj2 = Telescope.from_known_telescopes( - "test2", known_telescope_dict=test_telescope_dict + "test2", known_telescope_dict=test_telescope_dict, run_check=False ) assert telescope_obj2 == telescope_obj_ext @@ -206,3 +219,121 @@ def test_hera_loc(): rtol=hera_data.telescope._location.tols[0], atol=hera_data.telescope._location.tols[1], ) + + +def test_alternate_antenna_inputs(): + antpos_dict = { + 0: np.array([0.0, 0.0, 0.0]), + 1: np.array([0.0, 0.0, 1.0]), + 2: np.array([0.0, 0.0, 2.0]), + } + + antpos_array = np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]], dtype=float) + antnum = np.array([0, 1, 2]) + antname = np.array(["000", "001", "002"]) + + pos, names, nums = get_antenna_params(antenna_positions=antpos_dict) + pos2, names2, nums2 = get_antenna_params( + antenna_positions=antpos_array, antenna_numbers=antnum, antenna_names=antname + ) + + assert np.allclose(pos, pos2) + assert np.all(names == names2) + assert np.all(nums == nums2) + + antpos_dict = { + "000": np.array([0, 0, 0]), + "001": np.array([0, 0, 1]), + "002": np.array([0, 0, 2]), + } + pos, names, nums = get_antenna_params(antenna_positions=antpos_dict) + assert np.allclose(pos, pos2) + assert np.all(names == names2) + assert np.all(nums == nums2) + + +def test_bad_antenna_inputs(simplest_working_params): + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises( + ValueError, match="Either antenna_numbers or antenna_names must be provided" + ): + Telescope.from_params( + antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + antenna_numbers=None, + antenna_names=None, + **badp, + ) + + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises( + ValueError, + match=( + "antenna_positions must be a dictionary with keys that are all type int " + "or all type str" + ), + ): + Telescope.from_params(antenna_positions={1: [0, 1, 2], "2": [3, 4, 5]}, **badp) + + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises(ValueError, match="Antenna names must be integers"): + Telescope.from_params( + antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + antenna_numbers=None, + antenna_names=["foo", "bar", "baz"], + **badp, + ) + + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises(ValueError, match="antenna_positions must be a numpy array"): + Telescope.from_params( + antenna_positions="foo", + antenna_numbers=[0, 1, 2], + antenna_names=["foo", "bar", "baz"], + **badp, + ) + + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises(ValueError, match="antenna_positions must be a 2D array"): + Telescope.from_params( + antenna_positions=np.array([0, 0, 0]), antenna_numbers=np.array([0]), **badp + ) + + with pytest.raises(ValueError, match="Duplicate antenna names found"): + Telescope.from_params( + antenna_names=["foo", "bar", "foo"], **simplest_working_params + ) + + badp = { + k: v for k, v in simplest_working_params.items() if k != "antenna_positions" + } + with pytest.raises(ValueError, match="Duplicate antenna numbers found"): + Telescope.from_params( + antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + antenna_numbers=[0, 1, 0], + antenna_names=["foo", "bar", "baz"], + **badp, + ) + + with pytest.raises( + ValueError, match="antenna_numbers and antenna_names must have the same length" + ): + Telescope.from_params(antenna_names=["foo", "bar"], **simplest_working_params) + + +@pytest.mark.parametrize("xorient", ["e", "n", "east", "NORTH"]) +def test_passing_xorient(simplest_working_params, xorient): + tel = Telescope.from_params(x_orientation=xorient, **simplest_working_params) + if xorient.lower().startswith("e"): + assert tel.x_orientation == "east" + else: + assert tel.x_orientation == "north" diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 6992a04551..54abf4fcb2 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -39,6 +39,7 @@ "CONJ_POL_DICT", "JONES_STR2NUM_DICT", "JONES_NUM2STR_DICT", + "XORIENTMAP", "LatLonAlt_from_XYZ", "XYZ_from_LatLonAlt", "rotECEF_from_ECEF", @@ -125,6 +126,15 @@ # fmt: on +XORIENTMAP = { + "east": "east", + "north": "north", + "e": "east", + "n": "north", + "ew": "east", + "ns": "north", +} + _range_dict = { "itrs": (6.35e6, 6.39e6, "Earth"), "mcmf": (1717100.0, 1757100.0, "Moon"), @@ -1206,12 +1216,13 @@ def baseline_index_flip(baseline, *, Nants_telescope): def _x_orientation_rep_dict(x_orientation): """Create replacement dict based on x_orientation.""" - if x_orientation.lower() == "east" or x_orientation.lower() == "e": - return {"x": "e", "y": "n"} - elif x_orientation.lower() == "north" or x_orientation.lower() == "n": - return {"x": "n", "y": "e"} - else: - raise ValueError("x_orientation not recognized.") + try: + if XORIENTMAP[x_orientation.lower()] == "east": + return {"x": "e", "y": "n"} + elif XORIENTMAP[x_orientation.lower()] == "north": + return {"x": "n", "y": "e"} + except KeyError as e: + raise ValueError("x_orientation not recognized.") from e def np_cache(function): diff --git a/pyuvdata/uvbeam/initializers.py b/pyuvdata/uvbeam/initializers.py index 3bec4444c1..f3728f58a0 100644 --- a/pyuvdata/uvbeam/initializers.py +++ b/pyuvdata/uvbeam/initializers.py @@ -12,7 +12,6 @@ from astropy.time import Time from .. import __version__, utils -from ..uvdata.initializers import XORIENTMAP def new_uvbeam( @@ -243,7 +242,7 @@ def new_uvbeam( uvb.Nfreqs = freq_array.size if x_orientation is not None: - uvb.x_orientation = XORIENTMAP[x_orientation.lower()] + uvb.x_orientation = utils.XORIENTMAP[x_orientation.lower()] if basis_vector_array is not None: if uvb.pixel_coordinate_system == "healpix": diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index 3de740ff05..67b0b911a4 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -5,6 +5,7 @@ """From-memory initializers for UVCal objects.""" from __future__ import annotations +import warnings from typing import Literal import numpy as np @@ -12,14 +13,8 @@ from .. import Telescope, __version__, utils from ..docstrings import combine_docstrings -from ..uvdata.initializers import ( - XORIENTMAP, - Locations, - get_antenna_params, - get_freq_params, - get_spw_params, - get_time_params, -) +from ..telescopes import Locations, get_antenna_params +from ..uvdata.initializers import get_freq_params, get_spw_params, get_time_params def new_uvcal( @@ -103,34 +98,36 @@ def new_uvcal( telescope name and location, x_orientation and antenna names, numbers and positions. telescope_location : EarthLocation or MoonLocation object - Telescope location as an astropy EarthLocation object or MoonLocation - object. Not required or used if a Telescope object is passed to `telescope`. + Deprecated. Telescope location as an astropy EarthLocation object or + MoonLocation object. Not required or used if a Telescope object is + passed to `telescope`. telescope_name : str - Telescope name. Not required or used if a Telescope object is passed to - `telescope`. + Deprecated. Telescope name. Not required or used if a Telescope object + is passed to `telescope`. x_orientation : str - Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', 'ew', 'ns'. - Not required or used if a Telescope object is passed to `telescope`. + Deprecated. Orientation of the x-axis. Options are 'east', 'north', + 'e', 'n', 'ew', 'ns'. Not required or used if a Telescope object is + passed to `telescope`. antenna_positions : ndarray of float or dict of ndarray of float - Array of antenna positions in ECEF coordinates in meters. + Deprecated. Array of antenna positions in ECEF coordinates in meters. If a dict, keys can either be antenna numbers or antenna names, and values are position arrays. Keys are interpreted as antenna numbers if they are integers, otherwise they are interpreted as antenna names if strings. You cannot provide a mix of different types of keys. Not required or used if a Telescope object is passed to `telescope`. antenna_names : list of str, optional - List of antenna names. If not provided, antenna numbers will be used to form - the antenna_names, according to the antname_format. Not required or used - if a Telescope object is passed to `telescope` or if antenna_positions - is a dict with string keys. + Deprecated. List of antenna names. Not required or used if a Telescope + object is passed to `telescope` or if antenna_positions is a dict with + string keys. Otherwise, if not provided, antenna numbers will be used + to form the antenna_names, according to the antname_format antenna_numbers : list of int, optional - List of antenna numbers. If not provided, antenna names will be used to form - the antenna_numbers, but in this case the antenna_names must be strings that - can be converted to integers. Not required or used - if a Telescope object is passed to `telescope` or if antenna_positions - is a dict with integer keys. + Deprecated. List of antenna numbers. Not required or used if a Telescope + object is passed to `telescope` or if antenna_positions is a dict with + integer keys. Otherwise, if not provided, antenna names will be used to + form the antenna_numbers, but in this case the antenna_names must be + strings that can be converted to integers. antname_format : str, optional - Format string for antenna names. Default is '{0:03d}'. + Deprecated. Format string for antenna names. Default is '{0:03d}'. ant_array : ndarray of int, optional Array of antenna numbers actually found in data (in the order of the data in gain_array etc.) @@ -186,32 +183,61 @@ def new_uvcal( for key, value in required_without_tel.items(): if value is None: raise ValueError(f"{key} is required if telescope is not provided.") - else: - required_on_tel = { + + instrument = kwargs.pop("instrument", None) + antenna_diameters = kwargs.pop("antenna_diameters", None) + old_params = { + "telescope_name": telescope_name, + "telescope_location": telescope_location, "antenna_positions": antenna_positions, + "antenna_names": antenna_names, + "antenna_numbers": antenna_numbers, + "instrument": instrument, "x_orientation": x_orientation, + "antenna_diameters": antenna_diameters, } - for key, value in required_on_tel.items(): - if value is None and getattr(telescope, key) is None: - raise ValueError(f"{key} is required if it is not set on telescope.") - - if telescope is not None: - antenna_numbers = telescope.antenna_numbers - telescope_location = telescope.location + warn_params = [] + for param, val in old_params.items(): + if val is not None: + warn_params.append(param) + warnings.warn( + f"Passing {', '.join(warn_params)} is deprecated in favor of passing " + "a Telescope object via the `telescope` parameter. This will become " + "an error in version 3.2", + DeprecationWarning, + ) else: - antenna_positions, antenna_names, antenna_numbers = get_antenna_params( + required_on_tel = [ + "antenna_positions", + "antenna_names", + "antenna_numbers", + "Nants", + "x_orientation", + ] + for key in required_on_tel: + if getattr(telescope, key) is None: + raise ValueError( + f"{key} must be set on the Telescope object passed to `telescope`." + ) + + if telescope is None: + telescope = Telescope.from_params( + name=telescope_name, + location=telescope_location, antenna_positions=antenna_positions, antenna_names=antenna_names, antenna_numbers=antenna_numbers, antname_format=antname_format, + instrument=instrument, + x_orientation=x_orientation, + antenna_diameters=antenna_diameters, ) - x_orientation = XORIENTMAP[x_orientation.lower()] if ant_array is None: - ant_array = antenna_numbers + ant_array = telescope.antenna_numbers else: # Ensure they all exist - missing = [ant for ant in ant_array if ant not in antenna_numbers] + missing = [ant for ant in ant_array if ant not in telescope.antenna_numbers] if missing: raise ValueError( f"The following ants are not in antenna_numbers: {missing}" @@ -219,14 +245,14 @@ def new_uvcal( if time_array is not None: lst_array, integration_time = get_time_params( - telescope_location=telescope_location, + telescope_location=telescope.location, time_array=time_array, integration_time=integration_time, astrometry_library=astrometry_library, ) if time_range is not None: lst_range, integration_time = get_time_params( - telescope_location=telescope_location, + telescope_location=telescope.location, time_array=time_range, integration_time=integration_time, astrometry_library=astrometry_library, @@ -276,7 +302,9 @@ def new_uvcal( else: jones_array = np.array(jones_array) if jones_array.dtype.kind != "i": - jones_array = utils.jstr2num(jones_array, x_orientation=x_orientation) + jones_array = utils.jstr2num( + jones_array, x_orientation=telescope.x_orientation + ) history += ( f"Object created by new_uvcal() at {Time.now().iso} using " @@ -292,20 +320,7 @@ def new_uvcal( ) # Now set all the metadata - if telescope is not None: - uvc.telescope = telescope - else: - new_telescope = Telescope() - - new_telescope.name = telescope_name - new_telescope.location = telescope_location - new_telescope.antenna_names = antenna_names - new_telescope.antenna_numbers = antenna_numbers - new_telescope.Nants = len(antenna_numbers) - new_telescope.antenna_positions = antenna_positions - new_telescope.x_orientation = x_orientation - - uvc.telescope = new_telescope + uvc.telescope = telescope # set the appropriate telescope attributes as required uvc._set_telescope_requirements() @@ -585,7 +600,13 @@ def new_uvcal_from_uvdata( setattr(new_telescope, tele_name, kwargs.pop(param)) if "x_orientation" in kwargs: - new_telescope.x_orientation = XORIENTMAP[kwargs.pop("x_orientation").lower()] + new_telescope.x_orientation = utils.XORIENTMAP[ + kwargs.pop("x_orientation").lower() + ] + if new_telescope.x_orientation is None: + raise ValueError( + "x_orientation must be provided if it is not set on the UVData object." + ) ant_array = kwargs.pop( "ant_array", np.union1d(uvdata.ant_1_array, uvdata.ant_2_array) diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index fa25dd18d9..58260bf7b5 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -8,8 +8,9 @@ import pytest from astropy.coordinates import EarthLocation +import pyuvdata.tests as uvtest +from pyuvdata import Telescope, UVCal from pyuvdata.tests.test_utils import selenoids -from pyuvdata.uvcal import UVCal from pyuvdata.uvcal.initializers import new_uvcal, new_uvcal_from_uvdata from pyuvdata.uvdata.initializers import new_uvdata @@ -19,38 +20,105 @@ def uvd_kw(): return { "freq_array": np.linspace(100e6, 200e6, 10), "times": np.linspace(2459850, 2459851, 12), + "telescope": Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="mock", + instrument="mock", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), + "polarization_array": np.array([-5, -6, -7, -8]), + } + + +@pytest.fixture(scope="function") +def uvc_only_kw(): + return { + "cal_style": "redundant", + "gain_convention": "multiply", + "x_orientation": "n", + "jones_array": "linear", + "cal_type": "gain", + } + + +@pytest.fixture(scope="function") +def uvc_simplest_no_telescope(uvc_only_kw): + return { + "freq_array": np.linspace(100e6, 200e6, 10), + "time_array": np.linspace(2459850, 2459851, 12), + "telescope_location": EarthLocation.from_geodetic(0, 0, 0), + "telescope_name": "mock", + "x_orientation": "n", "antenna_positions": { 0: [0.0, 0.0, 0.0], 1: [0.0, 0.0, 1.0], 2: [0.0, 0.0, 2.0], }, - "telescope_location": EarthLocation.from_geodetic(0, 0, 0), - "telescope_name": "mock", - "polarization_array": np.array([-5, -6, -7, -8]), + "cal_style": "redundant", + "gain_convention": "multiply", + "jones_array": "linear", + "cal_type": "gain", } @pytest.fixture(scope="function") -def uvc_only_kw(): +def uvc_simplest(): return { + "freq_array": np.linspace(100e6, 200e6, 10), + "time_array": np.linspace(2459850, 2459851, 12), + "telescope": Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="mock", + x_orientation="n", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), "cal_style": "redundant", "gain_convention": "multiply", - "x_orientation": "n", "jones_array": "linear", "cal_type": "gain", } @pytest.fixture(scope="function") -def uvc_kw(uvd_kw, uvc_only_kw): - uvd_kw["time_array"] = uvd_kw["times"] - del uvd_kw["times"] - del uvd_kw["polarization_array"] - return {**uvc_only_kw, **uvd_kw} +def uvc_simplest_moon(): + pytest.importorskip("lunarsky") + from pyuvdata.utils import MoonLocation + + return { + "freq_array": np.linspace(100e6, 200e6, 10), + "time_array": np.linspace(2459850, 2459851, 12), + "telescope": Telescope.from_params( + location=MoonLocation.from_selenodetic(0, 0, 0), + name="mock", + x_orientation="n", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), + "cal_style": "redundant", + "gain_convention": "multiply", + "jones_array": "linear", + "cal_type": "gain", + } -def test_new_uvcal_simplest(uvc_kw): - uvc = UVCal.new(**uvc_kw) +def test_new_uvcal_simplest(uvc_simplest_no_telescope): + with uvtest.check_warnings( + DeprecationWarning, + match="Passing telescope_name, telescope_location, antenna_positions, " + "x_orientation is deprecated in favor of passing a Telescope object", + ): + uvc = UVCal.new(**uvc_simplest_no_telescope) assert uvc.Nants_data == 3 assert uvc.telescope.Nants == 3 assert uvc.Nfreqs == 10 @@ -58,47 +126,42 @@ def test_new_uvcal_simplest(uvc_kw): @pytest.mark.parametrize("selenoid", selenoids) -def test_new_uvcal_simple_moon(uvc_kw, selenoid): - pytest.importorskip("lunarsky") - from pyuvdata.utils import MoonLocation - - uvc_kw["telescope_location"] = MoonLocation.from_selenodetic( - 0, 0, 0, ellipsoid=selenoid - ) - uvc = UVCal.new(**uvc_kw) +def test_new_uvcal_simple_moon(uvc_simplest_moon, selenoid): + uvc_simplest_moon["telescope"].location.ellipsoid = selenoid + uvc = UVCal.new(**uvc_simplest_moon) assert uvc.telescope._location.frame == "mcmf" assert uvc.telescope._location.ellipsoid == selenoid - assert uvc.telescope.location == uvc_kw["telescope_location"] + assert uvc.telescope.location == uvc_simplest_moon["telescope"].location assert uvc.telescope.location.ellipsoid == selenoid -def test_new_uvcal_time_range(uvc_kw): - tdiff = np.mean(np.diff(uvc_kw["time_array"])) - tstarts = uvc_kw["time_array"] - tdiff / 2 - tends = uvc_kw["time_array"] + tdiff / 2 - uvc_kw["time_range"] = np.stack((tstarts, tends), axis=1) - del uvc_kw["time_array"] +def test_new_uvcal_time_range(uvc_simplest): + tdiff = np.mean(np.diff(uvc_simplest["time_array"])) + tstarts = uvc_simplest["time_array"] - tdiff / 2 + tends = uvc_simplest["time_array"] + tdiff / 2 + uvc_simplest["time_range"] = np.stack((tstarts, tends), axis=1) + del uvc_simplest["time_array"] - UVCal.new(**uvc_kw) + UVCal.new(**uvc_simplest) - uvc_kw["integration_time"] = tdiff * 86400 - uvc = UVCal.new(**uvc_kw) + uvc_simplest["integration_time"] = tdiff * 86400 + uvc = UVCal.new(**uvc_simplest) assert uvc.Ntimes == 12 - uvc_kw["integration_time"] = np.full((5,), tdiff * 86400) + uvc_simplest["integration_time"] = np.full((5,), tdiff * 86400) with pytest.raises( ValueError, match="integration_time must be the same length as the first axis of " "time_range.", ): - uvc = UVCal.new(**uvc_kw) + uvc = UVCal.new(**uvc_simplest) -def test_new_uvcal_bad_inputs(uvc_kw): +def test_new_uvcal_bad_inputs(uvc_simplest): with pytest.raises( ValueError, match="The following ants are not in antenna_numbers" ): - new_uvcal(ant_array=[0, 1, 2, 3], **uvc_kw) + new_uvcal(ant_array=[0, 1, 2, 3], **uvc_simplest) with pytest.raises( ValueError, @@ -107,7 +170,8 @@ def test_new_uvcal_bad_inputs(uvc_kw): ), ): new_uvcal( - cal_style="sky", **{k: v for k, v in uvc_kw.items() if k != "cal_style"} + cal_style="sky", + **{k: v for k, v in uvc_simplest.items() if k != "cal_style"} ) with pytest.raises( @@ -117,30 +181,34 @@ def test_new_uvcal_bad_inputs(uvc_kw): cal_style="wrong", ref_antenna_name="mock", sky_catalog="mock", - **{k: v for k, v in uvc_kw.items() if k != "cal_style"} + **{k: v for k, v in uvc_simplest.items() if k != "cal_style"} ) with pytest.raises(ValueError, match="Unrecognized keyword argument"): - new_uvcal(bad_kwarg=True, **uvc_kw) + new_uvcal(bad_kwarg=True, **uvc_simplest) with pytest.raises( ValueError, match=re.escape("Provide *either* freq_range *or* freq_array") ): - new_uvcal(freq_range=[100e6, 200e6], **uvc_kw) + new_uvcal(freq_range=[100e6, 200e6], **uvc_simplest) with pytest.raises(ValueError, match="You must provide either freq_array"): - new_uvcal(**{k: v for k, v in uvc_kw.items() if k != "freq_array"}) + new_uvcal(**{k: v for k, v in uvc_simplest.items() if k != "freq_array"}) with pytest.raises(ValueError, match="cal_type must be either 'gain' or 'delay'"): new_uvcal( cal_type="wrong", freq_range=[150e6, 180e6], - **{k: v for k, v in uvc_kw.items() if k not in ("freq_array", "cal_type")} + **{ + k: v + for k, v in uvc_simplest.items() + if k not in ("freq_array", "cal_type") + } ) -def test_new_uvcal_jones_array(uvc_kw): - uvc = {k: v for k, v in uvc_kw.items() if k != "jones_array"} +def test_new_uvcal_jones_array(uvc_simplest): + uvc = {k: v for k, v in uvc_simplest.items() if k != "jones_array"} lin = new_uvcal(jones_array="linear", **uvc) assert lin.Njones == 4 @@ -161,8 +229,8 @@ def test_new_uvcal_jones_array(uvc_kw): assert np.allclose(linear_physical.jones_array, np.array([-5, -6, -7, -8])) -def test_new_uvcal_set_sky(uvc_kw): - uvc = {k: v for k, v in uvc_kw.items() if k != "cal_style"} +def test_new_uvcal_set_sky(uvc_simplest): + uvc = {k: v for k, v in uvc_simplest.items() if k != "cal_style"} sk = new_uvcal(cal_style="sky", ref_antenna_name="mock", sky_catalog="mock", **uvc) assert sk.cal_style == "sky" @@ -170,19 +238,19 @@ def test_new_uvcal_set_sky(uvc_kw): assert sk.sky_catalog == "mock" -def test_new_uvcal_set_extra_keywords(uvc_kw): - uvc = new_uvcal(extra_keywords={"test": "test", "test2": "test2"}, **uvc_kw) +def test_new_uvcal_set_extra_keywords(uvc_simplest): + uvc = new_uvcal(extra_keywords={"test": "test", "test2": "test2"}, **uvc_simplest) assert uvc.extra_keywords["test"] == "test" assert uvc.extra_keywords["test2"] == "test2" -def test_new_uvcal_set_empty(uvc_kw): - uvc = new_uvcal(empty=True, **uvc_kw) +def test_new_uvcal_set_empty(uvc_simplest): + uvc = new_uvcal(empty=True, **uvc_simplest) assert uvc.flag_array.dtype == bool -def test_new_uvcal_set_delay(uvc_kw): - uvc = {k: v for k, v in uvc_kw.items() if k not in ("freq_array", "cal_type")} +def test_new_uvcal_set_delay(uvc_simplest): + uvc = {k: v for k, v in uvc_simplest.items() if k not in ("freq_array", "cal_type")} new = new_uvcal( delay_array=np.linspace(1, 10, 10), freq_range=[150e6, 180e6], @@ -202,7 +270,7 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): assert np.all(uvc.time_array == uvd_kw["times"]) assert np.all(uvc.freq_array == uvd_kw["freq_array"]) - assert uvc.telescope.name == uvd_kw["telescope_name"] + assert uvc.telescope.name == uvd_kw["telescope"].name uvc = new_uvcal_from_uvdata(uvd, time_array=uvd_kw["times"][:-1], **uvc_only_kw) assert np.all(uvc.time_array == uvd_kw["times"][:-1]) @@ -215,14 +283,16 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): uvc = new_uvcal_from_uvdata( uvd, antenna_positions={ - 0: uvd_kw["antenna_positions"][0], - 1: uvd_kw["antenna_positions"][1], + 0: uvd_kw["telescope"].antenna_positions[0], + 1: uvd_kw["telescope"].antenna_positions[1], }, antenna_diameters=[10.0, 10.0], **uvc_only_kw ) - assert np.all(uvc.telescope.antenna_positions[0] == uvd_kw["antenna_positions"][0]) + assert np.all( + uvc.telescope.antenna_positions[0] == uvd_kw["telescope"].antenna_positions[0] + ) assert len(uvc.telescope.antenna_positions) == 2 uvd.telescope.antenna_diameters = np.zeros(uvd.telescope.Nants, dtype=float) + 5.0 diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index ab05151429..50a66fcc11 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -668,14 +668,7 @@ def __init__(self): def _set_telescope_requirements(self): """Set the UVParameter required fields appropriately for UVCal.""" - self.telescope._name.required = True - self.telescope._location.required = True self.telescope._instrument.required = False - self.telescope._Nants.required = True - self.telescope._antenna_names.required = True - self.telescope._antenna_numbers.required = True - self.telescope._antenna_positions.required = True - self.telescope._antenna_diameters.required = False self.telescope._x_orientation.required = True @staticmethod @@ -1998,6 +1991,8 @@ def check( elif self.cal_style == "redundant": self._set_redundant() + self._set_telescope_requirements() + # if wide_band is True, Nfreqs must be 1. if self.wide_band: if self.Nfreqs != 1: diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 5d63c58ece..9c14537dac 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -7,92 +7,14 @@ import warnings from itertools import combinations_with_replacement -from typing import Any, Literal, Sequence, Union +from typing import Any, Literal, Sequence import numpy as np from astropy.coordinates import EarthLocation from astropy.time import Time from .. import Telescope, __version__, utils - -try: - from lunarsky import MoonLocation - - hasmoon = True - Locations = Union[MoonLocation, EarthLocation] -except ImportError: - hasmoon = False - Locations = EarthLocation - - -XORIENTMAP = { - "east": "east", - "north": "north", - "e": "east", - "n": "north", - "ew": "east", - "ns": "north", -} - - -def get_antenna_params( - *, - antenna_positions: np.ndarray | dict[str | int, np.ndarray], - antenna_names: list[str] | None = None, - antenna_numbers: list[int] | None = None, - antname_format: str = "{0:03d}", -) -> tuple[np.ndarray, list[str], list[int]]: - """Configure antenna parameters for new UVData object.""" - # Get Antenna Parameters - - if isinstance(antenna_positions, dict): - keys = list(antenna_positions.keys()) - if all(isinstance(key, int) for key in keys): - antenna_numbers = list(antenna_positions.keys()) - elif all(isinstance(key, str) for key in keys): - antenna_names = list(antenna_positions.keys()) - else: - raise ValueError( - "antenna_positions must be a dictionary with keys that are all type " - "int or all type str." - ) - antenna_positions = np.array(list(antenna_positions.values())) - - if antenna_numbers is None and antenna_names is None: - raise ValueError( - "Either antenna_numbers or antenna_names must be provided unless " - "antenna_positions is a dict." - ) - - if antenna_names is None: - antenna_names = [antname_format.format(i) for i in antenna_numbers] - elif antenna_numbers is None: - try: - antenna_numbers = [int(name) for name in antenna_names] - except ValueError as e: - raise ValueError( - "Antenna names must be integers if antenna_numbers is not provided." - ) from e - - if not isinstance(antenna_positions, np.ndarray): - raise ValueError("antenna_positions must be a numpy array or a dictionary.") - - if antenna_positions.shape != (len(antenna_numbers), 3): - raise ValueError( - "antenna_positions must be a 2D array with shape (N_antennas, 3), " - f"got {antenna_positions.shape}" - ) - - if len(antenna_names) != len(set(antenna_names)): - raise ValueError("Duplicate antenna names found.") - - if len(antenna_numbers) != len(set(antenna_numbers)): - raise ValueError("Duplicate antenna numbers found.") - - if len(antenna_numbers) != len(antenna_names): - raise ValueError("antenna_numbers and antenna_names must have the same length.") - - return antenna_positions, antenna_names, antenna_numbers +from ..telescopes import Locations def get_time_params( @@ -360,14 +282,15 @@ def new_uvdata( *, freq_array: np.ndarray, polarization_array: np.ndarray | list[str | int] | tuple[str | int], - antenna_positions: np.ndarray | dict[str | int, np.ndarray], - telescope_location: Locations, - telescope_name: str, times: np.ndarray, + telescope: Telescope | None = None, antpairs: Sequence[tuple[int, int]] | np.ndarray | None = None, do_blt_outer: bool | None = None, integration_time: float | np.ndarray | None = None, channel_width: float | np.ndarray | None = None, + telescope_location: Locations | None = None, + telescope_name: str | None = None, + antenna_positions: np.ndarray | dict[str | int, np.ndarray] | None = None, antenna_names: list[str] | None = None, antenna_numbers: list[int] | None = None, blts_are_rectangular: bool | None = None, @@ -376,7 +299,7 @@ def new_uvdata( nsample_array: np.ndarray | None = None, flex_spw_id_array: np.ndarray | None = None, history: str = "", - instrument: str = "", + instrument: str | None = None, vis_units: Literal["Jy", "K str", "uncalib"] = "uncalib", antname_format: str = "{0:03d}", empty: bool = False, @@ -395,20 +318,16 @@ def new_uvdata( Array of frequencies in Hz. polarization_array : sequence of int or str Array of polarization integers or strings (eg. 'xx' or 'ee') - antenna_positions : ndarray of float or dict of ndarray of float - Array of antenna positions in ECEF coordinates in meters. If a dict, keys are - antenna names or numbers and values are antenna positions in ECEF coordinates - in meters. - telescope_location : astropy EarthLocation or MoonLocation - Location of the telescope. - telescope_name : str - Name of the telescope. times : ndarray of float, optional Array of times in Julian Date. These may be the *unique* times of the data if each baseline observes the same set of times, otherwise they should be an Nblts-length array of each time observed by each baseline. It is recommended to set the ``do_blt_outer`` parameter to specify whether to apply the times to each baseline. + telescope : pyuvdata.Telescope + Telescope object containing the telescope-related metadata including + telescope name and location, x_orientation and antenna names, numbers + and positions. antpairs : sequence of 2-tuples of int or 2D array of int, optional Antenna pairs in the data. If an ndarray, must have shape (Nants, 2). These may be the *unique* antpairs of the data if @@ -439,15 +358,40 @@ def new_uvdata( If not provided and freq_array is length-one, the channel_width will be set to 1 Hz (and a warning issued). If an ndarray is provided, it must have the same shape as freq_array. + telescope_location : EarthLocation or MoonLocation object + Deprecated. Telescope location as an astropy EarthLocation object or + MoonLocation object. Not required or used if a Telescope object is + passed to `telescope`. + telescope_name : str + Deprecated. Telescope name. Not required or used if a Telescope object + is passed to `telescope`. + antenna_positions : ndarray of float or dict of ndarray of float + Deprecated. Array of antenna positions in ECEF coordinates in meters. + If a dict, keys can either be antenna numbers or antenna names, and values are + position arrays. Keys are interpreted as antenna numbers if they are integers, + otherwise they are interpreted as antenna names if strings. You cannot + provide a mix of different types of keys. + Not required or used if a Telescope object is passed to `telescope`. antenna_names : list of str, optional - List of antenna names. If not provided, antenna numbers will be used to form - the antenna_names, according to the antname_format. antenna_names need not be - provided if antenna_positions is a dict with string keys. + Deprecated. List of antenna names. Not required or used if a Telescope + object is passed to `telescope` or if antenna_positions is a dict with + string keys. Otherwise, if not provided, antenna numbers will be used + to form the antenna_names, according to the antname_format antenna_numbers : list of int, optional - List of antenna numbers. If not provided, antenna names will be used to form - the antenna_numbers, but in this case the antenna_names must be strings that - can be converted to integers. antenna_numbers need not be provided if - antenna_positions is a dict with integer keys. + Deprecated. List of antenna numbers. Not required or used if a Telescope + object is passed to `telescope` or if antenna_positions is a dict with + integer keys. Otherwise, if not provided, antenna names will be used to + form the antenna_numbers, but in this case the antenna_names must be + strings that can be converted to integers. + antname_format : str, optional + Deprecated. Format string for antenna names. Default is '{0:03d}'. + x_orientation : str, optional + Deprecated. Orientation of the x-axis. Options are 'east', 'north', + 'e', 'n', 'ew', 'ns'. Not used if a Telescope object is passed to + `telescope`. + instrument : str, optional + Deprecated. Instrument name. Default is the ``telescope_name``. Not used + if a Telescope object is passed to `telescope`. blts_are_rectangular : bool, optional Set to True if the time_array and antpair_array are rectangular, i.e. if they are formed from the outer product of a unique set of times/antenna pairs. @@ -470,13 +414,9 @@ def new_uvdata( history : str, optional History string to be added to the object. Default is a simple string containing the date and time and pyuvdata version. - instrument : str, optional - Instrument name. Default is the ``telescope_name``. vis_units : str, optional Visibility units. Default is 'uncalib'. Must be one of 'Jy', 'K str', or 'uncalib'. - antname_format : str, optional - Format string for antenna names. Default is '{0:03d}'. empty : bool, optional Set to True to create an empty (but not metadata-only) UVData object. Default is False. @@ -492,8 +432,6 @@ def new_uvdata( phase_center_id_array : ndarray of int, optional Array of phase center ids. If not provided, it will be initialized to the first id found in ``phase_center_catalog``. It must have shape ``(Nblts,)``. - x_orientation : str - Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', 'ew', 'ns'. astrometry_library : str Library used for calculating LSTs. Allowed options are 'erfa' (which uses the pyERFA), 'novas' (which uses the python-novas library), and 'astropy' @@ -515,28 +453,76 @@ def new_uvdata( obj = UVData() - antenna_positions, antenna_names, antenna_numbers = get_antenna_params( - antenna_positions=antenna_positions, - antenna_names=antenna_names, - antenna_numbers=antenna_numbers, - antname_format=antname_format, - ) + if telescope is None: + required_without_tel = { + "antenna_positions": antenna_positions, + "telescope_location": telescope_location, + "telescope_name": telescope_name, + } + for key, value in required_without_tel.items(): + if value is None: + raise ValueError(f"{key} is required if telescope is not provided.") + antenna_diameters = kwargs.pop("antenna_diameters", None) + old_params = { + "telescope_name": telescope_name, + "telescope_location": telescope_location, + "antenna_positions": antenna_positions, + "antenna_names": antenna_names, + "antenna_numbers": antenna_numbers, + "instrument": instrument, + "x_orientation": x_orientation, + "antenna_diameters": antenna_diameters, + } + warn_params = [] + for param, val in old_params.items(): + if val is not None: + warn_params.append(param) + warnings.warn( + f"Passing {', '.join(warn_params)} is deprecated in favor of passing " + "a Telescope object via the `telescope` parameter. This will become " + "an error in version 3.2", + DeprecationWarning, + ) + else: + required_on_tel = [ + "name", + "location", + "antenna_positions", + "antenna_names", + "antenna_numbers", + "Nants", + "instrument", + ] + for key in required_on_tel: + if getattr(telescope, key) is None: + raise ValueError( + f"{key} must be set on the Telescope object passed to `telescope`." + ) - if not isinstance(telescope_location, tuple(utils.allowed_location_types)): - raise ValueError( - "telescope_location has an unsupported type, it must be one of " - f"{utils.allowed_location_types}" + if telescope is None: + if instrument is None: + instrument = telescope_name + telescope = Telescope.from_params( + name=telescope_name, + location=telescope_location, + antenna_positions=antenna_positions, + antenna_names=antenna_names, + antenna_numbers=antenna_numbers, + antname_format=antname_format, + instrument=instrument, + x_orientation=x_orientation, + antenna_diameters=antenna_diameters, ) lst_array, integration_time = get_time_params( - telescope_location=telescope_location, + telescope_location=telescope.location, time_array=times, integration_time=integration_time, astrometry_library=astrometry_library, ) if antpairs is None: - antpairs = list(combinations_with_replacement(antenna_numbers, 2)) + antpairs = list(combinations_with_replacement(telescope.antenna_numbers, 2)) do_blt_outer = True ( @@ -556,7 +542,7 @@ def new_uvdata( time_sized_arrays=(lst_array, integration_time), ) baseline_array = get_baseline_params( - antenna_numbers=antenna_numbers, antpairs=antpairs + antenna_numbers=telescope.antenna_numbers, antpairs=antpairs ) # Re-get the ant arrays because the baseline array may have changed @@ -570,18 +556,12 @@ def new_uvdata( flex_spw_id_array=flex_spw_id_array, freq_array=freq_array ) - if x_orientation is not None: - x_orientation = XORIENTMAP[x_orientation.lower()] - polarization_array = np.array(polarization_array) if polarization_array.dtype.kind != "i": polarization_array = utils.polstr2num( polarization_array, x_orientation=x_orientation ) - if not instrument: - instrument = telescope_name - if vis_units not in ["Jy", "K str", "uncalib"]: raise ValueError("vis_units must be one of 'Jy', 'K str', or 'uncalib'.") @@ -591,27 +571,21 @@ def new_uvdata( ) # Now set all the metadata - # initialize telescope object first - obj.telescope = Telescope() + obj.telescope = telescope + # set the appropriate telescope attributes as required + obj._set_telescope_requirements() obj.freq_array = freq_array obj.polarization_array = polarization_array - obj.telescope.antenna_positions = antenna_positions - obj.telescope.location = telescope_location - obj.telescope.name = telescope_name obj.baseline_array = baseline_array obj.ant_1_array = ant_1_array obj.ant_2_array = ant_2_array obj.time_array = time_array obj.lst_array = lst_array obj.channel_width = channel_width - obj.telescope.antenna_names = antenna_names - obj.telescope.antenna_numbers = antenna_numbers obj.history = history - obj.telescope.instrument = instrument obj.vis_units = vis_units obj.Nants_data = len(set(np.concatenate([ant_1_array, ant_2_array]))) - obj.telescope.Nants = len(antenna_numbers) obj.Nbls = nbls obj.Nblts = len(baseline_array) obj.Nfreqs = len(freq_array) @@ -621,7 +595,6 @@ def new_uvdata( obj.spw_array = spw_array obj.flex_spw_id_array = flex_spw_id_array obj.integration_time = integration_time - obj.telescope.x_orientation = x_orientation set_phase_params( obj, diff --git a/pyuvdata/uvdata/mir.py b/pyuvdata/uvdata/mir.py index 8b8e575047..02fcb0e57b 100644 --- a/pyuvdata/uvdata/mir.py +++ b/pyuvdata/uvdata/mir.py @@ -12,6 +12,7 @@ from docstring_parser import DocstringStyle from pyuvdata import Telescope, UVData +from pyuvdata.telescopes import known_telescope_location from .. import utils as uvutils from ..docstrings import copy_replace_short_description @@ -60,8 +61,9 @@ def generate_sma_antpos_dict(filepath): # We need the antenna positions in ECEF, rather than the native rotECEF format that # they are stored in. Get the longitude info, and use the appropriate function in # utils to get these values the way that we want them. - _, lon, _ = Telescope.from_known_telescopes("SMA").location_lat_lon_alt - mir_antpos["xyz_pos"] = uvutils.ECEF_from_rotECEF(mir_antpos["xyz_pos"], lon) + mir_antpos["xyz_pos"] = uvutils.ECEF_from_rotECEF( + mir_antpos["xyz_pos"], known_telescope_location("SMA").lon.rad + ) # Create a dictionary that can be used for updates. return {item["antenna"]: item["xyz_pos"] for item in mir_antpos} @@ -525,13 +527,15 @@ def _init_from_mir_parser( np.concatenate((mir_data.bl_data["iant1"], mir_data.bl_data["iant2"])) ) ) - self.telescope.Nants = 8 self.Nbls = int(self.Nants_data * (self.Nants_data - 1) / 2) self.Nblts = Nblts self.Npols = Npols self.Ntimes = len(mir_data.in_data) - self.telescope.antenna_names = ["Ant%i" % idx for idx in range(1, 9)] + self.telescope = Telescope() + self._set_telescope_requirements() + self.telescope.Nants = 8 + self.telescope.antenna_names = ["Ant%i" % idx for idx in range(1, 9)] self.telescope.antenna_numbers = np.arange(1, 9) # Prepare the XYZ coordinates of the antenna positions. @@ -543,7 +547,7 @@ def _init_from_mir_parser( ] # Get the coordinates from the entry in telescope.py - self.telescope.location = Telescope.from_known_telescopes("SMA").location + self.telescope.location = known_telescope_location("SMA") # Calculate antenna positions in ECEF frame. Note that since both # coordinate systems are in relative units, no subtraction from diff --git a/pyuvdata/uvdata/mir_parser.py b/pyuvdata/uvdata/mir_parser.py index face74f9ab..8b80f7f5d3 100644 --- a/pyuvdata/uvdata/mir_parser.py +++ b/pyuvdata/uvdata/mir_parser.py @@ -4130,8 +4130,8 @@ def _make_v3_compliant(self): from astropy.time import Time - from .. import Telescope from .. import utils as uvutils + from ..telescopes import known_telescope_location # First thing -- we only want modern (i.e., SWARM) data, since the older (ASIC) # data is not currently supported by the data handling tools, due to changes @@ -4139,7 +4139,7 @@ def _make_v3_compliant(self): # if swarm_only: # self.select(where=("correlator", "eq", 1)) # Get SMA coordinates for various data-filling stuff - telescope_location = Telescope.from_known_telescopes("SMA").location + telescope_location = known_telescope_location("SMA") # in_data updates: mjd, lst, ara, adec # First sort out the time stamps using the day reference inside codes_data, and diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index 09ea20f742..a1f69d45cc 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -16,9 +16,9 @@ from astropy.time import Time from docstring_parser import DocstringStyle -from .. import Telescope from .. import utils as uvutils from ..docstrings import copy_replace_short_description +from ..telescopes import known_telescope_location from .uvdata import UVData, _future_array_shapes_warning, reporting_request __all__ = ["Miriad"] @@ -301,37 +301,37 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): # get info from known telescopes. # Check to make sure the lat/lon values match reasonably well try: - telescope_obj = Telescope.from_known_telescopes(self.telescope.name) + telescope_loc = known_telescope_location(self.telescope.name) except ValueError: - telescope_obj = None - if telescope_obj is not None: + telescope_loc = None + if telescope_loc is not None: tol = 2 * np.pi * 1e-3 / (60.0 * 60.0 * 24.0) # 1mas in radians lat_close = np.isclose( - telescope_obj.location.lat.rad, latitude, rtol=0, atol=tol + telescope_loc.lat.rad, latitude, rtol=0, atol=tol ) lon_close = np.isclose( - telescope_obj.location.lon.rad, longitude, rtol=0, atol=tol + telescope_loc.lon.rad, longitude, rtol=0, atol=tol ) if correct_lat_lon: - self.telescope.location = telescope_obj.location + self.telescope.location = telescope_loc else: self.telescope.location = EarthLocation.from_geodetic( lat=latitude * units.rad, lon=longitude * units.rad, - height=telescope_obj.location.height, + height=telescope_loc.height, ) if lat_close and lon_close: if correct_lat_lon: warnings.warn( "Altitude is not present in Miriad file, " "using known location values for " - f"{telescope_obj.name}." + f"{self.telescope.name}." ) else: warnings.warn( "Altitude is not present in Miriad file, " "using known location altitude value " - f"for {telescope_obj.name} and lat/lon from " + f"for {self.telescope.name} and lat/lon from " "file." ) else: @@ -353,13 +353,13 @@ def _load_telescope_coords(self, uv, *, correct_lat_lon=True): ) if correct_lat_lon: warn_string = ( - warn_string + f"for {telescope_obj.name} in known " + warn_string + f"for {self.telescope.name} in known " "telescopes. Using values from known telescopes." ) warnings.warn(warn_string) else: warn_string = ( - warn_string + f"for {telescope_obj.name} in known " + warn_string + f"for {self.telescope.name} in known " "telescopes. Using altitude value from known " "telescopes and lat/lon from file." ) diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index 42f06235df..2135f40785 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -12,12 +12,12 @@ import pytest from astropy.coordinates import EarthLocation -from pyuvdata import UVData +import pyuvdata.tests as uvtest +from pyuvdata import Telescope, UVData from pyuvdata.tests.test_utils import selenoids from pyuvdata.utils import polnum2str from pyuvdata.uvdata.initializers import ( configure_blt_rectangularity, - get_antenna_params, get_freq_params, get_spw_params, get_time_params, @@ -25,7 +25,7 @@ @pytest.fixture(scope="function") -def simplest_working_params() -> dict[str, Any]: +def simplest_working_params_no_telescope() -> dict[str, Any]: return { "freq_array": np.linspace(1e8, 2e8, 100), "polarization_array": ["xx", "yy"], @@ -40,6 +40,25 @@ def simplest_working_params() -> dict[str, Any]: } +@pytest.fixture(scope="function") +def simplest_working_params() -> dict[str, Any]: + return { + "freq_array": np.linspace(1e8, 2e8, 100), + "polarization_array": ["xx", "yy"], + "telescope": Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="test", + instrument="test", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), + "times": np.linspace(2459855, 2459856, 20), + } + + @pytest.fixture def lunar_simple_params() -> dict[str, Any]: pytest.importorskip("lunarsky") @@ -48,19 +67,27 @@ def lunar_simple_params() -> dict[str, Any]: return { "freq_array": np.linspace(1e8, 2e8, 100), "polarization_array": ["xx", "yy"], - "antenna_positions": { - 0: [0.0, 0.0, 0.0], - 1: [0.0, 0.0, 1.0], - 2: [0.0, 0.0, 2.0], - }, - "telescope_location": MoonLocation.from_selenodetic(0, 0, 0), - "telescope_name": "test", + "telescope": Telescope.from_params( + location=MoonLocation.from_selenodetic(0, 0, 0), + name="test", + instrument="test", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), "times": np.linspace(2459855, 2459856, 20), } -def test_simplest_new_uvdata(simplest_working_params: dict[str, Any]): - uvd = UVData.new(**simplest_working_params) +def test_simplest_new_uvdata(simplest_working_params_no_telescope: dict[str, Any]): + with uvtest.check_warnings( + DeprecationWarning, + match="Passing telescope_name, telescope_location, antenna_positions is " + "deprecated in favor of passing a Telescope object", + ): + uvd = UVData.new(**simplest_working_params_no_telescope) assert uvd.Nfreqs == 100 assert uvd.Npols == 2 @@ -73,7 +100,7 @@ def test_simplest_new_uvdata(simplest_working_params: dict[str, Any]): @pytest.mark.parametrize("selenoid", selenoids) def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: str): - lunar_simple_params["telescope_location"].ellipsoid = selenoid + lunar_simple_params["telescope"].location.ellipsoid = selenoid uvd = UVData.new(**lunar_simple_params) assert uvd.telescope._location.frame == "mcmf" @@ -90,86 +117,10 @@ def test_bad_inputs(simplest_working_params: dict[str, Any]): UVData.new(**simplest_working_params, derp="foo") -def test_bad_antenna_inputs(simplest_working_params: dict[str, Any]): - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises( - ValueError, match="Either antenna_numbers or antenna_names must be provided" - ): - UVData.new( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=None, - antenna_names=None, - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises( - ValueError, - match=( - "antenna_positions must be a dictionary with keys that are all type int " - "or all type str" - ), - ): - UVData.new(antenna_positions={1: [0, 1, 2], "2": [3, 4, 5]}, **badp) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="Antenna names must be integers"): - UVData.new( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=None, - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="antenna_positions must be a numpy array"): - UVData.new( - antenna_positions="foo", - antenna_numbers=[0, 1, 2], - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="antenna_positions must be a 2D array"): - UVData.new( - antenna_positions=np.array([0, 0, 0]), antenna_numbers=np.array([0]), **badp - ) - - with pytest.raises(ValueError, match="Duplicate antenna names found"): - UVData.new(antenna_names=["foo", "bar", "foo"], **simplest_working_params) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="Duplicate antenna numbers found"): - UVData.new( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=[0, 1, 0], - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - with pytest.raises( - ValueError, match="antenna_numbers and antenna_names must have the same length" - ): - UVData.new(antenna_names=["foo", "bar"], **simplest_working_params) - - def test_bad_time_inputs(simplest_working_params: dict[str, Any]): with pytest.raises(ValueError, match="time_array must be a numpy array"): get_time_params( - telescope_location=simplest_working_params["telescope_location"], + telescope_location=simplest_working_params["telescope"].location, time_array="hello this is a string", ) @@ -177,7 +128,7 @@ def test_bad_time_inputs(simplest_working_params: dict[str, Any]): TypeError, match="integration_time must be array_like of floats" ): get_time_params( - telescope_location=simplest_working_params["telescope_location"], + telescope_location=simplest_working_params["telescope"].location, integration_time={"a": "dict"}, time_array=simplest_working_params["times"], ) @@ -187,7 +138,7 @@ def test_bad_time_inputs(simplest_working_params: dict[str, Any]): ): get_time_params( integration_time=np.ones(len(simplest_working_params["times"]) + 1), - telescope_location=simplest_working_params["telescope_location"], + telescope_location=simplest_working_params["telescope"].location, time_array=simplest_working_params["times"], ) @@ -265,37 +216,6 @@ def test_bad_rectangularity_inputs(): ) -def test_alternate_antenna_inputs(): - antpos_dict = { - 0: np.array([0.0, 0.0, 0.0]), - 1: np.array([0.0, 0.0, 1.0]), - 2: np.array([0.0, 0.0, 2.0]), - } - - antpos_array = np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]], dtype=float) - antnum = np.array([0, 1, 2]) - antname = np.array(["000", "001", "002"]) - - pos, names, nums = get_antenna_params(antenna_positions=antpos_dict) - pos2, names2, nums2 = get_antenna_params( - antenna_positions=antpos_array, antenna_numbers=antnum, antenna_names=antname - ) - - assert np.allclose(pos, pos2) - assert np.all(names == names2) - assert np.all(nums == nums2) - - antpos_dict = { - "000": np.array([0, 0, 0]), - "001": np.array([0, 0, 1]), - "002": np.array([0, 0, 2]), - } - pos, names, nums = get_antenna_params(antenna_positions=antpos_dict) - assert np.allclose(pos, pos2) - assert np.all(names == names2) - assert np.all(nums == nums2) - - def test_alternate_time_inputs(): loc = EarthLocation.from_geodetic(0, 0, 0) @@ -565,15 +485,6 @@ def test_get_spw_params(): ) -@pytest.mark.parametrize("xorient", ["e", "n", "east", "NORTH"]) -def test_passing_xorient(simplest_working_params, xorient): - uvd = UVData.new(x_orientation=xorient, **simplest_working_params) - if xorient.lower().startswith("e"): - assert uvd.telescope.x_orientation == "east" - else: - assert uvd.telescope.x_orientation == "north" - - def test_passing_directional_pols(simplest_working_params): kw = {**simplest_working_params, **{"polarization_array": ["ee"]}} diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index f9c441ba0a..5ba0dc34a1 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -692,15 +692,8 @@ def __init__(self): def _set_telescope_requirements(self): """Set the UVParameter required fields appropriately for UVData.""" - self.telescope._name.required = True - self.telescope._location.required = True self.telescope._instrument.required = True - self.telescope._Nants.required = True - self.telescope._antenna_names.required = True - self.telescope._antenna_numbers.required = True - self.telescope._antenna_positions.required = True self.telescope._x_orientation.required = False - self.telescope._antenna_diameters.required = False @staticmethod def _clear_antpair2ind_cache(obj): @@ -2579,6 +2572,7 @@ def check( # first check for any old phase attributes set on the object and move the info # into the phase_center_catalog. self._convert_old_phase_attributes() + self._set_telescope_requirements() if self.flex_spw_id_array is None: warnings.warn( diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 1ca38016ac..2a0a362274 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -644,14 +644,7 @@ def __init__( def _set_telescope_requirements(self): """Set the UVParameter required fields appropriately for UVCal.""" - self.telescope._name.required = True - self.telescope._location.required = True self.telescope._instrument.required = False - self.telescope._Nants.required = True - self.telescope._antenna_names.required = True - self.telescope._antenna_numbers.required = True - self.telescope._antenna_positions.required = True - self.telescope._antenna_diameters.required = False self.telescope._x_orientation.required = False @property @@ -942,6 +935,8 @@ def check( else: self._flex_spw_id_array.required = False + self._set_telescope_requirements() + # first run the basic check from UVBase super().check( check_extra=check_extra, run_check_acceptability=run_check_acceptability From 00292008aa5120b6d2ec58198f4eb183e14d7951 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 24 Apr 2024 11:05:23 -0700 Subject: [PATCH 15/59] address review comments --- pyuvdata/telescopes.py | 45 ++-------------------------- pyuvdata/utils.py | 44 +++++++++++++++++++++++++++ pyuvdata/uvbase.py | 30 +++++-------------- pyuvdata/uvcal/tests/test_uvcal.py | 1 + pyuvdata/uvdata/tests/test_mir.py | 2 -- pyuvdata/uvdata/tests/test_uvdata.py | 1 + pyuvdata/uvflag/tests/test_uvflag.py | 3 ++ pyuvdata/uvflag/uvflag.py | 4 ++- 8 files changed, 62 insertions(+), 68 deletions(-) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 4294b46f73..62dd177f47 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -77,45 +77,6 @@ } -def _parse_antpos_file(antenna_positions_file): - """ - Interpret the antenna positions file. - - Parameters - ---------- - antenna_positions_file : str - Name of the antenna_positions_file, which is assumed to be in DATA_PATH. - Should contain antenna names, numbers and ECEF positions relative to the - telescope location. - - Returns - ------- - antenna_names : array of str - Antenna names. - antenna_names : array of int - Antenna numbers. - antenna_positions : array of float - Antenna positions in ECEF relative to the telescope location. - - """ - columns = ["name", "number", "x", "y", "z"] - formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble] - - dt = np.format_parser(formats, columns, []) - ant_array = np.genfromtxt( - antenna_positions_file, - delimiter=",", - autostrip=True, - skip_header=1, - dtype=dt.dtype, - ) - antenna_names = ant_array["name"] - antenna_numbers = ant_array["number"] - antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T - - return antenna_names, antenna_numbers, antenna_positions.astype("float") - - def known_telescopes(): """ Get list of known telescopes. @@ -194,7 +155,7 @@ def known_telescope_location( else: # no telescope matching this name raise ValueError( - f"Telescope {name} is not in astropy_sites or " "known_telescopes_dict." + f"Telescope {name} is not in astropy_sites or known_telescopes_dict." ) if not return_citation: @@ -480,8 +441,8 @@ def update_params_from_known_telescopes( antpos_file = os.path.join( DATA_PATH, telescope_dict["antenna_positions_file"] ) - antenna_names, antenna_numbers, antenna_positions = _parse_antpos_file( - antpos_file + antenna_names, antenna_numbers, antenna_positions = ( + uvutils.parse_antpos_file(antpos_file) ) ant_info = { "Nants": antenna_names.size, diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 54abf4fcb2..2907f6b74a 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -2062,6 +2062,50 @@ def ECEF_from_ENU( return xyz +def parse_antpos_file(antenna_positions_file): + """ + Interpret an antenna positions file. + + Parameters + ---------- + antenna_positions_file : str + Name of the antenna_positions_file, which is assumed to be in DATA_PATH. + Should be a csv file with the following columns: + + - "name": antenna names + - "number": antenna numbers + - "x": x ECEF coordinate relative to the telescope location. + - "y": y ECEF coordinate relative to the telescope location. + - "z": z ECEF coordinate relative to the telescope location. + + Returns + ------- + antenna_names : array of str + Antenna names. + antenna_names : array of int + Antenna numbers. + antenna_positions : array of float + Antenna positions in ECEF relative to the telescope location. + + """ + columns = ["name", "number", "x", "y", "z"] + formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble] + + dt = np.format_parser(formats, columns, []) + ant_array = np.genfromtxt( + antenna_positions_file, + delimiter=",", + autostrip=True, + skip_header=1, + dtype=dt.dtype, + ) + antenna_names = ant_array["name"] + antenna_numbers = ant_array["number"] + antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T + + return antenna_names, antenna_numbers, antenna_positions.astype("float") + + def old_uvw_calc(ra, dec, initial_uvw): """ Calculate old uvws from unphased ones in an icrs or gcrs frame. diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index 8ce047ff7a..e396ae07f5 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -392,11 +392,7 @@ def __iter__(self, uvparams_only=True): """ if uvparams_only: attribute_list = [ - a - for a in dir(self) - if a.startswith("_") - and not a.startswith("__") - and not callable(getattr(self, a)) + a for a in dir(self) if isinstance(getattr(self, a), uvp.UVParameter) ] else: attribute_list = [ @@ -425,19 +421,12 @@ def required(self): required UVParameters on this object. """ - attribute_list = [ - a - for a in dir(self) - if a.startswith("_") - and not a.startswith("__") - and not callable(getattr(self, a)) - ] + attribute_list = list(self.__iter__(uvparams_only=True)) required_list = [] for a in attribute_list: attr = getattr(self, a) - if isinstance(attr, uvp.UVParameter): - if attr.required: - required_list.append(a) + if attr.required: + required_list.append(a) for a in required_list: yield a @@ -451,17 +440,12 @@ def extra(self): optional (non-required) UVParameters on this object. """ - attribute_list = [ - a - for a in dir(self) - if not a.startswith("__") and not callable(getattr(self, a)) - ] + attribute_list = list(self.__iter__(uvparams_only=True)) extra_list = [] for a in attribute_list: attr = getattr(self, a) - if isinstance(attr, uvp.UVParameter): - if not attr.required: - extra_list.append(a) + if not attr.required: + extra_list.append(a) for a in extra_list: yield a diff --git a/pyuvdata/uvcal/tests/test_uvcal.py b/pyuvdata/uvcal/tests/test_uvcal.py index 855bdb2680..6f211f6c55 100644 --- a/pyuvdata/uvcal/tests/test_uvcal.py +++ b/pyuvdata/uvcal/tests/test_uvcal.py @@ -183,6 +183,7 @@ def test_required_parameter_iter(uvcal_data): ) uv_cal_object.flag_array = 1 + assert uv_cal_object.metadata_only is False required = [] for prop in uv_cal_object.required(): required.append(prop) diff --git a/pyuvdata/uvdata/tests/test_mir.py b/pyuvdata/uvdata/tests/test_mir.py index 0df134cfb2..e5d5e3bb98 100644 --- a/pyuvdata/uvdata/tests/test_mir.py +++ b/pyuvdata/uvdata/tests/test_mir.py @@ -74,8 +74,6 @@ def test_read_mir_write_uvfits(sma_mir, tmp_path, future_shapes): sma_mir.use_current_array_shapes() sma_mir.write_uvfits(testfile) uvfits_uv.read_uvfits(testfile, use_future_array_shapes=future_shapes) - print("sma_mir instrument", sma_mir.telescope.instrument) - print("uvfits_uv instrument", uvfits_uv.telescope.instrument) assert sma_mir.telescope.instrument == uvfits_uv.telescope.instrument for item in ["dut1", "earth_omega", "gst0", "rdate", "timesys"]: # Check to make sure that the UVFITS-specific paramters are set on the diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index dd0e614ee4..08a29c94ad 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -421,6 +421,7 @@ def test_required_parameter_iter(uvdata_props): uvdata_props.uv_object.data_array = 1 uvdata_props.uv_object.nsample_array = 1 uvdata_props.uv_object.flag_array = 1 + assert uvdata_props.uv_object.metadata_only is False required = [] for prop in uvdata_props.uv_object.required(): required.append(prop) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 79634a0e12..e2612b8350 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -1723,6 +1723,9 @@ def test_set_telescope_params(uvdata_obj): inplace=False, ) uvf = UVFlag(uvd2, use_future_array_shapes=True) + # the telescope objects aren't equal because they have different sets of + # required parameters (UVData's requires instrument while UVFlag's does not) + # so just test the relevant attributes assert uvf.telescope._antenna_names == uvd2.telescope._antenna_names assert uvf.telescope._antenna_numbers == uvd2.telescope._antenna_numbers assert uvf.telescope._antenna_positions == uvd2.telescope._antenna_positions diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 2a0a362274..bb86a027bd 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -1156,7 +1156,9 @@ def set_telescope_params( Raises ------ ValueError - if the telescope_name is not in known telescopes + if self.telescope.location is None or overwrite is True and the + self.telescope.name is not in known telescopes. + """ self.telescope.update_params_from_known_telescopes( overwrite=overwrite, From a11a9db5865dc14e15a478d3f0f52274a3683109 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 24 Apr 2024 17:03:21 -0700 Subject: [PATCH 16/59] consolidate HDF5 telescope metadata read/write in telescopes --- pyuvdata/hdf5_utils.py | 132 ++++++++++++++++++++------- pyuvdata/telescopes.py | 101 ++++++++++++++++++++ pyuvdata/uvcal/calh5.py | 71 ++++---------- pyuvdata/uvdata/tests/test_uvdata.py | 2 +- pyuvdata/uvdata/uvh5.py | 68 ++++---------- pyuvdata/uvflag/tests/test_uvflag.py | 2 +- pyuvdata/uvflag/uvflag.py | 126 ++++++------------------- 7 files changed, 262 insertions(+), 240 deletions(-) diff --git a/pyuvdata/hdf5_utils.py b/pyuvdata/hdf5_utils.py index f84860950a..be4df37e80 100644 --- a/pyuvdata/hdf5_utils.py +++ b/pyuvdata/hdf5_utils.py @@ -211,7 +211,7 @@ class HDF5Meta: Parameters ---------- - filename : str or Path + path : str or Path The filename to read from. Notes @@ -222,8 +222,8 @@ class HDF5Meta: """ _defaults = {} - _string_attrs = frozenset({}) - _int_attrs = frozenset({}) + _string_attrs = frozenset({"x_orientation", "telescope_name", "instrument"}) + _int_attrs = frozenset({"Nants_telescope"}) _float_attrs = frozenset({}) _bool_attrs = frozenset({}) @@ -367,6 +367,43 @@ def __getattr__(self, name: str) -> Any: except KeyError as e: raise AttributeError(f"{name} not found in {self.path}") from e + @property + def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: + """The telescope location in latitude, longitude, and altitude, in degrees.""" + h = self.header + if "latitude" in h and "longitude" in h and "altitude" in h: + return ( + self.latitude * np.pi / 180, + self.longitude * np.pi / 180, + self.altitude, + ) + elif "telescope_location" in h: + # this branch is for old UVFlag files, which were written with an + # ECEF 'telescope_location' key rather than the more standard + # latitude in degrees, longitude in degrees, altitude + return uvutils.LatLonAlt_from_XYZ( + self.telescope_location, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + + @property + def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: + """The telescope location in latitude, longitude, and altitude, in degrees.""" + h = self.header + if "latitude" in h and "longitude" in h and "altitude" in h: + return self.latitude, self.longitude, self.altitude + elif "telescope_location" in h: + # this branch is for old UVFlag files, which were written with an + # ECEF 'telescope_location' key rather than the more standard + # latitude in degrees, longitude in degrees, altitude + lat, lon, alt = uvutils.LatLonAlt_from_XYZ( + self.telescope_location, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + return lat * 180.0 / np.pi, lon * 180.0 / np.pi, alt + @cached_property def antpos_enu(self) -> np.ndarray: """The antenna positions in ENU coordinates, in meters.""" @@ -380,16 +417,6 @@ def antpos_enu(self) -> np.ndarray: ellipsoid=self.ellipsoid, ) - @property - def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude * np.pi / 180, self.longitude * np.pi / 180, self.altitude - - @property - def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: - """The telescope location in latitude, longitude, and altitude, in degrees.""" - return self.latitude, self.longitude, self.altitude - @property def telescope_frame(self) -> str: """The telescope frame.""" @@ -421,31 +448,70 @@ def ellipsoid(self) -> str: @cached_property def telescope_location_obj(self): """The telescope location object.""" - if self.telescope_frame == "itrs": - return EarthLocation.from_geodetic( - lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, - lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, - height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, - ) - else: - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with MCMF frames." + h = self.header + if "latitude" in h and "longitude" in h and "altitude" in h: + if self.telescope_frame == "itrs": + return EarthLocation.from_geodetic( + lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, + lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, + height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, ) - return MoonLocation.from_selenodetic( - lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, - lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, - height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, - ellipsoid=self.ellipsoid, - ) + else: + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frames." + ) + return MoonLocation.from_selenodetic( + lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, + lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, + height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, + ellipsoid=self.ellipsoid, + ) + elif "telescope_location" in h: + # this branch is for old UVFlag files, which were written with an + # ECEF 'telescope_location' key rather than the more standard + # latitude in degrees, longitude in degrees, altitude + loc_xyz = self.telescope_location + if self.telescope_frame == "itrs": + return EarthLocation.from_geocentric( + x=loc_xyz[0] * units.m, + y=loc_xyz[1] * units.m, + z=loc_xyz[2] * units.m, + ) + else: + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frames." + ) + moon_loc = MoonLocation.from_selenocentric( + x=loc_xyz[0] * units.m, + y=loc_xyz[1] * units.m, + z=loc_xyz[2] * units.m, + ) + moon_loc.ellipsoid = self.ellipsoid + return moon_loc @cached_property def telescope_location(self): """The telescope location in ECEF coordinates, in meters.""" - return uvutils.XYZ_from_LatLonAlt( - *self.telescope_location_lat_lon_alt, - frame=self.telescope_frame, - ellipsoid=self.ellipsoid, + h = self.header + if "telescope_location" in h: + # this branch is for old UVFlag files, which were written with an + # ECEF 'telescope_location' key rather than the more standard + # latitude in degrees, longitude in degrees, altitude + return h.telescope_location + elif "latitude" in h and "longitude" in h and "altitude" in h: + return uvutils.XYZ_from_LatLonAlt( + *self.telescope_location_lat_lon_alt, + frame=self.telescope_frame, + ellipsoid=self.ellipsoid, + ) + + @cached_property + def antenna_names(self) -> list[str]: + """The antenna names in the file.""" + return np.array( + [bytes(name).decode("utf8") for name in self.header["antenna_names"][:]] ) @cached_property diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 62dd177f47..4ab7481658 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -3,16 +3,20 @@ # Licensed under the 2-clause BSD License """Telescope information and known telescope list.""" +import copy import os import warnings +from pathlib import Path from typing import Literal, Union +import h5py import numpy as np from astropy import units from astropy.coordinates import Angle, EarthLocation from pyuvdata.data import DATA_PATH +from . import hdf5_utils from . import parameter as uvp from . import utils as uvutils from . import uvbase @@ -691,3 +695,100 @@ def from_params( tel_obj.check() return tel_obj + + @classmethod + def from_hdf5( + cls, + filename: str | Path | hdf5_utils.HDF5Meta, + required_keys: list | None = None, + run_check: bool = True, + check_extra: bool = True, + run_check_acceptability: bool = True, + ): + """ + Initialize a new Telescope object from an HDF5 file. + + The file must have a Header dataset that has the appropriate header + items. UVH5, CalH5 and UVFlag HDF5 files have these. + + Parameters + ---------- + path : str or Path or subclass of hdf5_utils.HDF5Meta + The filename to read from. + + """ + if required_keys is None: + required_keys = ["telescope_name", "latitude", "longitude", "altitude"] + tel_obj = cls() + + if not isinstance(filename, hdf5_utils.HDF5Meta): + if isinstance(filename, h5py.File): + path = Path(filename.filename).resolve() + elif isinstance(filename, h5py.Group): + path = Path(filename.file.filename).resolve() + else: + path = Path(filename).resolve() + meta = hdf5_utils.HDF5Meta(path) + + else: + meta = copy.deepcopy(filename) + + tel_obj.location = meta.telescope_location_obj + + telescope_attrs = { + "telescope_name": "name", + "Nants_telescope": "Nants", + "antenna_names": "antenna_names", + "antenna_numbers": "antenna_numbers", + "antenna_positions": "antenna_positions", + "instrument": "instrument", + "x_orientation": "x_orientation", + "antenna_diameters": "antenna_diameters", + } + for attr, tel_attr in telescope_attrs.items(): + try: + setattr(tel_obj, tel_attr, getattr(meta, attr)) + except (AttributeError, KeyError) as e: + if attr in required_keys: + raise KeyError(str(e)) from e + else: + pass + + if run_check: + tel_obj.check( + check_extra=check_extra, run_check_acceptability=run_check_acceptability + ) + + return tel_obj + + def write_hdf5_header(self, header): + """Write the telescope metadata to an hdf5 dataset. + + This is assumed to be writing to a general header (e.g. for uvh5), + so the header names include 'telescope'. + + Parameters + ---------- + header : HDF5 dataset + Dataset to write the telescope metadata to. + + """ + header["telescope_frame"] = np.string_(self._location.frame) + if self._location.frame == "mcmf": + header["ellipsoid"] = self._location.ellipsoid + lat, lon, alt = self.location_lat_lon_alt_degrees + header["latitude"] = lat + header["longitude"] = lon + header["altitude"] = alt + header["telescope_name"] = np.string_(self.name) + header["Nants_telescope"] = self.Nants + header["antenna_numbers"] = self.antenna_numbers + header["antenna_positions"] = self.antenna_positions + header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes") + + if self.instrument is not None: + header["instrument"] = np.string_(self.instrument) + if self.x_orientation is not None: + header["x_orientation"] = np.string_(self.x_orientation) + if self.antenna_diameters is not None: + header["antenna_diameters"] = self.antenna_diameters diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index c11bf15846..7dcb5bee7c 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -16,6 +16,7 @@ from .. import hdf5_utils from .. import utils as uvutils from ..docstrings import copy_replace_short_description +from ..telescopes import Telescope from .uvcal import UVCal, _future_array_shapes_warning hdf5plugin_present = True @@ -94,11 +95,6 @@ class FastCalH5Meta(hdf5_utils.HDF5Meta): _bool_attrs = frozenset(("wide_band",)) - @cached_property - def antenna_names(self) -> list[str]: - """The antenna names in the file.""" - return [bytes(name).decode("utf8") for name in self.header["antenna_names"][:]] - def has_key(self, antnum: int | None = None, jpol: str | int | None = None) -> bool: """Check if the file has a given antenna number or antenna number-pol key.""" if antnum is not None: @@ -180,8 +176,20 @@ def _read_header( """ # First, get the things relevant for setting LSTs, so that can be run in the # background if desired. - - self.telescope.location = meta.telescope_location_obj + required_telescope_keys = [ + "telescope_name", + "latitude", + "longitude", + "altitude", + "x_orientation", + "Nants_telescope", + "antenna_names", + "antenna_numbers", + "antenna_positions", + ] + self.telescope = Telescope.from_hdf5( + meta, required_keys=required_telescope_keys + ) if "time_array" in meta.header: self.time_array = meta.time_array @@ -224,21 +232,6 @@ def _read_header( except AttributeError as e: raise KeyError(str(e)) from e - # Required telescope parameters - telescope_attrs = { - "x_orientation": "x_orientation", - "telescope_name": "name", - "Nants_telescope": "Nants", - "antenna_names": "antenna_names", - "antenna_numbers": "antenna_numbers", - "antenna_positions": "antenna_positions", - } - for attr, tel_attr in telescope_attrs.items(): - try: - setattr(self.telescope, tel_attr, getattr(meta, attr)) - except AttributeError as e: - raise KeyError(str(e)) from e - self._set_future_array_shapes() if self.wide_band: self._set_wide_band() @@ -277,17 +270,6 @@ def _read_header( except AttributeError: pass - # Optional telescope parameters - telescope_attrs = { - "instrument": "instrument", - "antenna_diameters": "antenna_diameters", - } - for attr, tel_attr in telescope_attrs.items(): - try: - setattr(self.telescope, tel_attr, getattr(meta, attr)) - except AttributeError: - pass - # set telescope params try: self.set_telescope_params() @@ -731,33 +713,18 @@ def _write_header(self, header): header["version"] = np.string_("0.1") # write out telescope and source information - header["telescope_frame"] = np.string_(self.telescope._location.frame) - if self.telescope._location.frame == "mcmf": - header["ellipsoid"] = self.telescope._location.ellipsoid - lat, lon, alt = self.telescope.location_lat_lon_alt_degrees - header["latitude"] = lat - header["longitude"] = lon - header["altitude"] = alt - header["telescope_name"] = np.string_(self.telescope.name) + self.telescope.write_hdf5_header(header) # write out required UVParameters header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.telescope.Nants header["Nfreqs"] = self.Nfreqs header["Njones"] = self.Njones header["Nspws"] = self.Nspws header["Ntimes"] = self.Ntimes - header["antenna_numbers"] = self.telescope.antenna_numbers header["integration_time"] = self.integration_time header["jones_array"] = self.jones_array header["spw_array"] = self.spw_array header["ant_array"] = self.ant_array - header["antenna_positions"] = self.telescope.antenna_positions - # handle antenna_names; works for lists or arrays - header["antenna_names"] = np.asarray( - self.telescope.antenna_names, dtype="bytes" - ) - header["x_orientation"] = np.string_(self.telescope.x_orientation) header["cal_type"] = np.string_(self.cal_type) header["cal_style"] = np.string_(self.cal_style) header["gain_convention"] = np.string_(self.gain_convention) @@ -782,8 +749,6 @@ def _write_header(self, header): if self.scan_number_array is not None: header["scan_number_array"] = self.scan_number_array - if self.telescope.antenna_diameters is not None: - header["antenna_diameters"] = self.telescope.antenna_diameters if self.ref_antenna_array is not None: header["ref_antenna_array"] = self.ref_antenna_array @@ -827,10 +792,6 @@ def _write_header(self, header): else: this_group[key] = value - # extra telescope-related parameters - if self.telescope.instrument is not None: - header["instrument"] = np.string_(self.telescope.instrument) - # write out extra keywords if it exists and has elements if self.extra_keywords: extra_keywords = header.create_group("extra_keywords") diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index 08a29c94ad..22a35824dc 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -5400,7 +5400,7 @@ def test_telescope_loc_xyz_check(paper_uvh5, tmp_path): "itrs position vector magnitudes must be on the order " "of the radius of Earth -- they appear to lie well below this." ] - * 3, + * 4, ): uv.read(fname, use_future_array_shapes=True) diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index 899d8b8295..e151fd99e7 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -18,6 +18,7 @@ from .. import hdf5_utils from .. import utils as uvutils from ..docstrings import copy_replace_short_description +from ..telescopes import Telescope from .uvdata import UVData, _future_array_shapes_warning __all__ = ["UVH5", "FastUVH5Meta"] @@ -406,11 +407,6 @@ def unique_baseline_array(self) -> np.ndarray: Nants_telescope=self.Nants_telescope, ) - @cached_property - def antenna_names(self) -> list[str]: - """The antenna names in the file.""" - return [bytes(name).decode("utf8") for name in self.header["antenna_names"][:]] - @cached_property def antpairs(self) -> list[tuple[int, int]]: """Get the unique antenna pairs in the file.""" @@ -525,7 +521,21 @@ def _read_header_with_fast_meta( # First, get the things relevant for setting LSTs, so that can be run in the # background if desired. self.time_array = obj.time_array - self.telescope.location = obj.telescope_location_obj + required_telescope_keys = [ + "telescope_name", + "latitude", + "longitude", + "altitude", + "instrument", + "Nants_telescope", + "antenna_names", + "antenna_numbers", + "antenna_positions", + ] + self.telescope = Telescope.from_hdf5( + filename, required_keys=required_telescope_keys + ) + self._set_telescope_requirements() if "lst_array" in obj.header: self.lst_array = obj.header["lst_array"][:] @@ -571,21 +581,6 @@ def _read_header_with_fast_meta( except AttributeError as e: raise KeyError(str(e)) from e - # Required telescope parameters - telescope_attrs = { - "instrument": "instrument", - "telescope_name": "name", - "Nants_telescope": "Nants", - "antenna_names": "antenna_names", - "antenna_numbers": "antenna_numbers", - "antenna_positions": "antenna_positions", - } - for attr, tel_attr in telescope_attrs.items(): - try: - setattr(self.telescope, tel_attr, getattr(obj, attr)) - except AttributeError as e: - raise KeyError(str(e)) from e - # check this as soon as we have the inputs if self.freq_array.ndim == 1: arr_shape_msg = ( @@ -632,17 +627,6 @@ def _read_header_with_fast_meta( except AttributeError: pass - # Optional telescope parameters - telescope_attrs = { - "x_orientation": "x_orientation", - "antenna_diameters": "antenna_diameters", - } - for attr, tel_attr in telescope_attrs.items(): - try: - setattr(self.telescope, tel_attr, getattr(obj, attr)) - except AttributeError: - pass - if self.blt_order is not None: self._blt_order.form = (len(self.blt_order),) @@ -1217,26 +1201,16 @@ def _write_header(self, header): header["version"] = np.string_("1.2") # write out telescope and source information - header["telescope_frame"] = np.string_(self.telescope._location.frame) - if self.telescope._location.frame == "mcmf": - header["ellipsoid"] = self.telescope._location.ellipsoid - lat, lon, alt = self.telescope.location_lat_lon_alt_degrees - header["latitude"] = lat - header["longitude"] = lon - header["altitude"] = alt - header["telescope_name"] = np.string_(self.telescope.name) - header["instrument"] = np.string_(self.telescope.instrument) + self.telescope.write_hdf5_header(header) # write out required UVParameters header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.telescope.Nants header["Nbls"] = self.Nbls header["Nblts"] = self.Nblts header["Nfreqs"] = self.Nfreqs header["Npols"] = self.Npols header["Nspws"] = self.Nspws header["Ntimes"] = self.Ntimes - header["antenna_numbers"] = self.telescope.antenna_numbers header["uvw_array"] = self.uvw_array header["vis_units"] = np.string_(self.vis_units) header["channel_width"] = self.channel_width @@ -1248,12 +1222,8 @@ def _write_header(self, header): header["spw_array"] = self.spw_array header["ant_1_array"] = self.ant_1_array header["ant_2_array"] = self.ant_2_array - header["antenna_positions"] = self.telescope.antenna_positions header["flex_spw"] = self.flex_spw # handle antenna_names; works for lists or arrays - header["antenna_names"] = np.asarray( - self.telescope.antenna_names, dtype="bytes" - ) # write out phasing information # Write out the catalog, if available @@ -1286,12 +1256,8 @@ def _write_header(self, header): header["rdate"] = np.string_(self.rdate) if self.timesys is not None: header["timesys"] = np.string_(self.timesys) - if self.telescope.x_orientation is not None: - header["x_orientation"] = np.string_(self.telescope.x_orientation) if self.blt_order is not None: header["blt_order"] = np.string_(", ".join(self.blt_order)) - if self.telescope.antenna_diameters is not None: - header["antenna_diameters"] = self.telescope.antenna_diameters if self.uvplane_reference_time is not None: header["uvplane_reference_time"] = self.uvplane_reference_time if self.eq_coeffs is not None: diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index e2612b8350..801da67438 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -938,7 +938,7 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) ), ( "baseline", - ["telescope_location"], + ["latitude", "longitude", "altitude"], UserWarning, [ "telescope_location are not set or are being overwritten. " diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index bb86a027bd..bd5a5517b2 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -11,14 +11,6 @@ import h5py import numpy as np -from astropy.coordinates import EarthLocation - -try: - from lunarsky import MoonLocation - - hasmoon = True -except ImportError: - hasmoon = False from .. import Telescope from .. import parameter as uvp @@ -3447,13 +3439,6 @@ def read( ) ) - if "x_orientation" in header.keys(): - self.telescope.x_orientation = header["x_orientation"][()].decode( - "utf8" - ) - if "instrument" in header.keys(): - self.telescope.instrument = header["instrument"][()].decode("utf8") - self.time_array = header["time_array"][()] if "Ntimes" in header.keys(): self.Ntimes = int(header["Ntimes"][()]) @@ -3561,6 +3546,15 @@ def read( for param in params_to_check: if param in header.keys(): override_params.append(param) + # older files wrote the 'telescope_location' keys, newer + # files write latitude in degrees, longitude in degrees and + # altitude + if ( + "latitude" in header.keys() + and "longitude" in header.keys() + and "altitude" in header.keys() + ): + override_params.append("telescope_location") if len(override_params) > 0: warnings.warn( @@ -3569,65 +3563,24 @@ def read( f"the UVFlag file: {override_params}" ) else: - if telescope_name is not None: - self.telescope.name = telescope_name - - if "telescope_name" in header.keys(): - file_telescope_name = header["telescope_name"][()].decode( - "utf8" - ) - if telescope_name is not None: - if telescope_name.lower() != file_telescope_name.lower(): - warnings.warn( - f"Telescope_name parameter is set to " - f"{telescope_name}, which overrides the telescope " - f"name in the file ({file_telescope_name})." - ) - else: - self.telescope.name = file_telescope_name + # get as much telescope info as we can from the file. + # Turn off checking to avoid errors because we will later + # try to fill in any missing info from known telescopes + self.telescope = Telescope.from_hdf5( + f, required_keys=[], run_check=False + ) - if "telescope_location" in header.keys(): - if "telescope_frame" in header.keys(): - telescope_frame = header["telescope_frame"][()].decode( - "utf8" - ) - else: - telescope_frame = "itrs" - if telescope_frame == "itrs": - self.telescope.location = EarthLocation.from_geocentric( - *header["telescope_location"][()], unit="m" - ) - else: - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with " - "MCMF frames." - ) - ellipsoid = header["ellipsoid"][()].decode("utf8") - self.telescope.location = MoonLocation.from_selenocentric( - *header["telescope_location"][()], unit="m" + if telescope_name is not None: + if ( + self.telescope.name is not None + and telescope_name.lower() != self.telescope.name.lower() + ): + warnings.warn( + f"Telescope_name parameter is set to " + f"{telescope_name}, which overrides the telescope " + f"name in the file ({self.telescope.name})." ) - self.telescope.location.ellipsoid = ellipsoid - - if "antenna_numbers" in header.keys(): - self.telescope.antenna_numbers = header["antenna_numbers"][()] - - if "antenna_names" in header.keys(): - self.telescope.antenna_names = np.array( - [ - bytes(n).decode("utf8") - for n in header["antenna_names"][:] - ] - ) - - if "antenna_positions" in header.keys(): - self.telescope.antenna_positions = header["antenna_positions"][ - () - ] - if "antenna_diameters" in header.keys(): - self.telescope.antenna_diameters = header["antenna_diameters"][ - () - ] + self.telescope.name = telescope_name self.history = header["history"][()].decode("utf8") @@ -3707,9 +3660,6 @@ def read( else: self.Nants_data = len(self.ant_array) - if "Nants_telescope" in header.keys(): - self.telescope.Nants = int(header["Nants_telescope"][()]) - if self.telescope.name is None: warnings.warn( "telescope_name not available in file, so telescope related " @@ -3855,13 +3805,8 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["type"] = np.string_(self.type) header["mode"] = np.string_(self.mode) - if self.telescope.name is not None: - header["telescope_name"] = np.string_(self.telescope.name) - if self.telescope.location is not None: - header["telescope_location"] = self.telescope._location.xyz() - header["telescope_frame"] = np.string_(self.telescope._location.frame) - if self.telescope._location.frame == "mcmf": - header["ellipsoid"] = np.string_(self.telescope._location.ellipsoid) + # write out telescope and source information + self.telescope.write_hdf5_header(header) header["Ntimes"] = self.Ntimes header["time_array"] = self.time_array @@ -3878,13 +3823,6 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["Npols"] = self.Npols - if self.telescope.x_orientation is not None: - header["x_orientation"] = np.string_(self.telescope.x_orientation) - if self.telescope.instrument is not None: - header["instrument"] = np.string_(self.telescope.instrument) - if self.telescope.antenna_diameters is not None: - header["antenna_diameters"] = self.telescope.antenna_diameters - if isinstance(self.polarization_array.item(0), str): polarization_array = np.asarray( self.polarization_array, dtype=np.string_ @@ -3923,16 +3861,6 @@ def write(self, filename, *, clobber=False, data_compression="lzf"): header["ant_array"] = self.ant_array header["Nants_data"] = self.Nants_data - header["Nants_telescope"] = self.telescope.Nants - if self.telescope.antenna_names is not None: - header["antenna_names"] = np.asarray( - self.telescope.antenna_names, dtype="bytes" - ) - if self.telescope.antenna_numbers is not None: - header["antenna_numbers"] = self.telescope.antenna_numbers - if self.telescope.antenna_positions is not None: - header["antenna_positions"] = self.telescope.antenna_positions - dgrp = f.create_group("Data") if self.mode == "metric": dgrp.create_dataset( From 1b0445c37eb5666b38d65ca47e5afc0a5a10cb62 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 24 Apr 2024 17:16:24 -0700 Subject: [PATCH 17/59] Try to fix python 3.10 errors --- pyuvdata/telescopes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 4ab7481658..18ba5c9ca6 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -3,6 +3,8 @@ # Licensed under the 2-clause BSD License """Telescope information and known telescope list.""" +from __future__ import annotations + import copy import os import warnings From 85967725dd4590430300dbe3ab94971448e71933 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 24 Apr 2024 17:21:49 -0700 Subject: [PATCH 18/59] fix python 3.10 errors --- pyuvdata/telescopes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 18ba5c9ca6..a0750ab1b4 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -25,7 +25,14 @@ __all__ = ["Telescope", "known_telescopes"] -Locations = Union[uvutils.allowed_location_types] +try: + from lunarsky import MoonLocation + + # This can be built from uvutils.allowed_location_types in python >= 3.11 + # but in 3.10 Union has to be declare with types + Locations = Union[EarthLocation, MoonLocation] +except ImportError: + Locations = EarthLocation # We use astropy sites for telescope locations. The dict below is for # telescopes not in astropy sites, or to include extra information for a telescope. From f612b4defc1ef557ae43f31d7ea512ee4c2a6a04 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 08:57:06 -0700 Subject: [PATCH 19/59] fix warnings test and pre-commit errors Don't allow the newest flake-8 bugbear, too many false positives --- .pre-commit-config.yaml | 2 +- pyuvdata/uvcal/calh5.py | 12 ++++++++++-- pyuvdata/uvdata/uvh5.py | 6 +++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51ebf5e676..fb7852161d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: flake8 additional_dependencies: - - flake8-bugbear>=23.12.2 + - flake8-bugbear>=24.2.6, <24.4 - flake8-builtins>=2.2.0 - flake8-comprehensions>=3.14.0 - flake8-docstrings>=1.7.0 diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index 7dcb5bee7c..68749e7d03 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -146,8 +146,10 @@ def _read_header( meta: FastCalH5Meta, *, background_lsts: bool = True, - run_check_acceptability: bool = True, astrometry_library: str | None = None, + run_check: bool = True, + check_extra: bool = True, + run_check_acceptability=True, ): """ Read header information from a UVH5 file. @@ -188,7 +190,11 @@ def _read_header( "antenna_positions", ] self.telescope = Telescope.from_hdf5( - meta, required_keys=required_telescope_keys + meta, + required_keys=required_telescope_keys, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, ) if "time_array" in meta.header: @@ -645,6 +651,8 @@ def read_calh5( # open hdf5 file for reading self._read_header( meta, + run_check=run_check, + check_extra=check_extra, run_check_acceptability=run_check_acceptability, background_lsts=background_lsts, astrometry_library=astrometry_library, diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index e151fd99e7..a2041411d3 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -533,7 +533,11 @@ def _read_header_with_fast_meta( "antenna_positions", ] self.telescope = Telescope.from_hdf5( - filename, required_keys=required_telescope_keys + filename, + required_keys=required_telescope_keys, + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, ) self._set_telescope_requirements() From 00bf88ce39d89264add9d7e8d585e9404d12bfed Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 09:29:58 -0700 Subject: [PATCH 20/59] fix tutorials --- docs/uvcal_tutorial.rst | 58 ++++++++------- docs/uvdata_tutorial.rst | 152 +++++++++++++++++++++++---------------- 2 files changed, 124 insertions(+), 86 deletions(-) diff --git a/docs/uvcal_tutorial.rst b/docs/uvcal_tutorial.rst index 3aa3890b68..240a7ee2fc 100644 --- a/docs/uvcal_tutorial.rst +++ b/docs/uvcal_tutorial.rst @@ -13,18 +13,24 @@ polarization) type solutions. The ``cal_style`` attribute indicates whether the came from a "sky" or "redundant" style of calibration solution. Some metadata items only apply to one ``cal_type`` or ``cal_style``. +Starting in version 3.0, metadata that is associated with the telescope (as +opposed to the data set) is stored in a :class:`pyuvdata.Telescope` object as +the ``telescope`` attribute on a UVCal object. This includes metadata related +to the telescope location, antenna names, numbers and positions as well as other +telescope metadata. The antennas are described in two ways: with antenna numbers and antenna names. The antenna numbers should **not** be confused with indices -- they are not required to start at zero or to be contiguous, although it is not uncommon for some telescopes to number them like indices. On UVCal objects, the names and numbers are held in the -``antenna_names`` and ``antenna_numbers`` attributes respectively. These are arranged -in the same order so that an antenna number can be used to identify an antenna name and -vice versa. -Note that not all the antennas listed in ``antenna_numbers`` and ``antenna_names`` are -guaranteed to have calibration solutions associated with them in the ``gain_array`` -(or ``delay_array`` for delay type solutions). The antenna numbers associated with each -calibration solution is held in the ``ant_array`` attribute (which has the same length -as the ``gain_array`` or ``delay_array`` along the antenna axis). +``telescope.antenna_names`` and ``telescope.antenna_numbers`` attributes +respectively. These are arranged in the same order so that an antenna number +can be used to identify an antenna name and vice versa. +Note that not all the antennas listed in ``telescope.antenna_numbers`` and +``telescope.antenna_names`` are guaranteed to have calibration solutions +associated with them in the ``gain_array`` (or ``delay_array`` for delay type +solutions). The antenna numbers associated with each calibration solution is +held in the ``ant_array`` attribute (which has the same length as the +``gain_array`` or ``delay_array`` along the antenna axis). Calibration solutions can be described as either applying at a particular time (when calibrations were calculated for each integration), in which case the ``time_array`` @@ -38,7 +44,7 @@ set. For most users, the convenience methods for quick data access (see `UVCal: Quick data access`_) are the easiest way to get data for particular antennas. -Those methods take the antenna numbers (i.e. numbers listed in ``antenna_numbers``) +Those methods take the antenna numbers (i.e. numbers listed in ``telescope.antenna_numbers``) as inputs. @@ -241,27 +247,31 @@ of creating a consistent object from a minimal set of inputs .. code-block:: python - >>> from pyuvdata import UVCal + >>> from pyuvdata import Telescope, UVCal >>> from astropy.coordinates import EarthLocation >>> import numpy as np >>> uvc = UVCal.new( ... gain_convention = "multiply", - ... x_orientation = "east", ... cal_style = "redundant", ... freq_array = np.linspace(1e8, 2e8, 100), ... jones_array = ["ee", "nn"], - ... antenna_positions = { - ... 0: [0.0, 0.0, 0.0], - ... 1: [0.0, 0.0, 1.0], - ... 2: [0.0, 0.0, 2.0], - ... }, - ... telescope_location = EarthLocation.from_geodetic(0, 0, 0), - ... telescope_name = "test", + ... telescope = Telescope.from_params( + ... antenna_positions = { + ... 0: [0.0, 0.0, 0.0], + ... 1: [0.0, 0.0, 1.0], + ... 2: [0.0, 0.0, 2.0], + ... }, + ... location = EarthLocation.from_geodetic(0, 0, 0), + ... name = "test", + ... x_orientation = "east", + ... ), ... time_array = np.linspace(2459855, 2459856, 20), ... ) Notice that you need only provide the required parameters, and the rest will be -filled in with sensible defaults. +filled in with sensible defaults. The telescope related metadata is passed +directly to a simple Telescope constructor which also only requires the minimal +set of inputs but can accept any other parameters supported by the class. See the full documentation for the method :func:`pyuvdata.uvcal.UVCal.new` for more information. @@ -323,7 +333,7 @@ a) Calibration of UVData by UVCal ... ) >>> # this is an old calfits file which has the wrong antenna names, so we need to fix them first. >>> # fix the antenna names in the uvcal object to match the uvdata object - >>> uvc.antenna_names = np.array( + >>> uvc.telescope.antenna_names = np.array( ... [name.replace("ant", "HH") for name in uvc.antenna_names] ... ) >>> uvd_calibrated = utils.uvcalibrate(uvd, uvc, inplace=False) @@ -371,7 +381,7 @@ b) Select antennas to keep using the antenna names, also select frequencies to k >>> cal = UVCal.from_file(filename, use_future_array_shapes=True) >>> # print all the antenna names with data in the original file - >>> print([cal.antenna_names[np.where(cal.antenna_numbers==a)[0][0]] for a in cal.ant_array]) + >>> print([cal.telescope.antenna_names[np.where(cal.telescope.antenna_numbers==a)[0][0]] for a in cal.ant_array]) ['ant0', 'ant1', 'ant11', 'ant12', 'ant13', 'ant23', 'ant24', 'ant25'] >>> # print the first 10 frequencies in the original file @@ -381,7 +391,7 @@ b) Select antennas to keep using the antenna names, also select frequencies to k >>> cal.select(antenna_names=['ant11', 'ant13', 'ant25'], freq_chans=np.arange(0, 4)) >>> # print all the antenna names with data after the select - >>> print([cal.antenna_names[np.where(cal.antenna_numbers==a)[0][0]] for a in cal.ant_array]) + >>> print([cal.telescope.antenna_names[np.where(cal.telescope.antenna_numbers==a)[0][0]] for a in cal.ant_array]) ['ant11', 'ant13', 'ant25'] >>> # print all the frequencies after the select @@ -421,7 +431,7 @@ d) Select Jones components ************************** Selecting on Jones component can be done either using the component numbers or the component strings (e.g. "Jxx" or "Jyy" for linear polarizations or "Jrr" or -"Jll" for circular polarizations). If ``x_orientation`` is set on the object, strings +"Jll" for circular polarizations). If ``telescope.x_orientation`` is set, strings represting the physical orientation of the dipole can also be used (e.g. "Jnn" or "ee). .. code-block:: python @@ -463,7 +473,7 @@ represting the physical orientation of the dipole can also be used (e.g. "Jnn" o ['Jxx'] >>> # print x_orientation - >>> print(cal.x_orientation) + >>> print(cal.telescope.x_orientation) east >>> # make a copy of the object and select Jones components using the physical orientation strings diff --git a/docs/uvdata_tutorial.rst b/docs/uvdata_tutorial.rst index 06d8c1c5d8..ff54e3cc92 100644 --- a/docs/uvdata_tutorial.rst +++ b/docs/uvdata_tutorial.rst @@ -14,18 +14,24 @@ baseline-dependent averaging). Note that UVData can also support combining the f and polarization axis, which can be useful in certain circumstances, objects represented this way are called ``flex_pol`` objects and are more fully described in :ref:`flex_pol`. +Starting in version 3.0, metadata that is associated with the telescope (as +opposed to the data set) is stored in a :class:`pyuvdata.Telescope` object as +the ``telescope`` attribute on a UVData object. This includes metadata related +to the telescope location, antenna names, numbers and positions as well as other +telescope metadata. The antennas are described in two ways: with antenna numbers and antenna names. The antenna numbers should **not** be confused with indices -- they are not required to start at zero or to be contiguous, although it is not uncommon for some telescopes to number them like indices. On UVData objects, the names and numbers are held in the -``antenna_names`` and ``antenna_numbers`` attributes respectively. These are arranged -in the same order so that an antenna number can be used to identify an antenna name and -vice versa. -Note that not all the antennas listed in ``antenna_numbers`` and ``antenna_names`` are -guaranteed to have visibilities associated with them in the ``data_array``. The antenna -numbers associated with each visibility is held in the ``ant_1_array`` and ``ant_2_array`` -attributes. These arrays hold the antenna numbers for each visibility (they have the -same length as the ``data_array`` along the baseline-time axis) and which array they appear +``telescope.antenna_names`` and ``telescope.antenna_numbers`` attributes +respectively. These are arranged in the same order so that an antenna number +can be used to identify an antenna name and vice versa. +Note that not all the antennas listed in ``telescope.antenna_numbers`` and +``telescope.antenna_names`` are guaranteed to have visibilities associated with +them in the ``data_array``. The antenna numbers associated with each visibility +is held in the ``ant_1_array`` and ``ant_2_array`` attributes. These arrays hold +the antenna numbers for each visibility (they have the same length as the +``data_array`` along the baseline-time axis) and which array they appear in (``ant_1_array`` vs ``ant_2_array``) indicates the direction of the baseline. On UVData objects, the baseline vector is defined to point from antenna 1 to antenna 2, so it is given by the position of antenna 2 minus the position of antenna 1. Since the @@ -45,7 +51,7 @@ to the antenna arrays before calculating the baseline numbers. Reference issues For most users, the convenience methods for quick data access (see :ref:`quick_access`) are the easiest way to get data for particular sets of baselines. Those methods take -the antenna numbers (i.e. numbers listed in ``antenna_numbers``) as inputs. +the antenna numbers (i.e. numbers listed in ``telescope.antenna_numbers``) as inputs. .. _uvdata_future_shapes: @@ -417,25 +423,31 @@ of creating a consistent object from a minimal set of inputs .. code-block:: python - >>> from pyuvdata import UVData + >>> from pyuvdata import Telescope, UVData >>> from astropy.coordinates import EarthLocation >>> import numpy as np >>> uvd = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... antenna_positions = { - ... 0: [0.0, 0.0, 0.0], - ... 1: [0.0, 0.0, 1.0], - ... 2: [0.0, 0.0, 2.0], - ... }, - ... telescope_location = EarthLocation.from_geodetic(0, 0, 0), - ... telescope_name = "test", + ... telescope = Telescope.from_params( + ... antenna_positions = { + ... 0: [0.0, 0.0, 0.0], + ... 1: [0.0, 0.0, 1.0], + ... 2: [0.0, 0.0, 2.0], + ... }, + ... location = EarthLocation.from_geodetic(0, 0, 0), + ... name = "test", + ... instrument = "test", + ... ), ... times = np.linspace(2459855, 2459856, 20), ... ) Notice that you need only provide the required parameters, and the rest will be -filled in with sensible defaults. Importantly, the times and baselines can be provided -either as unique values, with the intention that their cartesian outer product should be +filled in with sensible defaults. The telescope related metadata is passed +directly to a simple Telescope constructor which also only requires the minimal +set of inputs but can accept any other parameters supported by the class. +Importantly, the times and baselines can be provided either as unique values, +with the intention that their cartesian outer product should be used (i.e. the combination of each provided time with each baseline), or as full length-Nblt arrays (if you don't require all combinations). While this behaviour can be inferred, it is best to set the ``do_blt_outer`` keyword to ``True`` or ``False`` @@ -445,7 +457,7 @@ where each baseline observed one time each. This case is ambiguous without the .. code-block:: python - >>> from pyuvdata import UVData + >>> from pyuvdata import Telescope, UVData >>> from astropy.coordinates import EarthLocation >>> import numpy as np >>> times = np.array([2459855.0, 2459855.1, 2459855.2, 2459855.3]) @@ -453,13 +465,16 @@ where each baseline observed one time each. This case is ambiguous without the >>> uvd = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... antenna_positions = { - ... 0: [0.0, 0.0, 0.0], - ... 1: [0.0, 0.0, 1.0], - ... 2: [0.0, 0.0, 2.0], - ... }, - ... telescope_location = EarthLocation.from_geodetic(0, 0, 0), - ... telescope_name = "test", + ... telescope = Telescope.from_params( + ... antenna_positions = { + ... 0: [0.0, 0.0, 0.0], + ... 1: [0.0, 0.0, 1.0], + ... 2: [0.0, 0.0, 2.0], + ... }, + ... location = EarthLocation.from_geodetic(0, 0, 0), + ... name = "test", + ... instrument = "test", + ... ), ... times = times, ... antpairs=antpairs, ... do_blt_outer=False, @@ -473,19 +488,22 @@ provided times and baselines, which would have resulted in 16 times: .. code-block:: python - >>> from pyuvdata import UVData + >>> from pyuvdata import Telescope, UVData >>> from astropy.coordinates import EarthLocation >>> import numpy as np >>> uvd_rect = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... antenna_positions = { - ... 0: [0.0, 0.0, 0.0], - ... 1: [0.0, 0.0, 1.0], - ... 2: [0.0, 0.0, 2.0], - ... }, - ... telescope_location = EarthLocation.from_geodetic(0, 0, 0), - ... telescope_name = "test", + ... telescope = Telescope.from_params( + ... antenna_positions = { + ... 0: [0.0, 0.0, 0.0], + ... 1: [0.0, 0.0, 1.0], + ... 2: [0.0, 0.0, 2.0], + ... }, + ... location = EarthLocation.from_geodetic(0, 0, 0), + ... name = "test", + ... instrument = "test", + ... ), ... times = times, ... antpairs=antpairs, ... do_blt_outer=True, @@ -499,28 +517,31 @@ To change the order of the blt-axis, set the ``time_axis_faster_than_bls`` keywo .. code-block:: python - >>> from pyuvdata import UVData + >>> from pyuvdata import Telescope, UVData >>> from astropy.coordinates import EarthLocation >>> import numpy as np - >>> uvd_rect = UVData.new( - ... freq_array = np.linspace(1e8, 2e8, 100), - ... polarization_array = ["xx", "yy"], - ... antenna_positions = { - ... 0: [0.0, 0.0, 0.0], - ... 1: [0.0, 0.0, 1.0], - ... 2: [0.0, 0.0, 2.0], - ... }, - ... telescope_location = EarthLocation.from_geodetic(0, 0, 0), - ... telescope_name = "test", - ... times = times, - ... antpairs=antpairs, - ... do_blt_outer=True, - ... time_axis_faster_than_bls=True, - ... ) - >>> uvd_rect.time_array - array([2459855. , 2459855.1, 2459855.2, 2459855.3, 2459855. , 2459855.1, - 2459855.2, 2459855.3, 2459855. , 2459855.1, 2459855.2, 2459855.3, - 2459855. , 2459855.1, 2459855.2, 2459855.3]) + >>> uvd_rect = UVData.new( + ... freq_array = np.linspace(1e8, 2e8, 100), + ... polarization_array = ["xx", "yy"], + ... telescope = Telescope.from_params( + ... antenna_positions = { + ... 0: [0.0, 0.0, 0.0], + ... 1: [0.0, 0.0, 1.0], + ... 2: [0.0, 0.0, 2.0], + ... }, + ... location = EarthLocation.from_geodetic(0, 0, 0), + ... name = "test", + ... instrument = "test", + ... ), + ... times = times, + ... antpairs=antpairs, + ... do_blt_outer=True, + ... time_axis_faster_than_bls=True, + ... ) + >>> uvd_rect.time_array + array([2459855. , 2459855.1, 2459855.2, 2459855.3, 2459855. , 2459855.1, + 2459855.2, 2459855.3, 2459855. , 2459855.1, 2459855.2, 2459855.3, + 2459855. , 2459855.1, 2459855.2, 2459855.3]) See the full documentation for the method :func:`pyuvdata.uvdata.UVData.new` for more information. @@ -959,11 +980,13 @@ A number of conversion methods exist to map between different coordinate systems for locations on the earth. a) Getting antenna positions in topocentric frame in units of meters + ******************************************************************** .. code-block:: python >>> # directly from UVData object >>> import os + >>> from astropy.units import Quantity >>> from pyuvdata import UVData >>> from pyuvdata.data import DATA_PATH >>> data_file = os.path.join(DATA_PATH, 'new.uvA') @@ -974,11 +997,16 @@ a) Getting antenna positions in topocentric frame in units of meters >>> from pyuvdata import utils >>> # get antennas positions in ECEF - >>> antpos = uvd.antenna_positions + uvd.telescope_location + >>> telescope_ecef_xyz = Quantity(uvd.telescope.location.geocentric).to("m").value + >>> antpos = uvd.telescope.antenna_positions + telescope_ecef_xyz >>> # convert to topocentric (East, North, Up or ENU) coords. - >>> lat, lon, alt = uvd.telescope_location_lat_lon_alt - >>> antpos = utils.ENU_from_ECEF(antpos, latitude=lat, longitude=lon, altitude=alt) + >>> antpos = utils.ENU_from_ECEF( + ... antpos, + ... latitude=uvd.telescope.location.lat.rad, + ... longitude=uvd.telescope.location.lon.rad, + ... altitude=uvd.telescope.location.height.to('m').value + ... ) UVData: Selecting data ---------------------- @@ -1023,7 +1051,7 @@ b) Select 3 antennas to keep using the antenna names, also select 5 frequencies >>> # print all the antenna names with data in the original file >>> unique_ants = np.unique(uvd.ant_1_array.tolist() + uvd.ant_2_array.tolist()) - >>> print([uvd.antenna_names[np.where(uvd.antenna_numbers==a)[0][0]] for a in unique_ants]) + >>> print([uvd.telescope.antenna_names[np.where(uvd.telescope.antenna_numbers==a)[0][0]] for a in unique_ants]) ['W09', 'E02', 'E09', 'W01', 'N06', 'N01', 'E06', 'E08', 'W06', 'W04', 'N05', 'E01', 'N04', 'E07', 'W05', 'N02', 'E03', 'N08'] >>> # print how many frequencies in the original file @@ -1033,7 +1061,7 @@ b) Select 3 antennas to keep using the antenna names, also select 5 frequencies >>> # print all the antenna names with data after the select >>> unique_ants = np.unique(uvd.ant_1_array.tolist() + uvd.ant_2_array.tolist()) - >>> print([uvd.antenna_names[np.where(uvd.antenna_numbers==a)[0][0]] for a in unique_ants]) + >>> print([uvd.telescope.antenna_names[np.where(uvd.telescope.antenna_numbers==a)[0][0]] for a in unique_ants]) ['E09', 'W06', 'N02'] >>> # print all the frequencies after the select @@ -1088,7 +1116,7 @@ e) Select polarizations *********************** Selecting on polarizations can be done either using the polarization numbers or the polarization strings (e.g. "xx" or "yy" for linear polarizations or "rr" or "ll" for -circular polarizations). If ``x_orientation`` is set on the object, strings representing +circular polarizations). If ``telescope.x_orientation`` is set, strings representing the physical orientation of the dipole can also be used (e.g. "nn" or "ee). .. code-block:: python @@ -1138,7 +1166,7 @@ the physical orientation of the dipole can also be used (e.g. "nn" or "ee). ['xx', 'yy'] >>> # print x_orientation - >>> print(uvd.x_orientation) + >>> print(uvd.telescope.x_orientation) NORTH >>> # select polarizations using the physical orientation strings From 38b004c6760f0f64e9196f0c2b1f5048e44c1992 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 12:58:37 -0700 Subject: [PATCH 21/59] Try to fix mac builds Also clarify setuptools_scm requirements since our minimum test version is 7.0.3 --- .github/workflows/macosx_windows_ci.yaml | 21 +++++++++---------- README.md | 2 +- ci/pyuvdata_min_deps_tests.yml | 2 +- ci/pyuvdata_tests.yml | 2 +- ci/pyuvdata_tests_mac_arm.yaml | 26 ++++++++++++++++++++++++ ci/pyuvdata_tests_windows.yml | 2 +- environment.yaml | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 9 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 ci/pyuvdata_tests_mac_arm.yaml diff --git a/.github/workflows/macosx_windows_ci.yaml b/.github/workflows/macosx_windows_ci.yaml index 7a9713dbf8..fa8ab4f7b9 100644 --- a/.github/workflows/macosx_windows_ci.yaml +++ b/.github/workflows/macosx_windows_ci.yaml @@ -27,20 +27,19 @@ jobs: matrix: os: [macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12"] + include: + - env_name: pyuvdata_tests_windows + os: windows-latest + - env_name: pyuvdata_tests_mac_arm + os: macos-latest + - env_name: pyuvdata_tests + os: macos-12 + python-version: "3.12" steps: - uses: actions/checkout@main with: fetch-depth: 0 - - name: Set ENV NAME - run: | - if [[ "${{ runner.os }}" = "Windows" ]]; then - echo "::set-output name=ENV_NAME::pyuvdata_tests_windows" - else - echo "::set-output name=ENV_NAME::pyuvdata_tests" - fi - id: env_name - - name: Setup Minimamba uses: conda-incubator/setup-miniconda@v3 with: @@ -48,8 +47,8 @@ jobs: miniforge-version: latest use-mamba: true python-version: ${{ matrix.python-version }} - environment-file: ci/${{ steps.env_name.outputs.ENV_NAME }}.yml - activate-environment: ${{ steps.env_name.outputs.ENV_NAME }} + environment-file: ci/${{ matrix.env_name }}.yml + activate-environment: ${{ matrix.env_name }} run-post: false - name: Conda Info diff --git a/README.md b/README.md index 82be66c770..139337a8d9 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Required: * python >= 3.10 * pyyaml >= 5.4.1 * scipy >= 1.7.3 -* setuptools_scm <7.0|>=7.0.3 +* setuptools_scm >= 7.0.3 Optional: diff --git a/ci/pyuvdata_min_deps_tests.yml b/ci/pyuvdata_min_deps_tests.yml index 1cfb49083c..45ec568c6e 100644 --- a/ci/pyuvdata_min_deps_tests.yml +++ b/ci/pyuvdata_min_deps_tests.yml @@ -15,5 +15,5 @@ dependencies: - pytest-cov - pytest-xdist - cython>=3.0 - - setuptools_scm<7.0|>=7.0.3 + - setuptools_scm>=7.0.3 - pip diff --git a/ci/pyuvdata_tests.yml b/ci/pyuvdata_tests.yml index 34ce0d3558..174ee00359 100644 --- a/ci/pyuvdata_tests.yml +++ b/ci/pyuvdata_tests.yml @@ -20,7 +20,7 @@ dependencies: - pytest-cov - pytest-xdist - cython>=3.0 - - setuptools_scm<7.0|>=7.0.3 + - setuptools_scm>=7.0.3 - pip - pip: - lunarsky>=0.2.2 diff --git a/ci/pyuvdata_tests_mac_arm.yaml b/ci/pyuvdata_tests_mac_arm.yaml new file mode 100644 index 0000000000..566a384251 --- /dev/null +++ b/ci/pyuvdata_tests_mac_arm.yaml @@ -0,0 +1,26 @@ +name: pyuvdata_tests_mac_arm +channels: + - conda-forge +dependencies: + - astropy>=6.0 + - astropy-healpix>=1.0.2 + - astroquery>=0.4.4 + - docstring_parser>=0.15 + - h5py>=3.4 + - hdf5plugin>=3.2.0 + - matplotlib # this is just for the doctests. + - numpy>=1.23 + - pyerfa>=2.0.1.1 + - python-casacore>=3.5.2 + - pyyaml>=5.4.1 + - scipy>=1.7.3 + - coverage + - pytest>=6.2.5 + - pytest-cases>=3.8.3 + - pytest-cov + - pytest-xdist + - cython>=3.0 + - setuptools_scm<7.0|>=7.0.3 + - pip + - pip: + - lunarsky>=0.2.2 diff --git a/ci/pyuvdata_tests_windows.yml b/ci/pyuvdata_tests_windows.yml index b219258cb6..7fbcabc972 100644 --- a/ci/pyuvdata_tests_windows.yml +++ b/ci/pyuvdata_tests_windows.yml @@ -19,7 +19,7 @@ dependencies: - pytest-cov - pytest-xdist - cython>=3.0 - - setuptools_scm<7.0|>=7.0.3 + - setuptools_scm>=7.0.3 - pip - pip: - lunarsky>=0.2.2 diff --git a/environment.yaml b/environment.yaml index b11f4521de..237a96a85b 100644 --- a/environment.yaml +++ b/environment.yaml @@ -23,7 +23,7 @@ dependencies: - python-casacore>=3.5.2 - pyyaml>=5.4.1 - scipy>=1.7.3 - - setuptools_scm<7.0|>=7.0.3 + - setuptools_scm>=7.0.3 - sphinx - pip: - lunarsky>=0.2.2 diff --git a/pyproject.toml b/pyproject.toml index 482dee663f..f25bbf9ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = ["setuptools>=61", "wheel", - "setuptools_scm!=7.0.0,!=7.0.1,!=7.0.2", + "setuptools_scm>=7.0.3", "oldest-supported-numpy", "cython>=3.0"] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 54898fa110..c4f5a4203b 100644 --- a/setup.py +++ b/setup.py @@ -151,7 +151,7 @@ def is_platform_windows(): "pyyaml>=5.4.1", "scipy>=1.7.3", "setuptools>=61", - "setuptools_scm!=7.0.0,!=7.0.1,!=7.0.2", + "setuptools_scm>=7.0.3", ], "extras_require": { "astroquery": astroquery_reqs, From 53b1b99de3e2c8fcf87d9e143be766f73b224016 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 13:09:24 -0700 Subject: [PATCH 22/59] fix new yaml name and try mac-13 for intel --- .github/workflows/macosx_windows_ci.yaml | 2 +- ci/{pyuvdata_tests_mac_arm.yaml => pyuvdata_tests_mac_arm.yml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ci/{pyuvdata_tests_mac_arm.yaml => pyuvdata_tests_mac_arm.yml} (100%) diff --git a/.github/workflows/macosx_windows_ci.yaml b/.github/workflows/macosx_windows_ci.yaml index fa8ab4f7b9..f587f7cc7a 100644 --- a/.github/workflows/macosx_windows_ci.yaml +++ b/.github/workflows/macosx_windows_ci.yaml @@ -33,7 +33,7 @@ jobs: - env_name: pyuvdata_tests_mac_arm os: macos-latest - env_name: pyuvdata_tests - os: macos-12 + os: macos-13 python-version: "3.12" steps: - uses: actions/checkout@main diff --git a/ci/pyuvdata_tests_mac_arm.yaml b/ci/pyuvdata_tests_mac_arm.yml similarity index 100% rename from ci/pyuvdata_tests_mac_arm.yaml rename to ci/pyuvdata_tests_mac_arm.yml From 31794374cd5b73c12e645b6b6e1d1c615352bbaf Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 14:07:55 -0700 Subject: [PATCH 23/59] fix setuptools_scm in mac_arm yaml --- ci/pyuvdata_tests_mac_arm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pyuvdata_tests_mac_arm.yml b/ci/pyuvdata_tests_mac_arm.yml index 566a384251..0b3b4fecd7 100644 --- a/ci/pyuvdata_tests_mac_arm.yml +++ b/ci/pyuvdata_tests_mac_arm.yml @@ -20,7 +20,7 @@ dependencies: - pytest-cov - pytest-xdist - cython>=3.0 - - setuptools_scm<7.0|>=7.0.3 + - setuptools_scm>=7.0.3 - pip - pip: - lunarsky>=0.2.2 From fa56d1c36796dff145cf4e0f6b69eee7c423f1f1 Mon Sep 17 00:00:00 2001 From: Garrett 'Karto' Keating Date: Wed, 24 Apr 2024 18:12:16 -0400 Subject: [PATCH 24/59] Reorder init to create Telescope object after full super init --- pyuvdata/uvcal/uvcal.py | 6 ++++-- pyuvdata/uvdata/uvdata.py | 6 ++++-- pyuvdata/uvflag/uvflag.py | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index 50a66fcc11..a7c2a6f950 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -658,14 +658,16 @@ def __init__(self): required=False, ) + super(UVCal, self).__init__() + + # Assign attributes to UVParameters after initialization, since UVBase.__init__ + # will link the properties to the underlying UVParameter.value attributes # initialize the telescope object self.telescope = Telescope() # set the appropriate telescope attributes as required self._set_telescope_requirements() - super(UVCal, self).__init__() - def _set_telescope_requirements(self): """Set the UVParameter required fields appropriately for UVCal.""" self.telescope._instrument.required = False diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 5ba0dc34a1..ab488c43c3 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -682,14 +682,16 @@ def __init__(self): self.__antpair2ind_cache = {} self.__key2ind_cache = {} + super(UVData, self).__init__() + + # Assign attributes to UVParameters after initialization, since UVBase.__init__ + # will link the properties to the underlying UVParameter.value attributes # initialize the telescope object self.telescope = Telescope() # set the appropriate telescope attributes as required self._set_telescope_requirements() - super(UVData, self).__init__() - def _set_telescope_requirements(self): """Set the UVParameter required fields appropriately for UVData.""" self.telescope._instrument.required = True diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index bd5a5517b2..ace44deb3f 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -541,15 +541,17 @@ def __init__( "filename", required=False, description=desc, expected_type=str ) + # initialize the underlying UVBase properties + super(UVFlag, self).__init__() + + # Assign attributes to UVParameters after initialization, since UVBase.__init__ + # will link the properties to the underlying UVParameter.value attributes # initialize the telescope object self.telescope = Telescope() # set the appropriate telescope attributes as required self._set_telescope_requirements() - # initialize the underlying UVBase properties - super(UVFlag, self).__init__() - self.history = "" # Added to at the end self.label = "" # Added to at the end From c102811f4c980c1a96f2d35d19f6f1353099fbfd Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 14:07:12 -0700 Subject: [PATCH 25/59] Ensure check gets run on Telescope when checking parent objects --- pyuvdata/uvbase.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index e396ae07f5..985c9ddaa1 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -683,6 +683,10 @@ def check( param=p, pshape=np.shape(param.value), eshape=eshape ) ) + # Handle UVBase objects (e.g. Telescope) separately + if isinstance(param.value, UVBase): + param.value.check() + # Handle SkyCoord objects separately if isinstance(param, uvp.SkyCoordParameter): if not issubclass(param.value.__class__, SkyCoord): From 2c1afac560abb70ac2f699add9b43aafbde9af83 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 15:20:06 -0700 Subject: [PATCH 26/59] better docs --- docs/make_telescope.py | 14 ++++++++++++-- docs/make_uvbeam.py | 3 ++- docs/make_uvcal.py | 27 ++++++++++++++++++--------- docs/make_uvdata.py | 12 ++++++++---- docs/make_uvflag.py | 7 +++++-- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/docs/make_telescope.py b/docs/make_telescope.py index aa501799b9..6854ce1da5 100644 --- a/docs/make_telescope.py +++ b/docs/make_telescope.py @@ -16,7 +16,7 @@ def write_telescope_rst(write_file=None): tel = Telescope() - out = "Telescope\n=========\n\n" + out = ".. _Telescope:\n\nTelescope\n=========\n\n" out += ( "Telescope is a helper class for telescope-related metadata.\n" "Several of the primary user classes need telescope metdata, so they " @@ -26,7 +26,17 @@ def write_telescope_rst(write_file=None): "describe interferometric telescopes. Under the hood, the attributes are\n" "implemented as properties based on :class:`pyuvdata.parameter.UVParameter`\n" "objects but this is fairly transparent to users.\n\n" - "When a new Telescope object is initialized, it has all of these \n" + "Most commonly, Telescope objects are found as the ``telescope`` attribute\n" + "on UVData, UVCal and UVFlag objects and they are typically initialized\n" + "when those objects are initialized.\n\n" + "Stand-alone Telescope objects can be initialized in many ways: from\n" + "arrays in memory using the :meth:`pyuvdata.Telescope.from_params`\n" + "class method, from our known telescope information using the\n" + ":meth:`pyuvdata.Telescope.from_known_telescopes` class method,\n" + "from uvh5, calh5 or uvflag HDF5 files using the " + ":meth:`pyuvdata.Telescope.from_hdf5` class method,\n" + "or as an empty object (as ``tel = Telescope()``).\n" + "When an empty Telescope object is initialized, it has all of these \n" "attributes defined but set to ``None``. The attributes\n" "can be set directly on the object. Some of these attributes\n" "are `required`_ to be set to have a fully defined object while others are\n" diff --git a/docs/make_uvbeam.py b/docs/make_uvbeam.py index f930a0f5e2..63a71f5562 100644 --- a/docs/make_uvbeam.py +++ b/docs/make_uvbeam.py @@ -31,7 +31,8 @@ def write_uvbeam_rst(write_file=None): "this is fairly transparent to users.\n\n" "UVBeam objects can be initialized from a file using the\n" ":meth:`pyuvdata.UVBeam.from_file` class method\n" - "(as ``beam = UVBeam.from_file()``) or be initialized as an empty\n" + "(as ``beam = UVBeam.from_file()``) or be initialized from arrays\n" + "in memory using the :meth:`pyuvdata.UVBeam.new` class method or as an empty\n" "object (as ``beam = UVBeam()``). When an empty UVBeam object is initialized,\n" "it has all of these attributes defined but set to ``None``. The attributes\n" "can be set by reading in a data file using the :meth:`pyuvdata.UVBeam.read`\n" diff --git a/docs/make_uvcal.py b/docs/make_uvcal.py index a038a077b6..fb2554580e 100644 --- a/docs/make_uvcal.py +++ b/docs/make_uvcal.py @@ -28,17 +28,26 @@ def write_uvcal_rst(write_file=None): "hood, the attributes are implemented as properties based on\n" ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" "transparent to users.\n" - "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" - "object, with its attributes available on the UVCal object as properties.\n\n" - "UVCal objects can be initialized as an empty object (as ``cal = UVCal()``).\n" + "Starting in version 3.0, metadata that is associated with the telescope\n" + "(as opposed to the data set) is stored in a :class:`pyuvdata.Telescope`\n" + "object (see :ref:`Telescope`) as the ``telescope`` attribute on a\n" + "UVCal object. This includes metadata related to the telescope location,\n" + "antenna names, numbers and positions as well as other telescope metadata.\n\n" + "UVCal objects can be initialized in many ways: from a file using the\n" + ":meth:`pyuvdata.UVCal.from_file` class method\n" + "(as ``uvc = UVCal.from_file()``), from arrays in memory using\n" + "the :meth:`pyuvdata.UVCal.new` class method, from a\n" + ":class:`pyvdata.UVData` object using the\n" + ":meth:`pyuvdata.UVCal.initialize_from_uvdata` class method, or as an\n" + "empty object (as ``cal = UVCal()``).\n" "When an empty UVCal object is initialized, it has all of these attributes\n" "defined but set to ``None``. The attributes can be set by reading in a data\n" - "file using the :meth:`pyuvdata.UVCal.read_calfits` or\n" - ":meth:`pyuvdata.UVCal.read_fhd_cal` methods or by setting them directly on\n" - "the object. Some of these attributes are `required`_ to be set to have a\n" - "fully defined calibration data set while others are `optional`_. The\n" - ":meth:`pyuvdata.UVCal.check` method can be called on the object to verify\n" - "that all of the required attributes have been set in a consistent way.\n\n" + "file using the :meth:`pyuvdata.UVCal.read` method or by setting them\n" + "directly on the object. Some of these attributes are `required`_ to be\n" + "set to have a fully defined calibration data set while others are\n" + "`optional`_. The :meth:`pyuvdata.UVCal.check` method can be called on\n" + "the object to verify that all of the required attributes have been set\n" + "in a consistent way.\n\n" 'Note that objects can be in a "metadata only" state where\n' "all of the metadata is defined but the data-like attributes (``gain_array``,\n" "``delay_array``, ``flag_array``, ``quality_array``) are not. The\n" diff --git a/docs/make_uvdata.py b/docs/make_uvdata.py index c12581076d..65b2b452bd 100644 --- a/docs/make_uvdata.py +++ b/docs/make_uvdata.py @@ -27,11 +27,15 @@ def write_uvdata_rst(write_file=None): "analyze interferometric data sets. Under the hood, the attributes are\n" "implemented as properties based on :class:`pyuvdata.parameter.UVParameter`\n" "objects but this is fairly transparent to users.\n" - "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" - "object, with its attributes available on the UVData object as properties.\n" - "UVData objects can be initialized from a file using the\n" + "Starting in version 3.0, metadata that is associated with the telescope\n" + "(as opposed to the data set) is stored in a :class:`pyuvdata.Telescope`\n" + "object (see :ref:`Telescope`) as the ``telescope`` attribute on a UVData\n" + "object. This includesmetadata related to the telescope location, antenna\n" + "names, numbers and positions as well as other telescope metadata.\n\n" + "UVData objects can be initialized in many ways: from a file using the\n" ":meth:`pyuvdata.UVData.from_file` class method\n" - "(as ``uvd = UVData.from_file()``) or be initialized as an empty\n" + "(as ``uvd = UVData.from_file()``), from arrays in memory\n" + "using the :meth:`pyuvdata.UVData.new` class method, or as an empty\n" "object (as ``uvd = UVData()``). When an empty UVData object is initialized,\n" "it has all of these attributes defined but set to ``None``. The attributes\n" "can be set by reading in a data file using the :meth:`pyuvdata.UVData.read`\n" diff --git a/docs/make_uvflag.py b/docs/make_uvflag.py index 1b50e53ff5..4a03424144 100644 --- a/docs/make_uvflag.py +++ b/docs/make_uvflag.py @@ -32,8 +32,11 @@ def write_uvflag_rst(write_file=None): "Under the hood, the attributes are implemented as properties based on\n" ":class:`pyuvdata.parameter.UVParameter` objects but this is fairly\n" "transparent to users.\n" - "The telescope attribute is implemented as a :class:`pyuvdata.Telescope`\n" - "object, with its attributes available on the UVFlag object as properties.\n\n" + "Starting in version 3.0, metadata that is associated with the telescope\n" + "(as opposed to the data set) is stored in a :class:`pyuvdata.Telescope`\n" + "object (see :ref:`Telescope`) as the ``telescope`` attribute on a UVFlag\n" + "object. This includes metadata related to the telescope location,\n" + "antenna names, numbers and positions as well as other telescope metadata.\n\n" "UVFlag objects can be initialized from a file or a :class:`pyuvdata.UVData`\n" "or :class:`pyuvdata.UVCal` object\n" "(as ``flag = UVFlag()``). Some of these attributes\n" From 8ab0f2a0012459421799dd3af662ec3ca91d78ef Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 25 Apr 2024 15:41:03 -0700 Subject: [PATCH 27/59] Move `get_enu_antpos` to Telescope, deprecate the one on UVData --- docs/uvdata_tutorial.rst | 26 ++++++++++++++++----- pyuvdata/telescopes.py | 16 +++++++++++++ pyuvdata/tests/test_telescopes.py | 10 ++++++++ pyuvdata/uvdata/tests/test_uvdata.py | 8 ++++++- pyuvdata/uvdata/uvdata.py | 34 +++++++++++++++++++--------- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/docs/uvdata_tutorial.rst b/docs/uvdata_tutorial.rst index ff54e3cc92..ec9895a27f 100644 --- a/docs/uvdata_tutorial.rst +++ b/docs/uvdata_tutorial.rst @@ -979,19 +979,34 @@ UVData: Location conversions A number of conversion methods exist to map between different coordinate systems for locations on the earth. -a) Getting antenna positions in topocentric frame in units of meters +Note that the ``UVData.telescope.location`` attribute is an +:class:`astropy.EarthLocation` object, so it can be used directly to get to any +astropy supported coordinate system. + +a) Getting antenna positions in East, North, Up (ENU) frame in units of meters +****************************************************************************** + +Note that the ENU frame is sometimes referred to as the topocentric frame but +in many references the topocentric frame has the pole on the axis of rotation +for the Earth rather than at the local zenith. We just call it the ENU frame for +clarity. + +Use the :meth:`pyuvdata.Telescope.get_enu_antpos` to get the antenna +positions in the ENU frame. Or use the ``telescope.location`` and +``telescope.antenna_positions`` attributes (which are ECEF positions relative +to the ``telescope.location``) with the :meth:`pyuvdata.utils.ENU_from_ECEF` +utility method. -******************************************************************** .. code-block:: python - >>> # directly from UVData object + >>> # directly from Telescope object >>> import os >>> from astropy.units import Quantity >>> from pyuvdata import UVData >>> from pyuvdata.data import DATA_PATH >>> data_file = os.path.join(DATA_PATH, 'new.uvA') >>> uvd = UVData.from_file(data_file, use_future_array_shapes=True) - >>> antpos, ants = uvd.get_ENU_antpos() + >>> antpos = uvd.telescope.get_enu_antpos() >>> # using utils >>> from pyuvdata import utils @@ -1000,7 +1015,7 @@ a) Getting antenna positions in topocentric frame in units of meters >>> telescope_ecef_xyz = Quantity(uvd.telescope.location.geocentric).to("m").value >>> antpos = uvd.telescope.antenna_positions + telescope_ecef_xyz - >>> # convert to topocentric (East, North, Up or ENU) coords. + >>> # convert to East, North, Up (ENU) coords. >>> antpos = utils.ENU_from_ECEF( ... antpos, ... latitude=uvd.telescope.location.lat.rad, @@ -2036,7 +2051,6 @@ the baseline array. 19 >>> # Using antenna positions instead - >>> antpos, antnums = uvd.get_ENU_antpos() >>> baseline_groups, vec_bin_centers, lengths = uvd.get_redundancies(tol=tol, use_antpos=True) >>> print(len(baseline_groups)) 20 diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index a0750ab1b4..30df89f95b 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -801,3 +801,19 @@ def write_hdf5_header(self, header): header["x_orientation"] = np.string_(self.x_orientation) if self.antenna_diameters is not None: header["antenna_diameters"] = self.antenna_diameters + + def get_enu_antpos(self): + """ + Get antenna positions in East, North, Up coordinates in units of meters. + + Returns + ------- + antpos : ndarray + Antenna positions in East, North, Up coordinates in units of + meters, shape=(Nants, 3) + + """ + antenna_xyz = self.antenna_positions + self._location.xyz() + antpos = uvutils.ENU_from_ECEF(antenna_xyz, center_loc=self.location) + + return antpos diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 0e8b41a1c5..2311928154 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -337,3 +337,13 @@ def test_passing_xorient(simplest_working_params, xorient): assert tel.x_orientation == "east" else: assert tel.x_orientation == "north" + + +def test_get_enu_antpos(): + filename = os.path.join(DATA_PATH, "zen.2457698.40355.xx.HH.uvcA.uvh5") + + tel = Telescope.from_hdf5(filename) + # no center, no pick data ants + antpos = tel.get_enu_antpos() + assert antpos.shape == (tel.Nants, 3) + assert np.isclose(antpos[0, 0], 19.340211050751535) diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index 22a35824dc..bb2ba765b6 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -5357,10 +5357,16 @@ def test_get_ants(casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.filterwarnings("ignore:This method is deprecated in favor of") def test_get_enu_antpos(hera_uvh5_xx): uvd = hera_uvh5_xx # no center, no pick data ants - antpos, ants = uvd.get_ENU_antpos(center=False, pick_data_ants=False) + with uvtest.check_warnings( + DeprecationWarning, + match="This method is deprecated in favor of `self.telescope.get_enu_antpos`. " + "This will become an error in version 3.2", + ): + antpos, ants = uvd.get_ENU_antpos(center=False, pick_data_ants=False) assert len(ants) == 113 assert np.isclose(antpos[0, 0], 19.340211050751535) assert ants[0] == 0 diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index ab488c43c3..b624d72589 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -3646,10 +3646,15 @@ def get_ENU_antpos(self, *, center=False, pick_data_ants=False): """ Get antenna positions in East, North, Up coordinates in units of meters. + Deprecated in favor of `self.telescope.get_enu_antpos()`. + Parameters ---------- center : bool - If True, subtract median of array position from antpos + If True, subtract median of antenna positions before returning the + positions. Note that this will make it so that the antenna positions + cannot be reliably converted back to ECEF positions because they will + not be only referenced to the telescope location. pick_data_ants : bool If True, return only antennas found in data @@ -3662,10 +3667,13 @@ def get_ENU_antpos(self, *, center=False, pick_data_ants=False): Antenna numbers matching ordering of antpos, shape=(Nants,) """ - antenna_xyz = self.telescope.antenna_positions + self.telescope._location.xyz() - antpos = uvutils.ENU_from_ECEF(antenna_xyz, center_loc=self.telescope.location) + warnings.warn( + "This method is deprecated in favor of `self.telescope.get_enu_antpos`. " + "This will become an error in version 3.2", + DeprecationWarning, + ) + antpos = self.telescope.get_enu_antpos() ants = self.telescope.antenna_numbers - if pick_data_ants: data_ants = np.unique(np.concatenate([self.ant_1_array, self.ant_2_array])) telescope_ants = self.telescope.antenna_numbers @@ -3978,8 +3986,8 @@ def conjugate_bls(self, convention="ant10", "v<0", "v>0"]: if use_enu is True: - enu, anum = self.get_ENU_antpos() - anum = anum.tolist() + enu = self.telescope.get_enu_antpos() + anum = self.telescope.antenna_numbers.tolist() uvw_array_use = np.zeros_like(self.uvw_array) for i in range(self.baseline_array.size): a1, a2 = self.ant_1_array[i], self.ant_2_array[i] @@ -9307,9 +9315,9 @@ def get_redundancies( use_grid_alg = True if use_antpos: - antpos, numbers = self.get_ENU_antpos(center=False) + antpos = self.telescope.get_enu_antpos() result = uvutils.get_antenna_redundancies( - numbers, + self.telescope.antenna_numbers, antpos, tol=tol, include_autos=include_autos, @@ -9335,10 +9343,14 @@ def get_redundancies( # that we aren't comparing uvws at different times ant1 = np.take(self.ant_1_array, unique_inds) ant2 = np.take(self.ant_2_array, unique_inds) - antpos, numbers = self.get_ENU_antpos(center=False) + antpos = self.telescope.get_enu_antpos() - ant1_inds = np.array([np.nonzero(numbers == ai)[0][0] for ai in ant1]) - ant2_inds = np.array([np.nonzero(numbers == ai)[0][0] for ai in ant2]) + ant1_inds = np.array( + [np.nonzero(self.telescope.antenna_numbers == ai)[0][0] for ai in ant1] + ) + ant2_inds = np.array( + [np.nonzero(self.telescope.antenna_numbers == ai)[0][0] for ai in ant2] + ) baseline_vecs = np.take(antpos, ant2_inds, axis=0) - np.take( antpos, ant1_inds, axis=0 From 807c92d760a6f4212b9778f1509116df5dec211c Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 08:56:54 -0700 Subject: [PATCH 28/59] Fix enu_antpos tests --- pyuvdata/tests/test_utils.py | 10 +++++++--- pyuvdata/uvdata/tests/test_miriad.py | 2 +- pyuvdata/uvdata/tests/test_ms.py | 2 +- pyuvdata/uvdata/tests/test_uvdata.py | 9 ++++++--- pyuvdata/uvdata/tests/test_uvfits.py | 2 +- pyuvdata/uvdata/tests/test_uvh5.py | 4 ++-- pyuvdata/uvdata/uvdata.py | 2 +- pyuvdata/uvflag/tests/test_uvflag.py | 2 +- 8 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 2610ae3199..48d79eb7da 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -2987,11 +2987,15 @@ def test_redundancy_finder(grid_alg): tol = 0.05 - antpos, antnums = uvd.get_ENU_antpos() + antpos = uvd.telescope.get_enu_antpos() with uvtest.check_warnings(warn_type, match=warn_str): baseline_groups_ants, vec_bin_centers, lens = uvutils.get_antenna_redundancies( - antnums, antpos, tol=tol, include_autos=False, use_grid_alg=grid_alg + uvd.telescope.antenna_numbers, + antpos, + tol=tol, + include_autos=False, + use_grid_alg=grid_alg, ) # Under these conditions, should see 19 redundant groups in the file. assert len(baseline_groups_ants) == 19 @@ -3029,7 +3033,7 @@ def test_redundancy_finder(grid_alg): for bi, bl in enumerate(gp): if bl in conjugates: bl_gps_unconj[gi][bi] = uvutils.baseline_index_flip( - bl, Nants_telescope=len(antnums) + bl, Nants_telescope=uvd.telescope.Nants ) bl_gps_unconj = [sorted(bgp) for bgp in bl_gps_unconj] bl_gps_ants = [sorted(bgp) for bgp in baseline_groups_ants] diff --git a/pyuvdata/uvdata/tests/test_miriad.py b/pyuvdata/uvdata/tests/test_miriad.py index dac7b66ce6..cee12ed76c 100644 --- a/pyuvdata/uvdata/tests/test_miriad.py +++ b/pyuvdata/uvdata/tests/test_miriad.py @@ -908,7 +908,7 @@ def test_miriad_only_itrs(tmp_path, paper_miriad): uv_in = paper_miriad testfile = os.path.join(tmp_path, "outtest_miriad.uv") - enu_antpos, _ = uv_in.get_ENU_antpos() + enu_antpos = uv_in.telescope.get_enu_antpos() latitude, longitude, altitude = uv_in.telescope.location_lat_lon_alt uv_in.telescope.location = MoonLocation.from_selenodetic( lat=latitude * units.rad, lon=longitude * units.rad, height=altitude * units.m diff --git a/pyuvdata/uvdata/tests/test_ms.py b/pyuvdata/uvdata/tests/test_ms.py index 95e5543eff..8b27c62fbe 100644 --- a/pyuvdata/uvdata/tests/test_ms.py +++ b/pyuvdata/uvdata/tests/test_ms.py @@ -125,7 +125,7 @@ def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): pytest.importorskip("lunarsky") from lunarsky import MoonLocation - enu_antpos, _ = uvobj.get_ENU_antpos() + enu_antpos = uvobj.telescope.get_enu_antpos() uvobj.telescope.location = MoonLocation.from_selenodetic( lat=uvobj.telescope.location.lat, lon=uvobj.telescope.location.lon, diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index bb2ba765b6..d48b0f95cf 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -5406,7 +5406,7 @@ def test_telescope_loc_xyz_check(paper_uvh5, tmp_path): "itrs position vector magnitudes must be on the order " "of the radius of Earth -- they appear to lie well below this." ] - * 4, + * 5, ): uv.read(fname, use_future_array_shapes=True) @@ -6192,10 +6192,13 @@ def test_get_antenna_redundancies(pyuvsim_redundant, grid_alg): assert conjs is None - apos, anums = uv0.get_ENU_antpos() + apos = uv0.telescope.get_enu_antpos() with uvtest.check_warnings(warn_type, match=warn_str): new_red_gps, new_centers, new_lengths = uvutils.get_antenna_redundancies( - anums, apos, include_autos=False, use_grid_alg=grid_alg + uv0.telescope.antenna_numbers, + apos, + include_autos=False, + use_grid_alg=grid_alg, ) # all redundancy info is the same diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index 572cb0e0b9..7b732560e2 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -529,7 +529,7 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se pytest.importorskip("lunarsky") from lunarsky import MoonLocation - enu_antpos, _ = uv_in.get_ENU_antpos() + enu_antpos = uv_in.telescope.get_enu_antpos() uv_in.telescope.location = MoonLocation.from_selenodetic( lat=uv_in.telescope.location.lat, lon=uv_in.telescope.location.lon, diff --git a/pyuvdata/uvdata/tests/test_uvh5.py b/pyuvdata/uvdata/tests/test_uvh5.py index da4e27e1d0..280eb780ea 100644 --- a/pyuvdata/uvdata/tests/test_uvh5.py +++ b/pyuvdata/uvdata/tests/test_uvh5.py @@ -204,7 +204,7 @@ def test_read_uvfits_write_uvh5_read_uvh5( pytest.importorskip("lunarsky") from lunarsky import MoonLocation - enu_antpos, _ = uv_in.get_ENU_antpos() + enu_antpos = uv_in.telescope.get_enu_antpos() uv_in.telescope.location = MoonLocation.from_selenodetic( lat=uv_in.telescope.location.lat, lon=uv_in.telescope.location.lon, @@ -3792,7 +3792,7 @@ def test_pols(self): def test_antpos_enu(self): meta = uvh5.FastUVH5Meta(self.fl) uvd = meta.to_uvdata() - assert np.allclose(meta.antpos_enu, uvd.get_ENU_antpos()[0]) + assert np.allclose(meta.antpos_enu, uvd.telescope.get_enu_antpos()) def test_phased_phase_type(self, sma_mir, tmp_path_factory): testdir = tmp_path_factory.mktemp("test_phased_phase_type") diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index b624d72589..9ecc548d2e 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -9642,7 +9642,7 @@ def inflate_by_redundancy( """ self.conjugate_bls("u>0") - red_gps, centers, lengths = self.get_redundancies( + red_gps, _, _ = self.get_redundancies( tol=tol, use_antpos=True, conjugate_bls=True, use_grid_alg=use_grid_alg ) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 801da67438..e115dd0ae3 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -873,7 +873,7 @@ def test_read_write_loop_spw(uvdata_obj, test_outfile, telescope_frame, selenoid pytest.importorskip("lunarsky") from lunarsky import MoonLocation - enu_antpos, _ = uv.get_ENU_antpos() + enu_antpos = uv.telescope.get_enu_antpos() uv.telescope.location = MoonLocation.from_selenodetic( lat=uv.telescope.location.lat, lon=uv.telescope.location.lon, From c08ad997fdec692d6e630d99bc6666b6cf6d90cd Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 09:23:55 -0700 Subject: [PATCH 29/59] Fix uvflag compatibility handling --- pyuvdata/uvflag/tests/test_uvflag.py | 51 +++++++++++---- pyuvdata/uvflag/uvflag.py | 97 +++++++++++++++++++--------- 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index e115dd0ae3..978bc90dd3 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -1560,14 +1560,17 @@ def test_init_list(uvdata_obj): uv = uvdata_obj uv.time_array -= 1 uv.set_lsts_from_time_array() - uvf = UVFlag([uv, test_f_file], use_future_array_shapes=True) + with uvtest.check_warnings( + UserWarning, + match=[ + "UVParameter instrument does not match. Combining anyway.", + "UVParameter antenna_diameters does not match. Combining anyway.", + ], + ): + uvf = UVFlag([uv, test_f_file], use_future_array_shapes=True) uvf1 = UVFlag(uv, use_future_array_shapes=True) uvf2 = UVFlag(test_f_file, use_future_array_shapes=True) - uv.telescope.location = uvf2.telescope.location - uv.telescope.antenna_names = uvf2.telescope.antenna_names - uvf = UVFlag([uv, test_f_file], use_future_array_shapes=True) - assert np.array_equal( np.concatenate((uvf1.metric_array, uvf2.metric_array), axis=0), uvf.metric_array ) @@ -1605,8 +1608,11 @@ def test_read_multiple_files( uvf = UVFlag(uv, use_future_array_shapes=write_future_shapes) uvf.write(test_outfile, clobber=True) - warn_msg = [] - warn_type = [] + warn_msg = [ + "UVParameter instrument does not match. Combining anyway.", + "UVParameter antenna_diameters does not match. Combining anyway.", + ] + warn_type = [UserWarning] * 2 if not read_future_shapes: warn_msg += [_future_array_shapes_warning] * 2 warn_type += [DeprecationWarning] * 2 @@ -2590,7 +2596,12 @@ def test_to_baseline_metric_error(uvdata_obj, uvf_from_uvcal): NotImplementedError, match="Cannot currently convert from antenna type, metric mode", ): - uvf.to_baseline(uv, force_pol=True) + with uvtest.check_warnings( + UserWarning, + match="x_orientation is not the same this object and on uv. Keeping " + "the value on this object.", + ): + uvf.to_baseline(uv, force_pol=True) @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @@ -2644,8 +2655,18 @@ def test_to_baseline_from_antenna( uv2.time_array[: uvf2.time_array.size] = uvf.time_array uv2.set_lsts_from_time_array() - uvf.to_baseline(uv, force_pol=True) - uvf2.to_baseline(uv2, force_pol=True) + with uvtest.check_warnings( + UserWarning, + match="x_orientation is not the same this object and on uv. Keeping " + "the value on this object.", + ): + uvf.to_baseline(uv, force_pol=True) + with uvtest.check_warnings( + UserWarning, + match="x_orientation is not the same this object and on uv. Keeping " + "the value on this object.", + ): + uvf2.to_baseline(uv2, force_pol=True) uvf.check() uvf2.select(bls=old_baseline, times=old_times) @@ -2893,7 +2914,15 @@ def test_to_antenna_add_version_str(uvcal_obj, uvc_future_shapes, uvf_future_sha uvf.history = uvf.history.replace(pyuvdata_version_str, "") assert pyuvdata_version_str not in uvf.history - uvf.to_antenna(uvc) + # also change the instrument name to check warning + uvf.telescope.instrument = uvf.telescope.name + + with uvtest.check_warnings( + UserWarning, + match="instrument is not the same this object and on uv. Keeping the " + "value on this object.", + ): + uvf.to_antenna(uvc) assert pyuvdata_version_str in uvf.history diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index ace44deb3f..1694943cbe 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -1623,7 +1623,7 @@ def to_baseline( f"on uv is {uv.freq_array}" ) - warn_compatibility_params = [ + compatibility_params = [ "telescope_name", "telescope_location", "antenna_names", @@ -1632,15 +1632,16 @@ def to_baseline( "channel_width", "spw_array", ] + warning_params = ["instrument", "x_orientation", "antenna_diameters"] if self.Nspws is not None and self.Nspws > 1: # TODO: make this always be in the compatibility list in version 3.0 - warn_compatibility_params.append("flex_spw_id_array") + compatibility_params.append("flex_spw_id_array") # sometimes the antenna sorting for the antenna names/numbers/positions # is different. If the sets are the same, re-sort self to match uv self.sort_ant_metadata_like(uv) - for param in warn_compatibility_params: + for param in compatibility_params + warning_params: if ( issubclass(uv.__class__, UVData) and param == "channel_width" @@ -1666,11 +1667,17 @@ def to_baseline( this_param = getattr(self, "_" + param) uv_param = getattr(uv, "_" + param) if this_param.value is not None and this_param != uv_param: - raise ValueError( - f"{param} is not the same this object and on uv. The value on " - f"this object is {this_param.value}; the value on uv is " - f"{uv_param.value}." - ) + if param in warning_params: + warnings.warn( + f"{param} is not the same this object and on uv. " + "Keeping the value on this object." + ) + else: + raise ValueError( + f"{param} is not the same this object and on uv. " + f"The value on this object is {this_param.value}; " + f"the value on uv is {uv_param.value}." + ) # Deal with polarization if force_pol and self.polarization_array.size == 1: @@ -1918,7 +1925,7 @@ def to_antenna( f"on uv is {uv.freq_array}" ) - warn_compatibility_params = [ + compatibility_params = [ "telescope_name", "telescope_location", "antenna_names", @@ -1927,15 +1934,16 @@ def to_antenna( "channel_width", "spw_array", ] + warning_params = ["instrument", "x_orientation", "antenna_diameters"] if self.Nspws is not None and self.Nspws > 1: # TODO: make this always be in the compatibility list in version 3.0 - warn_compatibility_params.append("flex_spw_id_array") + compatibility_params.append("flex_spw_id_array") # sometimes the antenna sorting for the antenna names/numbers/positions # is different. If the sets are the same, re-sort self to match uv self.sort_ant_metadata_like(uv) - for param in warn_compatibility_params: + for param in compatibility_params + warning_params: if ( issubclass(uv.__class__, UVCal) and param == "channel_width" @@ -1961,11 +1969,17 @@ def to_antenna( this_param = getattr(self, "_" + param) uv_param = getattr(uv, "_" + param) if this_param.value is not None and this_param != uv_param: - raise ValueError( - f"{param} is not the same this object and on uv. The value on " - f"this object is {this_param.value}; the value on uv is " - f"{uv_param.value}." - ) + if param in warning_params: + warnings.warn( + f"{param} is not the same this object and on uv. " + "Keeping the value on this object." + ) + else: + raise ValueError( + f"{param} is not the same this object and on uv. " + f"The value on this object is {this_param.value}; " + f"the value on uv is {uv_param.value}." + ) # Deal with polarization if issubclass(uv.__class__, UVCal): @@ -2292,25 +2306,24 @@ def __add__( ax = axis_nums[axis][type_nums[self.type]] - warn_compatibility_params = ["telescope_name", "telescope_location"] + compatibility_params = ["telescope_name", "telescope_location"] + warning_params = ["instrument", "x_orientation", "antenna_diameters"] if axis != "frequency": - warn_compatibility_params.extend( - ["freq_array", "channel_width", "spw_array"] - ) + compatibility_params.extend(["freq_array", "channel_width", "spw_array"]) if self.flex_spw_id_array is not None: # TODO: make this always be in the compatibility list in version 3.0 - warn_compatibility_params.append("flex_spw_id_array") + compatibility_params.append("flex_spw_id_array") if axis not in ["polarization", "pol", "jones"]: - warn_compatibility_params.extend(["polarization_array"]) + compatibility_params.extend(["polarization_array"]) if axis != "time": - warn_compatibility_params.extend(["time_array", "lst_array"]) + compatibility_params.extend(["time_array", "lst_array"]) if axis != "antenna" and self.type == "antenna": - warn_compatibility_params.extend( + compatibility_params.extend( ["ant_array", "antenna_names", "antenna_numbers", "antenna_positions"] ) if axis != "baseline" and self.type == "baseline": - warn_compatibility_params.extend( + compatibility_params.extend( [ "baseline_array", "ant_1_array", @@ -2321,7 +2334,7 @@ def __add__( ] ) - for param in warn_compatibility_params: + for param in compatibility_params + warning_params: # compare the UVParameter objects to properly handle tolerances if param in telescope_params: this_param = getattr(self.telescope, "_" + telescope_params[param]) @@ -2330,11 +2343,16 @@ def __add__( this_param = getattr(self, "_" + param) other_param = getattr(other, "_" + param) if this_param.value is not None and this_param != other_param: - raise ValueError( - f"{param} is not the same the two objects. The value on this " - f"object is {this_param.value}; the value on the other object is " - f"{other_param.value}." - ) + if param in warning_params: + warnings.warn( + "UVParameter " + param + " does not match. Combining anyway." + ) + else: + raise ValueError( + f"{param} is not the same the two objects. The value on " + f"this object is {this_param.value}; the value on the " + f"other object is {other_param.value}." + ) if axis == "time": this.time_array = np.concatenate([this.time_array, other.time_array]) @@ -2393,7 +2411,22 @@ def __add__( ) this.telescope.antenna_names = temp_ant_names[unique_inds] this.telescope.antenna_positions = temp_ant_pos[unique_inds] - this.Nants_telescope = len(this.telescope.antenna_numbers) + this.telescope.Nants = len(this.telescope.antenna_numbers) + + if ( + this.telescope.antenna_diameters is not None + and other.telescope.antenna_diameters is not None + ): + temp_ant_diameters = np.concatenate( + [ + this.telescope.antenna_diameters, + other.telescope.antenna_diameters, + ], + axis=0, + ) + this.telescope.antenna_diameters = temp_ant_diameters[unique_inds] + else: + this.telescope.antenna_diameters = None elif axis == "frequency": this.freq_array = np.concatenate( From 4645b1de0cc5ced564319ca1aed29409b9fd92fe Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 14:29:12 -0700 Subject: [PATCH 30/59] add parameter test coverage --- pyuvdata/parameter.py | 43 ++++++++++++----- pyuvdata/tests/test_parameter.py | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 11 deletions(-) diff --git a/pyuvdata/parameter.py b/pyuvdata/parameter.py index 28668641c3..0ee8e930ab 100644 --- a/pyuvdata/parameter.py +++ b/pyuvdata/parameter.py @@ -347,6 +347,22 @@ def __eq__(self, other, *, silent=False): print(f"{self.name} is None on right, but not left") return False + if isinstance(self.value, tuple(allowed_location_types)): + if not isinstance(other.value, tuple(allowed_location_types)): + if not silent: + print( + f"{self.name} parameter value is a Location, but other is not" + ) + return False + + if isinstance(self.value, SkyCoord): + if not isinstance(other.value, SkyCoord): + if not silent: + print( + f"{self.name} parameter value is a SkyCoord, but other is not" + ) + return False + if isinstance(self.value, np.recarray): # check both recarrays and field names match (order doesn't have to) # then iterate through field names and check that each matches @@ -409,6 +425,14 @@ def __eq__(self, other, *, silent=False): return False if isinstance(self.value, units.Quantity): + if not isinstance(other.value, units.Quantity): + if not silent: + print( + f"{self.name} parameter value is a Quantity, but " + "other is not." + ) + return False + if not self.value.unit.is_equivalent(other.value.unit): if not silent: print( @@ -956,12 +980,13 @@ def xyz(self): centric_coords = self.value.geocentric return units.Quantity(centric_coords).to("m").value - def set_xyz(self, xyz, frame=None, ellipsoid=None): + def set_xyz(self, xyz, *, frame=None, ellipsoid=None): """Set the body centric coordinates in meters.""" - if frame is None and hasmoon and isinstance(self.value, MoonLocation): - frame = "mcmf" - else: - frame = "itrs" + if frame is None: + if hasmoon and isinstance(self.value, MoonLocation): + frame = "mcmf" + else: + frame = "itrs" allowed_frames = ["itrs"] if hasmoon: @@ -1072,16 +1097,12 @@ def __eq__(self, other, *, silent=False): if not isinstance(self.value, tuple(allowed_location_types)) or not isinstance( other.value, tuple(allowed_location_types) ): + # one of these is not a proper location type. return super(LocationParameter, self).__eq__(other, silent=silent) - if self.value.shape != other.value.shape: - if not silent: - print(f"{self.name} parameter shapes are different") - return False - if not isinstance(other.value, self.value.__class__): if not silent: - print("Classes do not match") + print(f"{self.name} parameter classes do not match") return False if hasmoon and isinstance(self.value, MoonLocation): diff --git a/pyuvdata/tests/test_parameter.py b/pyuvdata/tests/test_parameter.py index 3d06da897a..478f637d00 100644 --- a/pyuvdata/tests/test_parameter.py +++ b/pyuvdata/tests/test_parameter.py @@ -418,6 +418,18 @@ def test_location_set_lat_lon_alt_degrees_none(): assert param1.value is None +def test_location_set_xyz(): + param1 = uvp.LocationParameter(name="p2", value=1) + param1.set_xyz(None) + + assert param1.value is None + + assert param1.xyz() is None + + with pytest.raises(ValueError, match="frame must be one of"): + param1.set_xyz(ref_xyz, frame="foo") + + @pytest.mark.parametrize(["frame", "selenoid"], frame_selenoid) def test_location_xyz_latlonalt_match(frame, selenoid): if frame == "itrs": @@ -497,6 +509,77 @@ def test_location_acceptability(): assert reason == f"Location must be an object of type: {allowed_location_types}" +@pytest.mark.parametrize(["frame", "selenoid"], frame_selenoid) +def test_location_equality(frame, selenoid): + if frame == "itrs": + loc_obj1 = EarthLocation.from_geocentric(*ref_xyz, unit="m") + xyz_adj = np.array(ref_xyz) + 8e-4 + loc_obj2 = EarthLocation.from_geocentric(*xyz_adj, unit="m") + else: + loc_obj1 = MoonLocation.from_selenocentric(*ref_xyz_moon[selenoid], unit="m") + loc_obj1.ellipsoid = selenoid + xyz_adj = np.array(ref_xyz_moon[selenoid]) + 8e-4 + loc_obj2 = MoonLocation.from_selenocentric(*xyz_adj, unit="m") + loc_obj2.ellipsoid = selenoid + param1 = uvp.LocationParameter("p1", value=loc_obj1) + param2 = uvp.LocationParameter("p1", value=loc_obj2) + assert param1 == param2 + + +@pytest.mark.parametrize( + ["change", "msg"], + [ + ["non_loc", "p1 parameter value is a Location, but other is not"], + ["class", "p1 parameter classes do not match"], + ["ellipsoid", "p1 parameter ellipsoid is not the same. "], + ["value", "p1 parameter is not close. "], + ], +) +def test_location_inequality(capsys, change, msg): + param1 = uvp.LocationParameter( + "p1", value=EarthLocation.from_geocentric(*ref_xyz, unit="m") + ) + if change == "non_loc": + param2 = uvp.LocationParameter( + "p1", value=units.Quantity(np.array(ref_xyz), unit="m") + ) + elif change == "class": + pytest.importorskip("lunarsky") + param2 = uvp.LocationParameter( + "p1", + value=MoonLocation.from_selenocentric(*ref_xyz_moon["SPHERE"], unit="m"), + ) + elif change == "ellipsoid": + pytest.importorskip("lunarsky") + param1 = uvp.LocationParameter( + "p1", + value=MoonLocation.from_selenodetic( + lat=ref_latlonalt_moon[0] * units.rad, + lon=ref_latlonalt_moon[1] * units.rad, + height=ref_latlonalt_moon[2] * units.m, + ellipsoid="SPHERE", + ), + ) + param2 = uvp.LocationParameter( + "p1", + value=MoonLocation.from_selenodetic( + lat=ref_latlonalt_moon[0] * units.rad, + lon=ref_latlonalt_moon[1] * units.rad, + height=ref_latlonalt_moon[2] * units.m, + ellipsoid="GSFC", + ), + ) + elif change == "value": + xyz_adj = np.array(ref_xyz) + 2e-3 + param2 = uvp.LocationParameter( + "p1", value=EarthLocation.from_geocentric(*xyz_adj, unit="m") + ) + + assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith(msg) + + @pytest.mark.parametrize( "sky2", [ From 3b23bfd8fcdc311d6ec3dce688f584132cdd1676 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 14:29:59 -0700 Subject: [PATCH 31/59] add telescope coverage --- pyuvdata/tests/test_telescopes.py | 226 ++++++++++++++++++++---------- 1 file changed, 152 insertions(+), 74 deletions(-) diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 2311928154..45bf6d141c 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -5,16 +5,19 @@ """Tests for telescope objects and functions. """ +import copy import os import numpy as np import pytest from astropy.coordinates import EarthLocation +from astropy.units import Quantity import pyuvdata +import pyuvdata.tests as uvtest from pyuvdata import Telescope, UVData from pyuvdata.data import DATA_PATH -from pyuvdata.telescopes import get_antenna_params +from pyuvdata.telescopes import KNOWN_TELESCOPES, get_antenna_params required_parameters = [ "_name", @@ -140,6 +143,52 @@ def test_known_telescopes(): assert sorted(pyuvdata.known_telescopes()) == sorted(expected_known_telescopes) +def test_update_params_from_known(): + """Cover some edge cases not covered by UVData/UVCal/UVFlag tests.""" + tel = Telescope() + with pytest.raises( + ValueError, + match="The telescope name attribute must be set to update from " + "known_telescopes.", + ): + tel.update_params_from_known_telescopes() + + hera_tel = Telescope.from_known_telescopes("hera") + known_dict = copy.deepcopy(KNOWN_TELESCOPES) + known_dict["HERA"]["antenna_diameters"] = hera_tel.antenna_diameters + + hera_tel_test = Telescope.from_known_telescopes( + "hera", known_telescope_dict=known_dict + ) + assert hera_tel == hera_tel_test + + known_dict["HERA"]["antenna_diameters"] = hera_tel.antenna_diameters[0:10] + known_dict["HERA"]["x_orientation"] = "east" + hera_tel_test.antenna_diameters = None + with uvtest.check_warnings( + UserWarning, + match=[ + "antenna_diameters are not set because the number of antenna_diameters " + "on known_telescopes_dict is more than one and does not match Nants " + "for telescope hera.", + "x_orientation are not set or are being overwritten. x_orientation " + "are set using values from known telescopes for hera.", + ], + ): + hera_tel_test.update_params_from_known_telescopes( + known_telescope_dict=known_dict + ) + assert hera_tel_test.antenna_diameters is None + assert hera_tel_test.x_orientation == "east" + + mwa_tel = Telescope.from_known_telescopes("mwa") + mwa_tel2 = Telescope() + mwa_tel2.name = "mwa" + mwa_tel2.update_params_from_known_telescopes(warn=False) + + assert mwa_tel == mwa_tel2 + + def test_from_known(): for inst in pyuvdata.known_telescopes(): # don't run check b/c some telescopes won't have antenna info defined @@ -252,82 +301,104 @@ def test_alternate_antenna_inputs(): assert np.all(nums == nums2) -def test_bad_antenna_inputs(simplest_working_params): - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises( - ValueError, match="Either antenna_numbers or antenna_names must be provided" - ): - Telescope.from_params( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=None, - antenna_names=None, - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } +def test_from_params_errors(simplest_working_params): + simplest_working_params["location"] = Quantity([0, 0, 0], unit="m") with pytest.raises( ValueError, - match=( - "antenna_positions must be a dictionary with keys that are all type int " - "or all type str" - ), + match="telescope_location has an unsupported type, it must be one of ", ): - Telescope.from_params(antenna_positions={1: [0, 1, 2], "2": [3, 4, 5]}, **badp) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="Antenna names must be integers"): - Telescope.from_params( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=None, - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="antenna_positions must be a numpy array"): - Telescope.from_params( - antenna_positions="foo", - antenna_numbers=[0, 1, 2], - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="antenna_positions must be a 2D array"): - Telescope.from_params( - antenna_positions=np.array([0, 0, 0]), antenna_numbers=np.array([0]), **badp - ) - - with pytest.raises(ValueError, match="Duplicate antenna names found"): - Telescope.from_params( - antenna_names=["foo", "bar", "foo"], **simplest_working_params - ) - - badp = { - k: v for k, v in simplest_working_params.items() if k != "antenna_positions" - } - with pytest.raises(ValueError, match="Duplicate antenna numbers found"): - Telescope.from_params( - antenna_positions=np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), - antenna_numbers=[0, 1, 0], - antenna_names=["foo", "bar", "baz"], - **badp, - ) - - with pytest.raises( - ValueError, match="antenna_numbers and antenna_names must have the same length" - ): - Telescope.from_params(antenna_names=["foo", "bar"], **simplest_working_params) + Telescope.from_params(**simplest_working_params) + + +@pytest.mark.parametrize( + ["kwargs", "err_msg"], + [ + [ + { + "antenna_positions": np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "Either antenna_numbers or antenna_names must be provided", + ], + [ + { + "antenna_positions": {1: [0, 1, 2], "2": [3, 4, 5]}, + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "antenna_positions must be a dictionary with keys that are all type int " + "or all type str", + ], + [ + { + "antenna_positions": np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + "antenna_names": ["foo", "bar", "baz"], + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "Antenna names must be integers", + ], + [ + { + "antenna_positions": "foo", + "antenna_numbers": [0, 1, 2], + "antenna_names": ["foo", "bar", "baz"], + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "antenna_positions must be a numpy array", + ], + [ + { + "antenna_positions": np.array([0, 0, 0]), + "antenna_numbers": np.array([0]), + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "antenna_positions must be a 2D array", + ], + [ + { + "antenna_positions": { + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + "antenna_names": ["foo", "bar", "foo"], + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "Duplicate antenna names found", + ], + [ + { + "antenna_positions": np.array([[0, 0, 0], [0, 0, 1], [0, 0, 2]]), + "antenna_numbers": [0, 1, 0], + "antenna_names": ["foo", "bar", "baz"], + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "Duplicate antenna numbers found", + ], + [ + { + "antenna_positions": { + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + "antenna_names": ["foo", "bar"], + "location": EarthLocation.from_geodetic(0, 0, 0), + "name": "test", + }, + "antenna_numbers and antenna_names must have the same length", + ], + ], +) +def test_bad_antenna_inputs(kwargs, err_msg): + with pytest.raises(ValueError, match=err_msg): + Telescope.from_params(**kwargs) @pytest.mark.parametrize("xorient", ["e", "n", "east", "NORTH"]) @@ -339,6 +410,13 @@ def test_passing_xorient(simplest_working_params, xorient): assert tel.x_orientation == "north" +def test_passing_diameters(simplest_working_params): + tel = Telescope.from_params( + antenna_diameters=np.array([14.0, 15.0, 16.0]), **simplest_working_params + ) + np.testing.assert_allclose(tel.antenna_diameters, np.array([14.0, 15.0, 16.0])) + + def test_get_enu_antpos(): filename = os.path.join(DATA_PATH, "zen.2457698.40355.xx.HH.uvcA.uvh5") From 8cca530ec3ed4e4ad864e6b62261bf71408ea978 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 15:33:19 -0700 Subject: [PATCH 32/59] add test coverage for utils --- pyuvdata/tests/test_utils.py | 306 ++++++++++++++++++++++------------- pyuvdata/utils.py | 3 - 2 files changed, 193 insertions(+), 116 deletions(-) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 48d79eb7da..4933e7f65f 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -196,7 +196,7 @@ def test_XYZ_from_LatLonAlt(): ) # Got reference by forcing http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm # to give additional precision. - assert np.allclose(ref_xyz, out_xyz, rtol=0, atol=1e-3) + np.testing.assert_allclose(ref_xyz, out_xyz, rtol=0, atol=1e-3) # test error checking with pytest.raises( @@ -225,28 +225,28 @@ def test_LatLonAlt_from_XYZ(): out_latlonalt = uvutils.LatLonAlt_from_XYZ(ref_xyz) # Got reference by forcing http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm # to give additional precision. - assert np.allclose(ref_latlonalt, out_latlonalt, rtol=0, atol=1e-3) + np.testing.assert_allclose(ref_latlonalt, out_latlonalt, rtol=0, atol=1e-3) pytest.raises(ValueError, uvutils.LatLonAlt_from_XYZ, ref_latlonalt) # test passing multiple values xyz_mult = np.stack((np.array(ref_xyz), np.array(ref_xyz))) lat_vec, lon_vec, alt_vec = uvutils.LatLonAlt_from_XYZ(xyz_mult) - assert np.allclose( + np.testing.assert_allclose( ref_latlonalt, (lat_vec[1], lon_vec[1], alt_vec[1]), rtol=0, atol=1e-3 ) # check error if array transposed - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match=re.escape("The expected shape of ECEF xyz array is (Npts, 3)."), + ): uvutils.LatLonAlt_from_XYZ(xyz_mult.T) - assert str(cm.value).startswith( - "The expected shape of ECEF xyz array is (Npts, 3)." - ) # check error if only 2 coordinates - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match=re.escape("The expected shape of ECEF xyz array is (Npts, 3)."), + ): uvutils.LatLonAlt_from_XYZ(xyz_mult[:, 0:2]) - assert str(cm.value).startswith( - "The expected shape of ECEF xyz array is (Npts, 3)." - ) # test error checking pytest.raises(ValueError, uvutils.LatLonAlt_from_XYZ, ref_xyz[0:1]) @@ -260,7 +260,7 @@ def test_XYZ_from_LatLonAlt_mcmf(selenoid): out_xyz = uvutils.XYZ_from_LatLonAlt( lat, lon, alt, frame="mcmf", ellipsoid=selenoid ) - assert np.allclose(ref_xyz_moon[selenoid], out_xyz, rtol=0, atol=1e-3) + np.testing.assert_allclose(ref_xyz_moon[selenoid], out_xyz, rtol=0, atol=1e-3) # Test errors with invalid frame with pytest.raises( @@ -276,7 +276,7 @@ def test_LatLonAlt_from_XYZ_mcmf(selenoid): out_latlonalt = uvutils.LatLonAlt_from_XYZ( ref_xyz_moon[selenoid], frame="mcmf", ellipsoid=selenoid ) - assert np.allclose(ref_latlonalt_moon, out_latlonalt, rtol=0, atol=1e-3) + np.testing.assert_allclose(ref_latlonalt_moon, out_latlonalt, rtol=0, atol=1e-3) # Test errors with invalid frame with pytest.raises( @@ -344,9 +344,9 @@ def test_lla_xyz_lla_roundtrip(): lons *= np.pi / 180.0 xyz = uvutils.XYZ_from_LatLonAlt(lats, lons, alts) lats_new, lons_new, alts_new = uvutils.LatLonAlt_from_XYZ(xyz) - assert np.allclose(lats_new, lats) - assert np.allclose(lons_new, lons) - assert np.allclose(alts_new, alts) + np.testing.assert_allclose(lats_new, lats) + np.testing.assert_allclose(lons_new, lons) + np.testing.assert_allclose(alts_new, alts) @pytest.fixture(scope="module") @@ -586,7 +586,7 @@ def test_xyz_from_latlonalt(enu_ecef_info): enu_ecef_info ) xyz = uvutils.XYZ_from_LatLonAlt(lats, lons, alts) - assert np.allclose(np.stack((x, y, z), axis=1), xyz, atol=1e-3) + np.testing.assert_allclose(np.stack((x, y, z), axis=1), xyz, atol=1e-3) def test_enu_from_ecef(enu_ecef_info): @@ -599,7 +599,17 @@ def test_enu_from_ecef(enu_ecef_info): enu = uvutils.ENU_from_ECEF( xyz, latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert np.allclose(np.stack((east, north, up), axis=1), enu, atol=1e-3) + np.testing.assert_allclose(np.stack((east, north, up), axis=1), enu, atol=1e-3) + + enu2 = uvutils.ENU_from_ECEF( + xyz, + center_loc=EarthLocation.from_geodetic( + lat=center_lat * units.rad, + lon=center_lon * units.rad, + height=center_alt * units.m, + ), + ) + np.testing.assert_allclose(enu, enu2) @pytest.mark.skipif(not hasmoon, reason="lunarsky not installed") @@ -620,7 +630,18 @@ def test_enu_from_mcmf(enu_mcmf_info, selenoid): ellipsoid=selenoid, ) - assert np.allclose(np.stack((east, north, up), axis=1), enu, atol=1e-3) + np.testing.assert_allclose(np.stack((east, north, up), axis=1), enu, atol=1e-3) + + enu2 = uvutils.ENU_from_ECEF( + xyz, + center_loc=MoonLocation.from_selenodetic( + lat=center_lat * units.rad, + lon=center_lon * units.rad, + height=center_alt * units.m, + ellipsoid=selenoid, + ), + ) + np.testing.assert_allclose(enu, enu2, atol=1e-3) def test_invalid_frame(): @@ -638,6 +659,20 @@ def test_invalid_frame(): np.zeros((2, 3)), latitude=0.0, longitude=0.0, altitude=0.0, frame="undef" ) + with pytest.raises( + ValueError, match="center_loc is not a supported type. It must be one of " + ): + uvutils.ENU_from_ECEF( + np.zeros((2, 3)), center_loc=units.Quantity(np.array([0, 0, 0]) * units.m) + ) + + with pytest.raises( + ValueError, match="center_loc is not a supported type. It must be one of " + ): + uvutils.ECEF_from_ENU( + np.zeros((2, 3)), center_loc=units.Quantity(np.array([0, 0, 0]) * units.m) + ) + @pytest.mark.parametrize("shape_type", ["transpose", "Nblts,2", "Nblts,1"]) def test_enu_from_ecef_shape_errors(enu_ecef_info, shape_type): @@ -654,13 +689,13 @@ def test_enu_from_ecef_shape_errors(enu_ecef_info, shape_type): xyz = xyz.copy()[:, 0:1] # check error if array transposed - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match=re.escape("The expected shape of ECEF xyz array is (Npts, 3)."), + ): uvutils.ENU_from_ECEF( xyz, longitude=center_lat, latitude=center_lon, altitude=center_alt ) - assert str(cm.value).startswith( - "The expected shape of ECEF xyz array is (Npts, 3)." - ) def test_enu_from_ecef_magnitude_error(enu_ecef_info): @@ -670,13 +705,30 @@ def test_enu_from_ecef_magnitude_error(enu_ecef_info): ) xyz = uvutils.XYZ_from_LatLonAlt(lats, lons, alts) # error checking - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match="ITRS vector magnitudes must be on the order of the radius of the earth", + ): uvutils.ENU_from_ECEF( xyz / 2.0, latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert str(cm.value).startswith( - "ITRS vector magnitudes must be on the order of the radius of the earth" - ) + + +def test_enu_from_ecef_error(): + # check error no center location info passed + with pytest.raises( + ValueError, + match="Either center_loc or all of latitude, longitude and altitude " + "must be passed.", + ): + uvutils.ENU_from_ECEF(np.array([0, 0, 0])) + + with pytest.raises( + ValueError, + match="Either center_loc or all of latitude, longitude and altitude " + "must be passed.", + ): + uvutils.ECEF_from_ENU(np.array([0, 0, 0])) @pytest.mark.parametrize(["frame", "selenoid"], frame_selenoid) @@ -689,6 +741,18 @@ def test_ecef_from_enu_roundtrip(enu_ecef_info, enu_mcmf_info, frame, selenoid): lats = lats[selenoid] lons = lons[selenoid] alts = alts[selenoid] + loc_obj = MoonLocation.from_selenodetic( + lat=center_lat * units.rad, + lon=center_lon * units.rad, + height=center_alt * units.m, + ellipsoid=selenoid, + ) + else: + loc_obj = EarthLocation.from_geodetic( + lat=center_lat * units.rad, + lon=center_lon * units.rad, + height=center_alt * units.m, + ) xyz = uvutils.XYZ_from_LatLonAlt(lats, lons, alts, frame=frame, ellipsoid=selenoid) enu = uvutils.ENU_from_ECEF( @@ -708,7 +772,10 @@ def test_ecef_from_enu_roundtrip(enu_ecef_info, enu_mcmf_info, frame, selenoid): frame=frame, ellipsoid=selenoid, ) - assert np.allclose(xyz, xyz_from_enu, atol=1e-3) + np.testing.assert_allclose(xyz, xyz_from_enu, atol=1e-3) + + xyz_from_enu2 = uvutils.ECEF_from_ENU(enu, center_loc=loc_obj) + np.testing.assert_allclose(xyz_from_enu, xyz_from_enu2, atol=1e-3) if selenoid == "SPHERE": enu = uvutils.ENU_from_ECEF( @@ -726,7 +793,7 @@ def test_ecef_from_enu_roundtrip(enu_ecef_info, enu_mcmf_info, frame, selenoid): altitude=center_alt, frame=frame, ) - assert np.allclose(xyz, xyz_from_enu, atol=1e-3) + np.testing.assert_allclose(xyz, xyz_from_enu, atol=1e-3) @pytest.mark.parametrize("shape_type", ["transpose", "Nblts,2", "Nblts,1"]) @@ -746,11 +813,12 @@ def test_ecef_from_enu_shape_errors(enu_ecef_info, shape_type): enu = enu.copy()[:, 0:1] # check error if array transposed - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match=re.escape("The expected shape of the ENU array is (Npts, 3).") + ): uvutils.ECEF_from_ENU( enu, latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert str(cm.value).startswith("The expected shape of the ENU array is (Npts, 3).") def test_ecef_from_enu_single(enu_ecef_info): @@ -764,7 +832,9 @@ def test_ecef_from_enu_single(enu_ecef_info): xyz[0, :], latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert np.allclose(np.array((east[0], north[0], up[0])), enu_single, atol=1e-3) + np.testing.assert_allclose( + np.array((east[0], north[0], up[0])), enu_single, atol=1e-3 + ) def test_ecef_from_enu_single_roundtrip(enu_ecef_info): @@ -781,12 +851,14 @@ def test_ecef_from_enu_single_roundtrip(enu_ecef_info): enu_single = uvutils.ENU_from_ECEF( xyz[0, :], latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert np.allclose(np.array((east[0], north[0], up[0])), enu[0, :], atol=1e-3) + np.testing.assert_allclose( + np.array((east[0], north[0], up[0])), enu[0, :], atol=1e-3 + ) xyz_from_enu = uvutils.ECEF_from_ENU( enu_single, latitude=center_lat, longitude=center_lon, altitude=center_alt ) - assert np.allclose(xyz[0, :], xyz_from_enu, atol=1e-3) + np.testing.assert_allclose(xyz[0, :], xyz_from_enu, atol=1e-3) def test_mwa_ecef_conversion(): @@ -831,11 +903,11 @@ def test_mwa_ecef_conversion(): enu = uvutils.ENU_from_ECEF(ecef_xyz, latitude=lat, longitude=lon, altitude=alt) - assert np.allclose(enu, enh) + np.testing.assert_allclose(enu, enh) # test other direction of ECEF rotation rot_xyz = uvutils.rotECEF_from_ECEF(new_xyz, lon) - assert np.allclose(rot_xyz.T, xyz) + np.testing.assert_allclose(rot_xyz.T, xyz) @pytest.mark.parametrize( @@ -849,9 +921,8 @@ def test_polar2_to_cart3_arg_errs(lon_array, lat_array, msg): """ Test that bad arguments to polar2_to_cart3 throw appropriate errors. """ - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils.polar2_to_cart3(lon_array=lon_array, lat_array=lat_array) - assert str(cm.value).startswith(msg) @pytest.mark.parametrize( @@ -866,9 +937,8 @@ def test_cart3_to_polar2_arg_errs(input1, msg): """ Test that bad arguments to cart3_to_polar2 throw appropriate errors. """ - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils.cart3_to_polar2(input1) - assert str(cm.value).startswith(msg) @pytest.mark.parametrize( @@ -884,11 +954,10 @@ def test_rotate_matmul_wrapper_arg_errs(input1, input2, input3, msg): """ Test that bad arguments to _rotate_matmul_wrapper throw appropriate errors. """ - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils._rotate_matmul_wrapper( xyz_array=input1, rot_matrix=input2, n_rot=input3 ) - assert str(cm.value).startswith(msg) def test_cart_to_polar_roundtrip(): @@ -1138,7 +1207,10 @@ def test_compare_one_to_two_axis(vector_list, rot1, axis1, rot2, rot3, axis2, ax ], [ {"lst_array": None, "use_ant_pos": False, "from_enu": True}, - (ValueError, "Must include lst_array if moving between ENU (i.e.,"), + ( + ValueError, + re.escape("Must include lst_array if moving between ENU (i.e.,"), + ), ], [ {"use_ant_pos": False, "old_app_ra": None}, @@ -1161,7 +1233,7 @@ def test_calc_uvw_input_errors(calc_uvw_args, arg_dict, err): for key in arg_dict.keys(): calc_uvw_args[key] = arg_dict[key] - with pytest.raises(err[0]) as cm: + with pytest.raises(err[0], match=err[1]): uvutils.calc_uvw( app_ra=calc_uvw_args["app_ra"], app_dec=calc_uvw_args["app_dec"], @@ -1181,7 +1253,6 @@ def test_calc_uvw_input_errors(calc_uvw_args, arg_dict, err): from_enu=calc_uvw_args["from_enu"], to_enu=calc_uvw_args["to_enu"], ) - assert str(cm.value).startswith(err[1]) def test_calc_uvw_no_op(calc_uvw_args): @@ -1234,8 +1305,8 @@ def test_calc_uvw_same_place(calc_uvw_args): old_frame_pa=calc_uvw_args["old_frame_pa"], ) - assert np.allclose(uvw_ant_check, calc_uvw_args["uvw_array"]) - assert np.allclose(uvw_base_check, calc_uvw_args["uvw_array"]) + np.testing.assert_allclose(uvw_ant_check, calc_uvw_args["uvw_array"]) + np.testing.assert_allclose(uvw_base_check, calc_uvw_args["uvw_array"]) @pytest.mark.parametrize("to_enu", [False, True]) @@ -1277,7 +1348,7 @@ def test_calc_uvw_base_vs_ants(calc_uvw_args, to_enu): to_enu=to_enu, ) - assert np.allclose(uvw_ant_check, uvw_base_check) + np.testing.assert_allclose(uvw_ant_check, uvw_base_check) def test_calc_uvw_enu_roundtrip(calc_uvw_args): @@ -1311,7 +1382,9 @@ def test_calc_uvw_enu_roundtrip(calc_uvw_args): from_enu=True, ) - assert np.allclose(calc_uvw_args["uvw_array"], uvw_base_enu_check) + np.testing.assert_allclose( + calc_uvw_args["uvw_array"], uvw_base_enu_check, atol=1e-16, rtol=0 + ) def test_calc_uvw_pa_ex_post_facto(calc_uvw_args): @@ -1353,7 +1426,7 @@ def test_calc_uvw_pa_ex_post_facto(calc_uvw_args): old_frame_pa=calc_uvw_args["old_frame_pa"], ) - assert np.allclose(uvw_base_check, uvw_base_late_pa_check) + np.testing.assert_allclose(uvw_base_check, uvw_base_late_pa_check) @pytest.mark.filterwarnings('ignore:ERFA function "pmsafe" yielded') @@ -1394,7 +1467,7 @@ def test_transform_icrs_to_app_arg_errs(astrometry_args, arg_dict, msg): default_args[key] = arg_dict[key] # Start w/ the transform_icrs_to_app block - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils.transform_icrs_to_app( time_array=default_args["time_array"], ra=default_args["icrs_ra"], @@ -1408,7 +1481,6 @@ def test_transform_icrs_to_app_arg_errs(astrometry_args, arg_dict, msg): epoch=default_args["epoch"], astrometry_library=default_args["library"], ) - assert str(cm.value).startswith(msg) @pytest.mark.parametrize( @@ -1428,7 +1500,7 @@ def test_transform_app_to_icrs_arg_errs(astrometry_args, arg_dict, msg): for key in arg_dict.keys(): default_args[key] = arg_dict[key] - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils.transform_app_to_icrs( time_array=default_args["time_array"], app_ra=default_args["app_ra"], @@ -1437,7 +1509,6 @@ def test_transform_app_to_icrs_arg_errs(astrometry_args, arg_dict, msg): telescope_frame=default_args["telescope_frame"], astrometry_library=default_args["library"], ) - assert str(cm.value).startswith(msg) def test_transform_sidereal_coords_arg_errs(): @@ -1445,7 +1516,7 @@ def test_transform_sidereal_coords_arg_errs(): Check for argument errors with transform_sidereal_coords """ # Next on to sidereal to sidereal - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="lon and lat must be the same shape."): uvutils.transform_sidereal_coords( longitude=[0.0], latitude=[0.0, 1.0], @@ -1454,9 +1525,8 @@ def test_transform_sidereal_coords_arg_errs(): in_coord_epoch="J2000.0", time_array=[0.0, 1.0, 2.0], ) - assert str(cm.value).startswith("lon and lat must be the same shape.") - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Shape of time_array must be either that of "): uvutils.transform_sidereal_coords( longitude=[0.0, 1.0], latitude=[0.0, 1.0], @@ -1466,7 +1536,6 @@ def test_transform_sidereal_coords_arg_errs(): out_coord_epoch=1984.0, time_array=[0.0, 1.0, 2.0], ) - assert str(cm.value).startswith("Shape of time_array must be either that of ") @pytest.mark.filterwarnings('ignore:ERFA function "d2dtf" yielded') @@ -1577,7 +1646,7 @@ def test_interpolate_ephem_arg_errs(bad_arg, msg): Check for argument errors with interpolate_ephem """ # Now moving on to the interpolation scheme - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match=msg): uvutils.interpolate_ephem( time_array=0.0, ephem_times=0.0 if ("etimes" == bad_arg) else [0.0, 1.0], @@ -1586,7 +1655,6 @@ def test_interpolate_ephem_arg_errs(bad_arg, msg): ephem_dist=0.0 if ("dist" == bad_arg) else [0.0, 1.0], ephem_vel=0.0 if ("vel" == bad_arg) else [0.0, 1.0], ) - assert str(cm.value).startswith(msg) def test_calc_app_coords_arg_errs(): @@ -1594,11 +1662,10 @@ def test_calc_app_coords_arg_errs(): Check for argument errors with calc_app_coords """ # Now on to app_coords - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Object type whoknows is not recognized."): uvutils.calc_app_coords( lon_coord=0.0, lat_coord=0.0, telescope_loc=(0, 1, 2), coord_type="whoknows" ) - assert str(cm.value).startswith("Object type whoknows is not recognized.") def test_transform_multi_sidereal_coords(astrometry_args): @@ -1802,7 +1869,7 @@ def test_calc_parallactic_angle(): telescope_lat=1.0, ) # Make sure things agree to better than ~0.1 uas (as it definitely should) - assert np.allclose(expected_vals, meas_vals, 0.0, 1e-12) + np.testing.assert_allclose(expected_vals, meas_vals, 0.0, 1e-12) def test_calc_frame_pos_angle(): @@ -1874,10 +1941,10 @@ def test_jphl_lookup(): pytest.skip("SSL/Connection error w/ JPL Horizons: " + str(err)) assert np.all(np.equal(ephem_times, 2456789.0)) - assert np.allclose(ephem_ra, 0.8393066751804976) - assert np.allclose(ephem_dec, 0.3120687480116649) - assert np.allclose(ephem_dist, 1.00996185750717) - assert np.allclose(ephem_vel, 0.386914) + np.testing.assert_allclose(ephem_ra, 0.8393066751804976) + np.testing.assert_allclose(ephem_dec, 0.3120687480116649) + np.testing.assert_allclose(ephem_dist, 1.00996185750717) + np.testing.assert_allclose(ephem_vel, 0.386914) def test_ephem_interp_one_point(): @@ -1951,14 +2018,14 @@ def test_ephem_interp_multi_point(): ) # Make sure that everything is consistent to floating point precision - assert np.allclose(ra_vals1, ra_vals2, 1e-15, 0.0) - assert np.allclose(dec_vals1, dec_vals2, 1e-15, 0.0) - assert np.allclose(dist_vals1, dist_vals2, 1e-15, 0.0) - assert np.allclose(vel_vals1, vel_vals2, 1e-15, 0.0) - assert np.allclose(time_array + 1.0, ra_vals2, 1e-15, 0.0) - assert np.allclose(time_array + 2.0, dec_vals2, 1e-15, 0.0) - assert np.allclose(time_array + 3.0, dist_vals2, 1e-15, 0.0) - assert np.allclose(time_array + 4.0, vel_vals2, 1e-15, 0.0) + np.testing.assert_allclose(ra_vals1, ra_vals2, 1e-15, 0.0) + np.testing.assert_allclose(dec_vals1, dec_vals2, 1e-15, 0.0) + np.testing.assert_allclose(dist_vals1, dist_vals2, 1e-15, 0.0) + np.testing.assert_allclose(vel_vals1, vel_vals2, 1e-15, 0.0) + np.testing.assert_allclose(time_array + 1.0, ra_vals2, 1e-15, 0.0) + np.testing.assert_allclose(time_array + 2.0, dec_vals2, 1e-15, 0.0) + np.testing.assert_allclose(time_array + 3.0, dist_vals2, 1e-15, 0.0) + np.testing.assert_allclose(time_array + 4.0, vel_vals2, 1e-15, 0.0) @pytest.mark.parametrize("frame", ["icrs", "fk5"]) @@ -2591,7 +2658,8 @@ def test_get_lst_for_time_errors(astrometry_args): @pytest.mark.filterwarnings("ignore:The get_frame_attr_names") @pytest.mark.skipif(not hasmoon, reason="lunarsky not installed") -def test_lst_for_time_moon(astrometry_args): +@pytest.mark.parametrize("selenoid", selenoids) +def test_lst_for_time_moon(astrometry_args, selenoid): """Test the get_lst_for_time function with MCMF frame""" from lunarsky import SkyCoord as LSkyCoord @@ -2608,6 +2676,7 @@ def test_lst_for_time_moon(astrometry_args): longitude=lon, altitude=alt, frame="mcmf", + ellipsoid=selenoid, astrometry_library="novas", ) @@ -2617,15 +2686,18 @@ def test_lst_for_time_moon(astrometry_args): longitude=lon, altitude=alt, frame="mcmf", + ellipsoid=selenoid, ) # Verify that lsts are close to local zenith RA - loc = MoonLocation.from_selenodetic(lon, lat, alt) + loc = MoonLocation.from_selenodetic(lon, lat, alt, ellipsoid=selenoid) for ii, tt in enumerate( LTime(astrometry_args["time_array"], format="jd", scale="utc", location=loc) ): src = LSkyCoord(alt="90d", az="0d", frame="lunartopo", obstime=tt, location=loc) - assert np.isclose(lst_array[ii], src.transform_to("icrs").ra.rad, atol=1e-4) + # TODO: would be nice to get this down to uvutils.RADIAN_TOL + # seems like maybe the ellipsoid isn't being used properly? + assert np.isclose(lst_array[ii], src.transform_to("icrs").ra.rad, atol=1e-5) def test_phasing_funcs(): @@ -2686,15 +2758,15 @@ def test_phasing_funcs(): mwa_tools_calcuvw_v = 50.388281 mwa_tools_calcuvw_w = -151.27976 - assert np.allclose(gcrs_uvw[0, 0], mwa_tools_calcuvw_u, atol=1e-3) - assert np.allclose(gcrs_uvw[0, 1], mwa_tools_calcuvw_v, atol=1e-3) - assert np.allclose(gcrs_uvw[0, 2], mwa_tools_calcuvw_w, atol=1e-3) + np.testing.assert_allclose(gcrs_uvw[0, 0], mwa_tools_calcuvw_u, atol=1e-3) + np.testing.assert_allclose(gcrs_uvw[0, 1], mwa_tools_calcuvw_v, atol=1e-3) + np.testing.assert_allclose(gcrs_uvw[0, 2], mwa_tools_calcuvw_w, atol=1e-3) # also test unphasing temp2 = uvutils.undo_old_uvw_calc( gcrs_coord.ra.rad, gcrs_coord.dec.rad, np.squeeze(gcrs_uvw) ) - assert np.allclose(gcrs_rel.value, temp2) + np.testing.assert_allclose(gcrs_rel.value, np.squeeze(temp2)) def test_pol_funcs(): @@ -2872,11 +2944,10 @@ def test_conj_pol(): assert pytest.raises(KeyError, uvutils.conj_pol, cjstr) # Test invalid pol - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="Polarization not recognized, cannot be conjugated." + ): uvutils.conj_pol(2.3) - assert str(cm.value).startswith( - "Polarization not recognized, cannot be conjugated." - ) @pytest.mark.parametrize("grid_alg", [True, False, None]) @@ -2931,7 +3002,7 @@ def test_redundancy_finder(grid_alg): for bl in gp: bl_ind = np.where(uvd.baseline_array == bl) bl_vec = bl_positions[bl_ind] - assert np.allclose( + np.testing.assert_allclose( np.sqrt(np.dot(bl_vec, vec_bin_centers[gi])), lens[gi], atol=tol ) @@ -2974,7 +3045,7 @@ def test_redundancy_finder(grid_alg): for bl in gp: bl_ind = np.where(uvd.baseline_array == bl) bl_vec = bl_positions[bl_ind] - assert np.allclose( + np.testing.assert_allclose( np.sqrt(np.abs(np.dot(bl_vec, vec_bin_centers[gi]))), lens[gi], atol=tol_use, @@ -3471,15 +3542,19 @@ def test_mean_weights_and_weights_square(): out, wo, wso = uvutils.mean_collapse( data, weights=w, axis=0, return_weights=True, return_weights_square=True ) - assert np.allclose(out * wo, data.shape[0]) - assert np.allclose(wo, float(data.shape[0]) / (np.arange(data.shape[1]) + 1)) - assert np.allclose(wso, float(data.shape[0]) / (np.arange(data.shape[1]) + 1) ** 2) + np.testing.assert_allclose(out * wo, data.shape[0]) + np.testing.assert_allclose( + wo, float(data.shape[0]) / (np.arange(data.shape[1]) + 1) + ) + np.testing.assert_allclose( + wso, float(data.shape[0]) / (np.arange(data.shape[1]) + 1) ** 2 + ) out, wo, wso = uvutils.mean_collapse( data, weights=w, axis=1, return_weights=True, return_weights_square=True ) - assert np.allclose(out * wo, data.shape[1]) - assert np.allclose(wo, np.sum(1.0 / (np.arange(data.shape[1]) + 1))) - assert np.allclose(wso, np.sum(1.0 / (np.arange(data.shape[1]) + 1) ** 2)) + np.testing.assert_allclose(out * wo, data.shape[1]) + np.testing.assert_allclose(wo, np.sum(1.0 / (np.arange(data.shape[1]) + 1))) + np.testing.assert_allclose(wso, np.sum(1.0 / (np.arange(data.shape[1]) + 1) ** 2)) # Zero weights w = np.ones_like(data) @@ -4339,9 +4414,10 @@ def test_apply_uvflag(uvdata_future_shapes, uvflag_future_shapes): uvdf += uvdf2 uvdf = uvutils.apply_uvflag(uvdf, uvf, inplace=False, force_pol=True) assert np.all(uvdf.flag_array[uvdf.antpair2ind(9, 10)][:2]) - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="Input uvf and uvd polarizations do not match" + ): uvutils.apply_uvflag(uvdf, uvf, inplace=False, force_pol=False) - assert "Input uvf and uvd polarizations do not match" in str(cm.value) # test unflag first uvdf = uvutils.apply_uvflag(uvd, uvf, inplace=False, unflag_first=True) @@ -4359,9 +4435,8 @@ def test_apply_uvflag(uvdata_future_shapes, uvflag_future_shapes): # test mode exception uvfm = uvf.copy() uvfm.mode = "metric" - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="UVFlag must be flag mode"): uvutils.apply_uvflag(uvd, uvfm) - assert "UVFlag must be flag mode" in str(cm.value) # test polarization exception uvd2 = uvd.copy() @@ -4369,35 +4444,40 @@ def test_apply_uvflag(uvdata_future_shapes, uvflag_future_shapes): uvf2 = UVFlag(uvd) uvf2.to_flag() uvd2.polarization_array[0] = -8 - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="Input uvf and uvd polarizations do not match" + ): uvutils.apply_uvflag(uvd2, uvf2, force_pol=False) - assert "Input uvf and uvd polarizations do not match" in str(cm.value) # test time and frequency mismatch exceptions if uvflag_future_shapes: uvf2 = uvf.select(frequencies=uvf.freq_array[:2], inplace=False) else: uvf2 = uvf.select(frequencies=uvf.freq_array[:, :2], inplace=False) - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="UVFlag and UVData have mismatched frequency arrays" + ): uvutils.apply_uvflag(uvd, uvf2) - assert "UVFlag and UVData have mismatched frequency arrays" in str(cm.value) uvf2 = uvf.copy() uvf2.freq_array += 1.0 - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="UVFlag and UVData have mismatched frequency arrays" + ): uvutils.apply_uvflag(uvd, uvf2) - assert "UVFlag and UVData have mismatched frequency arrays" in str(cm.value) uvf2 = uvf.select(times=np.unique(uvf.time_array)[:2], inplace=False) - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="UVFlag and UVData have mismatched time arrays" + ): uvutils.apply_uvflag(uvd, uvf2) - assert "UVFlag and UVData have mismatched time arrays" in str(cm.value) uvf2 = uvf.copy() uvf2.time_array += 1.0 - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="UVFlag and UVData have mismatched time arrays" + ): uvutils.apply_uvflag(uvd, uvf2) - assert "UVFlag and UVData have mismatched time arrays" in str(cm.value) # assert implicit broadcasting works if uvflag_future_shapes: @@ -4651,7 +4731,7 @@ def test_antnums_to_baseline_miriad_convention(): bl = uvutils.antnums_to_baseline( ant1, ant2, Nants_telescope=n_ant, use_miriad_convention=True ) - assert np.allclose(bl, bl_gold) + np.testing.assert_allclose(bl, bl_gold) def test_determine_rect_time_first(): @@ -4723,8 +4803,8 @@ def test_calc_app_coords_time_obj(): telescope_loc=telescope_location, ) - assert np.allclose(app_ra_to, app_ra_nto) - assert np.allclose(app_dec_to, app_dec_nto) + np.testing.assert_allclose(app_ra_to, app_ra_nto) + np.testing.assert_allclose(app_dec_to, app_dec_nto) @pytest.mark.skipif(hasmoon, reason="lunarsky installed") @@ -4813,7 +4893,7 @@ def test_uvw_track_generator_moon(selenoid): pytest.skip("SpiceUNKNOWNFRAME error: " + str(err)) # Check that the total lengths all match 1 - assert np.allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) + np.testing.assert_allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) if selenoid == "SPHERE": # check defaults @@ -4828,7 +4908,7 @@ def test_uvw_track_generator_moon(selenoid): ) # Check that the total lengths all match 1 - assert np.allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) + np.testing.assert_allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) @pytest.mark.parametrize("err_state", ["err", "warn", "none"]) diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 2907f6b74a..829fc3ecb8 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -4719,9 +4719,6 @@ def check_lsts_against_times( If the `lst_array` does not match the calculated LSTs to the lst_tols. """ - if frame == "mcmf" and ellipsoid is None: - ellipsoid = "SPHERE" - # Don't worry about passing the astrometry library because we test that they agree # to better than our standard lst tolerances. lsts = get_lst_for_time( From 8ef3bada22f6ff72b7182c60b4bdce735abe1acb Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 26 Apr 2024 16:57:19 -0700 Subject: [PATCH 33/59] add hdf5_utils coverage --- pyuvdata/uvflag/tests/test_uvflag.py | 187 ++++++++++++++++----------- 1 file changed, 115 insertions(+), 72 deletions(-) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 978bc90dd3..154b87a3ae 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -6,6 +6,7 @@ import os import pathlib import re +import shutil import warnings import h5py @@ -14,10 +15,10 @@ from _pytest.outcomes import Skipped import pyuvdata.tests as uvtest -from pyuvdata import UVCal, UVData, UVFlag, __version__ +from pyuvdata import UVCal, UVData, UVFlag, __version__, hdf5_utils from pyuvdata import utils as uvutils from pyuvdata.data import DATA_PATH -from pyuvdata.tests.test_utils import frame_selenoid +from pyuvdata.tests.test_utils import frame_selenoid, hasmoon from pyuvdata.uvflag.uvflag import _future_array_shapes_warning from ...uvbase import old_telescope_metadata_attrs @@ -346,15 +347,13 @@ def test_check_flex_spw_id_array(uvf_from_data): @pytest.mark.filterwarnings("ignore:telescope_location, antenna_positions") def test_init_bad_mode(uvdata_obj): uv = uvdata_obj - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Input mode must be within acceptable"): UVFlag(uv, mode="bad_mode", history="I made a UVFlag object", label="test") - assert str(cm.value).startswith("Input mode must be within acceptable") uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Input mode must be within acceptable"): UVFlag(uv, mode="bad_mode", history="I made a UVFlag object", label="test") - assert str(cm.value).startswith("Input mode must be within acceptable") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @@ -727,21 +726,22 @@ def test_init_waterfall_flag_uvdata(uvdata_obj): def test_init_waterfall_copy_flags(uvdata_obj): uv = UVCal() uv.read_calfits(test_c_file, use_future_array_shapes=True) - with pytest.raises(NotImplementedError) as cm: + with pytest.raises( + NotImplementedError, match="Cannot copy flags when initializing" + ): UVFlag(uv, copy_flags=True, mode="flag", waterfall=True) - assert str(cm.value).startswith("Cannot copy flags when initializing") uv = uvdata_obj - with pytest.raises(NotImplementedError) as cm: + with pytest.raises( + NotImplementedError, match="Cannot copy flags when initializing" + ): UVFlag(uv, copy_flags=True, mode="flag", waterfall=True) - assert str(cm.value).startswith("Cannot copy flags when initializing") def test_init_invalid_input(): # input is not UVData, UVCal, path, or list/tuple - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="input to UVFlag.__init__ must be one of:"): UVFlag(14) - assert str(cm.value).startswith("input to UVFlag.__init__ must be one of:") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @@ -826,21 +826,67 @@ def test_init_list_files_weights(read_future_shapes, write_future_shapes, tmpdir @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") def test_init_posix(): - # Test that weights are preserved when reading list of files testfile_posix = pathlib.Path(test_f_file) uvf1 = UVFlag(test_f_file, use_future_array_shapes=True) uvf2 = UVFlag(testfile_posix, use_future_array_shapes=True) assert uvf1 == uvf2 +@pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") +def test_hdf5_meta_telescope_location(test_outfile): + meta = hdf5_utils.HDF5Meta(test_f_file) + lat, lon, alt = meta.telescope_location_lat_lon_alt + + assert np.isclose(lat, meta.telescope_location_obj.lat.rad) + assert np.isclose(lon, meta.telescope_location_obj.lon.rad) + assert np.isclose(alt, meta.telescope_location_obj.height.to("m").value) + + lat_deg, lon_deg, alt = meta.telescope_location_lat_lon_alt_degrees + assert np.isclose(lat_deg, meta.telescope_location_obj.lat.deg) + assert np.isclose(lon_deg, meta.telescope_location_obj.lon.deg) + assert np.isclose(alt, meta.telescope_location_obj.height.to("m").value) + + if hasmoon: + from lunarsky import MoonLocation + + shutil.copyfile(test_f_file, test_outfile) + with h5py.File(test_outfile, "r+") as h5f: + h5f["Header/telescope_frame"] = "mcmf" + meta = hdf5_utils.HDF5Meta(test_outfile) + assert meta.telescope_frame == "mcmf" + assert isinstance(meta.telescope_location_obj, MoonLocation) + + +@pytest.mark.skipif(hasmoon, reason="Test only when lunarsky not installed.") +def test_hdf5_meta_no_moon(test_outfile, uvf_from_data): + """Check errors when calling HDF5Meta with MCMF without lunarsky.""" + shutil.copyfile(test_f_file, test_outfile) + with h5py.File(test_outfile, "r+") as h5f: + h5f["Header/telescope_frame"] = "mcmf" + + meta = hdf5_utils.HDF5Meta(test_outfile) + msg = "Need to install `lunarsky` package to work with MCMF frame." + with pytest.raises(ValueError, match=msg): + meta.telescope_location_obj + del meta + + uvf_from_data.write(test_outfile, clobber=True) + with h5py.File(test_outfile, "r+") as h5f: + del h5f["/Header/telescope_frame"] + h5f["Header/telescope_frame"] = "mcmf" + + meta = hdf5_utils.HDF5Meta(test_outfile) + with pytest.raises(ValueError, match=msg): + meta.telescope_location_obj + + @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_data_like_property_mode_tamper(uvdata_obj): uv = uvdata_obj uvf = UVFlag(uv, label="test", use_future_array_shapes=True) uvf.mode = "test" - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Invalid mode. Mode must be one of"): list(uvf.data_like_parameters) - assert str(cm.value).startswith("Invalid mode. Mode must be one of") @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @@ -900,6 +946,13 @@ def test_read_write_loop_spw(uvdata_obj, test_outfile, telescope_frame, selenoid assert uvf.__eq__(uvf2, check_history=True) assert uvf2.filename == [os.path.basename(test_outfile)] + meta = hdf5_utils.HDF5Meta(test_outfile) + loc_obj = meta.telescope_location_obj + + assert np.isclose(loc_obj.x, uvf.telescope.location.x) + assert np.isclose(loc_obj.y, uvf.telescope.location.y) + assert np.isclose(loc_obj.z, uvf.telescope.location.z) + @pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning) @pytest.mark.parametrize("future_shapes", [True, False]) @@ -2294,9 +2347,8 @@ def test_collapse_pol_add_pol_axis(): uvf.__add__(uvf2, inplace=True, axis="pol") # Concatenate to form multi-pol object uvf2 = uvf.copy() uvf2.collapse_pol() - with pytest.raises(NotImplementedError) as cm: + with pytest.raises(NotImplementedError, match="Two UVFlag objects with their"): uvf2.__add__(uvf2, axis="pol") - assert str(cm.value).startswith("Two UVFlag objects with their") @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") @@ -3071,9 +3123,8 @@ def test_or_error(): uvf = UVFlag(test_f_file, use_future_array_shapes=True) uvf2 = uvf.copy() uvf.to_flag() - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match='UVFlag object must be in "flag" mode'): uvf.__or__(uvf2) - assert str(cm.value).startswith('UVFlag object must be in "flag" mode') @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") @@ -3152,9 +3203,8 @@ def test_flag_to_flag(): def test_to_flag_unknown_mode(): uvf = UVFlag(test_f_file, use_future_array_shapes=True) uvf.mode = "foo" - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Unknown UVFlag mode: foo"): uvf.to_flag() - assert str(cm.value).startswith("Unknown UVFlag mode: foo") @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") @@ -3248,9 +3298,8 @@ def test_metric_to_metric(): def test_to_metric_unknown_mode(): uvf = UVFlag(test_f_file, use_future_array_shapes=True) uvf.mode = "foo" - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Unknown UVFlag mode: foo"): uvf.to_metric() - assert str(cm.value).startswith("Unknown UVFlag mode: foo") @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") @@ -3265,14 +3314,12 @@ def test_antpair2ind(): def test_antpair2ind_nonbaseline(): uvf = UVFlag(test_f_file, use_future_array_shapes=True) uvf.to_waterfall() - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match=f"UVFlag object of type {uvf.type} does not contain antenna pairs " + "to index.", + ): uvf.antpair2ind(0, 3) - assert str(cm.value).startswith( - "UVFlag object of type " - + uvf.type - + " does not contain antenna " - + "pairs to index." - ) @pytest.mark.filterwarnings("ignore:The lst_array is not self-consistent") @@ -3332,9 +3379,10 @@ def test_combine_metrics_not_inplace(uvcal_obj): def test_combine_metrics_not_uvflag(uvcal_obj): uvc = uvcal_obj uvf = UVFlag(uvc, use_future_array_shapes=True) - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match='"others" must be UVFlag or list of UVFlag objects' + ): uvf.combine_metrics("bubblegum") - assert str(cm.value).startswith('"others" must be UVFlag or list of UVFlag objects') def test_combine_metrics_not_metric(uvcal_obj): @@ -3344,9 +3392,10 @@ def test_combine_metrics_not_metric(uvcal_obj): uvf.metric_array = np.random.normal(size=uvf.metric_array.shape) uvf2 = uvf.copy() uvf2.to_flag() - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match='UVFlag object and "others" must be in "metric"' + ): uvf.combine_metrics(uvf2) - assert str(cm.value).startswith('UVFlag object and "others" must be in "metric"') def test_combine_metrics_wrong_shape(uvcal_obj): @@ -3356,9 +3405,8 @@ def test_combine_metrics_wrong_shape(uvcal_obj): uvf.metric_array = np.random.normal(size=uvf.metric_array.shape) uvf2 = uvf.copy() uvf2.to_waterfall() - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="UVFlag metric array shapes do not match."): uvf.combine_metrics(uvf2) - assert str(cm.value).startswith("UVFlag metric array shapes do not match.") def test_combine_metrics_add_version_str(uvcal_obj): @@ -3458,17 +3506,18 @@ def test_flags2waterfall_uvcal(uvcal_obj, future_shapes): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_flags2waterfall_errors(uvdata_obj): # First argument must be UVData or UVCal object - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match=re.escape( + "flags2waterfall() requires a UVData or UVCal object as the first argument." + ), + ): flags2waterfall(5) - assert str(cm.value).startswith( - "flags2waterfall() requires a UVData or " + "UVCal object" - ) uv = uvdata_obj # Flag array must have same shape as uv.flag_array - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Flag array must align with UVData or UVCal"): flags2waterfall(uv, flag_array=np.array([4, 5])) - assert str(cm.value).startswith("Flag array must align with UVData or UVCal") def test_and_rows_cols(): @@ -3487,13 +3536,13 @@ def test_and_rows_cols(): def test_select_waterfall_errors(uvf_from_waterfall): uvf = uvf_from_waterfall - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="Cannot select on antenna_nums with waterfall" + ): uvf.select(antenna_nums=[0, 1, 2]) - assert str(cm.value).startswith("Cannot select on antenna_nums with waterfall") - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Cannot select on bls with waterfall"): uvf.select(bls=[(0, 1), (0, 2)]) - assert str(cm.value).startswith("Cannot select on bls with waterfall") @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @@ -3649,9 +3698,8 @@ def test_select_antenna_nums_error(input_uvf, uvf_mode): # used to set the mode depending on which input is given to uvf_mode getattr(uvf, uvf_mode)() # also test for error if antenna numbers not present in data - with pytest.raises(ValueError) as cm: + with pytest.raises(ValueError, match="Antenna number 708 is not present"): uvf.select(antenna_nums=[708, 709, 710]) - assert str(cm.value).startswith("Antenna number 708 is not present") def sort_bl(p): @@ -3671,13 +3719,12 @@ def test_select_bls(input_uvf, uvf_mode): np.random.seed(0) if uvf.type != "baseline": - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match='Only "baseline" mode UVFlag objects may select along the ' + "baseline axis", + ): uvf.select(bls=[(0, 1)]) - assert str(cm.value).startswith( - 'Only "baseline" mode UVFlag ' - "objects may select along the " - "baseline axis" - ) else: old_history = copy.deepcopy(uvf.history) bls_select = np.random.choice( @@ -3805,13 +3852,12 @@ def test_select_bls_errors(input_uvf, uvf_mode, select_kwargs, err_msg): getattr(uvf, uvf_mode)() np.random.seed(0) if uvf.type != "baseline": - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, + match='Only "baseline" mode UVFlag objects may select along the ' + "baseline axis", + ): uvf.select(bls=[(0, 1)]) - assert str(cm.value).startswith( - 'Only "baseline" mode UVFlag ' - "objects may select along the " - "baseline axis" - ) else: if select_kwargs["bls"] == (97, 97): uvf.select(bls=[(97, 104), (97, 105), (88, 97)]) @@ -3875,11 +3921,10 @@ def test_select_times(input_uvf, uvf_mode, future_shapes): ) # check for errors associated with times not included in data bad_time = [np.min(unique_times) - 0.005] - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match=f"Time {bad_time[0]} is not present in the time_array" + ): uvf.select(times=bad_time) - assert str(cm.value).startswith( - "Time {t} is not present in" " the time_array".format(t=bad_time[0]) - ) @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @@ -3945,11 +3990,10 @@ def test_select_frequencies(input_uvf, uvf_mode, future_shapes): # check for errors associated with frequencies not included in data bad_freq = [np.max(uvf.freq_array) + 100] - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match=f"Frequency {bad_freq[0]} is not present in the freq_array" + ): uvf.select(frequencies=bad_freq) - assert str(cm.value).startswith( - "Frequency {f} is not present in the freq_array".format(f=bad_freq[0]) - ) @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @@ -4063,11 +4107,10 @@ def test_select_polarizations(uvf_mode, pols_to_keep, input_uvf, future_shapes): ) # check for errors associated with polarizations not included in data - with pytest.raises(ValueError) as cm: + with pytest.raises( + ValueError, match="Polarization -3 is not present in the polarization_array" + ): uvf2.select(polarizations=[-3]) - assert str(cm.value).startswith( - "Polarization {p} is not present in the polarization_array".format(p=-3) - ) @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") From e197ef16ed387b34bd10b1402b82733ee3d64177 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 29 Apr 2024 17:18:10 -0700 Subject: [PATCH 34/59] more test coverage --- pyuvdata/ms_utils.py | 16 ++++----- pyuvdata/telescopes.py | 7 ++++ pyuvdata/tests/test_parameter.py | 15 +++++++- pyuvdata/uvcal/tests/test_ms_cal.py | 2 +- pyuvdata/uvdata/tests/test_ms.py | 53 ++++++++++++++++++++++++++-- pyuvdata/uvdata/tests/test_uvfits.py | 49 ++++++++++++++++++++++++- pyuvdata/uvdata/uvfits.py | 34 +++++++----------- pyuvdata/uvflag/tests/test_uvflag.py | 22 ++++++------ 8 files changed, 150 insertions(+), 48 deletions(-) diff --git a/pyuvdata/ms_utils.py b/pyuvdata/ms_utils.py index 0d914eb07f..aa5967d396 100644 --- a/pyuvdata/ms_utils.py +++ b/pyuvdata/ms_utils.py @@ -2137,20 +2137,16 @@ def get_ms_telescope_location(*, tb_ant_dict, obs_dict): ) return telescope_loc else: - if xyz_telescope_frame not in ["itrs", "mcmf"]: - raise ValueError( - f"Telescope frame in file is {xyz_telescope_frame}. " - "Only 'itrs' and 'mcmf' are currently supported." - ) if xyz_telescope_frame == "mcmf": if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with " "MCMF frames." + # There is a test for this, but it is always skipped with our + # current CI setup because it requires that python-casacore is + # installed but lunarsky isn't. Doesn't seem worth setting up a + # whole separate CI for this. + raise ValueError( # pragma: no cover + "Need to install `lunarsky` package to work with MCMF frames." ) - if xyz_telescope_ellipsoid is None: - xyz_telescope_ellipsoid = "SPHERE" - if "telescope_location" in obs_dict: if xyz_telescope_frame == "itrs": return EarthLocation.from_geocentric( diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 30df89f95b..e3dfe82685 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -415,6 +415,13 @@ def update_params_from_known_telescopes( This should only be used for testing. This allows passing in a different dict to use in place of the KNOWN_TELESCOPES dict. + Raises + ------ + ValueError + If self.name is not set or if ((location is missing or overwrite is + set) and self.name is not found either astropy sites our our + known_telescopes dict) + """ if self.name is None: raise ValueError( diff --git a/pyuvdata/tests/test_parameter.py b/pyuvdata/tests/test_parameter.py index 478f637d00..ab01439015 100644 --- a/pyuvdata/tests/test_parameter.py +++ b/pyuvdata/tests/test_parameter.py @@ -108,7 +108,7 @@ def test_quantity_equality_error(): @pytest.mark.parametrize( - "vals,p2_atol", + ["vals", "p2_atol"], ( (np.array([0, 2, 4]) * units.m, 1 * units.mm), (np.array([0, 1, 3]) * units.mm, 1 * units.mm), @@ -132,6 +132,19 @@ def test_quantity_inequality(vals, p2_atol): assert param1.__ne__(param2, silent=False) +def test_quantity_array_inequality(capsys): + param1 = uvp.UVParameter( + name="p1", value=np.array([0.0, 1.0, 3.0]) * units.m, tols=1 * units.mm + ) + param2 = uvp.UVParameter(name="p2", value=np.array([0.0, 1.0, 3.0]), tols=1.0) + assert param1.__ne__(param2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is a Quantity, but other is not." + ) + + def test_quantity_equality_nans(): """Test array equality with nans present.""" param1 = uvp.UVParameter(name="p1", value=np.array([0, 1, np.nan] * units.m)) diff --git a/pyuvdata/uvcal/tests/test_ms_cal.py b/pyuvdata/uvcal/tests/test_ms_cal.py index 989ef58a4a..8146558736 100644 --- a/pyuvdata/uvcal/tests/test_ms_cal.py +++ b/pyuvdata/uvcal/tests/test_ms_cal.py @@ -26,7 +26,7 @@ sma_warnings = [ - "Unknown polarization basis for solutions, jones_array values may " "be spurious.", + "Unknown polarization basis for solutions, jones_array values may be spurious.", "Unknown x_orientation basis for solutions, assuming", "key CASA_Version in extra_keywords is longer than 8 characters. " "It will be truncated to 8 if written to a calfits file format.", diff --git a/pyuvdata/uvdata/tests/test_ms.py b/pyuvdata/uvdata/tests/test_ms.py index 8b27c62fbe..07f4c6aead 100644 --- a/pyuvdata/uvdata/tests/test_ms.py +++ b/pyuvdata/uvdata/tests/test_ms.py @@ -16,7 +16,7 @@ from ... import tests as uvtest from ... import utils as uvutils from ...data import DATA_PATH -from ...tests.test_utils import frame_selenoid +from ...tests.test_utils import frame_selenoid, hasmoon from ..uvdata import _future_array_shapes_warning pytest.importorskip("casacore") @@ -117,7 +117,8 @@ def test_cotter_ms(): @pytest.mark.filterwarnings("ignore:ITRF coordinate frame detected,") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") @pytest.mark.parametrize(["telescope_frame", "selenoid"], frame_selenoid) -def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): +@pytest.mark.parametrize("del_tel_loc", [True, False]) +def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid, del_tel_loc): """Test reading in a CASA tutorial ms file and looping it through write_ms.""" uvobj = nrao_uv @@ -158,6 +159,30 @@ def test_read_nrao_loopback(tmp_path, nrao_uv, telescope_frame, selenoid): ): uvobj.write_ms(testfile) + # check handling of default ellipsoid: remove the ellipsoid and check that + # it is properly defaulted to SPHERE + if telescope_frame == "mcmf" and selenoid == "SPHERE": + from casacore import tables + + tb_ant = tables.table( + os.path.join(testfile, "ANTENNA"), ack=False, readonly=False + ) + meas_info_dict = tb_ant.getcolkeyword("POSITION", "MEASINFO") + del meas_info_dict["RefEllipsoid"] + tb_ant.putcolkeyword("POSITION", "MEASINFO", meas_info_dict) + tb_ant.close() + + if del_tel_loc: + # This doesn't lead to test errors because the original data set didn't + # have a location, so we were already using the center of the antenna positions + from casacore import tables + + tb_obs = tables.table( + os.path.join(testfile, "OBSERVATION"), ack=False, readonly=False + ) + tb_obs.removecols("TELESCOPE_LOCATION") + tb_obs.close() + uvobj2 = UVData() uvobj2.read_ms(testfile, use_future_array_shapes=True) @@ -900,6 +925,30 @@ def test_ms_reader_errs(sma_mir, tmp_path, badcol, badval, errtype, msg): assert ms_uv._time_array == sma_mir._time_array +@pytest.mark.skipif(hasmoon, reason="Test only when lunarsky not installed.") +def test_ms_no_moon(sma_mir, tmp_path): + """Check errors when calling read_ms with MCMF without lunarsky.""" + from casacore import tables + + ms_uv = UVData() + testfile = os.path.join(tmp_path, "out_ms_reader_errs.ms") + sma_mir.write_ms(testfile) + + tb_obs = tables.table( + os.path.join(testfile, "OBSERVATION"), ack=False, readonly=False + ) + tb_obs.removecols("TELESCOPE_LOCATION") + tb_obs.putcol("TELESCOPE_NAME", "ABC") + tb_obs.close() + tb_ant = tables.table(os.path.join(testfile, "ANTENNA"), ack=False, readonly=False) + tb_ant.putcolkeyword("POSITION", "MEASINFO", {"type": "position", "Ref": "MCMF"}) + tb_ant.close() + + msg = "Need to install `lunarsky` package to work with MCMF frame." + with pytest.raises(ValueError, match=msg): + ms_uv.read(testfile, data_column="DATA", file_type="ms") + + def test_antenna_diameter_handling(hera_uvh5, tmp_path): uv_obj = hera_uvh5 diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index 7b732560e2..c378ff1ad9 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -16,7 +16,7 @@ import pyuvdata.utils as uvutils from pyuvdata import UVData from pyuvdata.data import DATA_PATH -from pyuvdata.tests.test_utils import frame_selenoid +from pyuvdata.tests.test_utils import frame_selenoid, hasmoon from pyuvdata.uvdata.uvdata import _future_array_shapes_warning casa_tutorial_uvfits = os.path.join( @@ -551,6 +551,24 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se write_file = str(tmp_path / "outtest_casa.uvfits") uv_in.write_uvfits(write_file) + + # check handling of default ellipsoid: remove the ellipsoid and check that + # it is properly defaulted to SPHERE + if telescope_frame == "mcmf" and selenoid == "SPHERE": + with fits.open(write_file, memmap=True) as hdu_list: + hdunames = uvutils._fits_indexhdus(hdu_list) + ant_hdu = hdu_list[hdunames["AIPS AN"]] + ant_hdr = ant_hdu.header.copy() + + del ant_hdr["ELLIPSOI"] + ant_hdu.header = ant_hdr + + vis_hdu = hdu_list[0] + source_hdu = hdu_list[hdunames["AIPS SU"]] + hdulist = fits.HDUList(hdus=[vis_hdu, ant_hdu, source_hdu]) + hdulist.writeto(write_file, overwrite=True) + hdulist.close() + uv_out.read(write_file, use_future_array_shapes=future_shapes) # make sure filenames are what we expect @@ -914,6 +932,35 @@ def test_readwriteread_error_single_time(tmp_path, casa_uvfits): return +@pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.skipif(hasmoon, reason="Test only when lunarsky not installed.") +def test_uvfits_no_moon(casa_uvfits, tmp_path): + """Check errors when reading uvfits with MCMF without lunarsky.""" + uv_in = casa_uvfits + write_file = str(tmp_path / "outtest_casa.uvfits") + + uv_in.write_uvfits(write_file) + + uv_out = UVData() + with fits.open(write_file, memmap=True) as hdu_list: + hdunames = uvutils._fits_indexhdus(hdu_list) + ant_hdu = hdu_list[hdunames["AIPS AN"]] + ant_hdr = ant_hdu.header.copy() + + ant_hdr["FRAME"] = "mcmf" + ant_hdu.header = ant_hdr + + vis_hdu = hdu_list[0] + source_hdu = hdu_list[hdunames["AIPS SU"]] + hdulist = fits.HDUList(hdus=[vis_hdu, ant_hdu, source_hdu]) + hdulist.writeto(write_file, overwrite=True) + hdulist.close() + + msg = "Need to install `lunarsky` package to work with MCMF frame." + with pytest.raises(ValueError, match=msg): + uv_out.read(write_file) + + @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") def test_readwriteread_unflagged_data_warnings(tmp_path, casa_uvfits): uv_in = casa_uvfits diff --git a/pyuvdata/uvdata/uvfits.py b/pyuvdata/uvdata/uvfits.py index 998c14eba7..6e0db20ecb 100644 --- a/pyuvdata/uvdata/uvfits.py +++ b/pyuvdata/uvdata/uvfits.py @@ -647,19 +647,12 @@ def read_uvfits( and longitude_degrees is not None and altitude is not None ): - if telescope_frame == "itrs": - self.telescope.location = EarthLocation.from_geodetic( - lat=latitude_degrees * units.deg, - lon=longitude_degrees * units.deg, - height=altitude * units.m, - ) - else: - self.telescope.location = MoonLocation.from_selenodetic( - lat=latitude_degrees * units.deg, - lon=longitude_degrees * units.deg, - height=altitude * units.m, - ellipsoid=self.ellipsoid, - ) + # only get here if the frame is set to "????", default to ITRS + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude_degrees * units.deg, + lon=longitude_degrees * units.deg, + height=altitude * units.m, + ) else: if telescope_frame == "itrs": self.telescope.location = EarthLocation.from_geocentric( @@ -706,14 +699,13 @@ def read_uvfits( if "DIAMETER" in ant_hdu.columns.names: self.telescope.antenna_diameters = ant_hdu.data.field("DIAMETER") - try: - self.set_telescope_params( - run_check=run_check, - check_extra=check_extra, - run_check_acceptability=run_check_acceptability, - ) - except ValueError as ve: - warnings.warn(str(ve)) + # This will not error because uvfits required keywords ensure we + # have everything that is required for this method. + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) # Now read in the random parameter info self._get_parameter_data( diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 154b87a3ae..ab2881ff3a 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -3686,7 +3686,7 @@ def test_select_antenna_nums(input_uvf, uvf_mode, dimension, future_shapes): assert ant in ants_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific antennas using pyuvdata.", + old_history + " Downselected to specific antennas using pyuvdata.", uvf2.history, ) @@ -3766,7 +3766,7 @@ def test_select_bls(input_uvf, uvf_mode): assert pair in sorted_pairs_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific baselines using pyuvdata.", + old_history + " Downselected to specific baselines using pyuvdata.", uvf2.history, ) @@ -3901,8 +3901,7 @@ def test_select_times(input_uvf, uvf_mode, future_shapes): assert t in times_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific times using pyuvdata.", - uvf2.history, + old_history + " Downselected to specific times using pyuvdata.", uvf2.history ) # check that it also works with higher dimension array uvf2 = uvf.copy() @@ -3916,8 +3915,7 @@ def test_select_times(input_uvf, uvf_mode, future_shapes): assert t in times_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific times using pyuvdata.", - uvf2.history, + old_history + " Downselected to specific times using pyuvdata.", uvf2.history ) # check for errors associated with times not included in data bad_time = [np.min(unique_times) - 0.005] @@ -3956,7 +3954,7 @@ def test_select_frequencies(input_uvf, uvf_mode, future_shapes): assert f in freqs_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific frequencies using pyuvdata.", + old_history + " Downselected to specific frequencies using pyuvdata.", uvf2.history, ) @@ -3971,7 +3969,7 @@ def test_select_frequencies(input_uvf, uvf_mode, future_shapes): assert f in freqs_to_keep assert uvutils._check_histories( - old_history + " Downselected to " "specific frequencies using pyuvdata.", + old_history + " Downselected to specific frequencies using pyuvdata.", uvf2.history, ) @@ -3984,7 +3982,7 @@ def test_select_frequencies(input_uvf, uvf_mode, future_shapes): assert f in [freqs_to_keep[0]] assert uvutils._check_histories( - old_history + " Downselected to " "specific frequencies using pyuvdata.", + old_history + " Downselected to specific frequencies using pyuvdata.", uvf2.history, ) @@ -4022,7 +4020,7 @@ def test_select_freq_chans(input_uvf, uvf_mode): assert f in uvf.freq_array[chans_to_keep] assert uvutils._check_histories( - old_history + " Downselected to " "specific frequencies using pyuvdata.", + old_history + " Downselected to specific frequencies using pyuvdata.", uvf2.history, ) @@ -4038,7 +4036,7 @@ def test_select_freq_chans(input_uvf, uvf_mode): assert f in uvf.freq_array[chans_to_keep] assert uvutils._check_histories( - old_history + " Downselected to " "specific frequencies using pyuvdata.", + old_history + " Downselected to specific frequencies using pyuvdata.", uvf2.history, ) @@ -4102,7 +4100,7 @@ def test_select_polarizations(uvf_mode, pols_to_keep, input_uvf, future_shapes): ) assert uvutils._check_histories( - old_history + " Downselected to " "specific polarizations using pyuvdata.", + old_history + " Downselected to specific polarizations using pyuvdata.", uvf2.history, ) From abc707075c0d5b7018f17d946bd2b641d9d9e175 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 29 Apr 2024 17:31:08 -0700 Subject: [PATCH 35/59] fix min_versions test error --- pyuvdata/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 4933e7f65f..1ad3378f8d 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -1383,7 +1383,7 @@ def test_calc_uvw_enu_roundtrip(calc_uvw_args): ) np.testing.assert_allclose( - calc_uvw_args["uvw_array"], uvw_base_enu_check, atol=1e-16, rtol=0 + calc_uvw_args["uvw_array"], uvw_base_enu_check, atol=1e-15, rtol=0 ) From 40d2627921f9273eec5593dd872e06593a59251a Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 1 May 2024 10:21:20 -0700 Subject: [PATCH 36/59] fix windows test --- pyuvdata/uvdata/tests/test_uvfits.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index c378ff1ad9..194792732f 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -549,9 +549,10 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se uv_out = UVData() write_file = str(tmp_path / "outtest_casa.uvfits") + write_file2 = str(tmp_path / "outtest_casa2.uvfits") uv_in.write_uvfits(write_file) - + file_read = write_file # check handling of default ellipsoid: remove the ellipsoid and check that # it is properly defaulted to SPHERE if telescope_frame == "mcmf" and selenoid == "SPHERE": @@ -566,15 +567,11 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se vis_hdu = hdu_list[0] source_hdu = hdu_list[hdunames["AIPS SU"]] hdulist = fits.HDUList(hdus=[vis_hdu, ant_hdu, source_hdu]) - hdulist.writeto(write_file, overwrite=True) + hdulist.writeto(write_file2, overwrite=True) hdulist.close() + file_read = write_file2 - uv_out.read(write_file, use_future_array_shapes=future_shapes) - - # make sure filenames are what we expect - assert uv_in.filename == ["day2_TDEM0003_10s_norx_1src_1spw.uvfits"] - assert uv_out.filename == ["outtest_casa.uvfits"] - uv_in.filename = uv_out.filename + uv_out.read(file_read, use_future_array_shapes=future_shapes) assert uv_in.telescope._location.frame == uv_out.telescope._location.frame assert uv_in.telescope._location.ellipsoid == uv_out.telescope._location.ellipsoid @@ -938,6 +935,7 @@ def test_uvfits_no_moon(casa_uvfits, tmp_path): """Check errors when reading uvfits with MCMF without lunarsky.""" uv_in = casa_uvfits write_file = str(tmp_path / "outtest_casa.uvfits") + write_file2 = str(tmp_path / "outtest_casa2.uvfits") uv_in.write_uvfits(write_file) @@ -953,12 +951,12 @@ def test_uvfits_no_moon(casa_uvfits, tmp_path): vis_hdu = hdu_list[0] source_hdu = hdu_list[hdunames["AIPS SU"]] hdulist = fits.HDUList(hdus=[vis_hdu, ant_hdu, source_hdu]) - hdulist.writeto(write_file, overwrite=True) + hdulist.writeto(write_file2, overwrite=True) hdulist.close() msg = "Need to install `lunarsky` package to work with MCMF frame." with pytest.raises(ValueError, match=msg): - uv_out.read(write_file) + uv_out.read(write_file2) @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") From 877dc44b2746c6e26c783c2730d9d9f3b77c9c41 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Tue, 30 Apr 2024 14:51:33 -0700 Subject: [PATCH 37/59] more test coverage --- pyuvdata/tests/test_utils.py | 43 ++++++++- pyuvdata/uvcal/calh5.py | 7 +- pyuvdata/uvcal/tests/test_calfits.py | 51 +++++++++- pyuvdata/uvcal/tests/test_calh5.py | 9 +- pyuvdata/uvcal/tests/test_initializers.py | 107 +++++++++++---------- pyuvdata/uvdata/miriad.py | 32 +++--- pyuvdata/uvdata/tests/test_initializers.py | 37 +++++-- pyuvdata/uvdata/tests/test_miriad.py | 26 +++++ pyuvdata/uvdata/uvdata.py | 3 - 9 files changed, 236 insertions(+), 79 deletions(-) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 1ad3378f8d..740aa86668 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -262,6 +262,11 @@ def test_XYZ_from_LatLonAlt_mcmf(selenoid): ) np.testing.assert_allclose(ref_xyz_moon[selenoid], out_xyz, rtol=0, atol=1e-3) + # test default ellipsoid + if selenoid == "SPHERE": + out_xyz = uvutils.XYZ_from_LatLonAlt(lat, lon, alt, frame="mcmf") + np.testing.assert_allclose(ref_xyz_moon[selenoid], out_xyz, rtol=0, atol=1e-3) + # Test errors with invalid frame with pytest.raises( ValueError, match="No cartesian to spherical transform defined for frame" @@ -278,6 +283,11 @@ def test_LatLonAlt_from_XYZ_mcmf(selenoid): ) np.testing.assert_allclose(ref_latlonalt_moon, out_latlonalt, rtol=0, atol=1e-3) + # test default ellipsoid + if selenoid == "SPHERE": + out_latlonalt = uvutils.LatLonAlt_from_XYZ(ref_xyz_moon[selenoid], frame="mcmf") + np.testing.assert_allclose(ref_latlonalt_moon, out_latlonalt, rtol=0, atol=1e-3) + # Test errors with invalid frame with pytest.raises( ValueError, match="Cannot check acceptability for unknown frame" @@ -1920,7 +1930,7 @@ def test_calc_frame_pos_angle(): assert np.isclose(frame_pa[-25], -0.0019098101664715339) -def test_jphl_lookup(): +def test_jphl_lookup(astrometry_args): """ A very simple lookup query to verify that the astroquery tools for accessing JPL-Horizons are working. This test is very limited, on account of not wanting to @@ -1946,6 +1956,26 @@ def test_jphl_lookup(): np.testing.assert_allclose(ephem_dist, 1.00996185750717) np.testing.assert_allclose(ephem_vel, 0.386914) + # check calling lookup_jplhorizons with EarthLocation vs lat/lon/alt passed + try: + ephem_info_latlon = uvutils.lookup_jplhorizons( + "Sun", 2456789.0, telescope_loc=astrometry_args["telescope_loc"] + ) + ephem_info_el = uvutils.lookup_jplhorizons( + "Sun", + 2456789.0, + telescope_loc=EarthLocation.from_geodetic( + lat=astrometry_args["telescope_loc"][0] * units.rad, + lon=astrometry_args["telescope_loc"][1] * units.rad, + height=astrometry_args["telescope_loc"][2] * units.m, + ), + ) + except (SSLError, RequestException) as err: + pytest.skip("SSL/Connection error w/ JPL Horizons: " + str(err)) + + for ind, item in enumerate(ephem_info_latlon): + assert item == ephem_info_el[ind] + def test_ephem_interp_one_point(): """ @@ -2699,6 +2729,17 @@ def test_lst_for_time_moon(astrometry_args, selenoid): # seems like maybe the ellipsoid isn't being used properly? assert np.isclose(lst_array[ii], src.transform_to("icrs").ra.rad, atol=1e-5) + # test default ellipsoid + if selenoid == "SPHERE": + lst_array_default = uvutils.get_lst_for_time( + jd_array=astrometry_args["time_array"], + latitude=lat, + longitude=lon, + altitude=alt, + frame="mcmf", + ) + np.testing.assert_allclose(lst_array, lst_array_default) + def test_phasing_funcs(): # these tests are based on a notebook where I tested against the mwa_tools diff --git a/pyuvdata/uvcal/calh5.py b/pyuvdata/uvcal/calh5.py index 68749e7d03..bfe1f656b7 100644 --- a/pyuvdata/uvcal/calh5.py +++ b/pyuvdata/uvcal/calh5.py @@ -276,11 +276,8 @@ def _read_header( except AttributeError: pass - # set telescope params - try: - self.set_telescope_params() - except ValueError as ve: - warnings.warn(str(ve)) + # set any extra telescope params + self.set_telescope_params() # ensure LSTs are set before checking them. if proc is not None: diff --git a/pyuvdata/uvcal/tests/test_calfits.py b/pyuvdata/uvcal/tests/test_calfits.py index 4efd26dcde..2d926cf454 100644 --- a/pyuvdata/uvcal/tests/test_calfits.py +++ b/pyuvdata/uvcal/tests/test_calfits.py @@ -15,7 +15,7 @@ import pyuvdata.utils as uvutils from pyuvdata import UVCal from pyuvdata.data import DATA_PATH -from pyuvdata.tests.test_utils import selenoids +from pyuvdata.tests.test_utils import hasmoon, selenoids from pyuvdata.uvcal.tests import extend_jones_axis, time_array_to_time_range from pyuvdata.uvcal.uvcal import _future_array_shapes_warning @@ -136,6 +136,55 @@ def test_moon_loopback(tmp_path, gain_data, selenoid): assert cal_in == cal_out + # check in case xyz is missing + write_file2 = str(tmp_path / "outtest_noxyz.fits") + with fits.open(write_file) as fname: + data = fname[0].data + primary_hdr = fname[0].header + hdunames = uvutils._fits_indexhdus(fname) + ant_hdu = fname[hdunames["ANTENNAS"]] + + primary_hdr.pop("ARRAYX") + primary_hdr.pop("ARRAYY") + primary_hdr.pop("ARRAYZ") + + prihdu = fits.PrimaryHDU(data=data, header=primary_hdr) + hdulist = fits.HDUList([prihdu, ant_hdu]) + + hdulist.writeto(write_file2, overwrite=True) + + cal_out = UVCal.from_file(write_file2, use_future_array_shapes=True) + assert cal_out == cal_in + + +@pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.skipif(hasmoon, reason="Test only when lunarsky not installed.") +def test_calfits_no_moon(gain_data, tmp_path): + """Check errors when reading uvfits with MCMF without lunarsky.""" + write_file = str(tmp_path / "outtest.calfits") + write_file2 = str(tmp_path / "outtest2.calfits") + + gain_data.write_calfits(write_file) + + with fits.open(write_file, memmap=True) as fname: + data = fname[0].data + primary_hdr = fname[0].header + hdunames = uvutils._fits_indexhdus(fname) + ant_hdu = fname[hdunames["ANTENNAS"]] + + primary_hdr["FRAME"] = "mcmf" + primary_hdr.pop("ARRAYY") + primary_hdr.pop("ARRAYZ") + + prihdu = fits.PrimaryHDU(data=data, header=primary_hdr) + hdulist = fits.HDUList([prihdu, ant_hdu]) + + hdulist.writeto(write_file2, overwrite=True) + + msg = "Need to install `lunarsky` package to work with MCMF frame." + with pytest.raises(ValueError, match=msg): + UVCal.from_file(write_file2) + @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.parametrize("future_shapes", [True, False]) diff --git a/pyuvdata/uvcal/tests/test_calh5.py b/pyuvdata/uvcal/tests/test_calh5.py index 1a70eb71ca..43577aa3d1 100644 --- a/pyuvdata/uvcal/tests/test_calh5.py +++ b/pyuvdata/uvcal/tests/test_calh5.py @@ -238,7 +238,7 @@ def test_none_extra_keywords(gain_data, tmp_path): @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") -def test_write_calh5_errors(gain_data, tmp_path): +def test_read_write_calh5_errors(gain_data, tmp_path): """ Test raising errors in write_calh5 function. """ @@ -266,6 +266,13 @@ def test_write_calh5_errors(gain_data, tmp_path): assert cal_obj == cal_out # check error if missing required params + with h5py.File(testfile, "r+") as h5f: + del h5f["/Header/cal_type"] + + with pytest.raises(KeyError, match="cal_type not found in"): + cal_out.read(testfile) + + # check error if missing required telescope params with h5py.File(testfile, "r+") as h5f: del h5f["/Header/telescope_name"] diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index 58260bf7b5..e4495e0b0e 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -46,7 +46,7 @@ def uvc_only_kw(): @pytest.fixture(scope="function") -def uvc_simplest_no_telescope(uvc_only_kw): +def uvc_simplest_no_telescope(): return { "freq_array": np.linspace(100e6, 200e6, 10), "time_array": np.linspace(2459850, 2459851, 12), @@ -157,54 +157,52 @@ def test_new_uvcal_time_range(uvc_simplest): uvc = UVCal.new(**uvc_simplest) -def test_new_uvcal_bad_inputs(uvc_simplest): - with pytest.raises( - ValueError, match="The following ants are not in antenna_numbers" - ): - new_uvcal(ant_array=[0, 1, 2, 3], **uvc_simplest) - - with pytest.raises( - ValueError, - match=re.escape( - "If cal_style is 'sky', ref_antenna_name and sky_catalog must be provided." - ), - ): - new_uvcal( - cal_style="sky", - **{k: v for k, v in uvc_simplest.items() if k != "cal_style"} - ) - - with pytest.raises( - ValueError, match="cal_style must be 'redundant' or 'sky'\\, got" - ): - UVCal.new( - cal_style="wrong", - ref_antenna_name="mock", - sky_catalog="mock", - **{k: v for k, v in uvc_simplest.items() if k != "cal_style"} - ) - - with pytest.raises(ValueError, match="Unrecognized keyword argument"): - new_uvcal(bad_kwarg=True, **uvc_simplest) - - with pytest.raises( - ValueError, match=re.escape("Provide *either* freq_range *or* freq_array") - ): - new_uvcal(freq_range=[100e6, 200e6], **uvc_simplest) - - with pytest.raises(ValueError, match="You must provide either freq_array"): - new_uvcal(**{k: v for k, v in uvc_simplest.items() if k != "freq_array"}) - - with pytest.raises(ValueError, match="cal_type must be either 'gain' or 'delay'"): - new_uvcal( - cal_type="wrong", - freq_range=[150e6, 180e6], - **{ - k: v - for k, v in uvc_simplest.items() - if k not in ("freq_array", "cal_type") - } - ) +@pytest.mark.parametrize( + ["update_dict", "err_msg"], + [ + [{"ant_array": [0, 1, 2, 3]}, "The following ants are not in antenna_numbers"], + [ + {"cal_style": "sky"}, + "If cal_style is 'sky', ref_antenna_name and sky_catalog must be provided.", + ], + [ + {"cal_style": "wrong", "ref_antenna_name": "mock", "sky_catalog": "mock"}, + "cal_style must be 'redundant' or 'sky', got", + ], + [{"bad_kwarg": True}, "Unrecognized keyword argument"], + [ + {"freq_range": [100e6, 200e6]}, + re.escape("Provide *either* freq_range *or* freq_array"), + ], + [{"freq_array": None}, "You must provide either freq_array"], + [ + {"cal_type": "wrong", "freq_range": [150e6, 180e6], "freq_array": None}, + "cal_type must be either 'gain' or 'delay'", + ], + [ + {"telescope": None}, + "antenna_positions is required if telescope is not provided.", + ], + [ + { + "telescope": Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="mock", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ) + }, + "x_orientation must be set on the Telescope object passed to `telescope`.", + ], + ], +) +def test_new_uvcal_bad_inputs(uvc_simplest, update_dict, err_msg): + uvc_simplest.update(update_dict) + with pytest.raises(ValueError, match=err_msg): + new_uvcal(**uvc_simplest) def test_new_uvcal_jones_array(uvc_simplest): @@ -300,6 +298,17 @@ def test_new_uvcal_from_uvdata(uvd_kw, uvc_only_kw): assert np.all(uvc.telescope.antenna_diameters == uvd.telescope.antenna_diameters) +def test_new_uvcal_from_uvdata_errors(uvd_kw, uvc_only_kw): + uvd = new_uvdata(**uvd_kw) + + uvc_only_kw.pop("x_orientation") + with pytest.raises( + ValueError, + match=("x_orientation must be provided if it is not set on the UVData object."), + ): + new_uvcal_from_uvdata(uvd, **uvc_only_kw) + + def test_new_uvcal_set_freq_range_for_gain_type(uvd_kw, uvc_only_kw): uvd = new_uvdata(**uvd_kw) uvc = new_uvcal_from_uvdata(uvd, freq_range=(150e6, 170e6), **uvc_only_kw) diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index a1f69d45cc..4dee8e442e 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -600,6 +600,16 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): warnings.warn("Antenna positions are not present in the file.") self.telescope.antenna_positions = None + if self.telescope.location is None: + self.telescope.location = EarthLocation.from_geodetic( + lat=latitude * units.rad, lon=longitude * units.rad + ) + warnings.warn( + "Telescope location is set at sealevel at the file lat/lon " + "coordinates because neither altitude nor antenna positions " + "are present in the file." + ) + if self.telescope.antenna_numbers is None: # there are no antenna_numbers or antenna_positions, so just use # the antennas present in the visibilities @@ -618,9 +628,10 @@ def _load_antpos(self, uv, *, sorted_unique_ants=None, correct_lat_lon=True): ant_name_list = ant_name_str[1:-1].split(", ") self.telescope.antenna_names = ant_name_list except KeyError: - self.telescope.antenna_names = self.telescope.antenna_numbers.astype( - str - ).tolist() + if self.telescope.antenna_numbers is not None: + self.telescope.antenna_names = self.telescope.antenna_numbers.astype( + str + ).tolist() # check for antenna diameters try: @@ -1581,14 +1592,13 @@ def read_miriad( # that obspa is not a standard keyword), then we can _just_ fill those # in. self._set_app_coords_helper(pa_only=record_app) - try: - self.set_telescope_params( - run_check=run_check, - check_extra=check_extra, - run_check_acceptability=run_check_acceptability, - ) - except ValueError as ve: - warnings.warn(str(ve)) + + # set any extra info + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) # if blt_order is defined, reorder data to match that order # this is required because the data are ordered by (time, baseline) on the read diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index 2135f40785..5e1f888632 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -107,14 +107,35 @@ def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: assert uvd.telescope._location.ellipsoid == selenoid -def test_bad_inputs(simplest_working_params: dict[str, Any]): - with pytest.raises(ValueError, match="vis_units must be one of"): - UVData.new(**simplest_working_params, vis_units="foo") - - with pytest.raises( - ValueError, match="Keyword argument derp is not a valid UVData attribute" - ): - UVData.new(**simplest_working_params, derp="foo") +@pytest.mark.parametrize( + ["update_dict", "err_msg"], + [ + [{"vis_units": "foo"}, "vis_units must be one of"], + [{"derp": "foo"}, "Keyword argument derp is not a valid UVData attribute"], + [ + {"telescope": None}, + "antenna_positions is required if telescope is not provided.", + ], + [ + { + "telescope": Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="test", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ) + }, + "instrument must be set on the Telescope object passed to `telescope`.", + ], + ], +) +def test_bad_inputs(simplest_working_params: dict[str, Any], update_dict, err_msg): + simplest_working_params.update(update_dict) + with pytest.raises(ValueError, match=err_msg): + UVData.new(**simplest_working_params) def test_bad_time_inputs(simplest_working_params: dict[str, Any]): diff --git a/pyuvdata/uvdata/tests/test_miriad.py b/pyuvdata/uvdata/tests/test_miriad.py index cee12ed76c..671fa97d93 100644 --- a/pyuvdata/uvdata/tests/test_miriad.py +++ b/pyuvdata/uvdata/tests/test_miriad.py @@ -628,6 +628,32 @@ def test_miriad_location_handling(paper_miriad_main, tmp_path): ): uv_out.read(testfile, use_future_array_shapes=True) + # test for handling no altitude, unknown telescope, no antenna positions + if os.path.exists(testfile): + shutil.rmtree(testfile) + aipy_uv = aipy_extracts.UV(paper_miriad_file) + aipy_uv2 = aipy_extracts.UV(testfile, status="new") + # initialize headers from old file + # change telescope name (so the position isn't set from known_telescopes) + # and use absolute antenna positions + aipy_uv2.init_from_uv(aipy_uv, override={"telescop": "foo"}, exclude=["antpos"]) + # copy data from old file + aipy_uv2.pipe(aipy_uv) + aipy_uv2.close() + with uvtest.check_warnings( + UserWarning, + match=[ + warn_dict["altitude_missing_foo"], + warn_dict["altitude_missing_foo"], # raised twice + "Antenna positions are not present in the file.", + "Antenna positions are not present in the file.", # raised twice + "Telescope location is set at sealevel at the file lat/lon " + "coordinates because neither altitude nor antenna positions are " + "present in the file", + ], + ): + uv_out.read(testfile, use_future_array_shapes=True, run_check=False) + # Test for handling when antenna positions have a different mean latitude # than the file latitude # make new file diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 9ecc548d2e..2296e7894a 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -4823,9 +4823,6 @@ def _phase_dict_helper( "Set lookup_name=False in order to continue." ) - # TODO I don't understand this comment: - # We only want to use the JPL-Horizons service if using a non-multi-phase-ctr - # instance of a UVData object. if lookup_name and (cat_name not in name_dict): if (cat_type is None) or (cat_type == "ephem"): [cat_times, cat_lon, cat_lat, cat_dist, cat_vrad] = ( From 0bfc74f8299f808fdb08b0ad90ac6a04817fc665 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 1 May 2024 10:25:57 -0700 Subject: [PATCH 38/59] Add an Nants_telescope property to UVData for eq_coeffs --- pyuvdata/uvbase.py | 4 +++- pyuvdata/uvcal/uvcal.py | 4 ++++ pyuvdata/uvdata/uvdata.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index 985c9ddaa1..7de42d9933 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -392,7 +392,9 @@ def __iter__(self, uvparams_only=True): """ if uvparams_only: attribute_list = [ - a for a in dir(self) if isinstance(getattr(self, a), uvp.UVParameter) + a + for a in dir(self) + if a.startswith("_") and isinstance(getattr(self, a), uvp.UVParameter) ] else: attribute_list = [ diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index a7c2a6f950..d7b3c17a1f 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -2019,6 +2019,10 @@ def check( "2.5", DeprecationWarning, ) + + # call metadata_only to make sure that parameter requirements are set properly + self.metadata_only + # first run the basic check from UVBase super(UVCal, self).check( check_extra=check_extra, run_check_acceptability=run_check_acceptability diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 2296e7894a..a9a79e2ea0 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -697,6 +697,23 @@ def _set_telescope_requirements(self): self.telescope._instrument.required = True self.telescope._x_orientation.required = False + # This is required for eq_coeffs, which has Nants_telescope as one of its + # shapes. That's to allow us to line up the antenna_numbers/names with + # eq_coeffs so that we know which antenna each eq_coeff goes with. + @property + def Nants_telescope(self): + """ + The number of antennas in the telescope. + + This property is stored on the Telescope object internally. + """ + return self._telescope.value.Nants + + # TODO: do we want a setter on UVData for this? + @Nants_telescope.setter + def Nants_telescope(self, val): + self._telescope.value.Nants = val + @staticmethod def _clear_antpair2ind_cache(obj): """Clear the antpair2ind cache.""" @@ -2589,6 +2606,9 @@ def check( "All values in the flex_spw_id_array must exist in the spw_array." ) + # call metadata_only to make sure that parameter requirements are set properly + self.metadata_only + # first run the basic check from UVBase logger.debug("Doing UVBase check...") From 982b9d3a4d063d6820498f1a7142eb549e1abb52 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 1 May 2024 17:06:33 -0700 Subject: [PATCH 39/59] more test coverage --- pyuvdata/tests/test_parameter.py | 196 +++++++++++++++++++++++---- pyuvdata/tests/test_telescopes.py | 13 ++ pyuvdata/tests/test_uvbase.py | 70 ++++++++-- pyuvdata/uvbase.py | 25 ++-- pyuvdata/uvcal/fhd_cal.py | 12 +- pyuvdata/uvdata/fhd.py | 2 + pyuvdata/uvdata/tests/test_fhd.py | 24 ++-- pyuvdata/uvdata/uvh5.py | 15 +- pyuvdata/uvflag/tests/test_uvflag.py | 35 ++++- pyuvdata/uvflag/uvflag.py | 5 +- 10 files changed, 317 insertions(+), 80 deletions(-) diff --git a/pyuvdata/tests/test_parameter.py b/pyuvdata/tests/test_parameter.py index ab01439015..4509dd3497 100644 --- a/pyuvdata/tests/test_parameter.py +++ b/pyuvdata/tests/test_parameter.py @@ -43,32 +43,57 @@ def sky_in(): ) -def test_class_inequality(): +def test_class_inequality(capsys): """Test equality error for different uvparameter classes.""" param1 = uvp.UVParameter(name="p1", value=1) param2 = uvp.AngleParameter(name="p2", value=1) # use `__ne__` rather than `!=` throughout so we can cover print lines assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter classes are different") + -def test_value_class_inequality(): +def test_value_class_inequality(capsys): """Test equality error for different uvparameter classes.""" param1 = uvp.UVParameter(name="p1", value=3) param2 = uvp.UVParameter(name="p2", value=np.array([3, 4, 5])) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is not an array, but other is an array" + ) + assert param2.__ne__(param1, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p2 parameter value is an array, but other is not") + param3 = uvp.UVParameter(name="p2", value="Alice") assert param1.__ne__(param3, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is not a string or a dict and cannot be cast as a " + "numpy array. The values are not equal." + ) -def test_array_inequality(): +def test_array_inequality(capsys): """Test equality error for different array values.""" param1 = uvp.UVParameter(name="p1", value=np.array([0, 1, 3])) param2 = uvp.UVParameter(name="p2", value=np.array([0, 2, 4])) assert param1.__ne__(param2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter value is array, values are not close") + param3 = uvp.UVParameter(name="p3", value=np.array([0, 1])) assert param1.__ne__(param3, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is an array, shapes are different" + ) + def test_array_equality_nans(): """Test array equality with nans present.""" @@ -108,29 +133,50 @@ def test_quantity_equality_error(): @pytest.mark.parametrize( - ["vals", "p2_atol"], + ["vals", "p2_atol", "msg"], ( - (np.array([0, 2, 4]) * units.m, 1 * units.mm), - (np.array([0, 1, 3]) * units.mm, 1 * units.mm), - (np.array([0, 1, 3]) * units.Jy, 1 * units.mJy), + ( + np.array([0, 2, 4]) * units.m, + 1 * units.mm, + "p1 parameter value is an astropy Quantity, values are not close", + ), + ( + np.array([0, 1, 3]) * units.mm, + 1 * units.mm, + "p1 parameter value is an astropy Quantity, values are not close", + ), + ( + np.array([0, 1, 3]) * units.Jy, + 1 * units.mJy, + "p1 parameter value is an astropy Quantity, units are not equivalent", + ), ( units.Quantity([0.101 * units.cm, 100.09 * units.cm, 2999.1 * units.mm]), 1 * units.mm, + "p1 parameter value is an astropy Quantity, values are not close", ), ( units.Quantity([0.09 * units.cm, 100.11 * units.cm, 2999.1 * units.mm]), 1 * units.mm, + "p1 parameter value is an astropy Quantity, values are not close", + ), + ( + np.array([0, 1000, 2998.9]) * units.mm, + 1 * units.mm, + "p1 parameter value is an astropy Quantity, values are not close", ), - (np.array([0, 1000, 2998.9]) * units.mm, 1 * units.mm), ), ) -def test_quantity_inequality(vals, p2_atol): +def test_quantity_inequality(capsys, vals, p2_atol, msg): param1 = uvp.UVParameter( name="p1", value=np.array([0, 1, 3]) * units.m, tols=1 * units.mm ) param2 = uvp.UVParameter(name="p2", value=vals, tols=p2_atol) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith(msg) + def test_quantity_array_inequality(capsys): param1 = uvp.UVParameter( @@ -152,19 +198,29 @@ def test_quantity_equality_nans(): assert param1 == param2 -def test_string_inequality(): +def test_string_inequality(capsys): """Test equality error for different string values.""" param1 = uvp.UVParameter(name="p1", value="Alice") param2 = uvp.UVParameter(name="p2", value="Bob") assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is a string, values are different" + ) -def test_string_list_inequality(): + +def test_string_list_inequality(capsys): """Test equality error for different string values.""" param1 = uvp.UVParameter(name="p1", value=["Alice", "Eve"]) param2 = uvp.UVParameter(name="p2", value=["Bob", "Eve"]) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is a list of strings, values are different" + ) + def test_string_equality(): """Test equality error for different string values.""" @@ -173,12 +229,18 @@ def test_string_equality(): assert param1 == param2 -def test_integer_inequality(): +def test_integer_inequality(capsys): """Test equality error for different non-array, non-string values.""" param1 = uvp.UVParameter(name="p1", value=1) param2 = uvp.UVParameter(name="p2", value=2) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value can be cast to an array and tested with np.allclose. " + "The values are not close" + ) + def test_dict_equality(): """Test equality for dict values.""" @@ -191,43 +253,61 @@ def test_dict_equality(): assert param1 == param2 -def test_dict_inequality_int(): +def test_dict_inequality_int(capsys): """Test equality error for integer dict values.""" param1 = uvp.UVParameter(name="p1", value={"v1": 1, "s1": "test", "n1": None}) param2 = uvp.UVParameter(name="p2", value={"v1": 2, "s1": "test", "n1": None}) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, key v1 is not equal") + -def test_dict_inequality_str(): +def test_dict_inequality_str(capsys): """Test equality error for string dict values.""" param1 = uvp.UVParameter(name="p1", value={"v1": 1, "s1": "test", "n1": None}) param4 = uvp.UVParameter(name="p3", value={"v1": 1, "s1": "foo", "n1": None}) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, key s1 is not equal") + -def test_dict_inequality_none(): +def test_dict_inequality_none(capsys): """Test equality error for string dict values.""" param1 = uvp.UVParameter(name="p1", value={"v1": 1, "s1": "test", "n1": None}) param4 = uvp.UVParameter(name="p3", value={"v1": 1, "s1": "test", "n1": 2}) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, key n1 is not equal") + -def test_dict_inequality_arr(): +def test_dict_inequality_arr(capsys): """Test equality error for string dict values.""" param1 = uvp.UVParameter(name="p1", value={"v1": 1, "arr1": [3, 4, 5]}) param4 = uvp.UVParameter(name="p3", value={"v1": 1, "arr1": [3, 4]}) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, key arr1 is not equal") + param4 = uvp.UVParameter(name="p3", value={"v1": 1, "arr1": [3, 4, 6]}) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, key arr1 is not equal") + -def test_dict_inequality_keys(): +def test_dict_inequality_keys(capsys): """Test equality error for different keys.""" param1 = uvp.UVParameter(name="p1", value={"v1": 1, "s1": "test", "n1": None}) param3 = uvp.UVParameter(name="p3", value={"v3": 1, "s1": "test", "n1": None}) assert param1.__ne__(param3, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("p1 parameter is a dict, keys are not the same.") + def test_nested_dict_equality(): """Test equality for nested dicts.""" @@ -240,7 +320,7 @@ def test_nested_dict_equality(): assert param1 == param3 -def test_nested_dict_inequality(): +def test_nested_dict_inequality(capsys): """Test equality error for nested dicts.""" param1 = uvp.UVParameter( name="p1", value={"d1": {"v1": 1, "s1": "test"}, "d2": {"v1": 1, "s1": "test"}} @@ -250,6 +330,11 @@ def test_nested_dict_inequality(): ) assert param1.__ne__(param3, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter is a dict, key d1 is a dict, key v1 is not equal" + ) + def test_recarray_equality(): """Test equality for recarray.""" @@ -340,19 +425,31 @@ def test_recarray_inequality(capsys, names2, values2, msg): assert captured.out.startswith(msg) -def test_equality_check_fail(): +def test_equality_check_fail(capsys): """Test equality error for non string, dict or array values.""" param1 = uvp.UVParameter(name="p1", value=uvp.UVParameter(name="p1", value="Alice")) param2 = uvp.UVParameter(name="p2", value=uvp.UVParameter(name="p1", value="Bob")) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value is not a string or a dict and cannot be cast as a " + "numpy array. The values are not equal." + ) -def test_notclose(): + +def test_notclose(capsys): """Test equality error for values not with tols.""" param1 = uvp.UVParameter(name="p1", value=1.0) param2 = uvp.UVParameter(name="p2", value=1.001) assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "p1 parameter value can be cast to an array and tested with np.allclose. " + "The values are not close" + ) + def test_close(): """Test equality error for values within tols.""" @@ -415,6 +512,7 @@ def test_angle_set_degree_none(): param1.set_degrees(None) assert param1.value is None + assert param1.degrees() is None def test_location_set_lat_lon_alt_none(): @@ -422,6 +520,7 @@ def test_location_set_lat_lon_alt_none(): param1.set_lat_lon_alt(None) assert param1.value is None + assert param1.lat_lon_alt() is None def test_location_set_lat_lon_alt_degrees_none(): @@ -429,6 +528,7 @@ def test_location_set_lat_lon_alt_degrees_none(): param1.set_lat_lon_alt_degrees(None) assert param1.value is None + assert param1.lat_lon_alt_degrees() is None def test_location_set_xyz(): @@ -620,15 +720,17 @@ def test_skycoord_param_equality(sky_in, sky2): @pytest.mark.parametrize( "change", ["frame", "representation", "separation", "shape", "type"] ) -def test_skycoord_param_inequality(sky_in, change): +def test_skycoord_param_inequality(sky_in, change, capsys): param1 = uvp.SkyCoordParameter(name="sky1", value=sky_in) if change == "frame": param2 = uvp.SkyCoordParameter(name="sky2", value=sky_in.transform_to("icrs")) + msg = "sky1 parameter has different frames, fk5 vs icrs." elif change == "representation": sky2 = sky_in.copy() sky2.representation_type = CartesianRepresentation param2 = uvp.SkyCoordParameter(name="sky2", value=sky2) + msg = "sky1 parameter has different representation_types" elif change == "separation": sky2 = SkyCoord( ra=Longitude(5.0, unit="hourangle"), @@ -637,6 +739,7 @@ def test_skycoord_param_inequality(sky_in, change): equinox="J2000", ) param2 = uvp.SkyCoordParameter(name="sky2", value=sky2) + msg = "sky1 parameter is not close." elif change == "shape": sky2 = SkyCoord( ra=Longitude([5.0, 5.0], unit="hourangle"), @@ -645,12 +748,17 @@ def test_skycoord_param_inequality(sky_in, change): equinox="J2000", ) param2 = uvp.SkyCoordParameter(name="sky2", value=sky2) + msg = "sky1 parameter shapes are different" elif change == "type": sky2 = Longitude(5.0, unit="hourangle") param2 = uvp.SkyCoordParameter(name="sky2", value=sky2) + msg = "sky1 parameter value is a SkyCoord, but other is not" assert param1.__ne__(param2, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith(msg) + def test_non_builtin_expected_type(): with pytest.raises(ValueError) as cm: @@ -686,7 +794,7 @@ def test_generic_type_conversion(in_type, out_type): assert param1.expected_type == out_type -def test_strict_expected_type_equality(): +def test_strict_expected_type_equality(capsys): # make sure equality passes if one is strict and one is generic param1 = uvp.UVParameter( "_test1", @@ -705,6 +813,10 @@ def test_strict_expected_type_equality(): "_test3", value=3.0, expected_type=float, strict_type_check=True ) assert param1.__ne__(param3, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("_test1 parameter has incompatible types.") + assert param3 != param1 assert param2 == param3 @@ -717,6 +829,9 @@ def test_strict_expected_type_equality(): ) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("_test1 parameter has incompatible types") + # make sure it passes when both are strict and equivalent param5 = uvp.UVParameter( "_test5", @@ -736,7 +851,7 @@ def test_strict_expected_type_equality(): return -def test_strict_expected_type_equality_arrays(): +def test_strict_expected_type_equality_arrays(capsys): # make sure it also works with numpy arrays when the dtype matches the strict type param1 = uvp.UVParameter( "_test1", @@ -760,6 +875,10 @@ def test_strict_expected_type_equality_arrays(): strict_type_check=True, ) assert param1.__ne__(param3, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("_test1 parameter has incompatible types") + assert param3 != param1 assert param2 == param3 @@ -772,6 +891,9 @@ def test_strict_expected_type_equality_arrays(): ) assert param1.__ne__(param4, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("_test1 parameter has incompatible types") + # make sure it passes when both are strict and equivalent param5 = uvp.UVParameter( "_test5", @@ -789,24 +911,48 @@ def test_strict_expected_type_equality_arrays(): strict_type_check=False, ) assert param1.__ne__(param6, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("_test1 parameter has incompatible dtypes.") + assert param6.__ne__(param1, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("_test6 parameter has incompatible dtypes.") -def test_scalar_array_parameter_mismatch(): +def test_scalar_array_parameter_mismatch(capsys): param1 = uvp.UVParameter("_test1", value=3.0, expected_type=float) param2 = uvp.UVParameter("_test2", value=np.asarray([3.0]), expected_type=float) assert param1.__ne__(param2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith( + "_test1 parameter value is not an array, but other is an array" + ) + assert param2.__ne__(param1, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith( + "_test2 parameter value is an array, but other is not" + ) + return -def test_value_none_parameter_mismatch(): +def test_value_none_parameter_mismatch(capsys): param1 = uvp.UVParameter("_test1", value=3.0, expected_type=float) param2 = uvp.UVParameter("_test2", value=None) assert param1.__ne__(param2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("_test1 is None on right, but not left") + assert param2.__ne__(param1, silent=False) + captured = capsys.readouterr() + assert captured.out.startswith("_test2 is None on left, but not right") + return diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index 45bf6d141c..e76f1b5f95 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -188,6 +188,19 @@ def test_update_params_from_known(): assert mwa_tel == mwa_tel2 + vla_tel = Telescope.from_known_telescopes("vla", run_check=False) + vla_tel2 = Telescope() + vla_tel2.name = "vla" + + with uvtest.check_warnings( + UserWarning, + match="telescope_location are not set or are being overwritten. " + "telescope_location are set using values from astropy sites for vla.", + ): + vla_tel2.update_params_from_known_telescopes(warn=True, run_check=False) + + assert vla_tel == vla_tel2 + def test_from_known(): for inst in pyuvdata.known_telescopes(): diff --git a/pyuvdata/tests/test_uvbase.py b/pyuvdata/tests/test_uvbase.py index 5e734a84c9..027c78179b 100644 --- a/pyuvdata/tests/test_uvbase.py +++ b/pyuvdata/tests/test_uvbase.py @@ -177,10 +177,25 @@ def __init__(self): form=(), ) - self.telescope = Telescope.from_known_telescopes("mwa") + self._telescope = uvp.UVParameter( + "telescope", + description="A telescope.", + value=Telescope.from_params( + location=EarthLocation.from_geodetic(0, 0, 0), + name="mock", + antenna_positions={ + 0: [0.0, 0.0, 0.0], + 1: [0.0, 0.0, 1.0], + 2: [0.0, 0.0, 2.0], + }, + ), + expected_type=Telescope, + ) super(UVTest, self).__init__() + self.telescope = Telescope.from_known_telescopes("mwa") + def test_equality(): """Basic equality test.""" @@ -196,15 +211,23 @@ def test_equality_nocheckextra(): assert test_obj.__eq__(test_obj2, check_extra=False) -def test_inequality_extra(): +def test_inequality_extra(capsys): """Basic equality test.""" test_obj = UVTest() test_obj2 = test_obj.copy() test_obj2.optional_int1 = 4 - assert test_obj != test_obj2 + # use `__ne__` rather than `!=` throughout so we can cover print lines + assert test_obj.__ne__(test_obj2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith( + "optional_int1 parameter value can be cast to an array and tested with " + "np.allclose. The values are not close\nparameter _optional_int1 does " + "not match. Left is 3, right is 4." + ) -def test_inequality_different_extras(): +def test_inequality_different_extras(capsys): """Basic equality test.""" test_obj = UVTest() test_obj2 = test_obj.copy() @@ -215,22 +238,40 @@ def test_inequality_different_extras(): value=7, required=False, ) - assert test_obj != test_obj2 + assert test_obj.__ne__(test_obj2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith( + "Sets of extra parameters do not match. Left is ['_optional_int1', " + "'_optional_int2', '_unset_int1'], right is ['_optional_int1', " + "'_optional_int2', '_optional_int3', '_unset_int1']." + ) + assert not (test_obj == test_obj2) -def test_inequality(): +def test_inequality(capsys): """Check that inequality is handled correctly.""" test_obj = UVTest() test_obj2 = test_obj.copy() test_obj2.float1 = 13 - assert test_obj != test_obj2 + assert test_obj.__ne__(test_obj2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith( + "float1 parameter value can be cast to an array and tested with " + "np.allclose. The values are not close\nparameter _float1 does not " + "match. Left is 18.2, right is 13." + ) -def test_class_inequality(): +def test_class_inequality(capsys): """Test equality error for different classes.""" test_obj = UVTest() - assert test_obj != test_obj._floatarr + assert test_obj.__ne__(test_obj._floatarr, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("Classes do not match") def test_check(): @@ -464,6 +505,17 @@ def test_name_error(): test_obj.check() +def test_telescope_inequality(capsys): + test_obj = UVTest() + test_obj2 = test_obj.copy() + test_obj2.telescope = Telescope.from_known_telescopes("hera") + + assert test_obj.__ne__(test_obj2, silent=False) + + captured = capsys.readouterr() + assert captured.out.startswith("parameter _telescope does not match.") + + def test_getattr_old_telescope(): test_obj = UVTest() diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index 7de42d9933..c1f0212c0a 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -538,15 +538,22 @@ def __eq__( for param in p_check: self_param = getattr(self, param) other_param = getattr(other, param) - if self_param.__ne__(other_param, silent=silent): - if isinstance(self_param.value, UVBase): - if self_param.value.__ne__( - other_param.value, check_extra=check_extra, silent=silent - ): - if not silent: - print(f"parameter {param} does not match.") - p_equal = False - else: + if isinstance(self_param.value, UVBase): + if self_param.value.__ne__( + other_param.value, check_extra=check_extra, silent=True + ): + if not silent: + print(f"parameter {param} does not match.") + # call again with silent passed to get the details + # about what is different on the UVBase object + self_param.value.__ne__( + other_param.value, + check_extra=check_extra, + silent=silent, + ) + p_equal = False + else: + if self_param.__ne__(other_param, silent=silent): if not silent: print( f"parameter {param} does not match. Left is " diff --git a/pyuvdata/uvcal/fhd_cal.py b/pyuvdata/uvcal/fhd_cal.py index d3c0cc7284..1efe5b78e3 100644 --- a/pyuvdata/uvcal/fhd_cal.py +++ b/pyuvdata/uvcal/fhd_cal.py @@ -226,16 +226,12 @@ def read_fhd_cal( self.telescope.x_orientation = "east" - try: - self.set_telescope_params() - except ValueError as ve: - warnings.warn(str(ve)) + self.set_telescope_params() # need to make sure telescope location is defined properly before this call - if self.telescope.location is not None: - proc = self.set_lsts_from_time_array( - background=background_lsts, astrometry_library=astrometry_library - ) + proc = self.set_lsts_from_time_array( + background=background_lsts, astrometry_library=astrometry_library + ) self._set_sky() self.gain_convention = "divide" diff --git a/pyuvdata/uvdata/fhd.py b/pyuvdata/uvdata/fhd.py index 90588dcea8..32a0db9c75 100644 --- a/pyuvdata/uvdata/fhd.py +++ b/pyuvdata/uvdata/fhd.py @@ -701,6 +701,8 @@ def read_fhd( ] self.telescope.Nants = len(self.telescope.antenna_names) + self.set_telescope_params() + # need to make sure telescope location is defined properly before this call proc = self.set_lsts_from_time_array( background=background_lsts, astrometry_library=astrometry_library diff --git a/pyuvdata/uvdata/tests/test_fhd.py b/pyuvdata/uvdata/tests/test_fhd.py index f017a96ed5..f564bd4cb4 100644 --- a/pyuvdata/uvdata/tests/test_fhd.py +++ b/pyuvdata/uvdata/tests/test_fhd.py @@ -193,20 +193,16 @@ def test_read_fhd_write_read_uvfits_no_layout(fhd_data_files, multi): else: fhd_data_files[ftype] = [fnames] * 2 - if not multi: - # check warning raised - with uvtest.check_warnings( - UserWarning, - match="The layout_file parameter was not passed, so antenna_postions will " - "not be defined and antenna names and numbers might be incorrect.", - ): - fhd_uv.read(**fhd_data_files, run_check=False, use_future_array_shapes=True) - - with pytest.raises( - ValueError, match="Required UVParameter _antenna_positions has not been set" - ): - with uvtest.check_warnings(UserWarning, "No layout file"): - fhd_uv.read(**fhd_data_files, use_future_array_shapes=True) + warn_msg = [ + "The layout_file parameter was not passed, so antenna_postions will " + "not be defined and antenna names and numbers might be incorrect.", + "antenna_positions are not set or are being overwritten. " + "antenna_positions are set using values from known telescopes for mwa.", + ] + if multi: + warn_msg = warn_msg * 2 + with uvtest.check_warnings(UserWarning, match=warn_msg): + fhd_uv.read(**fhd_data_files, use_future_array_shapes=True) @pytest.mark.filterwarnings("ignore:Telescope location derived from obs") diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index a2041411d3..3a1e8df3b2 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -664,15 +664,12 @@ def _read_header_with_fast_meta( cat_epoch=obj.phase_center_epoch, ) self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id - # set telescope params - try: - self.set_telescope_params( - run_check=run_check, - check_extra=check_extra, - run_check_acceptability=run_check_acceptability, - ) - except ValueError as ve: - warnings.warn(str(ve)) + # set any extra telescope params + self.set_telescope_params( + run_check=run_check, + check_extra=check_extra, + run_check_acceptability=run_check_acceptability, + ) # wait for the LST computation if needed if proc is not None: diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index ab2881ff3a..364b0d40fb 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -1332,12 +1332,23 @@ def test_missing_telescope_info_mwa(test_outfile): ): read_metafits(metafits) + uvf.write(test_outfile, clobber=True) + param_list = [ + "telescope_name", + "antenna_numbers", + "antenna_positions", + "Nants_telescope", + ] + with h5py.File(test_outfile, "r+") as h5f: + for param in param_list: + del h5f["/Header/" + param] + with uvtest.check_warnings( UserWarning, match=[ "An mwa_metafits_file was passed. The metadata from the metafits file are " "overriding the following parameters in the UVFlag file: " - "['telescope_location']", + "['antenna_names', 'telescope_location']", "The lst_array is not self-consistent with the time_array and telescope " "location. Consider recomputing with the `set_lsts_from_time_array` method", ], @@ -1920,7 +1931,8 @@ def test_add_baseline(): assert "Data combined along baseline axis. " in uv3.history -def test_add_antenna(uvcal_obj): +@pytest.mark.parametrize("diameters", ["both", "left", "right"]) +def test_add_antenna(uvcal_obj, diameters): uvc = uvcal_obj uv1 = UVFlag(uvc, use_future_array_shapes=True) uv2 = uv1.copy() @@ -1929,7 +1941,24 @@ def test_add_antenna(uvcal_obj): uv2.telescope.antenna_names = np.array( [name + "_new" for name in uv2.telescope.antenna_names] ) - uv3 = uv1.__add__(uv2, axis="antenna") + if diameters == "left": + uv2.antenna_diameters = None + elif diameters == "right": + uv2.antenna_diameters = None + + if diameters == "both": + warn_type = None + warn_msg = "" + else: + warn_type = UserWarning + warn_msg = "UVParameter antenna_diameters does not match. Combining anyway." + + with uvtest.check_warnings(warn_type, match=warn_msg): + uv3 = uv1.__add__(uv2, axis="antenna") + + if diameters != "both": + assert uv3.antenna_diameters is None + assert np.array_equal(np.concatenate((uv1.ant_array, uv2.ant_array)), uv3.ant_array) assert np.array_equal( np.concatenate((uv1.metric_array, uv2.metric_array), axis=0), uv3.metric_array diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 1694943cbe..ce748a3597 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -671,9 +671,7 @@ def data_like_parameters(self): @property def pol_collapsed(self): """Determine if this object has had pols collapsed.""" - if not hasattr(self, "polarization_array") or self.polarization_array is None: - return False - elif isinstance(self.polarization_array.item(0), str): + if isinstance(self.polarization_array.item(0), str): return True else: return False @@ -930,6 +928,7 @@ def check( self._flex_spw_id_array.required = False self._set_telescope_requirements() + self._check_pol_state() # first run the basic check from UVBase super().check( From 0f38debf3c7950f8741c12826f5cdcba9b37f119 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 11:44:06 -0700 Subject: [PATCH 40/59] Add more fhd layout test coverage, found that tols were too low Fixing the tols led to many fewer warnings in tests. --- .../1061316296_known_match_latlonalt_obs.sav | Bin 0 -> 80696 bytes .../1061316296_known_match_xyz_layout.sav | Bin 0 -> 8140 bytes pyuvdata/uvcal/fhd_cal.py | 2 - pyuvdata/uvcal/tests/conftest.py | 38 ++++++++---------- pyuvdata/uvcal/tests/test_fhd_cal.py | 27 ++----------- pyuvdata/uvdata/fhd.py | 17 ++++---- pyuvdata/uvdata/tests/test_fhd.py | 37 +++++++++++++---- 7 files changed, 56 insertions(+), 65 deletions(-) create mode 100644 pyuvdata/data/fhd_vis_data/1061316296_known_match_latlonalt_obs.sav create mode 100644 pyuvdata/data/fhd_vis_data/1061316296_known_match_xyz_layout.sav diff --git a/pyuvdata/data/fhd_vis_data/1061316296_known_match_latlonalt_obs.sav b/pyuvdata/data/fhd_vis_data/1061316296_known_match_latlonalt_obs.sav new file mode 100644 index 0000000000000000000000000000000000000000..15ce4c1a2f5b0883f5a9f853ed9e74f59fc9c1ee GIT binary patch literal 80696 zcmeHQ3w#vSy&ggmo(cjAzSkE>N!FcxWOvc6vzgtHV6utH^0H_MBq0$#dJItgW_JtySx-_xsQ8BxlYp1K!i$@5=oA z!TGNA;vsSWzE6leVv*jjHs_k(Adsr-3#Zsh2sJu8C&zN9*~qa80y9j~Sz{S4OL5 zRqBj&(fS!<~KnQ7Szmd0(cvD{?Odn?VxBqaA)24%UQJQRj>qq}n7l_3FpZ6a+ zcK6mdY(6#63Z&EC0De+F(D=iH_5&8wJ_FRB1RC?$w(Tu#)n4-Lt-X|=uf3GVx0lx0 z_EOqtFD<`r^Z!pj8~PfW8M+wz&%XS|K49o;Xi59t-pb6kEnhENj?orlf8JM{_T~S& z%|qjZo%$KOoKy;&1#AkJ|M(rpikVVo`MIJmJ9aJ7mz~;<8s{HMYMh@wR&&Zhr(t_K zuN&CgIQez{zRxGt1f%$E}xf z>wa!MU|x4~^}D(H-CX_p+?Z*vo2%c=)$iu&cXRc7xcWU@{T{A<569ob)$ifz_i*)l zxca?Z{a&tqFIT^ptKZAj@8# zxcbYu`t@5JjQ(X@{bgMJWnBGbT>XBoem_^gpR3=`@%MA}`?>o4T>XBo{s32hfU7^i z)gR#M4{-GdxcUQJ{Q<82AXk5ot3SxqALQx}a`gwf`h#5kL391G%Ur+gGS@G=%=ODI zbN#Z*T)*rx*Dt%w^~)}*>j76Mh54%iCj=N1to^+>_oOzU# zoLns>kG)7rPDDB7FG|VN9*~kv#qckZlINn{^%JC&_ZBUgcuz_dR%yux-jk9q_e#mP zP|q1ZL46%)&(utNs7EjeQnKbcDRru0mASr#>n5jj>YlC-+LJ4R@tdcgs@h z@vTbguc+sTiPl&;6Q@h5sn2LB&ji$)&{F6BNlRURj*=?Iwi8kAv522= zl$JVZos>H16kLAgxvxL0-~n=EZ6cNgI?o#8-8GVg|Cw8EbX`eC_mZ)VzPh(-P0FV^ zFBz40ke$K<{K} z=w7Xnx3_*b;vI;k{>#RHsb?{B5l2SPiS&9F=&PA@CaK@|A04f3EG_euIqB@EZg@u? zIP{s1)x^3sY@-N5L%bE>7P5KS=s}JdNo~cE_BqmTY<4+}(a|`L==n`nN1v03o zK)`4hDD~jdK&DqsQsOSwLOqpOL<` zWs#cr?&*kA1fe0`3UCYAnTR%VWOTF($8*4uxZu5t93AQT0GU}w%r4anx*tzJ_-3V= zxW5^GMGzX|tpK->H6q%?k6r;}IRxOxRhk!&4eIC%c${>%L+zTiBHj1HX=>um zt?(;?&=7A0xP|Onh&FL#bhHacqC{LK0KASguOS=MvCQXldVR6Cn$AZzZ%}zhfFs?*ISk-+B%WkL$0Ky4yQu1xFtPJ;a=y<$(MP2$IbJnSe%UThq2tq@=72p;! z711V+jE;8UsDLBgubKkzIu;_2Z18yW1wGC(nm=Co;LGVJpPr#6cC3eA5rl?#E5I#e zVMLoaGCJCYBaMr6PwsGl*D-f)1s&;Wu6ot7=JYN1Z-kDE5vK@3L%bE>7P4AIn>aE$ z+Jz%M44`{*^jw11k*?LrnB&pwq^G*-<y-}B+)YT}*)VO*^N2NOp|N4s#G4UTkA z?nr>wF?Vi-P&c2sC4JMUuR+I?5T^)2L%bE>7P2!CZQ{u2Xcvx^;7Ir6=-D8z zWA5Av*ExPVKVI|U4Fj%WThv7DF2pH<&=7A0xP`0=(I$?Jj&|Yr6L6$^a>oL!j&vPI z>tuuDsyh&H(%UF%)fG?gPW*j9P2d6jS6|1c(MA);td4f#xCb2Ro}2^Vbu2(0nMp@| zTs`Nkb9V1KcdeS}ybXRu5E|mG0Jo4mg=iB;Mn}7FOoJoclRFOJbo zo3Sx{+(!vDv4;9V5rl?#E5I#e1BfzFgQa+8iUf4uO;(~bZ`N&94qb9qq#LKfsahk<&bo*D+^qMLN3a{J8#?o6;+1o}(tdb0A!=KFf1tbhHb{ zXTXu}kx$K|W6s=)bkyGm*bsd?J^Qhsnz)kobwv;w;;jI;ko^?VCXS4bcH#I-aGU~6 z2iW7WFuRTgd!2*(a&$ju(^aRWH+_cp9&f{J@++_7uh2#l$E=Qa;`j@2q zkVnQ`uX;1jA2)sYtMtlKo7Dt9wD8r}@fEbu#F5d_E*xJ1N4iHo1K@ScomaE$+J)nPfg{}`F9moV^Ukg4({t+O$H&s~+jpsn zD>fod5rl?#E5I#ezeTi(Bcr2TIQ|A4=^W<-cpY=+R=6)mpL$f+zxvAVA4Ts}6RW99 z6hUZ+w*uTk_Bx_X92p($!tp=B@fhF~fY&k4+{)|JkH@P|T$5h7B#!wbomUk>Xo$B0 z+(PymqD>qb9qq#LRdA$dR=7)(qhs#eDuCyY0UVE;|GIE@M=S2<^sGUgA_xufR)AZ` zevN1oM@C1xaQqk?9e@X5k4HLh&^p=Bbq>Z=`Zj{{Pj5ateNN~+HPK3QY()?n;;jI; zko^PE4q%XDR!2K={0RJx1H1sQV*&EWXrCR4&$0UP=|@+(^wP1ayy_~oU%MOmiXb$^ zTLEq%`#Yjd{1^@G!0!X_J08H%m7`zooC?PweJe-#%>`R`Klp=5YNF(Q#3_Q%5N`#z zh3rE_n>aEW+J)oa!0`kCciD1uEJPmJC<1++T=wgatEBWD$5oa;ahp%=f3p%kMGzX| ztpK->{Wqdb92p($!tr0=cp~5j`0>=3QyDtC-7fw0DrsQd$A3}}f7GKUPCF2e$1iw} zjE;8U_&GS91O)Qvm@}u6aX`|yP3?Zh_?dVLiSHYn>aE$+J)mM z;5ZGK3Gm};o;ek+b8tml{#o4!$jYGOUbD}vAvZw0u8>{CRWI5Ilgh2tN<@nj$b zusYIsL+fNi^G4a@lyT&$3!IbEFKzuTo^#v{zaj_?@m7Fa$o`0E6UVHMcH;OuaGVZ& z6X10$Kpq*RquWW}zNofNU!8tx+(LXWKqdT&AT-2V0d67tJ)%t<86EAy@hx!t25>sS z>zF&YlHE@FRz$UBZbka8hwnZ>z8Bzao+G29T{yl0j>bK1UdKZC$V|thm&Vl&$5ZLs zC;jUH(eX`iFmYsbvr0Z4sR#A18v_5?U-B&#z&X0fPIWjuh zh2vkqk)9ow=h2a_1IPw-bkn$s>s58xdoQQ29{Jb-^8WFA;9%m&=x7&??}8&eJ5~Yy zc+8nwk&gQ3$(uiaAbnwEJKnFNF-sAIhIlK$Eo6U2w2335qg^<@1CI3UI0EoGjzk`r z>3F1XFILZfYioM;zzu3*3*8S=1fe0`3UCV<-K#KhWOTF($HTypo*mBuSRH9UrFF6~ z1pPj_D~KBbWs(|tcH@9b@2y_H9X>@68se=0w~*1h$0m+h9qq(%5;$TeA<;aL*RcS3 zWUP*5^hJ;5yQ-fbsF3f&{T%unnj#1d@m7Fa$PPuci6f(Imb3mJVDz{HW!(JmaPfFr%r5d(M~jk(p3j&jiL#Ls7`8$%xr?0o)Zwg0xAh*Jci zA>Imb3)$g_HgRNhv{T{MpZSJaqc01JanM)kNjT@GFAQ z5N`#zg{&9RCXS4bcH!6sj`U7P1HkK;Gq=(LFCk5E|mG z0Jo5xhiDT=Mn}7FyaXIi1Lgp{j>g<-NJlyzafeI2q32)e7v6mK0Qnx|wLC{gN4s#m z2ps7htGRh}H0D-j9dXB5y{7Bo^fU9v<32fE3o3%p5N`#zh3sNPn>aE$+J)nV;7IRS z%>(%3(U@B?I=b}tkku!*^$tAsReVUgkg3u6e1-ON5HKI)%86EAyu@xK@;9CHz zEMSPBjbFvu~hqn$W5gI^d}2=F=GJ2*{tXAh`LP8YOdJ^Avzhw(}er9qq!A#+6E- z1z>fgYa3c88@$d5y1eulS&AIvkoOF`^aHi8rVf5Z5E|mG0Jo6QF=yhK)zMBI3&D}z zd29uE9Se|0#^~tMekJ-RdfY;HOTe0tT z$j%_%vwC4&@t$#S-=p?D^jpL!g3u6e1-ONbc$hdcI@*OJb(nK&{!+J)o6;7IR0(zO7uBaKItH*`F@ zfP#%C1l znGx<&`;Wof0blv!@gTI(#F5d_E*xphi~-94UdNod6^*MwC|CZ2QT{!beTeU~S~m@G ziXb$^TLEq%qy5mtk~I|~Qyd9zr3a`R*G zD}vAvZw0u8>^q1yam?yyCyv*HhS|7mn9~BfW!s9>D9EJGb(BeDuS}5w-1=fzl`N9lA$8jW|UR z8se=0w~*00wkD2@j&|XA4LH&}$n-ukuVW$d$jrxMz%4}@rTPJF^=oS1uFv3C1fe0` z3UCYAwTL!xWOTF($J@Y>-a)2!u6Z44Zbdevqkeu2N|A3qIc-2K+^+VI`6K*_AT-2V z0d66?9nmI^jE;8UxE&nl06hS&WA5C_>%kYyL&viB77eWa?wxAiA83D81fe0`3UCYA zt%x>pWOTF($D6^C-a+mKcpZ(o6?;753s`+p#5L*lfg2V~R{M58i#SCP8se=0w~*a} zXcI?9N4s#m5gh3qWCWwE-A9PaLt}`&N)ea()ASL6hUZ+w*uTkb`zpa z92p($!tpY2BwDKhR!6#DN9$y)j`+z3%pV`Rw?5)Ie45%{L-%JDL1>7#0^CBj3DG8w zSsm@faRWFm0O<1pyp9FPBV%>M41;?`h`V0iGWA5C_=f?}>n0F{gbgCV-3)TKV(mgvx5E|mG0Jo5>N3@9}qoZ9o zYT!uk%hTtscpVFoM>cdkmih4_GRD>F_vflh-grvw|LrdL6+vi-w*uTkwhqxIj*O0W z;dnJT(q{tbvt_)F`Q}#mfote^#*6jky-(ry0`=b-QV=Tw4e?fhTgbK|+QgC3(JmaX z1V{Qzz$E~$W4^hS*XhCearPa5NiVzPGqwNQ1Bg=up&{N1a0}U0h&FL#bhHb{%fay+ zU@gGwm^-(k^P@W;MOJLRxP0vI7paL8Jcv^Sp&{N1a0}TLh&FL#bhHb{&EQC%3D5vu z$9!`uuhWHb_2DZvp1$tS8`ZvFRU=Ljgob!4z%6835N+bf=x7&?kAvePfUfCS9qC;N zS|=O2&hdI(`u&_$S8s}(`T1FD--5~TD}vAvZw0u8>aE$+J)o8;J5^!`*XaGg~%ft z($N?2Io&}??YU@rWXy;c)V>pGeys>XL%bE>7P22A+QgC3(JmZ!fnzg3_aJ#4bLUoN zbU(){sgK_LX5`?vcBp;f1L3}0lIO_iXcvwNaBKnSx|P>4cWy<;qZc~<;^Onvk5)XQ z_N{puaf%=`#9IMwA^S0+O&l2=?ZUAS99sdpHs^KBom zh_?dVLiQx0O&l2=?ZWXMaBKtU-W97OJy)c4vZ3o7uQ&5~tI6rDkuA4PQg?2q_s|qU zXo$B0+(Py}M4LEfb+i-5yTOq@qeJ)0c^wOoM`qU17mx;C-yubAIB~kVbNc7-D}vAv zZw0u8?7N6Iab$F~3&%Ub@mzqO3Gh1R&aKJ<_=35hG_aw1r21*ydbMx+bi^rw&=7A0 zxP|O4M4LD=I@*Qf4scus(0i=Bj)ll0V~d$_y-f-h%YJcsK@GFAQ5N`#z zh3pPQn>aE$+J)l}z;QW1@9Oe8=FY9?{D}8+(ud#svHE2F2WtO}jfhhOp&{N1a0}T( zh&FL#bhHb{2f%R!a2>$wm^-&B!x!n|i^;Sd_ypFkZD_;P=Q|6OWH7|Xj{!hy~wXgN3h*JciA>Imb3)%gMHgRNh zv;uJw>h_?dVLiT+`n>aE$+J$2o zIIaY~1F$-d%dTU=e2%ru?UY@Tv}fH;+zU8wi~8i21L66j9~?{^vpU*|qZb_MGjlfr zyp9FPBOAJ2m0d16Kklh}`3Cj%OLnS#2gLmxA2^seGCJCYqZ=H%fSUkbM`La^8otr= zd#rvOk1iR%|M>ZRYtzfTL3|I=>+mUp&=7A0xP^?~Au@4fbhHacjFS3i=5EH4*Re3$ zdnO&}`;Z6zaE$+J)n3;7FgD+YazL8gna?j`Vvv15X4$ zSGE87NbTP~8*z#tG{jp0ZXpXH+QgC3(JmZkf+Kxq?pA=;(U@DAbfn+Y8Mr-Mtls(R zc(s2$y>F)oLPNY2;1;q|5pCkg=x7&?0dS-_Aiej->uAia7#+R(?^_MrG_6{_YwY`K z|22ydrwBqrycOUUvLK>O92p($!f`4%UI5$yusV*JKTPmKHo9$+@>E4{0zTW z-uF56gCYnG@m7Fa$fhCM#4)R*oj9Hdj;nz?0ba)f>1_NRXizaj_?@m7Fa z$c{s_i6f(nvS>zF&Yl5u+#cew_7k8Iv^^sk>)`=7cKaf%=`#9IMwAuC0+ zi6f(TrY{ zc`WWY#j&KbqqVbZMYp4;(@{|sZ*X+?G<7sLbv5VN9$7}Y)qw20?RBBtj@}iETe=*btws&)D_T0b+dDhDhYDj- zM!C`X@n|fvmvZAwzk%586qs?p)m6 z(sh1Q4_ZuJ-PL2R&?q-Lzpl1nPJJ{_KghB>TW)i2SEeAYTyv57no({=RdqBLu0gre z+S-@4HFmc&b$52ijc%{6%;yeFL&>qG+?M7kF_UGKTQeJ-3(ILQ73-(=HaD$CSI~C* zZtc*17753rrM2Y^Xs_EPdrDn_Qnx2A2WGmwGrcZq?ziT|&vrzbdRiRVZ}WC@rlpy` zMtiFpGNPlsE__B@_Iunx&!Fk;ZCQG@!`O^1B5&F#w=$e@qrEQIO#1Jh#sr|bZJoVc z=m=V%#Y>l49~wrv;j^l0=EN&=`&V!OV0*(Cwy)^zX`?P)+1cLF)84Vvv81J=2S<$A z2FtCESH+uBSpeyLr_uppf zUu?F|rs@qh%8f)-eVz8RQg@ltAMliUJwZ9(_qqe#!S+U4mh4}_M!EIjy2@~a`8d`q zGWTz$z0ED%?OiR+j+Jdq-7VJri}vaT=NuQM_-x5g{}L^9#J&qQ+S@QERvM{lh-b=` zeGscG=$7RmCR3U5LGRy4I}PVcS{$uiohx#72@LrSy&b0ht*Nbz4VCM0;|KZhs|r4k zx6JSN_=fs-U**Wsr+X;*u*ql3maD>IclrzYR=hCKr9<;u;RmG)k;QLb7a4aaNiQSP+J z?4~6+8*uHdWzG*vx>}lgI{8D_REbe;U48BBs2ZnoXSrOiAU5Lqp{w)UmL)xos?0cT zDkRT}QEp9b1Xm9NLC1oYv64_hgDRGzhN@RVg4=?FyKflG%)v}}o%P#heZZP(+*pH( z9WBrGufySXQ$NH8=Plh!nlM>8Sud@c^CI!eoc4NfZ&1JPt0CRuojvH>#kgX(5)Bm?<Uz-O}S&+|skErKKZx3a3j+vybi` zqgg)PLMRo%ubkDv6!?` zZq2+fiq_A^=gUrvXQguK(pGmcxMZ;x0|m;R7p_}?ac6%DWpqOrbp_f>{opNixl3_* zDwoal1!qFH8Log!Ked~S$8CeXTu2!0jYn}IqSn@-T!hN*K-_e_!!X>xq*1QkScD9= zH(2U%#od_4d1t!ymd=T1W}DcvS}|ri^oum==)7i>tKWRcyT$)46XoI_(vfj24LcMG zx`$K@b?!-&wgzdJ<;+t$h<5nQ`C6%xVLWru`B zaMk5nNCa0rbWlhHTi$snB!bPJMIjMfcJnDA5o|ol84|(zAKe-f!KG(?FC>C>lV1*r zVC^rqg+y@ib;*zj)-0DpB3Qj_Oh^Q)*0zL1(0l*AArW-{cV9>ZU8lVn62Z#34IvS9 ztgjAvC~5$Sk+P#5<%~_zlKE6{hAUILD$riLn2tI%nga4 zW4R|Jg5}ryLIS|K0_T_$k1|d$SWX7$98&-o^BAP{>5GOuJ%Q!30L^hUAm9AO$TQyk zz-C|De)fRT$NRbYSAH|>V^cMzl~a)ZdLLst+VplTh9wvPOlfH;aYfO3r z(gv5!NE@H`xspj=!)$XslYb+Vzn#h7!KCkI()Tjy2eRo&SndSqvp1A}1|W@)9#)BE z5da&;^x?2LGP2x*IE?yjB;ZBYY#!Rafchlc zR|V83R6g2Su!G6J8|iF&3hrgn#Mj7wm`Oi|v{5E~mfA=geL`6&{K>XQ8mr>Hna1qH zI^>V8is5PCoEkjA#2Wez#}vJ`MCnlxJcEqUQ$1Qk8F;is8`42HI#!Oi(yH+=Tt5V* zgCZ62#&~6Yw4t)LI-+kbjgQpMDX)$;*5RF+s%$ZZXW;EzxQipvS(y|xLY-myf(^>2 zbakdJlwThn%E!BC`byDR(b!-<(nfwEwx_abxqz1N>FZe1`Uw4nw@l zt8vJA(9pV3v{V#ptgEfotCxynjr7)nzA`40ZlqTjC<8uvB5e35&HCu6uHmEfpbzEG zt;%$zG*VADR#CT)%B1VV^Yk#3Uop7hLFLhK4K|BMXVr(ZZ5xGFHj;qZvWCx&xF~XL zO?ZBzA(_5j6=T<-2fd%9S2DahqxkUZur9>#tRQ1_HuSJNT2C8})Yl<7rFAx$bYU2~ z6!MC~)#y~(66rdlhcHIqoh8Z}iOs7bbM%!_VBZ+4t!l`0k93gHR`9E+rLEBy^a!~T z4??qLj?~vHBUIO+*q`vmU1LKXUN%E-G@^|)**=)K7tbg?f2jNsl~J@<-#zu?g^tg$ z0F7yM-hh=f;}0E6Mjqwo18qa`p#OF*<&!M}jC5NzP1|3f@bE`Wd8Y z8`}OwCjSkjjk?}p@;@2M*NZmVT%>>I08Ute8qt?UDh|}Ro|KBzdSeKs^ju>MC{p7K z>Y|x#M&ekWg)xCvN8otQCZ*wZbKSHBmiWLi`ZYXXKaNM#N9p9CM~_l#YwK}HWrqc6 zboO0N_=Rc%|;*D~x;<@_E9ZY<_I3v%fpNpsJ_q+WROYJw> zi6k9n`yEfQ4uFZr+WyDq)c?g7_qBb=_>-|c?YGn~umv~b50z`=8U4jCX&Y*{p^Man45RY{8I^5}5mS&h@{eNDMYzU_=tms3 zuZl$)tMUFk{B+HcIUDKM6=y~3GbchSkM2cJKHuZ?mEz^r(xojeJ{R7IDs67>>RAn! zUeE9qtC}cH*NvlxKKmm{R9CjXQh)6_`hEh5zH}Kg zKm!oH8${1;4WoUbV7@+d=;akGpewgb*#+~N^n%Pbx}H_~CYkXebLo{y&tdZCA{{RO z@jH%lWhLVM=SF4A^YZM`iF!CzS@JY(; zik^W#)c1$w`2dsC1@)9a-}K25SJoNZ4la|`&pl5o)E_Le8yte=1YjaS=Rn@{{>Z*v Rrk{MifMFx!GtK(;{|i?g@>Ku; literal 0 HcmV?d00001 diff --git a/pyuvdata/data/fhd_vis_data/1061316296_known_match_xyz_layout.sav b/pyuvdata/data/fhd_vis_data/1061316296_known_match_xyz_layout.sav new file mode 100644 index 0000000000000000000000000000000000000000..a71bb026d54f7f8505e7526f689f57ae1147a63b GIT binary patch literal 8140 zcmeI0iC+`f+Q)+;F1X-Q+%@hfE}5_+!V@4Y0!c`Mf(rx?1Vr|o00Rgvs70lU#f7Rx zt+>`*)MM2awO*H2aYgD@6mM^-dR?mZea-~#E9dk2dG8<4`S8u0`Q71x7N^Ex3&X6YU{Aw+BX5)?Gd`AAg%2TY||Q? zPbb=@I<~vecFR1?TXmGo_4v}_WzJ)}Wqk9}kMy#ZHjZOn`BBZDk81Y+Z;hy{Os$p$ zg}BKTTDcl^GdHDD?e49RY2_gvYMGy$)_Q8R65N~0WooTgh`ZcV<&KAEWS~Yw0+~=uU0S=srZL_Qn$;(|Riv)I-qPOiieFZK-$tKk8lE zeiV8=3-rT1{hsM{i(6;}zR>qtwNG8ow8qR(D&n!-(jQv}k;na~`cj?t$7@Hp%v)*dNb}$k{`hG-KPPGH z&q><)bCS0HoTRNkCu!@?N!t2zlD7Vwgz;zckK-7Bj`8Ohe~$6z7=MoO=NNw`|2QW9 zI41u%CjU4l|2QW9I41u%CjU4l|2QW9I41u%CjU4l|2QW9I41u%CjU4l|2QW9I41u% zCjU4l|2QW9I41u%CjU4l|2QW9I41u%CjU4l|2QW9I41u%CjU4l|2QW9I41u%CjU4l z|2QW9I41u%L;lfq3|%+T^$%SsbU;iHx{jx70s5N-|2HEtMd(7S8-l;FL_H8Qq$kn~ z>5cS3K10lrz6f1c_D2RF1Cc?*E#3AuW0+NU%A<0Mzl8U4u=|~2WiDV(!2>q>=gXAK42oL%NBCA>Z zR+6wILkJ4s->y0D_>xG_Z=HSIRZ>c(*Ub_O`pc1icZ_wQUwpD^agH&WCO%Us=q5If z!z%(ouWm|Ov-%y;R9B-c0>$>F%QS&{(3{>aTDrTM1k}*)Q3O-D{f(l=M#yeb@3J7L zNKo02gz8>yLFGs0)IfH<{!#VOXJq=ckquP$O#a7Mhh>8x+au!wU({_6qTA3FsIEP|?A_dnNuWP>H09vFG!mM9f~UG>e2n8Ehc6+gTS{od zfaN6k=^ioF16*Uiuh3P2{*{y2l&LGobo)m-s%ukr$k+cm5^{{{{k`sXB%0KONQLBZ$UjvX1I%{@lis&pjZ=a;x*z zz4u9^h!azNT6}QrpvNa5Yu_8qq8Dmnx8bl*KPuTl+qbe5vb`4tdw94JyNk*OT0g}r zyuF(b>Qyggju#&hcjq-c)!hSknk38YA^Yf<+B#lEeB4q9)#Wp9DsvnyKtFKqykpnj z5|yoCo<8#UF~{WVKp#{Bir+>N{{@EguN=^!Yh~p#(5IWuZa$n(6diqpaa4sjq@lMO zL9e$kPW)O-yr(S`)A3b@?$<4R3ZUQV{KZqNFNx}d;W|-|yIV7RiU;WTEjwP2F$Kqo zZ(^nsGk1ip_zbOt zCn!-wtZ@0 z+Vk&)`nUnPVLP^thIt9)`D6RVLbz?Dj@D1c^Oi4eNR2=FrKI$x$sn9*1cST8N7X$;Sr@(_0l%1tsqTBe^x3TS3dkONSr+YY zL1y^>*?>CO=Xh4d{Pa6yFLYRXWM>lz{Hlqkx@(uK+I4xpkewgDeDmj>h&HW{pv!hV z86JO@hitMXqb%?}(cIriXuW*xD2?YWW6+BxtNdqK61Cy{fOqM!bwh*BK=z*S)pzy% zi2o^bVI233nN_iuMnZOOk8{`CMG>XfD#Q2-r(HSs{smsogAS%Y3MAfvNy7L}XSW?x zEVGBK@Rbc0WABrwJmLCr+3P%E!_I9YNIx?4tdUquqQ18Q5q{o)0q@T$cZEaT4c9)k z10RyaAB6jYq-KA`N6-ENzpr~&o;XTLhHG3Ut&b7OniUNukdoK`^(5tDqM!Pvf$EVr zi%#Fj0mu@!i(O^%kmUIF(owzRv3*+(jp89|XulUFXG%y~Z5UAfvjHET<)l=CK0bdi zf4_jF_dlvb9sFla8nK;^go46e=PlQ5BXhd{LhB(lYk6=jcNB8#m+*UIT#3#oqk-z7 z?@Hq@H@iTNe|kvo$oIrQF;GnP={_AsdsTOVoTy`)c22A$nq9So>VfUke59{lLk|D! z`-_u~lAw1EIzhj&sfW%x4synu*51GELxN1(p^SMlI{rZSsdFGNVBb3P{Z1s~kRgBO zM1510uYCh~4;GH!9p05BWDuU#M}`ls?J%qxWR*9qZmiy7^czQe$#tFFI zeYaD&Bjf}xRbA^AL411|@_G7U%bhP4ML}+(roO@7oWQLqLVcJ?sb`OaE>O_%`H0xp zD)2Tl%qKbc{+lJr9*}?G@Y;(N9uQ#f-$3V+6tnBz#3{c)q1SPz$+y-KULxEtka6={ zZK25NG9UoBD6*7LE^mWtT@npfg8Xfu)_i`7L4Ku1B>sDpY*SCj|q9{9{^RWq@ zd2jvW3`oySc>m=}Yf@To7-w1&->)e12^18p++7hihLm>ltfcj+E5FSvm*+xhUH|77 zA1);+cf^G1Wvk+vdJaDc3r&*-=$&Io?3`;Ti=gl)U(d(BHBb~A`S$c^5s5e`_(DwVR!@e)nEJW*Nj_Luz16*_C_%=q~wC(xxaXV zWus0ih2>ng{ffMJC=NF~uS?%a(#uc3gTgVJc5a$c21!QUb#$EcKR+BTyU-tsyliZ3 z6;&|*oFSL^@!PJg?otoMBG*5cCqzN4zF#G+&#zb+=G3(uivBR_ykt-#C=MIunV)rJ z;@YVhP;|SGqv`5r#4r9H2=z0YJrZ?;Vc|_D9e3y&$krLwAw|xq!G5p4gXQj`L&yI4 zv&-*O4A1cueQm!R{Kp7bq@3ICZcsTaaVZhTDULQTj($E3ij(Yf@}jL_WrShfQgEU= zN_8n13IE=%SGxevyct$x!8 z`FFH9O%a4k#FE&~^}vVvY`(O~3UZen6ntay4>X<+oCo=DH_YrAw-opVh8#%VwDq)d zd<_(?KVUNDn>CPrWk4kzC-e6Q+f~;(K}LPp^ga7yVTG09dE_jwKiunNAm}D9PV}A{ z4lB?ui4D9#s<>7ZAy#1?@ z%3b;3x=6?e!hasWIpX|*udJd`^3SqU0 z6%L8JNM_6Ph~II?KW_942piC?dS6uu39T{Y2LIM6wO@5QaoOa&Z2N9Pa*Yl1T-vv( z?uU9OGBr@N;^c2$m`iSzbo^!ElkX_XqQPrm^CgSM22x~ZiL!_+9LQBR{Hr&_TK1Ki ztiMDGj|tBOk} 1: message_list *= 2 message_list.append("UVParameter diffuse_model does not match") @@ -415,13 +398,9 @@ def test_break_read_fhdcal(cal_file, obs_file, layout_file, settings_file, nfile ) def test_read_multi(tmp_path, concat_method, read_method): """Test reading in multiple files.""" - warn_type = [UserWarning] * 4 + warn_type = [UserWarning] * 2 msg = [ "UVParameter diffuse_model does not match", - "Telescope location derived from obs lat/lon/alt values does not match the " - "location in the layout file.", - "Telescope location derived from obs lat/lon/alt values does not match the " - "location in the layout file.", "Some FHD input files do not have the expected subfolder so FHD folder " "matching could not be done. The affected file types are: " "['cal', 'obs', 'settings']", diff --git a/pyuvdata/uvdata/fhd.py b/pyuvdata/uvdata/fhd.py index 32a0db9c75..4259823c93 100644 --- a/pyuvdata/uvdata/fhd.py +++ b/pyuvdata/uvdata/fhd.py @@ -206,8 +206,6 @@ def get_fhd_layout_info( latitude, longitude, altitude, - radian_tol, - loc_tols, obs_tile_names, run_check_acceptability=True, ): @@ -226,10 +224,6 @@ def get_fhd_layout_info( telescope longitude in radians altitude : float telescope altitude in meters - loc_tols : float - telescope_location tolerance in meters. - radian_tols : float - lat/lon tolerance in radians. obs_tile_names : array-like of str Tile names from the bl_info structure inside the obs structure. Only used if telescope_name is "mwa". @@ -274,13 +268,18 @@ def get_fhd_layout_info( latlonalt_arr_center = uvutils.LatLonAlt_from_XYZ( arr_center, check_acceptability=run_check_acceptability ) - + # tolerances are limited by the fact that lat/lon/alt are only saved + # as floats in the obs structure + loc_tols = (0, 0.1) # in meters + radian_tol = 10.0 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0) # 10mas # check both lat/lon/alt and xyz because of subtle differences # in tolerances if _xyz_close(location_latlonalt, arr_center, loc_tols) or _latlonalt_close( (latitude, longitude, altitude), latlonalt_arr_center, radian_tol, loc_tols ): - telescope_location = EarthLocation.from_geocentric(arr_center, unit="m") + telescope_location = EarthLocation.from_geocentric( + *location_latlonalt, unit="m" + ) else: # values do not agree with each other to within the tolerances. # this is a known issue with FHD runs on cotter uvfits @@ -658,8 +657,6 @@ def read_fhd( latitude=latitude, longitude=longitude, altitude=altitude, - radian_tol=uvutils.RADIAN_TOL, - loc_tols=self.telescope._location.tols, obs_tile_names=obs_tile_names, run_check_acceptability=True, ) diff --git a/pyuvdata/uvdata/tests/test_fhd.py b/pyuvdata/uvdata/tests/test_fhd.py index f564bd4cb4..bd1dd83177 100644 --- a/pyuvdata/uvdata/tests/test_fhd.py +++ b/pyuvdata/uvdata/tests/test_fhd.py @@ -15,7 +15,7 @@ import pyuvdata.tests as uvtest import pyuvdata.utils as uvutils -from pyuvdata import UVData +from pyuvdata import Telescope, UVData from pyuvdata.data import DATA_PATH from pyuvdata.uvdata.uvdata import _future_array_shapes_warning @@ -312,6 +312,35 @@ def test_read_fhd_write_read_uvfits_variant_flag(tmp_path, fhd_data_files): assert fhd_uv == uvfits_uv +def test_read_fhd_latlonalt_match_xyz(fhd_data_files): + + fhd_data_files["layout_file"] = os.path.join( + DATA_PATH, "fhd_vis_data/", "1061316296_known_match_xyz_layout.sav" + ) + + fhd_data_files["obs_file"] = os.path.join( + DATA_PATH, "fhd_vis_data/", "1061316296_known_match_latlonalt_obs.sav" + ) + + with uvtest.check_warnings( + UserWarning, + match=[ + "The FHD input files do not all have matching prefixes, so they " + "may not be for the same data.", + "Some FHD input files do not have the expected subfolder so FHD " + "folder matching could not be done. The affected file types are: " + "['layout', 'obs']", + ], + ): + fhd_uv = UVData.from_file(**fhd_data_files, use_future_array_shapes=True) + + mwa_tel = Telescope.from_known_telescopes("mwa") + + np.testing.assert_allclose( + mwa_tel._location.xyz(), fhd_uv.telescope._location.xyz() + ) + + def test_read_fhd_write_read_uvfits_fix_layout(tmp_path, fhd_data_files): """ FHD to uvfits loopback test with fixed array center layout file. @@ -334,8 +363,6 @@ def test_read_fhd_write_read_uvfits_fix_layout(tmp_path, fhd_data_files): "matching could not be done. The affected file types are: ['layout']", "The FHD input files do not all have matching prefixes, so they may not be " "for the same data.", - "Telescope location derived from obs lat/lon/alt values does not match the " - "location in the layout file. Using the value from known_telescopes.", ], ): fhd_uv.read(**fhd_data_files, use_future_array_shapes=True) @@ -358,8 +385,6 @@ def test_read_fhd_write_read_uvfits_fix_layout(tmp_path, fhd_data_files): "not be for the same FHD run.", "The FHD input files do not all have matching prefixes, so they may not be " "for the same data.", - "Telescope location derived from obs lat/lon/alt values does not match the " - "location in the layout file. Using the value from known_telescopes.", ], ): fhd_uv.read(**fhd_data_files, use_future_array_shapes=True) @@ -756,7 +781,6 @@ def test_single_time(): UserWarning, [ "tile_names from obs structure does not match", - "Telescope location derived from obs lat/lon/alt", "Some FHD input files do not have the expected subfolder so FHD folder " "matching could not be done. The affected file types are: ['vis', 'vis', " "'flags', 'layout', 'params', 'settings']", @@ -784,7 +808,6 @@ def test_conjugation(): UserWarning, [ "tile_names from obs structure does not match", - "Telescope location derived from obs lat/lon/alt", "Some FHD input files do not have the expected subfolder so FHD folder " "matching could not be done. The affected file types are: ['vis', 'vis', " "'flags', 'layout', 'params', 'settings']", From 4dfd14cf0f5f88dd92a06dbf1d20c9e0e4457614 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 11:45:07 -0700 Subject: [PATCH 41/59] removed Nants_telescope setter for now --- pyuvdata/uvdata/uvdata.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index a9a79e2ea0..5b19de7ecd 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -700,6 +700,7 @@ def _set_telescope_requirements(self): # This is required for eq_coeffs, which has Nants_telescope as one of its # shapes. That's to allow us to line up the antenna_numbers/names with # eq_coeffs so that we know which antenna each eq_coeff goes with. + # TODO: do we want a setter on UVData for this? @property def Nants_telescope(self): """ @@ -709,11 +710,6 @@ def Nants_telescope(self): """ return self._telescope.value.Nants - # TODO: do we want a setter on UVData for this? - @Nants_telescope.setter - def Nants_telescope(self, val): - self._telescope.value.Nants = val - @staticmethod def _clear_antpair2ind_cache(obj): """Clear the antpair2ind cache.""" From 6e5a090c589c2ce99145c2d9d13ebef80a78173b Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 12:06:04 -0700 Subject: [PATCH 42/59] deprecate the old telescope parameters --- pyuvdata/tests/test_uvbase.py | 87 ++++++++++++++++++++++++++++++++--- pyuvdata/uvbase.py | 30 ++++++++++-- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/pyuvdata/tests/test_uvbase.py b/pyuvdata/tests/test_uvbase.py index 027c78179b..1af5e65dab 100644 --- a/pyuvdata/tests/test_uvbase.py +++ b/pyuvdata/tests/test_uvbase.py @@ -520,8 +520,16 @@ def test_getattr_old_telescope(): test_obj = UVTest() for param, tel_param in old_telescope_metadata_attrs.items(): - param_val = getattr(test_obj, param) if tel_param is not None: + with uvtest.check_warnings( + DeprecationWarning, + match=f"The UVData.{param} attribute now just points to the " + f"{tel_param} attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + param_val = getattr(test_obj, param) tel_param_val = getattr(test_obj.telescope, tel_param) if not isinstance(param_val, np.ndarray): assert param_val == tel_param_val @@ -531,12 +539,39 @@ def test_getattr_old_telescope(): else: assert param_val.tolist() == tel_param_val.tolist() elif param == "telescope_location": + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location attribute now just points " + "to the location attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + param_val = getattr(test_obj, param) np.testing.assert_allclose(param_val, test_obj.telescope._location.xyz()) elif param == "telescope_location_lat_lon_alt": + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location_lat_lon_alt attribute now " + "just points to the location attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + param_val = getattr(test_obj, param) np.testing.assert_allclose( param_val, test_obj.telescope._location.lat_lon_alt() ) elif param == "telescope_location_lat_lon_alt_degrees": + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location_lat_lon_alt_degrees attribute " + "now just points to the location attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + param_val = getattr(test_obj, param) np.testing.assert_allclose( param_val, test_obj.telescope._location.lat_lon_alt_degrees() ) @@ -550,8 +585,24 @@ def test_setattr_old_telescope(): for param, tel_param in old_telescope_metadata_attrs.items(): if tel_param is not None: tel_val = getattr(new_telescope, tel_param) - setattr(test_obj, param, tel_val) - param_val = getattr(test_obj, param) + with uvtest.check_warnings( + DeprecationWarning, + match=f"The UVData.{param} attribute now just points to the " + f"{tel_param} attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + setattr(test_obj, param, tel_val) + with uvtest.check_warnings( + DeprecationWarning, + match=f"The UVData.{param} attribute now just points to the " + f"{tel_param} attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + param_val = getattr(test_obj, param) if not isinstance(param_val, np.ndarray): assert param_val == tel_val else: @@ -561,17 +612,41 @@ def test_setattr_old_telescope(): assert param_val.tolist() == tel_val.tolist() elif param == "telescope_location": tel_val = new_telescope._location.xyz() - test_obj.telescope_location = tel_val + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location attribute now just points " + "to the location attribute on the telescope object " + "(at UVData.telescope). Accessing it this way is deprecated, " + "please access it via the telescope object. This will " + "become an error in version 3.2.", + ): + test_obj.telescope_location = tel_val assert new_telescope.location == test_obj.telescope.location elif param == "telescope_location_lat_lon_alt": tel_val = new_telescope._location.lat_lon_alt() - test_obj.telescope_location_lat_lon_alt = tel_val + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location_lat_lon_alt attribute now " + "just points to the location attribute on the telescope object " + "(at UVData.telescope). Accessing it this way is deprecated, " + "please access it via the telescope object. This will " + "become an error in version 3.2.", + ): + test_obj.telescope_location_lat_lon_alt = tel_val np.testing.assert_allclose( new_telescope._location.xyz(), test_obj.telescope._location.xyz() ) elif param == "telescope_location_lat_lon_alt_degrees": tel_val = new_telescope._location.lat_lon_alt_degrees() - test_obj.telescope_location_lat_lon_alt_degrees = tel_val + with uvtest.check_warnings( + DeprecationWarning, + match="The UVData.telescope_location_lat_lon_alt_degrees " + "attribute now just points to the location attribute on the " + "telescope object (at UVData.telescope). Accessing it this " + "way is deprecated, please access it via the telescope " + "object. This will become an error in version 3.2.", + ): + test_obj.telescope_location_lat_lon_alt_degrees = tel_val np.testing.assert_allclose( new_telescope._location.xyz(), test_obj.telescope._location.xyz() ) diff --git a/pyuvdata/uvbase.py b/pyuvdata/uvbase.py index c1f0212c0a..85444a2380 100644 --- a/pyuvdata/uvbase.py +++ b/pyuvdata/uvbase.py @@ -144,9 +144,20 @@ def __setstate__(self, state): def __getattr__(self, __name): """Handle old names for telescope metadata.""" if __name in old_telescope_metadata_attrs: - # _warn_old_phase_attr(__name) - if hasattr(self, "telescope"): + if old_telescope_metadata_attrs[__name] is not None: + tel_param = old_telescope_metadata_attrs[__name] + else: + tel_param = "location" + warnings.warn( + f"The UVData.{__name} attribute now just points to the " + f"{tel_param} attribute on the telescope object (at " + "UVData.telescope). Accessing it this way is deprecated, " + "please access it via the telescope object. This will " + "become an error in version 3.2.", + DeprecationWarning, + ) + tel_name = old_telescope_metadata_attrs[__name] if tel_name is not None: # if it's a simple remapping, just return the value @@ -166,9 +177,20 @@ def __getattr__(self, __name): def __setattr__(self, __name, __value): """Handle old names for telescope metadata.""" if __name in old_telescope_metadata_attrs: - # _warn_old_phase_attr(__name) - if hasattr(self, "telescope"): + if old_telescope_metadata_attrs[__name] is not None: + tel_param = old_telescope_metadata_attrs[__name] + else: + tel_param = "location" + warnings.warn( + f"The UVData.{__name} attribute now just points to the " + f"{tel_param} attribute on the telescope object (at " + "UVData.telescope). Accessing it this way is deprecated, " + "please access it via the telescope object. This will " + "become an error in version 3.2.", + DeprecationWarning, + ) + tel_name = old_telescope_metadata_attrs[__name] if tel_name is not None: # if it's a simple remapping, just set the value From a8a6a2d3497bef9771f40dbfc7fb9a8829628e33 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 12:13:13 -0700 Subject: [PATCH 43/59] Fix a couple of problems I missed --- pyuvdata/uvflag/tests/test_uvflag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 364b0d40fb..8cf419a096 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -1942,9 +1942,9 @@ def test_add_antenna(uvcal_obj, diameters): [name + "_new" for name in uv2.telescope.antenna_names] ) if diameters == "left": - uv2.antenna_diameters = None + uv2.telescope.antenna_diameters = None elif diameters == "right": - uv2.antenna_diameters = None + uv2.telescope.antenna_diameters = None if diameters == "both": warn_type = None @@ -1957,7 +1957,7 @@ def test_add_antenna(uvcal_obj, diameters): uv3 = uv1.__add__(uv2, axis="antenna") if diameters != "both": - assert uv3.antenna_diameters is None + assert uv3.telescope.antenna_diameters is None assert np.array_equal(np.concatenate((uv1.ant_array, uv2.ant_array)), uv3.ant_array) assert np.array_equal( From 61562507e72ca2c608d7bb7f30348154c7fb6cbc Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 12:20:13 -0700 Subject: [PATCH 44/59] fix a lingering reference in the tutorials --- docs/uvcal_tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/uvcal_tutorial.rst b/docs/uvcal_tutorial.rst index 240a7ee2fc..120a5be0cc 100644 --- a/docs/uvcal_tutorial.rst +++ b/docs/uvcal_tutorial.rst @@ -334,7 +334,7 @@ a) Calibration of UVData by UVCal >>> # this is an old calfits file which has the wrong antenna names, so we need to fix them first. >>> # fix the antenna names in the uvcal object to match the uvdata object >>> uvc.telescope.antenna_names = np.array( - ... [name.replace("ant", "HH") for name in uvc.antenna_names] + ... [name.replace("ant", "HH") for name in uvc.telescope.antenna_names] ... ) >>> uvd_calibrated = utils.uvcalibrate(uvd, uvc, inplace=False) From 137643992d22c965b6e936fd0edb12f23f56bd8e Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Thu, 2 May 2024 12:28:57 -0700 Subject: [PATCH 45/59] speed up fhd test --- pyuvdata/uvdata/tests/test_fhd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyuvdata/uvdata/tests/test_fhd.py b/pyuvdata/uvdata/tests/test_fhd.py index bd1dd83177..977a9c2420 100644 --- a/pyuvdata/uvdata/tests/test_fhd.py +++ b/pyuvdata/uvdata/tests/test_fhd.py @@ -332,7 +332,9 @@ def test_read_fhd_latlonalt_match_xyz(fhd_data_files): "['layout', 'obs']", ], ): - fhd_uv = UVData.from_file(**fhd_data_files, use_future_array_shapes=True) + fhd_uv = UVData.from_file( + **fhd_data_files, use_future_array_shapes=True, read_data=False + ) mwa_tel = Telescope.from_known_telescopes("mwa") From 3aaab3f612d433824992a5a12443b9a1494181f3 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 11:56:01 -0700 Subject: [PATCH 46/59] Address review comments --- pyuvdata/hdf5_utils.py | 81 +++++++++----------------- pyuvdata/telescopes.py | 87 ++++++++++++---------------- pyuvdata/tests/test_telescopes.py | 19 ++---- pyuvdata/uvflag/tests/test_uvflag.py | 15 +++++ 4 files changed, 85 insertions(+), 117 deletions(-) diff --git a/pyuvdata/hdf5_utils.py b/pyuvdata/hdf5_utils.py index be4df37e80..882bfdc3cf 100644 --- a/pyuvdata/hdf5_utils.py +++ b/pyuvdata/hdf5_utils.py @@ -396,12 +396,9 @@ def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]: elif "telescope_location" in h: # this branch is for old UVFlag files, which were written with an # ECEF 'telescope_location' key rather than the more standard + # latitude in degrees, longitude in degrees, altitude - lat, lon, alt = uvutils.LatLonAlt_from_XYZ( - self.telescope_location, - frame=self.telescope_frame, - ellipsoid=self.ellipsoid, - ) + lat, lon, alt = self.telescope_location_lat_lon_alt return lat * 180.0 / np.pi, lon * 180.0 / np.pi, alt @cached_property @@ -448,48 +445,25 @@ def ellipsoid(self) -> str: @cached_property def telescope_location_obj(self): """The telescope location object.""" - h = self.header - if "latitude" in h and "longitude" in h and "altitude" in h: - if self.telescope_frame == "itrs": - return EarthLocation.from_geodetic( - lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, - lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, - height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, - ) - else: - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with MCMF frames." - ) - return MoonLocation.from_selenodetic( - lat=self.telescope_location_lat_lon_alt_degrees[0] * units.deg, - lon=self.telescope_location_lat_lon_alt_degrees[1] * units.deg, - height=self.telescope_location_lat_lon_alt_degrees[2] * units.m, - ellipsoid=self.ellipsoid, - ) - elif "telescope_location" in h: - # this branch is for old UVFlag files, which were written with an - # ECEF 'telescope_location' key rather than the more standard - # latitude in degrees, longitude in degrees, altitude - loc_xyz = self.telescope_location - if self.telescope_frame == "itrs": - return EarthLocation.from_geocentric( - x=loc_xyz[0] * units.m, - y=loc_xyz[1] * units.m, - z=loc_xyz[2] * units.m, - ) - else: - if not hasmoon: - raise ValueError( - "Need to install `lunarsky` package to work with MCMF frames." - ) - moon_loc = MoonLocation.from_selenocentric( - x=loc_xyz[0] * units.m, - y=loc_xyz[1] * units.m, - z=loc_xyz[2] * units.m, + lla_deg = self.telescope_location_lat_lon_alt_degrees + if lla_deg is None: + return None + lat_deg, lon_deg, alt = lla_deg + if self.telescope_frame == "itrs": + return EarthLocation.from_geodetic( + lat=lat_deg * units.deg, lon=lon_deg * units.deg, height=alt * units.m + ) + else: + if not hasmoon: + raise ValueError( + "Need to install `lunarsky` package to work with MCMF frames." ) - moon_loc.ellipsoid = self.ellipsoid - return moon_loc + return MoonLocation.from_selenodetic( + lat=lat_deg * units.deg, + lon=lon_deg * units.deg, + height=alt * units.m, + ellipsoid=self.ellipsoid, + ) @cached_property def telescope_location(self): @@ -501,18 +475,19 @@ def telescope_location(self): # latitude in degrees, longitude in degrees, altitude return h.telescope_location elif "latitude" in h and "longitude" in h and "altitude" in h: - return uvutils.XYZ_from_LatLonAlt( - *self.telescope_location_lat_lon_alt, - frame=self.telescope_frame, - ellipsoid=self.ellipsoid, + loc_obj = self.telescope_location_obj + return np.array( + [ + loc_obj.x.to("m").value, + loc_obj.y.to("m").value, + loc_obj.z.to("m").value, + ] ) @cached_property def antenna_names(self) -> list[str]: """The antenna names in the file.""" - return np.array( - [bytes(name).decode("utf8") for name in self.header["antenna_names"][:]] - ) + return np.char.decode(self.header["antenna_names"][:], encoding="utf8") @cached_property def extra_keywords(self) -> dict: diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index e3dfe82685..8afc64f4f6 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -44,20 +44,20 @@ # relative to the telescope location. KNOWN_TELESCOPES = { "PAPER": { - "center_xyz": None, - "latitude": Angle("-30d43m17.5s").radian, - "longitude": Angle("21d25m41.9s").radian, - "altitude": 1073.0, + "location": EarthLocation.from_geodetic( + lat=Angle("-30d43m17.5s"), lon=Angle("21d25m41.9s"), height=1073.0 * units.m + ), "citation": ( "value taken from capo/cals/hsa7458_v000.py, " "comment reads KAT/SA (GPS), altitude from elevationmap.net" ), }, "HERA": { - "center_xyz": None, - "latitude": Angle("-30.72152612068925d").radian, - "longitude": Angle("21.42830382686301d").radian, - "altitude": 1051.69, + "location": EarthLocation.from_geodetic( + lat=Angle("-30.72152612068925d"), + lon=Angle("21.42830382686301d"), + height=1051.69 * units.m, + ), "antenna_diameters": 14.0, "antenna_positions_file": "hera_ant_pos.csv", "citation": ( @@ -66,24 +66,27 @@ ), }, "SMA": { - "center_xyz": None, - "latitude": Angle("19d49m27.13895s").radian, - "longitude": Angle("-155d28m39.08279s").radian, - "altitude": 4083.948144, + "location": EarthLocation.from_geodetic( + lat=Angle("19d49m27.13895s"), + lon=Angle("-155d28m39.08279s"), + height=4083.948144 * units.m, + ), "citation": "Ho, P. T. P., Moran, J. M., & Lo, K. Y. 2004, ApJL, 616, L1", }, "SZA": { - "center_xyz": None, - "latitude": Angle("37d16m49.3698s").radian, - "longitude": Angle("-118d08m29.9126s").radian, - "altitude": 2400.0, + "location": EarthLocation.from_geodetic( + lat=Angle("37d16m49.3698s"), + lon=Angle("-118d08m29.9126s"), + height=2400.0 * units.m, + ), "citation": "Unknown", }, "OVRO-LWA": { - "center_xyz": None, - "latitude": Angle("37.239777271d").radian, - "longitude": Angle("-118.281666695d").radian, - "altitude": 1183.48, + "location": EarthLocation.from_geodetic( + lat=Angle("37.239777271d"), + lon=Angle("-118.281666695d"), + height=1183.48 * units.m, + ), "citation": "OVRO Sharepoint Documentation", }, "MWA": {"antenna_positions_file": "mwa_ant_pos.csv"}, @@ -131,40 +134,24 @@ def known_telescope_location( """ astropy_sites = EarthLocation.get_site_names() - telescope_keys = list(known_telescope_dict.keys()) - telescope_list = [tel.lower() for tel in telescope_keys] + known_telescopes = {k.lower(): v for k, v in known_telescope_dict.items()} # first deal with location. if name in astropy_sites: location = EarthLocation.of_site(name) citation = "astropy sites" - elif name.lower() in telescope_list: - telescope_index = telescope_list.index(name.lower()) - telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] + elif name.lower() in known_telescopes: + telescope_dict = known_telescopes[name.lower()] citation = telescope_dict["citation"] - if telescope_dict["center_xyz"] is not None: - location = EarthLocation.from_geocentric( - *telescope_dict["center_xyz"], unit="m" - ) - else: - if ( - telescope_dict["latitude"] is None - or telescope_dict["longitude"] is None - or telescope_dict["altitude"] is None - ): - raise ValueError( - "Bad location information in known_telescopes_dict " - f"for telescope {name}. Either the center_xyz " - "or the latitude, longitude and altitude of the " - "telescope must be specified." - ) - location = EarthLocation.from_geodetic( - lat=telescope_dict["latitude"] * units.rad, - lon=telescope_dict["longitude"] * units.rad, - height=telescope_dict["altitude"] * units.m, - ) + try: + location = telescope_dict["location"] + except KeyError as ke: + raise KeyError( + "Missing location information in known_telescopes_dict " + f"for telescope {name}." + ) from ke else: # no telescope matching this name raise ValueError( @@ -428,8 +415,7 @@ def update_params_from_known_telescopes( "The telescope name attribute must be set to update from " "known_telescopes." ) - telescope_keys = list(known_telescope_dict.keys()) - telescope_list = [tel.lower() for tel in telescope_keys] + known_telescopes = {k.lower(): v for k, v in known_telescope_dict.items()} astropy_sites_list = [] known_telescope_list = [] @@ -447,9 +433,8 @@ def update_params_from_known_telescopes( known_telescope_list.append("telescope_location") # check for extra info - if self.name.lower() in telescope_list: - telescope_index = telescope_list.index(self.name.lower()) - telescope_dict = known_telescope_dict[telescope_keys[telescope_index]] + if self.name.lower() in known_telescopes: + telescope_dict = known_telescopes[self.name.lower()] if "antenna_positions_file" in telescope_dict.keys() and ( overwrite diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index e76f1b5f95..a9adf9a5b8 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -211,20 +211,14 @@ def test_from_known(): def test_get_telescope_center_xyz(): ref_xyz = (-2562123.42683, 5094215.40141, -2848728.58869) - ref_latlonalt = (-26.7 * np.pi / 180.0, 116.7 * np.pi / 180.0, 377.8) + # ref_latlonalt = (-26.7 * np.pi / 180.0, 116.7 * np.pi / 180.0, 377.8) test_telescope_dict = { "test": { - "center_xyz": ref_xyz, - "latitude": None, - "longitude": None, - "altitude": None, + "location": EarthLocation.from_geocentric(*ref_xyz, unit="m"), "citation": "", }, "test2": { - "center_xyz": ref_xyz, - "latitude": ref_latlonalt[0], - "longitude": ref_latlonalt[1], - "altitude": ref_latlonalt[2], + "location": EarthLocation.from_geocentric(*ref_xyz, unit="m"), "citation": "", }, } @@ -256,10 +250,9 @@ def test_get_telescope_no_loc(): } } with pytest.raises( - ValueError, - match="Bad location information in known_telescopes_dict for telescope " - "test. Either the center_xyz or the latitude, longitude and altitude of " - "the telescope must be specified.", + KeyError, + match="Missing location information in known_telescopes_dict " + "for telescope test.", ): Telescope.from_known_telescopes( "test", known_telescope_dict=test_telescope_dict diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 8cf419a096..51b6a0e8cb 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -9,6 +9,7 @@ import shutil import warnings +import astropy.units as units import h5py import numpy as np import pytest @@ -849,9 +850,23 @@ def test_hdf5_meta_telescope_location(test_outfile): if hasmoon: from lunarsky import MoonLocation + moon_loc = MoonLocation.from_selenodetic( + lat * units.rad, lon * units.rad, alt * units.m + ) + moon_xyz = np.array( + [ + moon_loc.x.to("m").value, + moon_loc.y.to("m").value, + moon_loc.z.to("m").value, + ] + ) + shutil.copyfile(test_f_file, test_outfile) with h5py.File(test_outfile, "r+") as h5f: h5f["Header/telescope_frame"] = "mcmf" + del h5f["Header/telescope_location"] + h5f["Header/telescope_location"] = moon_xyz + meta = hdf5_utils.HDF5Meta(test_outfile) assert meta.telescope_frame == "mcmf" assert isinstance(meta.telescope_location_obj, MoonLocation) From 5445b270a8aa4ca5cbd7acaddb87c1d7fde78e1d Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 15:04:11 -0700 Subject: [PATCH 47/59] Fix a few things for astropy future warnings --- pyuvdata/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 829fc3ecb8..3dc582f702 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -3250,6 +3250,7 @@ def transform_icrs_to_app( frame="altaz", ) ) + time_obj_array = Time(time_obj_array, location=site_loc) else: sky_coord = LunarSkyCoord( ra=ra_coord, @@ -3270,7 +3271,6 @@ def transform_icrs_to_app( ) time_obj_array = LTime(time_obj_array) - time_obj_array.location = site_loc app_ha, app_dec = erfa.ae2hd( azel_data.az.rad, azel_data.alt.rad, site_loc.lat.rad ) @@ -3563,8 +3563,8 @@ def transform_app_to_icrs( if astrometry_library == "astropy": if hasmoon and isinstance(site_loc, MoonLocation): time_obj_array = LTime(time_obj_array) - - time_obj_array.location = site_loc + else: + time_obj_array = Time(time_obj_array, location=site_loc) az_coord, el_coord = erfa.hd2ae( np.mod( From 12ab07d8a90d587266ab38fb10e4b3818f74efd3 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 15:14:31 -0700 Subject: [PATCH 48/59] fix astropy warnings for lunar locations --- pyuvdata/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 3dc582f702..3d981369f8 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -3269,7 +3269,7 @@ def transform_icrs_to_app( frame="lunartopo", ) ) - time_obj_array = LTime(time_obj_array) + time_obj_array = LTime(time_obj_array, location=site_loc) app_ha, app_dec = erfa.ae2hd( azel_data.az.rad, azel_data.alt.rad, site_loc.lat.rad @@ -3562,7 +3562,7 @@ def transform_app_to_icrs( if astrometry_library == "astropy": if hasmoon and isinstance(site_loc, MoonLocation): - time_obj_array = LTime(time_obj_array) + time_obj_array = LTime(time_obj_array, location=site_loc) else: time_obj_array = Time(time_obj_array, location=site_loc) From aec9a04958164435b00f27f1daf892168f71c196 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 16:19:00 -0700 Subject: [PATCH 49/59] Fix docs errors --- docs/make_telescope.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/make_telescope.py b/docs/make_telescope.py index 6854ce1da5..59b6eab9e8 100644 --- a/docs/make_telescope.py +++ b/docs/make_telescope.py @@ -4,6 +4,7 @@ Format the Telescope object parameters into a sphinx rst file. """ +import copy import inspect import json import os @@ -79,18 +80,27 @@ def write_telescope_rst(write_file=None): out += "Methods\n-------\n.. autoclass:: pyuvdata.Telescope\n :members:\n\n" out += ( - "Known Telescopes\n================\n\n" - "Known Telescope Data\n--------------------\n" + "Known Telescopes\n----------------\n\n" "pyuvdata uses `Astropy sites\n" "`_\n" "for telescope location information, in addition to the following\n" - "telescope information that is tracked within pyuvdata. Note that for\n" + "telescope information that is tracked within pyuvdata. Note that the\n" + "location entry is actually stored as an\n" + ":class:`astropy.coordinates.EarthLocation` object, which\n" + "is shown here using the Geodetic representation. Also note that for\n" "some telescopes we store csv files giving antenna layout information\n" "which can be used if data files are missing that information.\n\n" ) - json_obj = json.dumps(KNOWN_TELESCOPES, sort_keys=True, indent=4) + known_tel_use = copy.deepcopy(KNOWN_TELESCOPES) + for tel, tel_dict in KNOWN_TELESCOPES.items(): + if "location" in tel_dict: + known_tel_use[tel]["location"] = ( + tel_dict["location"].to_geodetic().__repr__() + ) + + json_obj = json.dumps(known_tel_use, sort_keys=True, indent=4) json_obj = json_obj[:-1] + " }" out += ".. code-block:: JavaScript\n\n {json_str}\n\n".format(json_str=json_obj) From 87bcf822b1c4dc8e5a4cc6a42bd245f1e40b9885 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 17:01:23 -0700 Subject: [PATCH 50/59] update the changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d4cff61b..f1ccece61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- Several new methods on `Telescope` objects, including the classmethods +`from_known_telescopes` and `from_params` to instantiate new objects and the +`get_enu_antpos` method to get antenna positions in East, North, Up coordinates. - Support for writing "MODEL_DATA" and "CORRECTED_DATA" columns has been added to the `UVData.write_ms` method. - Support for "flexible-Jones" `UVCal` objects -- where different spectral windows can @@ -27,6 +30,18 @@ time for each time range or the time_array (if there's a time_array and no time_ - Added new keyword handling for v.6 of the MIR data format within `MirParser`. ### Changed +- Telescope-related metadata (including antenna metadata) on `UVData`, `UVCal` +and `UVFlag` have been refactored into a `Telescope` object (attached to these +objects as the `telescope` attribute) and most of the code related to these +attributes from the three objects has been consolidated on the `Telescope` object. +These attributes are still accessible via their old names on the objects, although +accessing them that way is now deprecated. Note that the telescope locations are +now stored under the hood as astropy EarthLocation objects (e.g. `UVData.telescope.location`) +(or as MoonLocation objects for simulated arrays on the moon if `lunarsky` is +installed). +- The `UVData.new` and `UVCal.new` methods now accept a telescope object for +setting the telescope-related metadata (the older parameters are still accepted +but deprecated.) - When FHD calibration solutions are read in, the `time_range` is now set to be one quarter of an integration time before and after the earliest and latest times respectively. This is a change from extending it by half an integration time to @@ -49,6 +64,15 @@ times for those solutions. - updated minimum cython dependency to 3.0. - Updated minimum optional dependency versions: astropy-healpix>=1.0.2 +### Deprecated +- Accessing the telescope-related metadata through their old attribute names on +`UVData`, `UVCal` and `UVFlag` rather than via their attributes on the attached +`Telescope` object (e.g. `UVData.telescope_name` -> `UVData.telescope.name` and +`UVData.antenna_positions` -> `UVData.telescope.antenna_positions`). +- Passing telescope-related metadata as separate parameters to `UVData.new` and +`UVCal.new` rather than `Telescope` objects. +- The `UVData.get_ENU_antpos` method in favor of `UVData.telescope.get_enu_antpos`. + ### Fixed - Fixed a bug in `UVBase` where `allowed_failures` was being ignored if a parameter had `required=True` set. From 4453446c78f37050cc78efc150cc5c4ebd5fc7de Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 6 May 2024 17:28:27 -0700 Subject: [PATCH 51/59] Add handling for transient spiceypy error --- pyuvdata/uvdata/tests/test_uvfits.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyuvdata/uvdata/tests/test_uvfits.py b/pyuvdata/uvdata/tests/test_uvfits.py index 194792732f..1775ad13ab 100644 --- a/pyuvdata/uvdata/tests/test_uvfits.py +++ b/pyuvdata/uvdata/tests/test_uvfits.py @@ -528,6 +528,7 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se if telescope_frame == "mcmf": pytest.importorskip("lunarsky") from lunarsky import MoonLocation + from spiceypy.utils.exceptions import SpiceUNKNOWNFRAME enu_antpos = uv_in.telescope.get_enu_antpos() uv_in.telescope.location = MoonLocation.from_selenodetic( @@ -544,7 +545,10 @@ def test_readwriteread(tmp_path, casa_uvfits, future_shapes, telescope_frame, se ) uv_in.set_lsts_from_time_array() uv_in.set_uvws_from_antenna_positions() - uv_in._set_app_coords_helper() + try: + uv_in._set_app_coords_helper() + except SpiceUNKNOWNFRAME as err: + pytest.skip("SpiceUNKNOWNFRAME error: " + str(err)) uv_in.check() uv_out = UVData() From 02464823dd4593a7eb7615d4b16f0a1084c4914b Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Tue, 7 May 2024 08:48:44 -0700 Subject: [PATCH 52/59] change Telescope.from_params to Telescope.new to match other objects --- CHANGELOG.md | 2 +- docs/uvcal_tutorial.rst | 2 +- docs/uvdata_tutorial.rst | 8 ++++---- pyuvdata/telescopes.py | 2 +- pyuvdata/tests/test_telescopes.py | 10 +++++----- pyuvdata/tests/test_uvbase.py | 2 +- pyuvdata/uvcal/initializers.py | 2 +- pyuvdata/uvcal/tests/test_initializers.py | 8 ++++---- pyuvdata/uvdata/initializers.py | 2 +- pyuvdata/uvdata/tests/test_initializers.py | 6 +++--- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ccece61d..1223141efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. ### Added - Several new methods on `Telescope` objects, including the classmethods -`from_known_telescopes` and `from_params` to instantiate new objects and the +`from_known_telescopes` and `new` to instantiate new objects and the `get_enu_antpos` method to get antenna positions in East, North, Up coordinates. - Support for writing "MODEL_DATA" and "CORRECTED_DATA" columns has been added to the `UVData.write_ms` method. diff --git a/docs/uvcal_tutorial.rst b/docs/uvcal_tutorial.rst index 120a5be0cc..c10dae5a77 100644 --- a/docs/uvcal_tutorial.rst +++ b/docs/uvcal_tutorial.rst @@ -255,7 +255,7 @@ of creating a consistent object from a minimal set of inputs ... cal_style = "redundant", ... freq_array = np.linspace(1e8, 2e8, 100), ... jones_array = ["ee", "nn"], - ... telescope = Telescope.from_params( + ... telescope = Telescope.new( ... antenna_positions = { ... 0: [0.0, 0.0, 0.0], ... 1: [0.0, 0.0, 1.0], diff --git a/docs/uvdata_tutorial.rst b/docs/uvdata_tutorial.rst index ec9895a27f..ea3d379772 100644 --- a/docs/uvdata_tutorial.rst +++ b/docs/uvdata_tutorial.rst @@ -429,7 +429,7 @@ of creating a consistent object from a minimal set of inputs >>> uvd = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... telescope = Telescope.from_params( + ... telescope = Telescope.new( ... antenna_positions = { ... 0: [0.0, 0.0, 0.0], ... 1: [0.0, 0.0, 1.0], @@ -465,7 +465,7 @@ where each baseline observed one time each. This case is ambiguous without the >>> uvd = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... telescope = Telescope.from_params( + ... telescope = Telescope.new( ... antenna_positions = { ... 0: [0.0, 0.0, 0.0], ... 1: [0.0, 0.0, 1.0], @@ -494,7 +494,7 @@ provided times and baselines, which would have resulted in 16 times: >>> uvd_rect = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... telescope = Telescope.from_params( + ... telescope = Telescope.new( ... antenna_positions = { ... 0: [0.0, 0.0, 0.0], ... 1: [0.0, 0.0, 1.0], @@ -523,7 +523,7 @@ To change the order of the blt-axis, set the ``time_axis_faster_than_bls`` keywo >>> uvd_rect = UVData.new( ... freq_array = np.linspace(1e8, 2e8, 100), ... polarization_array = ["xx", "yy"], - ... telescope = Telescope.from_params( + ... telescope = Telescope.new( ... antenna_positions = { ... 0: [0.0, 0.0, 0.0], ... 1: [0.0, 0.0, 1.0], diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 8afc64f4f6..7e501b79b6 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -607,7 +607,7 @@ def from_known_telescopes( return tel_obj @classmethod - def from_params( + def new( cls, name: str, location: Locations, diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index a9adf9a5b8..a06b0b51dd 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -307,13 +307,13 @@ def test_alternate_antenna_inputs(): assert np.all(nums == nums2) -def test_from_params_errors(simplest_working_params): +def test_new_errors(simplest_working_params): simplest_working_params["location"] = Quantity([0, 0, 0], unit="m") with pytest.raises( ValueError, match="telescope_location has an unsupported type, it must be one of ", ): - Telescope.from_params(**simplest_working_params) + Telescope.new(**simplest_working_params) @pytest.mark.parametrize( @@ -404,12 +404,12 @@ def test_from_params_errors(simplest_working_params): ) def test_bad_antenna_inputs(kwargs, err_msg): with pytest.raises(ValueError, match=err_msg): - Telescope.from_params(**kwargs) + Telescope.new(**kwargs) @pytest.mark.parametrize("xorient", ["e", "n", "east", "NORTH"]) def test_passing_xorient(simplest_working_params, xorient): - tel = Telescope.from_params(x_orientation=xorient, **simplest_working_params) + tel = Telescope.new(x_orientation=xorient, **simplest_working_params) if xorient.lower().startswith("e"): assert tel.x_orientation == "east" else: @@ -417,7 +417,7 @@ def test_passing_xorient(simplest_working_params, xorient): def test_passing_diameters(simplest_working_params): - tel = Telescope.from_params( + tel = Telescope.new( antenna_diameters=np.array([14.0, 15.0, 16.0]), **simplest_working_params ) np.testing.assert_allclose(tel.antenna_diameters, np.array([14.0, 15.0, 16.0])) diff --git a/pyuvdata/tests/test_uvbase.py b/pyuvdata/tests/test_uvbase.py index 1af5e65dab..0cb93e7c82 100644 --- a/pyuvdata/tests/test_uvbase.py +++ b/pyuvdata/tests/test_uvbase.py @@ -180,7 +180,7 @@ def __init__(self): self._telescope = uvp.UVParameter( "telescope", description="A telescope.", - value=Telescope.from_params( + value=Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="mock", antenna_positions={ diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index 67b0b911a4..9eafc45827 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -221,7 +221,7 @@ def new_uvcal( ) if telescope is None: - telescope = Telescope.from_params( + telescope = Telescope.new( name=telescope_name, location=telescope_location, antenna_positions=antenna_positions, diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index e4495e0b0e..9b41c1124a 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -20,7 +20,7 @@ def uvd_kw(): return { "freq_array": np.linspace(100e6, 200e6, 10), "times": np.linspace(2459850, 2459851, 12), - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="mock", instrument="mock", @@ -70,7 +70,7 @@ def uvc_simplest(): return { "freq_array": np.linspace(100e6, 200e6, 10), "time_array": np.linspace(2459850, 2459851, 12), - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="mock", x_orientation="n", @@ -95,7 +95,7 @@ def uvc_simplest_moon(): return { "freq_array": np.linspace(100e6, 200e6, 10), "time_array": np.linspace(2459850, 2459851, 12), - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=MoonLocation.from_selenodetic(0, 0, 0), name="mock", x_orientation="n", @@ -185,7 +185,7 @@ def test_new_uvcal_time_range(uvc_simplest): ], [ { - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="mock", antenna_positions={ diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 9c14537dac..3743dd2087 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -502,7 +502,7 @@ def new_uvdata( if telescope is None: if instrument is None: instrument = telescope_name - telescope = Telescope.from_params( + telescope = Telescope.new( name=telescope_name, location=telescope_location, antenna_positions=antenna_positions, diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index 5e1f888632..112254cd83 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -45,7 +45,7 @@ def simplest_working_params() -> dict[str, Any]: return { "freq_array": np.linspace(1e8, 2e8, 100), "polarization_array": ["xx", "yy"], - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="test", instrument="test", @@ -67,7 +67,7 @@ def lunar_simple_params() -> dict[str, Any]: return { "freq_array": np.linspace(1e8, 2e8, 100), "polarization_array": ["xx", "yy"], - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=MoonLocation.from_selenodetic(0, 0, 0), name="test", instrument="test", @@ -118,7 +118,7 @@ def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: ], [ { - "telescope": Telescope.from_params( + "telescope": Telescope.new( location=EarthLocation.from_geodetic(0, 0, 0), name="test", antenna_positions={ From e174936a5737ca03cbc57ebfbb2a147b4e7ae3ea Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Tue, 7 May 2024 16:00:55 -0700 Subject: [PATCH 53/59] Fix bug in phase_to_time for moon locations, add test exposed in pyuvsim tests --- pyuvdata/uvdata/tests/test_uvdata.py | 61 ++++++++++++++++++++++++++++ pyuvdata/uvdata/uvdata.py | 25 ++++++++---- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index d48b0f95cf..07d5894201 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -24,6 +24,7 @@ import pyuvdata.utils as uvutils from pyuvdata import UVCal, UVData from pyuvdata.data import DATA_PATH +from pyuvdata.tests.test_utils import frame_selenoid from pyuvdata.uvdata.tests.test_mwa_corr_fits import filelist as mwa_corr_files from pyuvdata.uvdata.uvdata import _future_array_shapes_warning, old_phase_attrs @@ -1099,6 +1100,66 @@ def test_phase_to_time_error(hera_uvh5): uv_phase.phase_to_time("foo") +@pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +@pytest.mark.parametrize(["telescope_frame", "selenoid"], frame_selenoid) +def test_phase_to_time(casa_uvfits, telescope_frame, selenoid): + uv_in = casa_uvfits + phase_time = Time(uv_in.time_array[0], format="jd") + + if telescope_frame == "mcmf": + pytest.importorskip("lunarsky") + from lunarsky import MoonLocation + + enu_antpos = uv_in.telescope.get_enu_antpos() + uv_in.telescope.location = MoonLocation.from_selenodetic( + lat=uv_in.telescope.location.lat, + lon=uv_in.telescope.location.lon, + height=uv_in.telescope.location.height, + ellipsoid=selenoid, + ) + new_full_antpos = uvutils.ECEF_from_ENU( + enu=enu_antpos, center_loc=uv_in.telescope.location + ) + uv_in.telescope.antenna_positions = ( + new_full_antpos - uv_in.telescope._location.xyz() + ) + uv_in.set_lsts_from_time_array() + uv_in.check() + + zenith_coord = uvutils.LunarSkyCoord( + alt=Angle(90 * units.deg), + az=Angle(0 * units.deg), + obstime=phase_time, + frame="lunartopo", + location=uv_in.telescope.location, + ) + else: + zenith_coord = SkyCoord( + alt=Angle(90 * units.deg), + az=Angle(0 * units.deg), + obstime=phase_time, + frame="altaz", + location=uv_in.telescope.location, + ) + zen_icrs = zenith_coord.transform_to("icrs") + + uv_in.phase_to_time(uv_in.time_array[0]) + + assert np.isclose(uv_in.phase_center_catalog[1]["cat_lat"], zen_icrs.dec.rad) + assert np.isclose(uv_in.phase_center_catalog[1]["cat_lon"], zen_icrs.ra.rad) + + assert np.isclose( + uv_in.phase_center_catalog[1]["cat_lon"], uv_in.lst_array[0], 1e-3 + ) + + if telescope_frame == "itrs": + assert np.isclose( + uv_in.phase_center_catalog[1]["cat_lat"], + uv_in.telescope.location.lat.rad, + 1e-2, + ) + + @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when") @pytest.mark.filterwarnings("ignore:The original `phase` method is deprecated") @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 5b19de7ecd..16213bc153 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -5307,13 +5307,24 @@ def phase_to_time( # Generate ra/dec of zenith at time in the phase_frame coordinate # system to use for phasing - zenith_coord = SkyCoord( - alt=Angle(90 * units.deg), - az=Angle(0 * units.deg), - obstime=time, - frame="altaz", - location=self.telescope.location, - ) + if uvutils.hasmoon and isinstance( + self.telescope.location, uvutils.MoonLocation + ): + zenith_coord = uvutils.LunarSkyCoord( + alt=Angle(90 * units.deg), + az=Angle(0 * units.deg), + obstime=time, + frame="lunartopo", + location=self.telescope.location, + ) + else: + zenith_coord = SkyCoord( + alt=Angle(90 * units.deg), + az=Angle(0 * units.deg), + obstime=time, + frame="altaz", + location=self.telescope.location, + ) obs_zenith_coord = zenith_coord.transform_to(phase_frame) zenith_ra = obs_zenith_coord.ra.rad From e1f34dd0e1b885f8d413d1633c7eb9f146d66981 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Tue, 7 May 2024 23:50:42 -0700 Subject: [PATCH 54/59] Fix some things found in pyuvsim testing --- pyuvdata/uvdata/initializers.py | 19 +++++++++++++- pyuvdata/uvdata/tests/test_initializers.py | 29 ++++++++++++++++++++++ pyuvdata/uvdata/uvfits.py | 4 ++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 3743dd2087..181188b363 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -329,7 +329,7 @@ def new_uvdata( telescope name and location, x_orientation and antenna names, numbers and positions. antpairs : sequence of 2-tuples of int or 2D array of int, optional - Antenna pairs in the data. If an ndarray, must have shape (Nants, 2). + Antenna pairs in the data. If an ndarray, must have shape (N antpairs, 2). These may be the *unique* antpairs of the data if each antpair observes the same set of times, otherwise they should be an Nblts-length array of each antpair at each time. It is recommended @@ -522,8 +522,11 @@ def new_uvdata( ) if antpairs is None: + bl_order = ("ant1", "ant2") antpairs = list(combinations_with_replacement(telescope.antenna_numbers, 2)) do_blt_outer = True + else: + bl_order = None ( nbls, @@ -541,6 +544,19 @@ def new_uvdata( time_axis_faster_than_bls=time_axis_faster_than_bls, time_sized_arrays=(lst_array, integration_time), ) + + if not do_blt_outer: + if time_array.size != antpairs.shape[0]: + raise ValueError("Length of time array must match the number of antpairs.") + + if bl_order is not None and blts_are_rectangular: + if time_axis_faster_than_bls: + blt_order = ("time", "ant1") + else: + blt_order = ("ant1", "ant2") + else: + blt_order = None + baseline_array = get_baseline_params( antenna_numbers=telescope.antenna_numbers, antpairs=antpairs ) @@ -595,6 +611,7 @@ def new_uvdata( obj.spw_array = spw_array obj.flex_spw_id_array = flex_spw_id_array obj.integration_time = integration_time + obj.blt_order = blt_order set_phase_params( obj, diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index 112254cd83..e478737abc 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -130,6 +130,13 @@ def test_lunar_simple_new_uvdata(lunar_simple_params: dict[str, Any], selenoid: }, "instrument must be set on the Telescope object passed to `telescope`.", ], + [ + { + "antpairs": np.array([(0, 1), (0, 2), (1, 2), (0, 3)]), + "do_blt_outer": False, + }, + "Length of time array must match the number of antpairs.", + ], ], ) def test_bad_inputs(simplest_working_params: dict[str, Any], update_dict, err_msg): @@ -138,6 +145,28 @@ def test_bad_inputs(simplest_working_params: dict[str, Any], update_dict, err_ms UVData.new(**simplest_working_params) +@pytest.mark.parametrize( + ["update_dict", "blt_order"], + [ + [{}, ("ant1", "ant2")], + [{"time_axis_faster_than_bls": True}, ("time", "ant1")], + [ + { + "antpairs": np.array([(0, 1), (0, 2), (1, 2), (0, 1)]), + "times": np.array([2459855, 2459855, 2459855.5, 2459855.5]), + "integration_time": np.full((4,), 12.0, dtype=float), + "do_blt_outer": False, + }, + None, + ], + ], +) +def test_blt_order(simplest_working_params, update_dict, blt_order): + simplest_working_params.update(update_dict) + uvd = UVData.new(**simplest_working_params) + assert uvd.blt_order == blt_order + + def test_bad_time_inputs(simplest_working_params: dict[str, Any]): with pytest.raises(ValueError, match="time_array must be a numpy array"): get_time_params( diff --git a/pyuvdata/uvdata/uvfits.py b/pyuvdata/uvdata/uvfits.py index 6e0db20ecb..53901f82ae 100644 --- a/pyuvdata/uvdata/uvfits.py +++ b/pyuvdata/uvdata/uvfits.py @@ -930,7 +930,9 @@ def write_uvfits( start_freq_array += [freq_array_use[chan_mask][0]] # Need the array direction here since channel_width is always supposed # to be > 0, but channels can be in decending freq order - freq_dir = np.sign(np.median(np.diff(freq_array_use[chan_mask]))) + freq_dir = 1.0 + if nchan_list[-1] > 1: + freq_dir = np.sign(np.median(np.diff(freq_array_use[chan_mask]))) delta_freq_array += [ np.median(self.channel_width[chan_mask]) * freq_dir ] From d1151aa0c005d575cdf3eda79863cc79e870ed20 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Mon, 13 May 2024 12:23:00 -0700 Subject: [PATCH 55/59] handle another transient spiceypy error --- pyuvdata/tests/test_utils.py | 44 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 740aa86668..eda53e2e4c 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -2069,27 +2069,33 @@ def test_calc_app_sidereal(astrometry_args, frame, telescope_frame, selenoid): if telescope_frame == "itrs": telescope_loc = astrometry_args["telescope_loc"] else: + from spiceypy.utils.exceptions import SpiceUNKNOWNFRAME + telescope_loc = astrometry_args["moon_telescope_loc"] - check_ra, check_dec = uvutils.calc_app_coords( - lon_coord=( - astrometry_args["fk5_ra"] - if (frame == "fk5") - else astrometry_args["icrs_ra"] - ), - lat_coord=( - astrometry_args["fk5_dec"] - if (frame == "fk5") - else astrometry_args["icrs_dec"] - ), - coord_type="sidereal", - telescope_loc=telescope_loc, - telescope_frame=telescope_frame, - ellipsoid=selenoid, - time_array=astrometry_args["time_array"], - coord_frame=frame, - coord_epoch=astrometry_args["epoch"], - ) + try: + check_ra, check_dec = uvutils.calc_app_coords( + lon_coord=( + astrometry_args["fk5_ra"] + if (frame == "fk5") + else astrometry_args["icrs_ra"] + ), + lat_coord=( + astrometry_args["fk5_dec"] + if (frame == "fk5") + else astrometry_args["icrs_dec"] + ), + coord_type="sidereal", + telescope_loc=telescope_loc, + telescope_frame=telescope_frame, + ellipsoid=selenoid, + time_array=astrometry_args["time_array"], + coord_frame=frame, + coord_epoch=astrometry_args["epoch"], + ) + except SpiceUNKNOWNFRAME as err: + pytest.skip("SpiceUNKNOWNFRAME error: " + str(err)) + check_coord = SkyCoord(check_ra, check_dec, unit="rad") if telescope_frame == "itrs": From 46761111e6f0f4ae083115645550762fdadbb7d4 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Tue, 14 May 2024 14:38:01 -0700 Subject: [PATCH 56/59] Do proper deprecation of old telescope attributes and functions --- CHANGELOG.md | 6 ++ docs/make_telescope.py | 6 +- pyuvdata/__init__.py | 9 ++- pyuvdata/telescopes.py | 124 ++++++++++++++++++++++++++++-- pyuvdata/tests/test_telescopes.py | 91 +++++++++++++++++++++- pyuvdata/uvcal/uvcal.py | 3 +- pyuvdata/uvdata/uvdata.py | 2 +- 7 files changed, 226 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1223141efb..2ab1df8454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,12 @@ times for those solutions. - Passing telescope-related metadata as separate parameters to `UVData.new` and `UVCal.new` rather than `Telescope` objects. - The `UVData.get_ENU_antpos` method in favor of `UVData.telescope.get_enu_antpos`. +- The `Telescope.telescope_location` and `Telescope.telescope_name` attributes +in favor of `Telescope.location` and `Telescope.name`. +- The `get_telescope` function in favor of the `known_telescope_location` function +and the `Telescope.from_known_telescopes` classmethod. +- The KNOWN_TELESCOPE dict in favor of the `known_telescope_location` function +and the `Telescope.from_known_telescopes` classmethod. ### Fixed - Fixed a bug in `UVBase` where `allowed_failures` was being ignored if a parameter had diff --git a/docs/make_telescope.py b/docs/make_telescope.py index 59b6eab9e8..40b8cacb0c 100644 --- a/docs/make_telescope.py +++ b/docs/make_telescope.py @@ -12,7 +12,7 @@ from astropy.time import Time from pyuvdata import Telescope -from pyuvdata.telescopes import KNOWN_TELESCOPES +from pyuvdata.telescopes import _KNOWN_TELESCOPES def write_telescope_rst(write_file=None): @@ -93,8 +93,8 @@ def write_telescope_rst(write_file=None): "which can be used if data files are missing that information.\n\n" ) - known_tel_use = copy.deepcopy(KNOWN_TELESCOPES) - for tel, tel_dict in KNOWN_TELESCOPES.items(): + known_tel_use = copy.deepcopy(_KNOWN_TELESCOPES) + for tel, tel_dict in _KNOWN_TELESCOPES.items(): if "location" in tel_dict: known_tel_use[tel]["location"] = ( tel_dict["location"].to_geodetic().__repr__() diff --git a/pyuvdata/__init__.py b/pyuvdata/__init__.py index 99980d70a9..93835f4d39 100644 --- a/pyuvdata/__init__.py +++ b/pyuvdata/__init__.py @@ -30,7 +30,12 @@ warnings.filterwarnings("ignore", message="numpy.dtype size changed") warnings.filterwarnings("ignore", message="numpy.ufunc size changed") -from .telescopes import Telescope, known_telescopes # noqa +from .telescopes import ( # noqa + Telescope, + get_telescope, + known_telescope_location, + known_telescopes, +) from .uvbeam import UVBeam # noqa from .uvcal import UVCal # noqa from .uvdata import FastUVH5Meta # noqa @@ -39,11 +44,13 @@ __all__ = [ "UVData", + "FastUVH5Meta", "UVCal", "UVFlag", "UVBeam", "Telescope", "known_telescopes", + "known_telescope_location", "get_telescope", ] diff --git a/pyuvdata/telescopes.py b/pyuvdata/telescopes.py index 7e501b79b6..aa91e3892b 100644 --- a/pyuvdata/telescopes.py +++ b/pyuvdata/telescopes.py @@ -8,6 +8,7 @@ import copy import os import warnings +from collections.abc import Mapping from pathlib import Path from typing import Literal, Union @@ -23,7 +24,7 @@ from . import utils as uvutils from . import uvbase -__all__ = ["Telescope", "known_telescopes"] +__all__ = ["Telescope", "known_telescopes", "known_telescope_location", "get_telescope"] try: from lunarsky import MoonLocation @@ -42,7 +43,7 @@ # Antenna positions can be specified via a csv file with the following columns: # "name" -- antenna name, "number" -- antenna number, "x", "y", "z" -- ECEF coordinates # relative to the telescope location. -KNOWN_TELESCOPES = { +_KNOWN_TELESCOPES = { "PAPER": { "location": EarthLocation.from_geodetic( lat=Angle("-30d43m17.5s"), lon=Angle("21d25m41.9s"), height=1073.0 * units.m @@ -93,6 +94,45 @@ } +# Deprecation to handle accessing old keys of KNOWN_TELESCOPES +class TelMapping(Mapping): + def __init__(self, mapping=()): + self._mapping = dict(mapping) + + def __getitem__(self, key): + warnings.warn( + "Directly accessing the KNOWN_TELESCOPES dict is deprecated. If you " + "need a telescope location, use the known_telescope_location function. " + "For a full Telescope object use the classmethod " + "Telescope.from_known_telescopes.", + DeprecationWarning, + ) + if key in ["latitude", "longitude", "altitude", "center_xyz"]: + if key == "latitude": + return self._mapping["location"].lat.rad + if key == "longitude": + return self._mapping["location"].lon.rad + if key == "altitude": + return self._mapping["location"].height.to("m").value + if key == "center_xyz": + return ( + units.Quantity(self._mapping["location"].geocentric).to("m").value + ) + + return self._mapping[key] + + def __len__(self): + return len(self._mapping) + + def __iter__(self): + return iter(self._mapping) + + +KNOWN_TELESCOPES = TelMapping( + (name, TelMapping(tel_dict)) for name, tel_dict in _KNOWN_TELESCOPES.items() +) + + def known_telescopes(): """ Get list of known telescopes. @@ -103,14 +143,43 @@ def known_telescopes(): List of known telescope names. """ astropy_sites = [site for site in EarthLocation.get_site_names() if site != ""] - known_telescopes = list(set(astropy_sites + list(KNOWN_TELESCOPES.keys()))) + known_telescopes = list(set(astropy_sites + list(_KNOWN_TELESCOPES.keys()))) return known_telescopes +def get_telescope(telescope_name, telescope_dict_in=_KNOWN_TELESCOPES): + """ + Get Telescope object for a telescope in telescope_dict. Deprecated. + + Parameters + ---------- + telescope_name : str + Name of a telescope + telescope_dict_in: dict + telescope info dict. Default is None, meaning use KNOWN_TELESCOPES + (other values are only used for testing) + + Returns + ------- + Telescope object + The Telescope object associated with telescope_name. + """ + warnings.warn( + "This method is deprecated and will be removed in version 3.2. If you " + "just need a telescope location, use the known_telescope_location function. " + "For a full Telescope object use the classmethod " + "Telescope.from_known_telescopes.", + DeprecationWarning, + ) + return Telescope.from_known_telescopes( + telescope_name, known_telescope_dict=telescope_dict_in, run_check=False + ) + + def known_telescope_location( name: str, return_citation: bool = False, - known_telescope_dict: dict = KNOWN_TELESCOPES, + known_telescope_dict: dict = _KNOWN_TELESCOPES, ): """ Get the location for a known telescope. @@ -321,6 +390,48 @@ def __init__(self): super(Telescope, self).__init__() + def __getattr__(self, __name): + """Handle old names attributes.""" + if __name == "telescope_location": + warnings.warn( + "The Telescope.telescope_location attribute is deprecated, use " + "Telescope.location instead (which contains an astropy " + "EarthLocation object). This will become an error in version 3.2.", + DeprecationWarning, + ) + return self._location.xyz() + elif __name == "telescope_name": + warnings.warn( + "The Telescope.telescope_name attribute is deprecated, use " + "Telescope.name instead. This will become an error in version 3.2.", + DeprecationWarning, + ) + return self.name + + return super().__getattribute__(__name) + + def __setattr__(self, __name, __value): + """Handle old names for telescope metadata.""" + if __name == "telescope_location": + warnings.warn( + "The Telescope.telescope_location attribute is deprecated, use " + "Telescope.location instead (which should be set to an astropy " + "EarthLocation object). This will become an error in version 3.2.", + DeprecationWarning, + ) + self._location.set_xyz(__value) + return + elif __name == "telescope_name": + warnings.warn( + "The Telescope.telescope_name attribute is deprecated, use " + "Telescope.name instead. This will become an error in version 3.2.", + DeprecationWarning, + ) + self.name = __value + return + + return super().__setattr__(__name, __value) + def check(self, *, check_extra=True, run_check_acceptability=True): """ Add some extra checks on top of checks on UVBase class. @@ -371,7 +482,7 @@ def update_params_from_known_telescopes( run_check: bool = True, check_extra: bool = True, run_check_acceptability: bool = True, - known_telescope_dict: dict = KNOWN_TELESCOPES, + known_telescope_dict: dict = _KNOWN_TELESCOPES, ): """ Update the parameters based on telescope in known_telescopes. @@ -427,6 +538,7 @@ def update_params_from_known_telescopes( known_telescope_dict=known_telescope_dict, ) self.location = location + self.citation = citation if "astropy sites" in citation: astropy_sites_list.append("telescope_location") else: @@ -569,7 +681,7 @@ def from_known_telescopes( run_check: bool = True, check_extra: bool = True, run_check_acceptability: bool = True, - known_telescope_dict: dict = KNOWN_TELESCOPES, + known_telescope_dict: dict = _KNOWN_TELESCOPES, ): """ Create a new Telescope object using information from known_telescopes. diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index a06b0b51dd..b47bfc9848 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -15,9 +15,9 @@ import pyuvdata import pyuvdata.tests as uvtest -from pyuvdata import Telescope, UVData +from pyuvdata import Telescope, UVData, get_telescope from pyuvdata.data import DATA_PATH -from pyuvdata.telescopes import KNOWN_TELESCOPES, get_antenna_params +from pyuvdata.telescopes import _KNOWN_TELESCOPES, get_antenna_params required_parameters = [ "_name", @@ -154,7 +154,7 @@ def test_update_params_from_known(): tel.update_params_from_known_telescopes() hera_tel = Telescope.from_known_telescopes("hera") - known_dict = copy.deepcopy(KNOWN_TELESCOPES) + known_dict = copy.deepcopy(_KNOWN_TELESCOPES) known_dict["HERA"]["antenna_diameters"] = hera_tel.antenna_diameters hera_tel_test = Telescope.from_known_telescopes( @@ -208,6 +208,91 @@ def test_from_known(): telescope_obj = Telescope.from_known_telescopes(inst, run_check=False) assert telescope_obj.name == inst + with uvtest.check_warnings( + DeprecationWarning, + match="This method is deprecated and will be removed in version 3.2. " + "If you just need a telescope location, use the " + "known_telescope_location function. For a full Telescope object use " + "the classmethod Telescope.from_known_telescopes.", + ): + tel_obj2 = get_telescope(inst) + + assert tel_obj2 == telescope_obj + + +def test_old_attr_names(): + mwa_tel = Telescope.from_known_telescopes("mwa") + with uvtest.check_warnings( + DeprecationWarning, + match="The Telescope.telescope_name attribute is deprecated, use " + "Telescope.name instead. This will become an error in version 3.2.", + ): + assert mwa_tel.telescope_name == mwa_tel.name + + with uvtest.check_warnings( + DeprecationWarning, + match="The Telescope.telescope_location attribute is deprecated, use " + "Telescope.location instead (which contains an astropy " + "EarthLocation object). This will become an error in version 3.2.", + ): + np.testing.assert_allclose(mwa_tel.telescope_location, mwa_tel._location.xyz()) + + with uvtest.check_warnings( + DeprecationWarning, + match="The Telescope.telescope_name attribute is deprecated, use " + "Telescope.name instead. This will become an error in version 3.2.", + ): + mwa_tel.telescope_name = "foo" + assert mwa_tel.name == "foo" + + hera_tel = Telescope.from_known_telescopes("hera") + with uvtest.check_warnings( + DeprecationWarning, + match="The Telescope.telescope_location attribute is deprecated, use " + "Telescope.location instead (which should be set to an astropy " + "EarthLocation object). This will become an error in version 3.2.", + ): + mwa_tel.telescope_location = hera_tel._location.xyz() + assert mwa_tel._location == hera_tel._location + + +@pytest.mark.filterwarnings("ignore:Directly accessing the KNOWN_TELESCOPES") +def test_old_known_tel_dict_keys(): + from pyuvdata.telescopes import KNOWN_TELESCOPES + + hera_tel = Telescope.from_known_telescopes("hera") + + warn_msg = [ + "Directly accessing the KNOWN_TELESCOPES dict is deprecated. If you " + "need a telescope location, use the known_telescope_location function. " + "For a full Telescope object use the classmethod " + "Telescope.from_known_telescopes." + ] * 2 + + with uvtest.check_warnings(DeprecationWarning, match=warn_msg): + assert KNOWN_TELESCOPES["HERA"]["latitude"] == hera_tel.location.lat.rad + + with uvtest.check_warnings(DeprecationWarning, match=warn_msg): + assert KNOWN_TELESCOPES["HERA"]["longitude"] == hera_tel.location.lon.rad + + with uvtest.check_warnings(DeprecationWarning, match=warn_msg): + assert ( + KNOWN_TELESCOPES["HERA"]["altitude"] + == hera_tel.location.height.to("m").value + ) + + with uvtest.check_warnings(DeprecationWarning, match=warn_msg): + np.testing.assert_allclose( + KNOWN_TELESCOPES["HERA"]["center_xyz"], + Quantity(hera_tel.location.geocentric).to("m").value, + ) + with uvtest.check_warnings(DeprecationWarning, match=warn_msg): + assert KNOWN_TELESCOPES["HERA"]["citation"] == hera_tel.citation + + assert len(KNOWN_TELESCOPES["MWA"]) == 1 + for key, val in KNOWN_TELESCOPES.items(): + assert val == _KNOWN_TELESCOPES[key] + def test_get_telescope_center_xyz(): ref_xyz = (-2562123.42683, 5094215.40141, -2848728.58869) diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index d7b3c17a1f..c045e9f502 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -613,7 +613,7 @@ def __init__(self): "(position in RA/Dec which moves with time), 'driftscan' (fixed postion in " "Az/El, NOT the same as the old `phase_type`='drift') or 'unprojected' " "(baseline coordinates in ENU, but data are not phased, similar to " - "the old `phase_type`='drift')" + "the old `phase_type`='drift') " "'cat_lon' (longitude coord, e.g. RA, either a single value or a one " "dimensional array of length Npts --the number of ephemeris data points-- " "for ephem type phase centers), " @@ -638,6 +638,7 @@ def __init__(self): "'info_source' (describes where catalog info came from). " "Most typically used with MS calibration tables." ) + self._phase_center_catalog = uvp.UVParameter( "phase_center_catalog", description=desc, expected_type=dict, required=False ) diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index 16213bc153..2525bfb70c 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -431,7 +431,7 @@ def __init__(self): "(position in RA/Dec which moves with time), 'driftscan' (fixed postion in " "Az/El, NOT the same as the old `phase_type`='drift') or 'unprojected' " "(baseline coordinates in ENU, but data are not phased, similar to " - "the old `phase_type`='drift')" + "the old `phase_type`='drift') " "'cat_lon' (longitude coord, e.g. RA, either a single value or a one " "dimensional array of length Npts --the number of ephemeris data points-- " "for ephem type phase centers), " From 7ef0d90c606c70ad4989f7cc49dd8507b722ea69 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 15 May 2024 10:38:02 -0700 Subject: [PATCH 57/59] Fix typos in UVFlag warnings --- pyuvdata/uvflag/tests/test_uvflag.py | 16 ++++++++-------- pyuvdata/uvflag/uvflag.py | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 51b6a0e8cb..deea8f97f2 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -2198,7 +2198,7 @@ def test_add_errors(uvdata_obj, uvcal_obj): uv3.telescope.name = "foo" with pytest.raises( ValueError, - match="telescope_name is not the same the two objects. The value on this " + match="telescope_name is not the same on the two objects. The value on this " "object is HERA; the value on the other object is foo.", ): uv1.__add__(uv3) @@ -2694,7 +2694,7 @@ def test_to_baseline_metric_error(uvdata_obj, uvf_from_uvcal): ): with uvtest.check_warnings( UserWarning, - match="x_orientation is not the same this object and on uv. Keeping " + match="x_orientation is not the same on this object and on uv. Keeping " "the value on this object.", ): uvf.to_baseline(uv, force_pol=True) @@ -2753,13 +2753,13 @@ def test_to_baseline_from_antenna( with uvtest.check_warnings( UserWarning, - match="x_orientation is not the same this object and on uv. Keeping " + match="x_orientation is not the same on this object and on uv. Keeping " "the value on this object.", ): uvf.to_baseline(uv, force_pol=True) with uvtest.check_warnings( UserWarning, - match="x_orientation is not the same this object and on uv. Keeping " + match="x_orientation is not the same on this object and on uv. Keeping " "the value on this object.", ): uvf2.to_baseline(uv2, force_pol=True) @@ -2830,7 +2830,7 @@ def test_to_baseline_antenna_errors(uvdata_obj, uvcal_obj, method, uv_future_sha uvf2 = uvf.copy() uvf2.channel_width = uvf2.channel_width / 2.0 with pytest.raises( - ValueError, match="channel_width is not the same this object and on uv" + ValueError, match="channel_width is not the same on this object and on uv" ): getattr(uvf2, method)(uv) @@ -2839,7 +2839,7 @@ def test_to_baseline_antenna_errors(uvdata_obj, uvcal_obj, method, uv_future_sha uvf2.spw_array = np.array([0, 1]) uvf2.check() with pytest.raises( - ValueError, match="spw_array is not the same this object and on uv" + ValueError, match="spw_array is not the same on this object and on uv" ): getattr(uvf2, method)(uv) uv2 = uv.copy() @@ -2850,7 +2850,7 @@ def test_to_baseline_antenna_errors(uvdata_obj, uvcal_obj, method, uv_future_sha uvf2.flex_spw_id_array[: uv.Nfreqs // 2] = 1 uvf2.check() with pytest.raises( - ValueError, match="flex_spw_id_array is not the same this object and on uv" + ValueError, match="flex_spw_id_array is not the same on this object and on uv" ): getattr(uvf2, method)(uv2) @@ -3015,7 +3015,7 @@ def test_to_antenna_add_version_str(uvcal_obj, uvc_future_shapes, uvf_future_sha with uvtest.check_warnings( UserWarning, - match="instrument is not the same this object and on uv. Keeping the " + match="instrument is not the same on this object and on uv. Keeping the " "value on this object.", ): uvf.to_antenna(uvc) diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index ce748a3597..22157fa4d6 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -1653,7 +1653,7 @@ def to_baseline( atol=self._channel_width.tols[1], ): raise ValueError( - "channel_width is not the same this object and on uv. The " + "channel_width is not the same on this object and on uv. The " f"value on this object is {self.channel_width}; the value on " f"uv is {uv.channel_width}." ) @@ -1668,12 +1668,12 @@ def to_baseline( if this_param.value is not None and this_param != uv_param: if param in warning_params: warnings.warn( - f"{param} is not the same this object and on uv. " + f"{param} is not the same on this object and on uv. " "Keeping the value on this object." ) else: raise ValueError( - f"{param} is not the same this object and on uv. " + f"{param} is not the same on this object and on uv. " f"The value on this object is {this_param.value}; " f"the value on uv is {uv_param.value}." ) @@ -1955,7 +1955,7 @@ def to_antenna( atol=self._channel_width.tols[1], ): raise ValueError( - "channel_width is not the same this object and on uv. The " + "channel_width is not the same on this object and on uv. The " f"value on this object is {self.channel_width}; the value on " f"uv is {uv.channel_width}." ) @@ -1970,12 +1970,12 @@ def to_antenna( if this_param.value is not None and this_param != uv_param: if param in warning_params: warnings.warn( - f"{param} is not the same this object and on uv. " + f"{param} is not the same on this object and on uv. " "Keeping the value on this object." ) else: raise ValueError( - f"{param} is not the same this object and on uv. " + f"{param} is not the same on this object and on uv. " f"The value on this object is {this_param.value}; " f"the value on uv is {uv_param.value}." ) @@ -2348,7 +2348,7 @@ def __add__( ) else: raise ValueError( - f"{param} is not the same the two objects. The value on " + f"{param} is not the same on the two objects. The value on " f"this object is {this_param.value}; the value on the " f"other object is {other_param.value}." ) From 22c741077459783a4acddd17b815fde4c7901843 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Wed, 15 May 2024 12:03:37 -0700 Subject: [PATCH 58/59] Fix bug in blt_order in UVData initializers --- pyuvdata/uvdata/initializers.py | 4 ++-- pyuvdata/uvdata/tests/test_initializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 181188b363..4b001bc40f 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -551,9 +551,9 @@ def new_uvdata( if bl_order is not None and blts_are_rectangular: if time_axis_faster_than_bls: - blt_order = ("time", "ant1") - else: blt_order = ("ant1", "ant2") + else: + blt_order = ("time", "ant1") else: blt_order = None diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index e478737abc..ff88399a7a 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -148,8 +148,8 @@ def test_bad_inputs(simplest_working_params: dict[str, Any], update_dict, err_ms @pytest.mark.parametrize( ["update_dict", "blt_order"], [ - [{}, ("ant1", "ant2")], - [{"time_axis_faster_than_bls": True}, ("time", "ant1")], + [{}, ("time", "ant1")], + [{"time_axis_faster_than_bls": True}, ("ant1", "ant2")], [ { "antpairs": np.array([(0, 1), (0, 2), (1, 2), (0, 1)]), From f52fc0aa303d42b6b5c49bd0972e2e4f7c1b6aa3 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Fri, 17 May 2024 15:07:59 -0700 Subject: [PATCH 59/59] remove unnecessary test --- pyuvdata/tests/test_telescopes.py | 40 +------------------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/pyuvdata/tests/test_telescopes.py b/pyuvdata/tests/test_telescopes.py index b47bfc9848..9a31c520f8 100644 --- a/pyuvdata/tests/test_telescopes.py +++ b/pyuvdata/tests/test_telescopes.py @@ -294,46 +294,8 @@ def test_old_known_tel_dict_keys(): assert val == _KNOWN_TELESCOPES[key] -def test_get_telescope_center_xyz(): - ref_xyz = (-2562123.42683, 5094215.40141, -2848728.58869) - # ref_latlonalt = (-26.7 * np.pi / 180.0, 116.7 * np.pi / 180.0, 377.8) - test_telescope_dict = { - "test": { - "location": EarthLocation.from_geocentric(*ref_xyz, unit="m"), - "citation": "", - }, - "test2": { - "location": EarthLocation.from_geocentric(*ref_xyz, unit="m"), - "citation": "", - }, - } - telescope_obj = Telescope.from_known_telescopes( - "test", known_telescope_dict=test_telescope_dict, run_check=False - ) - telescope_obj_ext = Telescope() - telescope_obj_ext.citation = "" - telescope_obj_ext.name = "test" - telescope_obj_ext.location = EarthLocation(*ref_xyz, unit="m") - - assert telescope_obj == telescope_obj_ext - - telescope_obj_ext.name = "test2" - telescope_obj2 = Telescope.from_known_telescopes( - "test2", known_telescope_dict=test_telescope_dict, run_check=False - ) - assert telescope_obj2 == telescope_obj_ext - - def test_get_telescope_no_loc(): - test_telescope_dict = { - "test": { - "center_xyz": None, - "latitude": None, - "longitude": None, - "altitude": None, - "citation": "", - } - } + test_telescope_dict = {"test": {"citation": ""}} with pytest.raises( KeyError, match="Missing location information in known_telescopes_dict "