diff --git a/.flake8 b/.flake8 index 5964dd59e6..cf31b96a6c 100644 --- a/.flake8 +++ b/.flake8 @@ -15,8 +15,10 @@ per-file-ignores = src/pyuvdata/uvdata/uvdata.py: N802 src/pyuvdata/uvbeam/mwa_beam.py: N802 src/pyuvdata/utils/coordinates.py: N802, N803 + src/pyuvdata/utils/io/__init__.py: A005 tests/utils/test_coordinates.py: D,N802 tests/__init__.py: D,N802 + tests/utils/io/__init__.py: A005 docstring-convention = numpy select = C,E,W,T4,B9,F,D,A,N,RST,B rst-roles = diff --git a/docs/developer_docs.rst b/docs/developer_docs.rst index a925e32219..620c09ff50 100644 --- a/docs/developer_docs.rst +++ b/docs/developer_docs.rst @@ -26,6 +26,9 @@ attribute shapes and values. .. autoclass:: pyuvdata.parameter.LocationParameter :members: +.. autoclass:: pyuvdata.parameter.SkyCoordParameter + :members: + .. autoclass:: pyuvdata.uvbase.UVBase :members: @@ -36,6 +39,10 @@ specific code. The read and write methods on the user classes convert between the user classes and the file-specific classes automatically as needed, so users generally do not need to interact with these classes, but developers may need to. + +UVData +****** + .. autoclass:: pyuvdata.uvdata.fhd.FHD :members: @@ -57,12 +64,21 @@ generally do not need to interact with these classes, but developers may need to .. autoclass:: pyuvdata.uvdata.uvh5.UVH5 :members: +UVCal +***** + .. autoclass:: pyuvdata.uvcal.calfits.CALFITS :members: +.. autoclass:: pyuvdata.uvcal.calh5.CalH5 + :members: + .. autoclass:: pyuvdata.uvcal.fhd_cal.FHDCal :members: +UVBeam +****** + .. autoclass:: pyuvdata.uvbeam.beamfits.BeamFITS :members: @@ -73,6 +89,159 @@ generally do not need to interact with these classes, but developers may need to :members: +.. _Developer Docs Utility Functions: + +Utility Functions +----------------- +Note that we are also listing private functions here (functions that start with +an underscore). While they are listed here, **they are not considered part of the +public API, so they can change without notice**. If you find that you need to rely +one of them let us know in a github issue and we can consider making it part of +the public API. + + +File I/O Utility Functions +************************** + +Antenna position files +++++++++++++++++++++++ + +.. automodule:: pyuvdata.utils.io.antpos + :members: + :private-members: + :undoc-members: + +FHD files ++++++++++ + +.. automodule:: pyuvdata.utils.io.fhd + :members: + :private-members: + :undoc-members: + +FITS files +++++++++++ + +.. automodule:: pyuvdata.utils.io.fits + :members: + :private-members: + :undoc-members: + +HDF5 files +++++++++++ + +.. automodule:: pyuvdata.utils.io.hdf5 + :members: + :private-members: + :undoc-members: + +Measurement Set files ++++++++++++++++++++++ + +.. automodule:: pyuvdata.utils.io.ms + :members: + :private-members: + :undoc-members: + +Array collapse functions for flags +********************************** + +.. automodule:: pyuvdata.utils.array_collapse + :members: + :private-members: + :undoc-members: + +Functions for working with baseline numbers +******************************************* + +.. automodule:: pyuvdata.utils.bls + :members: + :private-members: + :undoc-members: + :ignore-module-all: + +Functions for working with the baseline-time axis +************************************************* + +.. automodule:: pyuvdata.utils.bltaxis + :members: + :private-members: + :undoc-members: + +Functions for working with telescope coordinates +************************************************ + +.. automodule:: pyuvdata.utils.coordinates + :members: + :private-members: + :undoc-members: + :ignore-module-all: + +Functions for working with the frequency axis +********************************************* + +.. automodule:: pyuvdata.utils.frequency + :members: + :private-members: + :undoc-members: + +Functions for working with history +********************************** + +.. automodule:: pyuvdata.utils.history + :members: + :private-members: + :undoc-members: + +Functions for working with phase center catalogs +************************************************ + +.. automodule:: pyuvdata.utils.phase_center_catalog + :members: + :private-members: + :undoc-members: + +Functions for working with phasing +********************************** + +.. automodule:: pyuvdata.utils.phasing + :members: + :private-members: + :undoc-members: + +Functions for working with polarizations +**************************************** + +.. automodule:: pyuvdata.utils.pol + :members: + :private-members: + :undoc-members: + :ignore-module-all: + +Functions for working with baseline redundancies +************************************************ + +.. automodule:: pyuvdata.utils.redundancy + :members: + :private-members: + :undoc-members: + +Functions for working with times and LSTs +***************************************** + +.. automodule:: pyuvdata.utils.times + :members: + :private-members: + :undoc-members: + +General utility functions +************************* + +.. automodule:: pyuvdata.utils.tools + :members: + :private-members: + :undoc-members: + Mir Parser ---------- .. automodule:: pyuvdata.uvdata.mir_parser @@ -82,10 +251,8 @@ Mir Parser :members: -Functions ----------- - -.. autofunction:: pyuvdata.uvdata.fhd.get_fhd_history +Other Functions +--------------- .. autofunction:: pyuvdata.uvbeam.mwa_beam.P1sin diff --git a/docs/functions.rst b/docs/functions.rst new file mode 100644 index 0000000000..3e187d8427 --- /dev/null +++ b/docs/functions.rst @@ -0,0 +1,50 @@ +Useful Functions +================ +There are some functions that interact with multiple types of objects to apply +calibrations solutions and flagging to other objects. + +.. autofunction:: pyuvdata.uvcalibrate + +.. autofunction:: pyuvdata.apply_uvflag + + +Utility Functions +----------------- +Some of our utility functions are widely used. The most commonly used ones are +noted here, for others see the developer docs: :ref:`developer docs utility functions`. + +.. autofunction:: pyuvdata.utils.baseline_to_antnums +.. autofunction:: pyuvdata.utils.antnums_to_baseline + +.. autofunction:: pyuvdata.utils.LatLonAlt_from_XYZ +.. autofunction:: pyuvdata.utils.XYZ_from_LatLonAlt +.. autofunction:: pyuvdata.utils.rotECEF_from_ECEF +.. autofunction:: pyuvdata.utils.ECEF_from_rotECEF +.. autofunction:: pyuvdata.utils.ENU_from_ECEF +.. autofunction:: pyuvdata.utils.ECEF_from_ENU + +.. autofunction:: pyuvdata.utils.polstr2num +.. autofunction:: pyuvdata.utils.polnum2str +.. autofunction:: pyuvdata.utils.jstr2num +.. autofunction:: pyuvdata.utils.jnum2str +.. autofunction:: pyuvdata.utils.conj_pol +.. autofunction:: pyuvdata.utils.x_orientation_pol_map +.. autofunction:: pyuvdata.utils.parse_polstr +.. autofunction:: pyuvdata.utils.parse_jpolstr + +.. autofunction:: pyuvdata.utils.get_lst_for_time + +.. autofunction:: pyuvdata.utils.uvw_track_generator + +.. autofunction:: pyuvdata.utils.collapse + +Polarization Dictionaries +------------------------- +We also define some useful dictionaries for mapping polarizations: + + * ``pyuvdata.utils.POL_STR2NUM_DICT``: maps visibility polarization strings to polarization integers + * ``pyuvdata.utils.POL_NUM2STR_DICT``: maps visibility polarization integers to polarization strings + * ``pyuvdata.utils.JONES_STR2NUM_DICT``: maps calibration polarization strings to polarization integers + * ``pyuvdata.utils.JONES_NUM2STR_DICT``: maps calibration polarization strings to polarization integers + * ``pyuvdata.utils.CONJ_POL_DICT``: maps how visibility polarizations change when antennas are swapped (visibilities are conjugated) + * ``pyuvdata.utils.XORIENTMAP``: maps x_orientation strings to cannonical names diff --git a/docs/make_index.py b/docs/make_index.py index a80bbdb673..64f44cf366 100644 --- a/docs/make_index.py +++ b/docs/make_index.py @@ -52,7 +52,7 @@ def write_index_rst(readme_file=None, write_file=None): " telescope\n" " fast_uvh5_meta\n" " fast_calh5_meta\n" - " utility_functions\n" + " functions\n" " developer_docs\n" ) diff --git a/docs/utility_functions.rst b/docs/utility_functions.rst deleted file mode 100644 index 2de1554868..0000000000 --- a/docs/utility_functions.rst +++ /dev/null @@ -1,5 +0,0 @@ -Utility Functions -================= - -.. automodule:: pyuvdata.utils - :members: diff --git a/src/pyuvdata/parameter.py b/src/pyuvdata/parameter.py index 57a0ce328b..1dcc2ec59e 100644 --- a/src/pyuvdata/parameter.py +++ b/src/pyuvdata/parameter.py @@ -31,7 +31,7 @@ hasmoon = False -__all__ = ["UVParameter", "AngleParameter", "LocationParameter"] +__all__ = ["UVParameter", "AngleParameter", "LocationParameter", "SkyCoordParameter"] def _get_generic_type(expected_type, strict_type_check=False): @@ -1228,6 +1228,17 @@ def __init__( ) def __eq__(self, other, *, silent=False): + """ + Test if classes match and values are within tolerances. + + Parameters + ---------- + other : UVParameter or subclass + The other UVParameter to compare with this one. + silent : bool + When set to False (default), descriptive text is printed out when parameters + do not match. If set to True, this text is not printed. + """ if not issubclass(self.value.__class__, SkyCoord) or not issubclass( other.value.__class__, SkyCoord ): diff --git a/src/pyuvdata/telescopes.py b/src/pyuvdata/telescopes.py index a69a7da63c..dd351d7cea 100644 --- a/src/pyuvdata/telescopes.py +++ b/src/pyuvdata/telescopes.py @@ -20,8 +20,8 @@ from . import parameter as uvp from . import utils from .data import DATA_PATH -from .utils.file_io import antpos -from .utils.file_io import hdf5 as hdf5_utils +from .utils.io import antpos +from .utils.io import hdf5 as hdf5_utils from .uvbase import UVBase __all__ = ["Telescope", "known_telescopes", "known_telescope_location", "get_telescope"] diff --git a/src/pyuvdata/utils/__init__.py b/src/pyuvdata/utils/__init__.py index 1769bb9f97..b477b06be5 100644 --- a/src/pyuvdata/utils/__init__.py +++ b/src/pyuvdata/utils/__init__.py @@ -22,9 +22,9 @@ from . import bls # noqa from . import bltaxis # noqa from . import coordinates # noqa -from . import file_io # noqa from . import frequency # noqa from . import history # noqa +from . import io # noqa from . import phase_center_catalog # noqa from . import phasing # noqa from . import pol # noqa @@ -63,7 +63,7 @@ def _fits_gethduaxis(hdu, axis): """ Make axis arrays for fits files. - Deprecated. Use pyuvdata.utils.file_io.fits._gethduaxis. + Deprecated. Use pyuvdata.utils.io.fits._gethduaxis. Parameters ---------- @@ -78,11 +78,11 @@ def _fits_gethduaxis(hdu, axis): Array of values for the specified axis. """ - from .file_io.fits import _gethduaxis + from .io.fits import _gethduaxis warnings.warn( "The _fits_gethduaxis function has moved, please import it as " - "pyuvdata.utils.file_io.fits._gethduaxis. This warnings will become an " + "pyuvdata.utils.io.fits._gethduaxis. This warnings will become an " "error in version 3.2", DeprecationWarning, ) @@ -94,7 +94,7 @@ def _fits_indexhdus(hdulist): """ Get a dict of table names and HDU numbers from a FITS HDU list. - Deprecated. Use pyuvdata.utils.file_io.fits._indexhdus. + Deprecated. Use pyuvdata.utils.io.fits._indexhdus. Parameters ---------- @@ -107,11 +107,11 @@ def _fits_indexhdus(hdulist): dictionary with table names as keys and HDU number as values. """ - from .file_io.fits import _indexhdus + from .io.fits import _indexhdus warnings.warn( "The _fits_indexhdus function has moved, please import it as " - "pyuvdata.utils.file_io.fits._indexhdus. This warnings will become an " + "pyuvdata.utils.io.fits._indexhdus. This warnings will become an " "error in version 3.2", DeprecationWarning, ) diff --git a/src/pyuvdata/utils/file_io/__init__.py b/src/pyuvdata/utils/io/__init__.py similarity index 100% rename from src/pyuvdata/utils/file_io/__init__.py rename to src/pyuvdata/utils/io/__init__.py diff --git a/src/pyuvdata/utils/file_io/antpos.py b/src/pyuvdata/utils/io/antpos.py similarity index 100% rename from src/pyuvdata/utils/file_io/antpos.py rename to src/pyuvdata/utils/io/antpos.py diff --git a/src/pyuvdata/utils/io/fhd.py b/src/pyuvdata/utils/io/fhd.py new file mode 100644 index 0000000000..235c862e5f --- /dev/null +++ b/src/pyuvdata/utils/io/fhd.py @@ -0,0 +1,415 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License +"""Utilities for working with FHD files.""" +import os +import warnings + +import numpy as np +from astropy import units +from astropy.coordinates import EarthLocation +from scipy.io import readsav + +from ... import Telescope +from .. import coordinates + + +def fhd_filenames( + *, + vis_files: list[str] | np.ndarray | str | None = None, + params_file: str | None = None, + obs_file: str | None = None, + flags_file: str | None = None, + layout_file: str | None = None, + settings_file: str | None = None, + cal_file: str | None = None, +): + """ + Check the FHD input files for matching prefixes and folders. + + Parameters + ---------- + vis_files : str or array-like of str, optional + FHD visibility save file names, can be data or model visibilities. + params_file : str + FHD params save file name. + obs_file : str + FHD obs save file name. + flags_file : str + FHD flag save file name. + layout_file : str + FHD layout save file name. + layout_file : str + FHD layout save file name. + settings_file : str + FHD settings text file name. + cal_file : str + FHD cal save file name. + + Returns + ------- + A list of file basenames to be used in the object `filename` attribute. + + """ + file_types = { + "vis": {"files": vis_files, "suffix": "_vis", "sub_folder": "vis_data"}, + "cal": {"files": cal_file, "suffix": "_cal", "sub_folder": "calibration"}, + "flags": {"files": flags_file, "suffix": "_flags", "sub_folder": "vis_data"}, + "layout": {"files": layout_file, "suffix": "_layout", "sub_folder": "metadata"}, + "obs": {"files": obs_file, "suffix": "_obs", "sub_folder": "metadata"}, + "params": {"files": params_file, "suffix": "_params", "sub_folder": "metadata"}, + "settings": { + "files": settings_file, + "suffix": "_settings", + "sub_folder": "metadata", + }, + } + + basename_list = [] + prefix_list = [] + folder_list = [] + missing_suffix = [] + missing_subfolder = [] + for ftype, fdict in file_types.items(): + if fdict["files"] is None: + continue + if isinstance(fdict["files"], (list, np.ndarray)): + these_files = fdict["files"] + else: + these_files = [fdict["files"]] + + for fname in these_files: + dirname, basename = os.path.split(fname) + basename_list.append(basename) + if fdict["suffix"] in basename: + suffix_loc = basename.find(fdict["suffix"]) + prefix_list.append(basename[:suffix_loc]) + else: + missing_suffix.append(ftype) + fhd_folder, subfolder = os.path.split(dirname) + if subfolder == fdict["sub_folder"]: + folder_list.append(fhd_folder) + else: + missing_subfolder.append(ftype) + + if len(missing_suffix) > 0: + warnings.warn( + "Some FHD input files do not have the expected suffix so prefix " + f"matching could not be done. The affected file types are: {missing_suffix}" + ) + if len(missing_subfolder) > 0: + warnings.warn( + "Some FHD input files do not have the expected subfolder so FHD " + "folder matching could not be done. The affected file types are: " + f"{missing_subfolder}" + ) + + if np.unique(prefix_list).size > 1: + warnings.warn( + "The FHD input files do not all have matching prefixes, so they " + "may not be for the same data." + ) + if np.unique(folder_list).size > 1: + warnings.warn( + "The FHD input files do not all have the same parent folder, so " + "they may not be for the same FHD run." + ) + + return basename_list + + +def get_fhd_history(settings_file, *, return_user=False): + """ + Small function to get the important history from an FHD settings text file. + + Includes information about the command line call, the user, machine name and date + + Parameters + ---------- + settings_file : str + FHD settings file name + return_user : bool + optionally return the username who ran FHD + + Returns + ------- + history : str + string of history extracted from the settings file + user : str + Only returned if return_user is True + + """ + with open(settings_file, "r") as f: + settings_lines = f.readlines() + main_loc = None + command_loc = None + obs_loc = None + user_line = None + for ind, line in enumerate(settings_lines): + if line.startswith("##MAIN"): + main_loc = ind + if line.startswith("##COMMAND_LINE"): + command_loc = ind + if line.startswith("##OBS"): + obs_loc = ind + if line.startswith("User"): + user_line = ind + if ( + main_loc is not None + and command_loc is not None + and obs_loc is not None + and user_line is not None + ): + break + + main_lines = settings_lines[main_loc + 1 : command_loc] + command_lines = settings_lines[command_loc + 1 : obs_loc] + history_lines = ["FHD history\n"] + main_lines + command_lines + for ind, line in enumerate(history_lines): + history_lines[ind] = line.rstrip().replace("\t", " ") + history = "\n".join(history_lines) + user = settings_lines[user_line].split()[1] + + if return_user: + return history, user + else: + return history + + +def _xyz_close(xyz1, xyz2, loc_tols): + return np.allclose(xyz1, xyz2, rtol=loc_tols[0], atol=loc_tols[1]) + + +def _latlonalt_close(latlonalt1, latlonalt2, radian_tol, loc_tols): + latlon_close = np.allclose( + np.array(latlonalt1[0:2]), np.array(latlonalt2[0:2]), rtol=0, atol=radian_tol + ) + alt_close = np.isclose( + latlonalt1[2], latlonalt2[2], rtol=loc_tols[0], atol=loc_tols[1] + ) + return latlon_close and alt_close + + +def get_fhd_layout_info( + *, + layout_file, + telescope_name, + latitude, + longitude, + altitude, + obs_tile_names, + run_check_acceptability=True, +): + """ + Get the telescope and antenna positions from an FHD layout file. + + Parameters + ---------- + layout_file : str + FHD layout file name + telescope_name : str + Telescope name + latitude : float + telescope latitude in radians + longitude : float + telescope longitude in radians + altitude : float + telescope altitude in meters + 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". + run_check_acceptability : bool + Option to check acceptable range of the telescope locations. + + Returns + ------- + dict + A dictionary of parameters from the layout file to assign to the object. The + keys are: + + * telescope_xyz : Telescope location in ECEF, shape (3,) (float) + * Nants_telescope : Number of antennas in the telescope (int) + * antenna_postions : Antenna positions in relative ECEF, + shape (Nants_telescope, 3) (float) + * antenna_names : Antenna names, length Nants_telescope (list of str) + * antenna_numbers : Antenna numbers, shape (Nants_telescope,) (array of int) + * gst0 : Greenwich sidereal time at midnight on reference date. (float) + * earth_omega : Earth's rotation rate in degrees per day. (float) + * dut1 : DUT1 (google it) AIPS 117 calls it UT1UTC. (float) + * timesys : Time system (should only ever be UTC). (str) + * diameters : Antenna diameters in meters. shape (Nants_telescope,) (float) + * extra_keywords : Dictionary of extra keywords to preserve on the object. + + """ + layout_dict = readsav(layout_file, python_dict=True) + layout = layout_dict["layout"] + + layout_fields = [name.lower() for name in layout.dtype.names] + # Try to get the telescope location from the layout file & + # compare it to the position from the obs structure. + arr_center = layout["array_center"][0] + layout_fields.remove("array_center") + + xyz_telescope_frame = layout["coordinate_frame"][0].decode("utf8").lower() + layout_fields.remove("coordinate_frame") + + if xyz_telescope_frame.strip() == "itrf": + # compare to lat/lon/alt + location_latlonalt = coordinates.XYZ_from_LatLonAlt( + latitude, longitude, altitude + ) + latlonalt_arr_center = coordinates.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( + *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 + # files for the MWA + # compare with the known_telescopes values + try: + telescope_obj = 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 None: + message += " Using the value from known_telescopes." + telescope_location = telescope_obj.location + else: + message += ( + " Telescope is not in known_telescopes. " + "Defaulting to using the obs derived values." + ) + telescope_location = EarthLocation.from_geocentric( + *location_latlonalt, unit="m" + ) + # issue warning + warnings.warn(message) + else: + 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 + rot_ecef_positions = layout["antenna_coords"][0] + layout_fields.remove("antenna_coords") + # use the longitude from the layout file because that's how the antenna + # positions were calculated + latitude, longitude, altitude = coordinates.LatLonAlt_from_XYZ( + arr_center, check_acceptability=run_check_acceptability + ) + antenna_positions = coordinates.ECEF_from_rotECEF(rot_ecef_positions, longitude) + + antenna_names = [ant.decode("utf8") for ant in layout["antenna_names"][0].tolist()] + layout_fields.remove("antenna_names") + + # make these 0-indexed (rather than one indexed) + antenna_numbers = layout["antenna_numbers"][0] + layout_fields.remove("antenna_numbers") + + Nants_telescope = int(layout["n_antenna"][0]) + layout_fields.remove("n_antenna") + + if telescope_name.lower() == "mwa": + # check that obs.baseline_info.tile_names match the antenna names + # (accounting for possible differences in white space) + # this only applies for MWA because the tile_names come from + # metafits files. layout["antenna_names"] comes from the antenna table + # in the uvfits file and will be used if no metafits was submitted + if [ant.strip() for ant in obs_tile_names] != [ + ant.strip() for ant in antenna_names + ]: + warnings.warn( + "tile_names from obs structure does not match " + "antenna_names from layout" + ) + + gst0 = float(layout["gst0"][0]) + layout_fields.remove("gst0") + + if layout["ref_date"][0] != "": + rdate = layout["ref_date"][0].decode("utf8").lower() + else: + rdate = None + layout_fields.remove("ref_date") + + earth_omega = float(layout["earth_degpd"][0]) + layout_fields.remove("earth_degpd") + + dut1 = float(layout["dut1"][0]) + layout_fields.remove("dut1") + + timesys = layout["time_system"][0].decode("utf8").upper().strip() + layout_fields.remove("time_system") + + if "diameters" in layout_fields: + diameters = np.asarray(layout["diameters"][0]) + layout_fields.remove("diameters") + else: + diameters = None + + extra_keywords = {} + # ignore some fields, put everything else in extra_keywords + layout_fields_ignore = [ + "diff_utc", + "pol_type", + "n_pol_cal_params", + "mount_type", + "axis_offset", + "pola", + "pola_orientation", + "pola_cal_params", + "polb", + "polb_orientation", + "polb_cal_params", + "beam_fwhm", + ] + for field in layout_fields_ignore: + if field in layout_fields: + layout_fields.remove(field) + for field in layout_fields: + keyword = field + if len(keyword) > 8: + keyword = field.replace("_", "") + + value = layout[field][0] + if isinstance(value, bytes): + value = value.decode("utf8") + + extra_keywords[keyword.upper()] = value + + layout_param_dict = { + "telescope_location": telescope_location, + "Nants_telescope": Nants_telescope, + "antenna_positions": antenna_positions, + "antenna_names": antenna_names, + "antenna_numbers": antenna_numbers, + "gst0": gst0, + "rdate": rdate, + "earth_omega": earth_omega, + "dut1": dut1, + "timesys": timesys, + "diameters": diameters, + "extra_keywords": extra_keywords, + } + + return layout_param_dict diff --git a/src/pyuvdata/utils/file_io/fits.py b/src/pyuvdata/utils/io/fits.py similarity index 100% rename from src/pyuvdata/utils/file_io/fits.py rename to src/pyuvdata/utils/io/fits.py diff --git a/src/pyuvdata/utils/file_io/hdf5.py b/src/pyuvdata/utils/io/hdf5.py similarity index 100% rename from src/pyuvdata/utils/file_io/hdf5.py rename to src/pyuvdata/utils/io/hdf5.py diff --git a/src/pyuvdata/utils/file_io/ms.py b/src/pyuvdata/utils/io/ms.py similarity index 100% rename from src/pyuvdata/utils/file_io/ms.py rename to src/pyuvdata/utils/io/ms.py diff --git a/src/pyuvdata/utils/pol.py b/src/pyuvdata/utils/pol.py index d70250e0b8..3c01de1e5e 100644 --- a/src/pyuvdata/utils/pol.py +++ b/src/pyuvdata/utils/pol.py @@ -23,6 +23,7 @@ "jnum2str", "conj_pol", "_x_orientation_rep_dict", + "x_orientation_pol_map", "parse_polstr", "parse_jpolstr", ] @@ -84,7 +85,36 @@ def _x_orientation_rep_dict(x_orientation): - """Create replacement dict based on x_orientation.""" + """ + Create replacement dict based on x_orientation. + + Deprecated. Use x_orientation_pol_map instead. + + """ + warnings.warn( + "This function (_x_orientation_rep_dict) is deprecated, use " + "pyuvdata.utils.pol.x_orientation_pol_map instead.", + DeprecationWarning, + ) + + return x_orientation_pol_map(x_orientation) + + +def x_orientation_pol_map(x_orientation: str) -> dict: + """ + Return map from "x" and "y" pols to "e" and "n" based on x_orientation. + + Parameters + ---------- + x_orientation : str + String giving the x_orientation, one of "east" or "north". + + Returns + ------- + dict + Dictionary mapping "x" and "y" pols to "e" and "n" based on x_orientation. + + """ try: if XORIENTMAP[x_orientation.lower()] == "east": return {"x": "e", "y": "n"} @@ -151,7 +181,7 @@ def polstr2num(pol: str | IterableType[str], *, x_orientation: str | None = None dict_use = deepcopy(POL_STR2NUM_DICT) if x_orientation is not None: try: - rep_dict = _x_orientation_rep_dict(x_orientation) + rep_dict = x_orientation_pol_map(x_orientation) for key, value in POL_STR2NUM_DICT.items(): new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"]) dict_use[new_key] = value @@ -206,7 +236,7 @@ def polnum2str(num, *, x_orientation=None): dict_use = deepcopy(POL_NUM2STR_DICT) if x_orientation is not None: try: - rep_dict = _x_orientation_rep_dict(x_orientation) + rep_dict = x_orientation_pol_map(x_orientation) for key, value in POL_NUM2STR_DICT.items(): new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"]) dict_use[key] = new_val @@ -256,7 +286,7 @@ def jstr2num(jstr, *, x_orientation=None): dict_use = deepcopy(JONES_STR2NUM_DICT) if x_orientation is not None: try: - rep_dict = _x_orientation_rep_dict(x_orientation) + rep_dict = x_orientation_pol_map(x_orientation) for key, value in JONES_STR2NUM_DICT.items(): new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"]) dict_use[new_key] = value @@ -307,7 +337,7 @@ def jnum2str(jnum, *, x_orientation=None): dict_use = deepcopy(JONES_NUM2STR_DICT) if x_orientation is not None: try: - rep_dict = _x_orientation_rep_dict(x_orientation) + rep_dict = x_orientation_pol_map(x_orientation) for key, value in JONES_NUM2STR_DICT.items(): new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"]) dict_use[key] = new_val diff --git a/src/pyuvdata/uvbeam/beamfits.py b/src/pyuvdata/uvbeam/beamfits.py index ee40b4dde6..cfd155584e 100644 --- a/src/pyuvdata/uvbeam/beamfits.py +++ b/src/pyuvdata/uvbeam/beamfits.py @@ -12,7 +12,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import fits as fits_utils +from ..utils.io import fits as fits_utils from . import UVBeam __all__ = ["BeamFITS"] diff --git a/src/pyuvdata/uvbeam/uvbeam.py b/src/pyuvdata/uvbeam/uvbeam.py index 6c247a7901..6f9efbf231 100644 --- a/src/pyuvdata/uvbeam/uvbeam.py +++ b/src/pyuvdata/uvbeam/uvbeam.py @@ -3129,7 +3129,7 @@ def select( ) x_orient_dict = {} if beam_object.x_orientation is not None: - for key, value in utils._x_orientation_rep_dict( + for key, value in utils.x_orientation_pol_map( beam_object.x_orientation ).items(): if key in beam_object.feed_array: diff --git a/src/pyuvdata/uvcal/calfits.py b/src/pyuvdata/uvcal/calfits.py index 496e8b0d9f..251a275812 100644 --- a/src/pyuvdata/uvcal/calfits.py +++ b/src/pyuvdata/uvcal/calfits.py @@ -20,7 +20,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import fits as fits_utils +from ..utils.io import fits as fits_utils from . import UVCal __all__ = ["CALFITS"] diff --git a/src/pyuvdata/uvcal/calh5.py b/src/pyuvdata/uvcal/calh5.py index 41b05b4ab0..3b2976cae9 100644 --- a/src/pyuvdata/uvcal/calh5.py +++ b/src/pyuvdata/uvcal/calh5.py @@ -15,7 +15,7 @@ from .. import utils from ..docstrings import copy_replace_short_description from ..telescopes import Telescope -from ..utils.file_io import hdf5 as hdf5_utils +from ..utils.io import hdf5 as hdf5_utils from .uvcal import UVCal hdf5plugin_present = True diff --git a/src/pyuvdata/uvcal/fhd_cal.py b/src/pyuvdata/uvcal/fhd_cal.py index 664fad98d8..894ff24130 100644 --- a/src/pyuvdata/uvcal/fhd_cal.py +++ b/src/pyuvdata/uvcal/fhd_cal.py @@ -14,7 +14,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..uvdata.fhd import fhd_filenames, get_fhd_history, get_fhd_layout_info +from ..utils.io import fhd as fhd_utils from . import UVCal __all__ = ["FHDCal"] @@ -53,7 +53,7 @@ def read_fhd_cal( if not read_data and settings_file is None: raise ValueError("A settings_file must be provided if read_data is False.") - filenames = fhd_filenames( + filenames = fhd_utils.fhd_filenames( obs_file=obs_file, layout_file=layout_file, settings_file=settings_file, @@ -160,7 +160,7 @@ def read_fhd_cal( for ant in obs_tile_names ] - layout_param_dict = get_fhd_layout_info( + layout_param_dict = fhd_utils.get_fhd_layout_info( layout_file=layout_file, telescope_name=self.telescope.name, latitude=latitude, @@ -246,7 +246,7 @@ def read_fhd_cal( "[" + ", ".join(str(int(d)) for d in obs_data["delays"][0]) + "]" ) if settings_file is not None: - self.history, self.observer = get_fhd_history( + self.history, self.observer = fhd_utils.get_fhd_history( settings_file, return_user=True ) else: diff --git a/src/pyuvdata/uvcal/ms_cal.py b/src/pyuvdata/uvcal/ms_cal.py index 844d396e24..eea7deb33a 100644 --- a/src/pyuvdata/uvcal/ms_cal.py +++ b/src/pyuvdata/uvcal/ms_cal.py @@ -13,7 +13,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import ms as ms_utils +from ..utils.io import ms as ms_utils from . import UVCal __all__ = ["MSCal"] diff --git a/src/pyuvdata/uvdata/fhd.py b/src/pyuvdata/uvdata/fhd.py index 3d56719aa0..5d87717581 100644 --- a/src/pyuvdata/uvdata/fhd.py +++ b/src/pyuvdata/uvdata/fhd.py @@ -5,7 +5,6 @@ """Class for reading FHD save files.""" from __future__ import annotations -import os import warnings import numpy as np @@ -15,410 +14,12 @@ from docstring_parser import DocstringStyle from scipy.io import readsav -from .. import Telescope, utils +from .. import utils from ..docstrings import copy_replace_short_description +from ..utils.io import fhd as fhd_utils from . import UVData -__all__ = ["get_fhd_history", "get_fhd_layout_info", "FHD"] - - -def fhd_filenames( - *, - vis_files: list[str] | np.ndarray | str | None = None, - params_file: str | None = None, - obs_file: str | None = None, - flags_file: str | None = None, - layout_file: str | None = None, - settings_file: str | None = None, - cal_file: str | None = None, -): - """ - Check the FHD input files for matching prefixes and folders. - - Parameters - ---------- - vis_files : str or array-like of str, optional - FHD visibility save file names, can be data or model visibilities. - params_file : str - FHD params save file name. - obs_file : str - FHD obs save file name. - flags_file : str - FHD flag save file name. - layout_file : str - FHD layout save file name. - layout_file : str - FHD layout save file name. - settings_file : str - FHD settings text file name. - cal_file : str - FHD cal save file name. - - Returns - ------- - A list of file basenames to be used in the object `filename` attribute. - - """ - file_types = { - "vis": {"files": vis_files, "suffix": "_vis", "sub_folder": "vis_data"}, - "cal": {"files": cal_file, "suffix": "_cal", "sub_folder": "calibration"}, - "flags": {"files": flags_file, "suffix": "_flags", "sub_folder": "vis_data"}, - "layout": {"files": layout_file, "suffix": "_layout", "sub_folder": "metadata"}, - "obs": {"files": obs_file, "suffix": "_obs", "sub_folder": "metadata"}, - "params": {"files": params_file, "suffix": "_params", "sub_folder": "metadata"}, - "settings": { - "files": settings_file, - "suffix": "_settings", - "sub_folder": "metadata", - }, - } - - basename_list = [] - prefix_list = [] - folder_list = [] - missing_suffix = [] - missing_subfolder = [] - for ftype, fdict in file_types.items(): - if fdict["files"] is None: - continue - if isinstance(fdict["files"], (list, np.ndarray)): - these_files = fdict["files"] - else: - these_files = [fdict["files"]] - - for fname in these_files: - dirname, basename = os.path.split(fname) - basename_list.append(basename) - if fdict["suffix"] in basename: - suffix_loc = basename.find(fdict["suffix"]) - prefix_list.append(basename[:suffix_loc]) - else: - missing_suffix.append(ftype) - fhd_folder, subfolder = os.path.split(dirname) - if subfolder == fdict["sub_folder"]: - folder_list.append(fhd_folder) - else: - missing_subfolder.append(ftype) - - if len(missing_suffix) > 0: - warnings.warn( - "Some FHD input files do not have the expected suffix so prefix " - f"matching could not be done. The affected file types are: {missing_suffix}" - ) - if len(missing_subfolder) > 0: - warnings.warn( - "Some FHD input files do not have the expected subfolder so FHD " - "folder matching could not be done. The affected file types are: " - f"{missing_subfolder}" - ) - - if np.unique(prefix_list).size > 1: - warnings.warn( - "The FHD input files do not all have matching prefixes, so they " - "may not be for the same data." - ) - if np.unique(folder_list).size > 1: - warnings.warn( - "The FHD input files do not all have the same parent folder, so " - "they may not be for the same FHD run." - ) - - return basename_list - - -def get_fhd_history(settings_file, *, return_user=False): - """ - Small function to get the important history from an FHD settings text file. - - Includes information about the command line call, the user, machine name and date - - Parameters - ---------- - settings_file : str - FHD settings file name - return_user : bool - optionally return the username who ran FHD - - Returns - ------- - history : str - string of history extracted from the settings file - user : str - Only returned if return_user is True - - """ - with open(settings_file, "r") as f: - settings_lines = f.readlines() - main_loc = None - command_loc = None - obs_loc = None - user_line = None - for ind, line in enumerate(settings_lines): - if line.startswith("##MAIN"): - main_loc = ind - if line.startswith("##COMMAND_LINE"): - command_loc = ind - if line.startswith("##OBS"): - obs_loc = ind - if line.startswith("User"): - user_line = ind - if ( - main_loc is not None - and command_loc is not None - and obs_loc is not None - and user_line is not None - ): - break - - main_lines = settings_lines[main_loc + 1 : command_loc] - command_lines = settings_lines[command_loc + 1 : obs_loc] - history_lines = ["FHD history\n"] + main_lines + command_lines - for ind, line in enumerate(history_lines): - history_lines[ind] = line.rstrip().replace("\t", " ") - history = "\n".join(history_lines) - user = settings_lines[user_line].split()[1] - - if return_user: - return history, user - else: - return history - - -def _xyz_close(xyz1, xyz2, loc_tols): - return np.allclose(xyz1, xyz2, rtol=loc_tols[0], atol=loc_tols[1]) - - -def _latlonalt_close(latlonalt1, latlonalt2, radian_tol, loc_tols): - latlon_close = np.allclose( - np.array(latlonalt1[0:2]), np.array(latlonalt2[0:2]), rtol=0, atol=radian_tol - ) - alt_close = np.isclose( - latlonalt1[2], latlonalt2[2], rtol=loc_tols[0], atol=loc_tols[1] - ) - return latlon_close and alt_close - - -def get_fhd_layout_info( - *, - layout_file, - telescope_name, - latitude, - longitude, - altitude, - obs_tile_names, - run_check_acceptability=True, -): - """ - Get the telescope and antenna positions from an FHD layout file. - - Parameters - ---------- - layout_file : str - FHD layout file name - telescope_name : str - Telescope name - latitude : float - telescope latitude in radians - longitude : float - telescope longitude in radians - altitude : float - telescope altitude in meters - 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". - run_check_acceptability : bool - Option to check acceptable range of the telescope locations. - - Returns - ------- - dict - A dictionary of parameters from the layout file to assign to the object. The - keys are: - - * telescope_xyz : Telescope location in ECEF, shape (3,) (float) - * Nants_telescope : Number of antennas in the telescope (int) - * antenna_postions : Antenna positions in relative ECEF, - shape (Nants_telescope, 3) (float) - * antenna_names : Antenna names, length Nants_telescope (list of str) - * antenna_numbers : Antenna numbers, shape (Nants_telescope,) (array of int) - * gst0 : Greenwich sidereal time at midnight on reference date. (float) - * earth_omega : Earth's rotation rate in degrees per day. (float) - * dut1 : DUT1 (google it) AIPS 117 calls it UT1UTC. (float) - * timesys : Time system (should only ever be UTC). (str) - * diameters : Antenna diameters in meters. shape (Nants_telescope,) (float) - * extra_keywords : Dictionary of extra keywords to preserve on the object. - - """ - layout_dict = readsav(layout_file, python_dict=True) - layout = layout_dict["layout"] - - layout_fields = [name.lower() for name in layout.dtype.names] - # Try to get the telescope location from the layout file & - # compare it to the position from the obs structure. - arr_center = layout["array_center"][0] - layout_fields.remove("array_center") - - xyz_telescope_frame = layout["coordinate_frame"][0].decode("utf8").lower() - layout_fields.remove("coordinate_frame") - - if xyz_telescope_frame.strip() == "itrf": - # compare to lat/lon/alt - location_latlonalt = utils.XYZ_from_LatLonAlt(latitude, longitude, altitude) - latlonalt_arr_center = utils.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( - *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 - # files for the MWA - # compare with the known_telescopes values - try: - telescope_obj = 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 None: - message += " Using the value from known_telescopes." - telescope_location = telescope_obj.location - else: - message += ( - " Telescope is not in known_telescopes. " - "Defaulting to using the obs derived values." - ) - telescope_location = EarthLocation.from_geocentric( - *location_latlonalt, unit="m" - ) - # issue warning - warnings.warn(message) - else: - 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 - rot_ecef_positions = layout["antenna_coords"][0] - layout_fields.remove("antenna_coords") - # use the longitude from the layout file because that's how the antenna - # positions were calculated - latitude, longitude, altitude = utils.LatLonAlt_from_XYZ( - arr_center, check_acceptability=run_check_acceptability - ) - antenna_positions = utils.ECEF_from_rotECEF(rot_ecef_positions, longitude) - - antenna_names = [ant.decode("utf8") for ant in layout["antenna_names"][0].tolist()] - layout_fields.remove("antenna_names") - - # make these 0-indexed (rather than one indexed) - antenna_numbers = layout["antenna_numbers"][0] - layout_fields.remove("antenna_numbers") - - Nants_telescope = int(layout["n_antenna"][0]) - layout_fields.remove("n_antenna") - - if telescope_name.lower() == "mwa": - # check that obs.baseline_info.tile_names match the antenna names - # (accounting for possible differences in white space) - # this only applies for MWA because the tile_names come from - # metafits files. layout["antenna_names"] comes from the antenna table - # in the uvfits file and will be used if no metafits was submitted - if [ant.strip() for ant in obs_tile_names] != [ - ant.strip() for ant in antenna_names - ]: - warnings.warn( - "tile_names from obs structure does not match " - "antenna_names from layout" - ) - - gst0 = float(layout["gst0"][0]) - layout_fields.remove("gst0") - - if layout["ref_date"][0] != "": - rdate = layout["ref_date"][0].decode("utf8").lower() - else: - rdate = None - layout_fields.remove("ref_date") - - earth_omega = float(layout["earth_degpd"][0]) - layout_fields.remove("earth_degpd") - - dut1 = float(layout["dut1"][0]) - layout_fields.remove("dut1") - - timesys = layout["time_system"][0].decode("utf8").upper().strip() - layout_fields.remove("time_system") - - if "diameters" in layout_fields: - diameters = np.asarray(layout["diameters"][0]) - layout_fields.remove("diameters") - else: - diameters = None - - extra_keywords = {} - # ignore some fields, put everything else in extra_keywords - layout_fields_ignore = [ - "diff_utc", - "pol_type", - "n_pol_cal_params", - "mount_type", - "axis_offset", - "pola", - "pola_orientation", - "pola_cal_params", - "polb", - "polb_orientation", - "polb_cal_params", - "beam_fwhm", - ] - for field in layout_fields_ignore: - if field in layout_fields: - layout_fields.remove(field) - for field in layout_fields: - keyword = field - if len(keyword) > 8: - keyword = field.replace("_", "") - - value = layout[field][0] - if isinstance(value, bytes): - value = value.decode("utf8") - - extra_keywords[keyword.upper()] = value - - layout_param_dict = { - "telescope_location": telescope_location, - "Nants_telescope": Nants_telescope, - "antenna_positions": antenna_positions, - "antenna_names": antenna_names, - "antenna_numbers": antenna_numbers, - "gst0": gst0, - "rdate": rdate, - "earth_omega": earth_omega, - "dut1": dut1, - "timesys": timesys, - "diameters": diameters, - "extra_keywords": extra_keywords, - } - - return layout_param_dict +__all__ = ["FHD"] class FHD(UVData): @@ -516,7 +117,7 @@ def read_fhd( "information will be missing." ) - filenames = fhd_filenames( + filenames = fhd_utils.fhd_filenames( vis_files=vis_files, params_file=params_file, obs_file=obs_file, @@ -649,7 +250,7 @@ def read_fhd( "Tile" + "0" * (3 - len(ant.strip())) + ant.strip() for ant in obs_tile_names ] - layout_param_dict = get_fhd_layout_info( + layout_param_dict = fhd_utils.get_fhd_layout_info( layout_file=layout_file, telescope_name=self.telescope.name, latitude=latitude, @@ -768,7 +369,7 @@ def read_fhd( # history: add the first few lines from the settings file if settings_file is not None: - self.history = get_fhd_history(settings_file) + self.history = fhd_utils.get_fhd_history(settings_file) else: self.history = "" diff --git a/src/pyuvdata/uvdata/ms.py b/src/pyuvdata/uvdata/ms.py index def94a725c..c7b0f404b7 100644 --- a/src/pyuvdata/uvdata/ms.py +++ b/src/pyuvdata/uvdata/ms.py @@ -16,7 +16,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import ms as ms_utils +from ..utils.io import ms as ms_utils from . import UVData __all__ = ["MS"] diff --git a/src/pyuvdata/uvdata/mwa_corr_fits.py b/src/pyuvdata/uvdata/mwa_corr_fits.py index 06a45fb206..d1d11fd43e 100644 --- a/src/pyuvdata/uvdata/mwa_corr_fits.py +++ b/src/pyuvdata/uvdata/mwa_corr_fits.py @@ -20,7 +20,7 @@ from .. import Telescope, utils from ..data import DATA_PATH from ..docstrings import copy_replace_short_description -from ..utils.file_io import fits as fits_utils +from ..utils.io import fits as fits_utils from . import UVData, _corr_fits __all__ = ["input_output_mapping", "MWACorrFITS"] diff --git a/src/pyuvdata/uvdata/uvdata.py b/src/pyuvdata/uvdata/uvdata.py index 5f1a9c2691..427c9520b7 100644 --- a/src/pyuvdata/uvdata/uvdata.py +++ b/src/pyuvdata/uvdata/uvdata.py @@ -26,7 +26,7 @@ from .. import utils from ..docstrings import combine_docstrings, copy_replace_short_description from ..utils import phasing as phs_utils -from ..utils.file_io import hdf5 as hdf5_utils +from ..utils.io import hdf5 as hdf5_utils from ..uvbase import UVBase from .initializers import new_uvdata diff --git a/src/pyuvdata/uvdata/uvfits.py b/src/pyuvdata/uvdata/uvfits.py index e0639bba57..5e7f49cfc6 100644 --- a/src/pyuvdata/uvdata/uvfits.py +++ b/src/pyuvdata/uvdata/uvfits.py @@ -24,7 +24,7 @@ from .. import utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import fits as fits_utils +from ..utils.io import fits as fits_utils from . import UVData __all__ = ["UVFITS"] diff --git a/src/pyuvdata/uvdata/uvh5.py b/src/pyuvdata/uvdata/uvh5.py index d5e8cc4c2e..ca5f09ab78 100644 --- a/src/pyuvdata/uvdata/uvh5.py +++ b/src/pyuvdata/uvdata/uvh5.py @@ -17,7 +17,7 @@ from .. import Telescope, utils from ..docstrings import copy_replace_short_description -from ..utils.file_io import hdf5 as hdf5_utils +from ..utils.io import hdf5 as hdf5_utils from . import UVData __all__ = ["UVH5", "FastUVH5Meta"] diff --git a/tests/utils/file_io/__init__.py b/tests/utils/io/__init__.py similarity index 75% rename from tests/utils/file_io/__init__.py rename to tests/utils/io/__init__.py index b79a0ec501..ec5d480e53 100644 --- a/tests/utils/file_io/__init__.py +++ b/tests/utils/io/__init__.py @@ -1,4 +1,4 @@ # -*- mode: python; coding: utf-8 -*- # Copyright (c) 2024 Radio Astronomy Software Group # Licensed under the 2-clause BSD License -"""Tests for utility file_io functions.""" +"""Tests for utility file io functions.""" diff --git a/tests/utils/file_io/test_fits.py b/tests/utils/io/test_fits.py similarity index 85% rename from tests/utils/file_io/test_fits.py rename to tests/utils/io/test_fits.py index e8c14672b8..468eca0cae 100644 --- a/tests/utils/file_io/test_fits.py +++ b/tests/utils/io/test_fits.py @@ -23,7 +23,7 @@ def test_deprecated_utils_import(): with check_warnings( DeprecationWarning, match="The _fits_indexhdus function has moved, please import it as " - "pyuvdata.utils.file_io.fits._indexhdus. This warnings will become an " + "pyuvdata.utils.io.fits._indexhdus. This warnings will become an " "error in version 3.2", ): utils._fits_indexhdus(hdu_list) @@ -31,7 +31,7 @@ def test_deprecated_utils_import(): with check_warnings( DeprecationWarning, match="The _fits_gethduaxis function has moved, please import it as " - "pyuvdata.utils.file_io.fits._gethduaxis. This warnings will become an " + "pyuvdata.utils.io.fits._gethduaxis. This warnings will become an " "error in version 3.2", ): utils._fits_gethduaxis(vis_hdu, 5) diff --git a/tests/utils/file_io/test_hdf5.py b/tests/utils/io/test_hdf5.py similarity index 97% rename from tests/utils/file_io/test_hdf5.py rename to tests/utils/io/test_hdf5.py index c4b8fd573f..6baa537a8c 100644 --- a/tests/utils/file_io/test_hdf5.py +++ b/tests/utils/io/test_hdf5.py @@ -6,7 +6,7 @@ import numpy as np import pytest -import pyuvdata.utils.file_io.hdf5 as hdf5_utils +import pyuvdata.utils.io.hdf5 as hdf5_utils from pyuvdata import utils diff --git a/tests/utils/file_io/test_ms.py b/tests/utils/io/test_ms.py similarity index 99% rename from tests/utils/file_io/test_ms.py rename to tests/utils/io/test_ms.py index df65d1da8c..c1e04f2fb1 100644 --- a/tests/utils/file_io/test_ms.py +++ b/tests/utils/io/test_ms.py @@ -8,7 +8,7 @@ import numpy as np import pytest -import pyuvdata.utils.file_io.ms as ms_utils +import pyuvdata.utils.io.ms as ms_utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/utils/test_phasing.py b/tests/utils/test_phasing.py index 556615b302..ea9a234ffa 100644 --- a/tests/utils/test_phasing.py +++ b/tests/utils/test_phasing.py @@ -1946,15 +1946,18 @@ def test_uvw_track_generator_moon(selenoid): if selenoid == "SPHERE": # check defaults - gen_results = utils.uvw_track_generator( - lon_coord=0.0, - lat_coord=0.0, - coord_frame="icrs", - telescope_loc=(0, 0, 0), - time_array=2456789.0, - antenna_positions=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), - telescope_frame="mcmf", - ) + try: + gen_results = utils.uvw_track_generator( + lon_coord=0.0, + lat_coord=0.0, + coord_frame="icrs", + telescope_loc=(0, 0, 0), + time_array=2456789.0, + antenna_positions=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + telescope_frame="mcmf", + ) + except SpiceUNKNOWNFRAME as err: + pytest.skip("SpiceUNKNOWNFRAME error: " + str(err)) # Check that the total lengths all match 1 np.testing.assert_allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) diff --git a/tests/utils/test_pol.py b/tests/utils/test_pol.py index be9e2431bc..277d18a4c0 100644 --- a/tests/utils/test_pol.py +++ b/tests/utils/test_pol.py @@ -233,3 +233,14 @@ def test_pol_order(pols, aips_order, casa_order, order): assert all(check == casa_order) if order == "AIPS": assert all(check == aips_order) + + +def test_x_orientation_pol_map(): + with check_warnings( + DeprecationWarning, + match="This function (_x_orientation_rep_dict) is deprecated, use " + "pyuvdata.utils.pol.x_orientation_pol_map instead.", + ): + assert utils._x_orientation_rep_dict("east") == {"x": "e", "y": "n"} + + assert utils.x_orientation_pol_map("north") == {"x": "n", "y": "e"} diff --git a/tests/uvbeam/test_beamfits.py b/tests/uvbeam/test_beamfits.py index 343c4f0758..94e989373a 100644 --- a/tests/uvbeam/test_beamfits.py +++ b/tests/uvbeam/test_beamfits.py @@ -11,7 +11,7 @@ import pytest from astropy.io import fits -import pyuvdata.utils.file_io.fits as fits_utils +import pyuvdata.utils.io.fits as fits_utils from pyuvdata import UVBeam, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/uvcal/test_calfits.py b/tests/uvcal/test_calfits.py index 8771545c9e..3fe3c3313a 100644 --- a/tests/uvcal/test_calfits.py +++ b/tests/uvcal/test_calfits.py @@ -11,7 +11,7 @@ import pytest from astropy.io import fits -import pyuvdata.utils.file_io.fits as fits_utils +import pyuvdata.utils.io.fits as fits_utils from pyuvdata import UVCal, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/uvcal/test_uvcal.py b/tests/uvcal/test_uvcal.py index 9a6101070a..8163076243 100644 --- a/tests/uvcal/test_uvcal.py +++ b/tests/uvcal/test_uvcal.py @@ -15,7 +15,7 @@ from astropy.io import fits from astropy.table import Table -import pyuvdata.utils.file_io.fits as fits_utils +import pyuvdata.utils.io.fits as fits_utils from pyuvdata import UVCal, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/uvdata/test_uvfits.py b/tests/uvdata/test_uvfits.py index 9d346fd3fe..063a9efb31 100644 --- a/tests/uvdata/test_uvfits.py +++ b/tests/uvdata/test_uvfits.py @@ -12,7 +12,7 @@ import pytest from astropy.io import fits -import pyuvdata.utils.file_io.fits as fits_utils +import pyuvdata.utils.io.fits as fits_utils from pyuvdata import UVData, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/uvdata/test_uvh5.py b/tests/uvdata/test_uvh5.py index 81cf85d11b..d189d456c0 100644 --- a/tests/uvdata/test_uvh5.py +++ b/tests/uvdata/test_uvh5.py @@ -19,7 +19,7 @@ from astropy.time import Time from packaging import version -import pyuvdata.utils.file_io.hdf5 as hdf5_utils +import pyuvdata.utils.io.hdf5 as hdf5_utils from pyuvdata import UVData, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings diff --git a/tests/uvflag/test_uvflag.py b/tests/uvflag/test_uvflag.py index 07dde3e215..2baca5c73b 100644 --- a/tests/uvflag/test_uvflag.py +++ b/tests/uvflag/test_uvflag.py @@ -18,7 +18,7 @@ from pyuvdata import UVCal, UVData, UVFlag, __version__, utils from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings -from pyuvdata.utils.file_io import hdf5 as hdf5_utils +from pyuvdata.utils.io import hdf5 as hdf5_utils from pyuvdata.uvbase import old_telescope_metadata_attrs from pyuvdata.uvflag import and_rows_cols, flags2waterfall