diff --git a/CHANGELOG.md b/CHANGELOG.md index 121cfae09..aa6646dcd 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 +- New UVParameter `pol_convention` on `UVData` and `UVCal`. This specifies the convention +assumed for converting linear to stokes polarizations -- either "sum" or "avg". Also +added to `uvcalibrate` to apply from the `UVCal` to the `UVData`. - New `ignore_telescope_param_update_warnings_for` function that globally ignores warnings for specific telescopes. - New `.telescope` cached property on the `FastUVH5Meta` object that auto-creates a diff --git a/docs/uvcal_tutorial.rst b/docs/uvcal_tutorial.rst index cf4b5e4e0..5ee8e480a 100644 --- a/docs/uvcal_tutorial.rst +++ b/docs/uvcal_tutorial.rst @@ -42,6 +42,26 @@ gives the beginning and end of the time range. The local sidereal times follow a pattern, UVCal objects should have either an ``lst_array`` or an ``lst_range`` attribute set. +Generating calibration solutions typically requires choosing a convention concerning how +polarized sky emission is mapped to the instrumental polarizations. For +linear polarizations ``XX`` and ``YY``, the stokes ``I`` sky emission can be mapped to +``I = (XX + YY)/2`` (the ``avg`` convention) or ``I = XX + YY`` (the ``sum`` +convention). This choice is generally encoded in the sky model to which the visibilities +are calibrated. Different tools and simulators make different choices, generally following +a standard choice for the field. For example, tasks in ``CASA`` (e.g., ``tclean``) and +``MIRIAD``, along with ``WSClean``, all assume the ``avg`` convention. FHD and the HERA +analysis stack use the ``sum`` convention. In ``pyuvdata`` either of these choices are +OK, but the +choice should be recorded as the ``pol_convention`` parameter in both ``UVCal`` and +``UVData`` objects. Since the ``pol_convention`` has always (at least implicitly) been +chosen for calibration solutions, we suggest *always* specifying this parameter on the +``UVCal`` object (though we do not enforce this, for backwards compatibility reasons). +Only *calibrated* ``UVData`` objects make sense to have the ``pol_convention`` specified. +To learn more about this parameter and how ``pyuvdata`` deals with it, please see the +section below `UVCal: Calibrating UVData`_. + + + 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 ``telescope.antenna_numbers``) @@ -260,6 +280,64 @@ UVCal: Calibrating UVData Calibration solutions in a :class:`pyuvdata.UVCal` object can be applied to a :class:`pyuvdata.UVData` object using the :func:`pyuvdata.utils.uvcalibrate` function. +Generating calibration solutions typically requires choosing a convention concerning how +polarized sky emission is mapped to the instrumental polarizations. For +linear polarizations ``XX`` and ``YY``, the stokes ``I`` sky emission can be mapped to +``I = (XX + YY)/2`` (the ``avg`` convention) or ``I = XX + YY`` (the ``sum`` +convention). This choice is generally encoded in the sky model to which the visibilities +are calibrated. Different tools and simulators make different choices, generally following +a standard choice for the field. When calibrating a ``UVData`` object with a ``UVCal`` +object using :func:`pyuvdata.utils.uvcalibrate`, it is *required* to specify this +convention. At this time, the convention can be specified either on the ``UVCal`` object +itself, or as a parameter to :func:`pyuvdata.utils.uvcalibrate`. The chosen ``pol_convention`` +will then be applied to and stored on the resulting ``UVData`` object. + +There are a few non-trivial combinations of parameters concerning the ``pol_convention`` +that should be noted: + +* There are two parameters to :func:`pyuvdata.utils.uvcalibrate` that specify how + the convention should be handled: ``uvd_pol_convention`` and ``uvc_pol_convention``, + and these act differently depending on whether ``undo`` is True or False. The + ``uvc_pol_convention`` is only ever meant to specify what convention the ``UVCal`` + object actually uses, and is therefore unnecessary if ``UVCal.pol_convention`` is + specified (regardless of whether calibrating or uncalibrating). On the other hand, + the ``uvd_pol_convention`` specifies the *desired* convention on the resulting + ``UVData`` object if calibrating, and otherwise specifies the actual convention on + the ``UVData`` object (if uncalibrating, and this convention is not already specified + on the object itself). +* Regardless of the value of ``undo``, the convention that is inferred for the + calibration solutions is determined as follows: + * If neither ``uvc_pol_convention`` nor ``uvcal.pol_convention`` are specified, a + a warning is raised (since the resulting calibrated data is not well-determined), + and it is *assumed* that the solutions have the same convention as the ``UVData`` + (i.e. the desired convention in the case of calibration, or the actual convention + in the case of uncalibration). If these are also not specified, no convention + corrections are applied, and the result is ambiguous. + * If both ``uvc_pol_convention`` and ``uvcal.pol_convention`` are specified and are + different, an error is raised. +* When **calibrating** in :func:`pyuvdata.utils.uvcalibrate` (i.e. ``undo=False``): + * If ``uvdata.pol_convention`` is specified, an error is raised, because you are + trying to calibrate already-calibrated data. + * The convention applied to the resulting ``UVData`` object is inferred in the + following precedence: (i) the value of ``uvd_pol_convention``, (ii) whatever is + specified as the convention of the ``UVCal`` object (either via ``uvc_pol_convention`` + or ``uvcal.pol_convention``, see above), (iii) if still unspecified, no convention + will be used and a warning will be raised. This was always the behaviour in earlier + versions of ``pyuvdata`` (pre-v3). +* When **un-calibrating** with :func:`pyuvdata.utils.uvcalibrate` (i.e. ``undo=True``): + * If both ``uvd_pol_convention`` and ``uvdata.pol_convention`` are defined and + are different, an error is raised. + * If neither are set, a warning is raised, since the resulting un-calibrated values + may not be the same as the original values before calibration (since a different + convention could have been used to calibrate originally than is being used to + de-calibrate). However, calibration will continue, assuming that the ``UVData`` + object has the same convention as the ``UVCal`` object used to de-calibrate. +* It is not supported to have ``pol_convention`` set on ``UVCal``, but *not* + ``gain_scale``. A ``pol_convention`` only makes sense in the context of having a + scale for the gains. +* Mis-matching ``uvd_pol_convention`` and ``uvc_pol_convention`` is perfectly fine: any + necessary corrections in the calibration will be made to obtain the correct desired + convention. a) Calibration of UVData by UVCal ********************************* @@ -267,6 +345,7 @@ a) Calibration of UVData by UVCal >>> # We can calibrate directly using a UVCal object >>> import os + >>> import numpy as np >>> from pyuvdata import UVData, UVCal, utils >>> from pyuvdata.data import DATA_PATH >>> uvd = UVData.from_file( @@ -281,7 +360,14 @@ a) Calibration of UVData by UVCal >>> uvc.telescope.antenna_names = np.array( ... [name.replace("ant", "HH") for name in uvc.telescope.antenna_names] ... ) + >>> # We should also set the gain_scale and pol_convention, which was not set + >>> # in this old file. In old HERA files, like this one, the pol_convention + >>> # was implicitly "avg" but in new files it is explicitly "sum" + >>> uvc.gain_scale = "Jy" + >>> uvc.pol_convention = "avg" >>> uvd_calibrated = utils.uvcalibrate(uvd, uvc, inplace=False) + >>> print(uvd_calibrated.pol_convention) + avg >>> # We can also un-calibrate using the same UVCal >>> uvd_uncalibrated = utils.uvcalibrate(uvd_calibrated, uvc, inplace=False, undo=True) diff --git a/src/pyuvdata/utils/uvcalibrate.py b/src/pyuvdata/utils/uvcalibrate.py index 50ac95808..fb01f55d8 100644 --- a/src/pyuvdata/utils/uvcalibrate.py +++ b/src/pyuvdata/utils/uvcalibrate.py @@ -3,24 +3,154 @@ # Licensed under the 2-clause BSD License """Code to apply calibration solutions to visibility data.""" import warnings +from typing import Literal import numpy as np from .pol import POL_TO_FEED_DICT, jnum2str, parse_jpolstr, polnum2str, polstr2num +def _get_pol_conventions( + uvdata, + uvcal, + undo: bool, + uvc_pol_convention: Literal["sum", "avg"] | None, + uvd_pol_convention: Literal["sum", "avg"] | None, +): + if uvc_pol_convention is None and uvcal.pol_convention is None: + warnings.warn( + message=( + "pol_convention is not specified on the UVCal object, and " + "uvc_pol_convention was not specified. Tentatively assuming " + "that the UVCal and UVData objects (implicitly) have the same " + "convention." + ), + stacklevel=2, + ) + uvc_pol_convention = uvd_pol_convention or uvdata.pol_convention + elif uvc_pol_convention is None: + uvc_pol_convention = uvcal.pol_convention + elif ( + uvcal.pol_convention is not None and uvc_pol_convention != uvcal.pol_convention + ): + raise ValueError( + "uvc_pol_convention is set, and different than uvcal.pol_convention. " + f"Got {uvc_pol_convention} and {uvcal.pol_convention}." + ) + + if undo: + if uvd_pol_convention is None and uvdata.pol_convention is None: + warnings.warn( + message=( + "pol_convention is not specified on the UVData object, and " + "uvd_pol_convention was not specified. Tentatively assuming " + "that the UVCal and UVData objects (implicitly) have the same " + "convention." + ), + stacklevel=2, + ) + uvd_pol_convention = uvc_pol_convention + elif uvd_pol_convention is None: + uvd_pol_convention = uvdata.pol_convention + elif ( + uvdata.pol_convention is not None + and uvd_pol_convention != uvdata.pol_convention + ): + raise ValueError( + "Both uvd_pol_convention and uvdata.pol_convention were specified with " + f"different values: {uvd_pol_convention} and {uvdata.pol_convention}." + ) + else: + if uvdata.pol_convention is not None: + raise ValueError("You are trying to calibrate already-calibrated data.") + if uvd_pol_convention is None: + uvd_pol_convention = uvc_pol_convention + + if uvd_pol_convention is None: + # Both uvc and uvd have no pol_convention specified + warnings.warn( + message=( + "Neither uvd_pol_convention nor uvc_pol_convention are specified, " + "so the resulting UVData object will have ambiguous convention. " + ), + stacklevel=2, + ) + if uvd_pol_convention not in ["sum", "avg", None]: + raise ValueError( + f"uvd_pol_convention must be 'sum' or 'avg'. Got {uvd_pol_convention}" + ) + if uvc_pol_convention not in ["sum", "avg", None]: + raise ValueError( + f"uvc_pol_convention must be 'sum' or 'avg'. Got {uvc_pol_convention}" + ) + + return uvc_pol_convention, uvd_pol_convention + + +def _apply_pol_convention_corrections( + uvdata, + undo: bool, + uvc_pol_convention: Literal["sum", "avg"] | None, + uvd_pol_convention: Literal["sum", "avg"] | None, +): + r""" + Apply corrections to calibration/de-calibration from differences in convention. + + This function corrects the UVData ``data_array`` in-place, when the polarization + convention desired for the UVData is different from the convention that was used + for the calibration. It also sets the corresponding ``pol_convention`` attribute + on the UVData object. + + The logic is as follows. If the convention of the calibration and UVData object + are the same, no correction is applied. If they are different, the correction + applied is either to multiply or divide by two. Let's start with a default case: + let's say that the calibration solutions assume that instrumental polarizations + are related to the stokes-I sky by the ``avg`` convention, in which case + :math:`XX \sim I`, and we are calibrating data where we want the result to have + the ``sum`` convention, i.e. :math:`XX \sim I/2`. Then, for data that is in + instrumental polarizations (i.e. XX) we would need to *divide* the result by 2. + This is flipped (i.e. flips between multiply and divide) for every difference from + the above scenario, i.e. + + * If we are de-calibrated rather than calibrating + * If the UVData is in stokes polarizations rather than instrumental (note that this + is not currently possible anyway, so we do not provide the ability here). + * If the conventions are swapped between the calibration solutions and the UVData. + + To be clear, if two of these are true, the resulting correction will be "flipped + twice" (i.e. remain as *divide* by two), but if only one or all three are true, + then the correction will be flipped to be multiply by two. + """ + if uvd_pol_convention != uvc_pol_convention: + correction = np.ones(uvdata.Npols) / 2 + + if undo: + # We are de-calibrating + correction = 1 / correction + + if uvc_pol_convention == "sum": + # pol convention difference is the other way around + correction = 1 / correction + + uvdata.data_array *= correction + + uvdata.pol_convention = None if undo else uvd_pol_convention + + def uvcalibrate( uvdata, uvcal, *, - inplace=True, - prop_flags=True, - d_term_cal=False, - flip_gain_conj=False, - delay_convention="minus", - undo=False, - time_check=True, - ant_check=True, + inplace: bool = True, + prop_flags: bool = True, + d_term_cal: bool = False, + flip_gain_conj: bool = False, + delay_convention: Literal["minus", "plus"] = "minus", + undo: bool = False, + time_check: bool = True, + ant_check: bool = True, + uvc_pol_convention: Literal["sum", "avg"] | None = None, + uvd_pol_convention: Literal["sum", "avg"] | None = None, ): """ Calibrate a UVData object with a UVCal object. @@ -61,6 +191,18 @@ def uvcalibrate( object have calibration solutions in the UVCal object. If this option is set to False, uvcalibrate will proceed without erroring and data for antennas without calibrations will be flagged. + uvc_pol_convention : str, {"sum", "avg"}, optional + The convention for how instrumental polarizations (e.g. XX and YY) are assumed + to have been converted to Stokes parameters in ``uvcal``. Options are 'sum' and + 'avg', corresponding to I=XX+YY and I=(XX+YY)/2 (for linear instrumental + polarizations) respectively. Only required if ``pol_convention`` is not set on + ``uvcal`` itself. If it is not specified and is not set on the UVCal + object, a deprecation warning is raised (will be an error in the future). + uvd_pol_convention : str, {"sum", "avg"}, optional + The same polarization convention as ``uvc_pol_convention``, except that this + represents either the convention that *has* been adopted in ``uvdata`` (in the + case that ``undo=True``), or the convention that is *desired* for the resulting + ``UVData`` object (if ``undo=False``). Returns ------- @@ -79,6 +221,33 @@ def uvcalibrate( "calibrations" ) + if np.any(uvdata.polarization_array > 0): + raise NotImplementedError( + "It is currently not possible to calibrate or de-calibrate data with " + "stokes polarizations, since it is impossible to define UVCal objects with " + "these polarizations. If you require this functionality, please submit an " + "issue at " + "https://github.com/RadioAstronomySoftwareGroup/pyuvdata/issues/new" + ) + + if uvcal.gain_scale is None: + warnings.warn( + "gain_scale is not set, so there is no way to know what the resulting units" + " are. For now, we assume that `gain_scale` matches whatever is on the " + "UVData object (i.e. we do not change its units). Furthermore, all " + "corrections concerning the pol_convention will be ignored.", + category=UserWarning, + stacklevel=2, + ) + elif undo and uvcal.gain_scale != uvdata.vis_units: + raise ValueError( + "Cannot undo calibration if gain_scale is not the same as the units on " + "the UVData object." + ) + + uvc_pol_convention, uvd_pol_convention = _get_pol_conventions( + uvdata, uvcal, undo, uvc_pol_convention, uvd_pol_convention + ) if not inplace: uvdata = uvdata.copy() @@ -417,5 +586,11 @@ def uvcalibrate( if uvcal_use.gain_scale is not None: uvdata.vis_units = uvcal_use.gain_scale + # Set pol convention properly + if uvcal.gain_scale is not None: + _apply_pol_convention_corrections( + uvdata, undo, uvc_pol_convention, uvd_pol_convention + ) + if not inplace: return uvdata diff --git a/src/pyuvdata/uvcal/calfits.py b/src/pyuvdata/uvcal/calfits.py index 68527f646..b70a649af 100644 --- a/src/pyuvdata/uvcal/calfits.py +++ b/src/pyuvdata/uvcal/calfits.py @@ -207,6 +207,8 @@ def write_calfits( prihdr["DIFFUSE"] = self.diffuse_model if self.gain_scale is not None: prihdr["GNSCALE"] = self.gain_scale + if self.pol_convention is not None: + prihdr["POLCONV"] = self.pol_convention if self.Ntimes > 1: prihdr["INTTIME"] = median_int_time else: @@ -599,6 +601,7 @@ def read_calfits( self.gain_convention = hdr.pop("GNCONVEN") self.gain_scale = hdr.pop("GNSCALE", None) + self.pol_convention = hdr.pop("POLCONV", None) self.cal_type = hdr.pop("CALTYPE") # old files might have a freq range for gain types but we don't want them @@ -606,6 +609,8 @@ def read_calfits( self.freq_range = np.array( [list(map(float, hdr.pop("FRQRANGE").split(",")))] ) + else: + hdr.pop("FRQRANGE", None) self.cal_style = hdr.pop("CALSTYLE") if self.cal_style == "sky": diff --git a/src/pyuvdata/uvcal/calh5.py b/src/pyuvdata/uvcal/calh5.py index df81922b3..f2716ce6c 100644 --- a/src/pyuvdata/uvcal/calh5.py +++ b/src/pyuvdata/uvcal/calh5.py @@ -72,6 +72,7 @@ class FastCalH5Meta(hdf5_utils.HDF5Meta): "sky_catalog", "instrument", "version", + "pol_convention", } ) @@ -253,6 +254,7 @@ def _read_header( "ref_antenna_array", "scan_number_array", "sky_catalog", + "pol_convention", ] for attr in required_parameters: @@ -773,6 +775,9 @@ def _write_header(self, header): header["phase_center_id_array"] = self.phase_center_id_array if self.Nphase is not None: header["Nphase"] = self.Nphase + if self.pol_convention is not None: + header["pol_convention"] = np.bytes_(self.pol_convention) + if self.phase_center_catalog is not None: pc_group = header.create_group("phase_center_catalog") for pc, pc_dict in self.phase_center_catalog.items(): diff --git a/src/pyuvdata/uvcal/fhd_cal.py b/src/pyuvdata/uvcal/fhd_cal.py index 894ff2413..db2728ad8 100644 --- a/src/pyuvdata/uvcal/fhd_cal.py +++ b/src/pyuvdata/uvcal/fhd_cal.py @@ -234,6 +234,8 @@ def read_fhd_cal( self._set_sky() self.gain_convention = "divide" + self.gain_scale = "Jy" + self.pol_convetions = "sum" self._set_gain() # currently don't have branch info. may change in future. diff --git a/src/pyuvdata/uvcal/ms_cal.py b/src/pyuvdata/uvcal/ms_cal.py index eea7deb33..289be0948 100644 --- a/src/pyuvdata/uvcal/ms_cal.py +++ b/src/pyuvdata/uvcal/ms_cal.py @@ -234,6 +234,7 @@ def read_ms_cal( self.sky_catalog = main_keywords.get("pyuvdata_sky_catalog", None) self.gain_scale = main_keywords.get("pyuvdata_gain_scale", None) + self.pol_convention = main_keywords.get("pyuvdata_polconv", None) self.observer = main_keywords.get("pyuvdata_observer", None) if "pyuvdata_cal_style" in main_keywords: self.cal_style = main_keywords["pyuvdata_cal_style"] @@ -491,6 +492,8 @@ def write_ms_cal(self, filename, clobber=False): if self.gain_scale is not None: ms.putkeyword("pyuvdata_gain_scale", self.gain_scale) + if self.pol_convention is not None: + ms.putkeyword("pyuvdata_polconv", self.pol_convention) if self.observer is not None: ms.putkeyword("pyuvdata_observer", self.observer) diff --git a/src/pyuvdata/uvcal/uvcal.py b/src/pyuvdata/uvcal/uvcal.py index 4daa1b851..18cec262a 100644 --- a/src/pyuvdata/uvcal/uvcal.py +++ b/src/pyuvdata/uvcal/uvcal.py @@ -597,6 +597,23 @@ def __init__(self): required=False, ) + desc = ( + "The convention for how instrumental polarizations (e.g. XX and YY) " + "are converted to Stokes parameters. Options are 'sum' and 'avg', " + "corresponding to I=XX+YY and I=(XX+YY)/2 (for linear instrumental " + "polarizations) respectively. This parameter is not required, for " + "backwards-compatibility reasons, but is highly recommended. If " + "pol_convention is set, gain_scale should also be set." + ) + self._pol_convention = uvp.UVParameter( + "pol_convention", + required=False, + description=desc, + form="str", + spoof_val="avg", + acceptable_vals=["sum", "avg"], + ) + super(UVCal, self).__init__() # Assign attributes to UVParameters after initialization, since UVBase.__init__ @@ -1642,6 +1659,14 @@ def check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) + # gain_scale should be set if pol_convention is set. + if self.pol_convention is not None and self.gain_scale is None: + warnings.warn( + "gain_scale should be set if pol_convention is set. When calibrating " + "data with `uvcalibrate`, pol_convention will be ignored if " + "gain_scale is not set." + ) + # 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: diff --git a/src/pyuvdata/uvdata/fhd.py b/src/pyuvdata/uvdata/fhd.py index 5373de335..f3ed09bef 100644 --- a/src/pyuvdata/uvdata/fhd.py +++ b/src/pyuvdata/uvdata/fhd.py @@ -178,6 +178,7 @@ def read_fhd( self.flex_spw_id_array = np.zeros(self.Nfreqs, dtype=int) self.vis_units = "Jy" + self.pol_convention = "sum" # bl_info.JDATE (a vector of length Ntimes) is the only safe date/time # to use in FHD files. diff --git a/src/pyuvdata/uvdata/mir.py b/src/pyuvdata/uvdata/mir.py index 457e8ea4c..c060f98f7 100644 --- a/src/pyuvdata/uvdata/mir.py +++ b/src/pyuvdata/uvdata/mir.py @@ -692,6 +692,7 @@ def _init_from_mir_parser( self.uvw_array = (-1.0) * uvw_array self.vis_units = "Jy" + self.pol_convention = "avg" isource = np.unique(mir_data.in_data["isource"]) for sou_id in isource: diff --git a/src/pyuvdata/uvdata/miriad.py b/src/pyuvdata/uvdata/miriad.py index b07b5c879..176051d09 100644 --- a/src/pyuvdata/uvdata/miriad.py +++ b/src/pyuvdata/uvdata/miriad.py @@ -88,6 +88,7 @@ def _load_miriad_variables(self, uv): "rdate", "timesys", "xorient", + "polconv", "cnt", "ra", "dec", @@ -240,6 +241,8 @@ def _load_miriad_variables(self, uv): self.timesys = uv["timesys"].replace("\x00", "") if "xorient" in uv.vartable.keys(): self.telescope.x_orientation = uv["xorient"].replace("\x00", "") + if "polconv" in uv.vartable.keys(): + self.pol_convention = uv["polconv"].replace("\x00", "") if "bltorder" in uv.vartable.keys(): blt_order_str = uv["bltorder"].replace("\x00", "") self.blt_order = tuple(blt_order_str.split(", ")) @@ -1871,6 +1874,9 @@ def write_miriad( if self.telescope.x_orientation is not None: uv.add_var("xorient", "a") uv["xorient"] = self.telescope.x_orientation + if self.pol_convention is not None: + uv.add_var("polconv", "a") + uv["polconv"] = self.pol_convention if self.blt_order is not None: blt_order_str = ", ".join(self.blt_order) uv.add_var("bltorder", "a") diff --git a/src/pyuvdata/uvdata/ms.py b/src/pyuvdata/uvdata/ms.py index d87d0e26e..73fd904a3 100644 --- a/src/pyuvdata/uvdata/ms.py +++ b/src/pyuvdata/uvdata/ms.py @@ -357,6 +357,8 @@ def write_ms( if self.telescope.x_orientation is not None: ms.putkeyword("pyuvdata_xorient", self.telescope.x_orientation) + if self.pol_convention is not None: + ms.putkeyword("pyuvdata_polconv", self.pol_convention) ms.done() @@ -451,6 +453,9 @@ def _read_ms_main( if "pyuvdata_xorient" in main_keywords.keys(): self.telescope.x_orientation = main_keywords["pyuvdata_xorient"] + if "pyuvdata_polconv" in main_keywords.keys(): + self.pol_convention = main_keywords["pyuvdata_polconv"] + default_vis_units = { "DATA": "uncalib", "CORRECTED_DATA": "Jy", diff --git a/src/pyuvdata/uvdata/uvdata.py b/src/pyuvdata/uvdata/uvdata.py index da48ea778..e577c8a06 100644 --- a/src/pyuvdata/uvdata/uvdata.py +++ b/src/pyuvdata/uvdata/uvdata.py @@ -583,6 +583,23 @@ def __init__(self): "filename", required=False, description=desc, expected_type=str ) + desc = ( + "The convention for how instrumental polarizations (e.g. XX and YY) " + "are converted to Stokes parameters. Options are 'sum' and 'avg', " + "corresponding to I=XX+YY and I=(XX+YY)/2 (for linear instrumental " + "polarizations) respectively. This parameter is not required, and " + "only makes sense for calibrated data. If pol_convention is set, " + "vis_units should be set to real units (as opposed to 'uncalib')." + ) + self._pol_convention = uvp.UVParameter( + "pol_convention", + required=False, + description=desc, + form="str", + spoof_val="avg", + acceptable_vals=["sum", "avg"], + ) + self.__antpair2ind_cache = {} self.__key2ind_cache = {} @@ -2199,6 +2216,13 @@ def check( ) logger.debug("... Done UVBase Check") + # Check consistency between pol_convention and units of data + if self.vis_units == "uncalib" and self.pol_convention is not None: + raise ValueError( + "pol_convention is set but the data is uncalibrated. This " + "is not allowed." + ) + # then run telescope object check self.telescope.check( check_extra=check_extra, run_check_acceptability=run_check_acceptability diff --git a/src/pyuvdata/uvdata/uvfits.py b/src/pyuvdata/uvdata/uvfits.py index 66923cf55..6a4a48309 100644 --- a/src/pyuvdata/uvdata/uvfits.py +++ b/src/pyuvdata/uvdata/uvfits.py @@ -485,6 +485,8 @@ def read_uvfits( if self.vis_units == "UNCALIB": self.vis_units = "uncalib" + self.pol_convention = vis_hdr.pop("POLCONV", None) + # PHSFRAME is not a standard UVFITS keyword, but was used by older # versions of pyuvdata. To ensure backwards compatibility, we look # for it first to determine the coordinate frame for the data @@ -1250,6 +1252,9 @@ def write_uvfits( hdu.header["RADESYS"] = frame break + if self.pol_convention is not None: + hdu.header["POLCONV"] = self.pol_convention + if self.telescope.x_orientation is not None: hdu.header["XORIENT"] = self.telescope.x_orientation diff --git a/src/pyuvdata/uvdata/uvh5.py b/src/pyuvdata/uvdata/uvh5.py index 4f76184ba..b6b6008a8 100644 --- a/src/pyuvdata/uvdata/uvh5.py +++ b/src/pyuvdata/uvdata/uvh5.py @@ -109,6 +109,7 @@ class FastUVH5Meta(hdf5_utils.HDF5Meta): "eq_coeffs_convention", "phase_center_frame", "version", + "pol_convention", } ) @@ -606,6 +607,7 @@ def _read_header_with_fast_meta( "flex_spw_id_array", "flex_spw_polarization_array", "extra_keywords", + "pol_convention", ]: try: setattr(self, attr, getattr(obj, attr)) @@ -1213,6 +1215,8 @@ def _write_header(self, header): header["blts_are_rectangular"] = self.blts_are_rectangular if self.time_axis_faster_than_bls is not None: header["time_axis_faster_than_bls"] = self.time_axis_faster_than_bls + if self.pol_convention is not None: + header["pol_convention"] = np.bytes_(self.pol_convention) # write out extra keywords if it exists and has elements if self.extra_keywords: diff --git a/tests/conftest.py b/tests/conftest.py index f880a7425..b17c78204 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,6 +58,9 @@ def uvcalibrate_init_data_main(): os.path.join(DATA_PATH, "zen.2458098.45361.HH.omni.calfits_downselected") ) + uvcal.pol_convention = "avg" + uvcal.gain_scale = "Jy" + yield uvdata, uvcal diff --git a/tests/utils/test_uvcalibrate.py b/tests/utils/test_uvcalibrate.py index 528ba63b1..475aae2a7 100644 --- a/tests/utils/test_uvcalibrate.py +++ b/tests/utils/test_uvcalibrate.py @@ -4,6 +4,7 @@ """Tests for uvcalibrate function.""" import os import re +from types import SimpleNamespace import numpy as np import pytest @@ -12,6 +13,116 @@ from pyuvdata.data import DATA_PATH from pyuvdata.testing import check_warnings from pyuvdata.utils import uvcalibrate +from pyuvdata.utils.uvcalibrate import _get_pol_conventions + + +class TestGetPolConventions: + def tets_nothing_specified(self): + with check_warnings( + UserWarning, + match=[ + "pol_convention is not specified on the UVCal object", + "Neither uvd_pol_convention not uvc_pol_convention are specified", + ], + ): + uvc, uvd = _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention=None), + uvcal=SimpleNamespace(pol_convention=None), + undo=False, + uvc_pol_convention=None, + uvd_pol_convention=None, + ) + assert uvc is None + assert uvd is None + + def test_uvc_pol_convention_set(self): + uvc, uvd = _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention=None), + uvcal=SimpleNamespace(pol_convention="avg"), + undo=False, + uvc_pol_convention=None, + uvd_pol_convention=None, + ) + assert uvc == "avg" + assert uvd == "avg" + + def test_uvc_uvcal_different(self): + with pytest.raises( + ValueError, match="uvc_pol_convention is set, and different" + ): + _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention=None), + uvcal=SimpleNamespace(pol_convention="sum"), + undo=False, + uvc_pol_convention="avg", + uvd_pol_convention=None, + ) + + def test_uvd_nor_uvdata_set(self): + with pytest.warns( + UserWarning, match="pol_convention is not specified on the UVData object" + ): + uvc, uvd = _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention=None), + uvcal=SimpleNamespace(pol_convention="avg"), + undo=True, + uvc_pol_convention=None, + uvd_pol_convention=None, + ) + assert uvc == "avg" + assert uvd == "avg" + + @pytest.mark.parametrize("undo", [True, False]) + def test_only_objects_set(self, undo): + uvc, uvd = _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention="sum" if undo else None), + uvcal=SimpleNamespace(pol_convention="avg"), + undo=undo, + uvc_pol_convention=None, + uvd_pol_convention=None if undo else "sum", + ) + assert uvc == "avg" + assert uvd == "sum" + + def test_uvd_uvdata_different(self): + with pytest.raises( + ValueError, match="Both uvd_pol_convention and uvdata.pol_convention" + ): + _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention="sum"), + uvcal=SimpleNamespace(pol_convention="avg"), + undo=True, + uvc_pol_convention="avg", + uvd_pol_convention="avg", + ) + + def test_calibrate_already_calibrated(self): + with pytest.raises( + ValueError, match="You are trying to calibrate already-calibrated data" + ): + _get_pol_conventions( + uvdata=SimpleNamespace(pol_convention="avg"), + uvcal=SimpleNamespace(pol_convention="avg"), + undo=False, + uvc_pol_convention="avg", + uvd_pol_convention="avg", + ) + + @pytest.mark.parametrize("which", ["uvc", "uvd"]) + def test_bad_convention(self, which): + good = SimpleNamespace(pol_convention="avg") + bad = SimpleNamespace(pol_convention="what a silly convention") + + with pytest.raises( + ValueError, match=f"{which}_pol_convention must be 'sum' or 'avg'" + ): + _get_pol_conventions( + uvdata=good if which == "uvc" else bad, + uvcal=good if which == "uvd" else bad, + undo=True, + uvc_pol_convention=None, + uvd_pol_convention=None, + ) @pytest.mark.filterwarnings("ignore:Fixing auto-correlations to be be real-only,") @@ -28,6 +139,8 @@ def test_uvcalibrate_apply_gains_oldfiles(uvcalibrate_uvdata_oldfiles): # downselect to match each other in shape (but not in actual values!) uvd.select(frequencies=uvd.freq_array[:10]) uvc.select(times=uvc.time_array[:3]) + uvc.gain_scale = "Jy" + uvc.pol_convention = "avg" with pytest.raises( ValueError, @@ -78,6 +191,8 @@ def test_uvcalibrate_delay_oldfiles(uvcalibrate_uvdata_oldfiles): # downselect to match uvc.select(times=uvc.time_array[3]) uvc.gain_convention = "multiply" + uvc.pol_convention = "avg" + uvc.gain_scale = "Jy" freq_array_use = np.squeeze(uvd.freq_array) chan_with_use = uvd.channel_width @@ -133,12 +248,27 @@ def test_uvcalibrate(uvcalibrate_data, flip_gain_conj, gain_convention, time_ran uvc.gain_convention = gain_convention if gain_convention == "divide": - assert uvc.gain_scale is None + # set the gain_scale to None to test handling + uvc.gain_scale = None + cal_warn_msg = [ + "gain_scale is not set, so there is no way to know", + "gain_scale should be set if pol_convention is set", + ] + cal_warn_type = UserWarning + undo_warn_msg = [ + "pol_convention is not specified on the UVData object", + "gain_scale is not set, so there is no way to know", + "gain_scale should be set if pol_convention is set", + ] + undo_warn_type = [UserWarning, UserWarning, UserWarning] else: - # set the gain_scale to "Jy" to test that vis units are set properly - uvc.gain_scale = "Jy" + cal_warn_msg = "" + cal_warn_type = None + undo_warn_msg = "" + undo_warn_type = None - uvdcal = uvcalibrate(uvd, uvc, inplace=False, flip_gain_conj=flip_gain_conj) + with check_warnings(cal_warn_type, match=cal_warn_msg): + uvdcal = uvcalibrate(uvd, uvc, inplace=False, flip_gain_conj=flip_gain_conj) if gain_convention == "divide": assert uvdcal.vis_units == "uncalib" else: @@ -171,15 +301,16 @@ def test_uvcalibrate(uvcalibrate_data, flip_gain_conj, gain_convention, time_ran ) # test undo - uvdcal = uvcalibrate( - uvdcal, - uvc, - prop_flags=True, - ant_check=False, - inplace=False, - undo=True, - flip_gain_conj=flip_gain_conj, - ) + with check_warnings(undo_warn_type, match=undo_warn_msg): + uvdcal = uvcalibrate( + uvdcal, + uvc, + prop_flags=True, + ant_check=False, + inplace=False, + undo=True, + flip_gain_conj=flip_gain_conj, + ) np.testing.assert_array_almost_equal(uvd.get_data(key), uvdcal.get_data(key)) assert uvdcal.vis_units == "uncalib" @@ -250,6 +381,7 @@ def test_uvcalibrate_flag_propagation(uvcalibrate_data): assert exp_err == str(errinfo.value) + uvc_sub.gain_scale = "Jy" with pytest.warns(UserWarning) as warninfo: uvdcal = uvcalibrate( uvd, uvc_sub, prop_flags=True, ant_check=False, inplace=False @@ -268,6 +400,7 @@ def test_uvcalibrate_flag_propagation(uvcalibrate_data): @pytest.mark.filterwarnings("ignore:Cannot preserve total_quality_array") def test_uvcalibrate_flag_propagation_name_mismatch(uvcalibrate_init_data): uvd, uvc = uvcalibrate_init_data + uvc.gain_scale = "Jy" # test flag propagation uvc.flag_array[0] = True @@ -320,6 +453,7 @@ def test_uvcalibrate_extra_cal_antennas(uvcalibrate_data): def test_uvcalibrate_antenna_names_mismatch(uvcalibrate_init_data): uvd, uvc = uvcalibrate_init_data + uvc.gain_scale = "Jy" with pytest.raises( ValueError, @@ -346,7 +480,7 @@ def test_uvcalibrate_antenna_names_mismatch(uvcalibrate_init_data): @pytest.mark.parametrize("time_range", [True, False]) def test_uvcalibrate_time_mismatch(uvcalibrate_data, time_range): uvd, uvc = uvcalibrate_data - + uvc.gain_scale = "Jy" if time_range: tstarts = uvc.time_array - uvc.integration_time / (86400 * 2) tends = uvc.time_array + uvc.integration_time / (86400 * 2) @@ -614,3 +748,118 @@ def test_uvcalibrate_delay_multispw(uvcalibrate_uvdata_oldfiles): "calibrations", ): uvcalibrate(uvd, uvc, inplace=False) + + +@pytest.mark.filterwarnings( + "ignore:pol_convention is not specified on the UVCal object" +) +@pytest.mark.filterwarnings( + "ignore:pol_convention is not specified on the UVData object" +) +@pytest.mark.filterwarnings( + "ignore:Neither uvd_pol_convention nor uvc_pol_convention are specified" +) +@pytest.mark.parametrize("convention_on_object", [True, False]) +@pytest.mark.parametrize("uvc_pol_convention", ["sum", "avg", None]) +@pytest.mark.parametrize("uvd_pol_convention", ["sum", "avg", None]) +@pytest.mark.parametrize("polkind", ["linear", "circular"]) # stokes not possible yet +def test_uvcalibrate_pol_conventions( + uvcalibrate_data, + convention_on_object, + uvc_pol_convention, + uvd_pol_convention, + polkind, +): + uvd, uvc = uvcalibrate_data + + # Set defaults + uvd.pol_convention = None + uvc.pol_convention = None + uvc.gain_array[:] = 1.0 + uvd.data_array[:] = 1.0 + + # if polkind=='stokes': + # uvd.polarization_array = np.array([1,2]) + # uvc.jones_array = -np.array([5,6]) + if polkind == "circular": + uvd.polarization_array = -np.array([1, 2]) + uvc.jones_array = -np.array([1, 2]) + else: + uvd.polarization_array = -np.array([5, 6]) + uvc.jones_array = -np.array([5, 6]) + + uvdpol = uvd_pol_convention + + if convention_on_object: + uvc.pol_convention = uvc_pol_convention + uvcpol = None + else: + uvcpol = uvc_pol_convention + + # go forwards and back + calib = uvcalibrate( + uvd, + uvc, + uvd_pol_convention=uvdpol, + uvc_pol_convention=uvcpol, + undo=False, + inplace=False, + ) + roundtrip = uvcalibrate( + calib, + uvc, + uvd_pol_convention=uvdpol, + uvc_pol_convention=uvcpol, + undo=True, + inplace=False, + ) + + assert calib.pol_convention == uvd_pol_convention or uvc_pol_convention + assert roundtrip.pol_convention is None + + # Check we went around the loop properly. + np.testing.assert_allclose(roundtrip.data_array, uvd.data_array) + + if ( + uvc_pol_convention == uvd_pol_convention + or uvc_pol_convention is None + or uvd_pol_convention is None + ): + np.testing.assert_almost_equal(calib.data_array, 1.0) + else: + if uvc_pol_convention == "sum": + # Then uvd pol convention is 'avg', so it has I = (XX+YY)/2, i.e. XX ~ I, + # but the cal intrinsically assumed that I = (XX+YY), i.e. XX ~ I/2. + # Therefore, the result should be 2.0 + np.testing.assert_allclose(calib.data_array, 2.0) + else: + # the opposite + np.testing.assert_allclose(calib.data_array, 0.5) + + +def test_gain_scale_wrong(uvcalibrate_data): + uvd, uvc = uvcalibrate_data + + uvc.gain_scale = "mK" + uvd.vis_units = "Jy" + + with pytest.raises( + ValueError, match="Cannot undo calibration if gain_scale is not the same" + ): + uvcalibrate(uvd, uvc, undo=True) + + +def test_uvdata_pol_array_in_stokes(uvcalibrate_data): + uvd, uvc = uvcalibrate_data + + # Set polarization_array to be in Stokes I, Q, U, V + uvd.polarization_array = np.array([1, 2, 3, 4]) + + with pytest.raises( + NotImplementedError, + match=( + "It is currently not possible to calibrate or de-calibrate data with " + "stokes polarizations" + ), + ): + uvcalibrate(uvd, uvc) diff --git a/tests/uvcal/test_uvcal.py b/tests/uvcal/test_uvcal.py index f55de147c..f93815b6f 100644 --- a/tests/uvcal/test_uvcal.py +++ b/tests/uvcal/test_uvcal.py @@ -114,6 +114,7 @@ def uvcal_data(): "total_quality_array", "extra_keywords", "gain_scale", + "pol_convention", "filename", "scan_number_array", "phase_center_catalog", @@ -3131,24 +3132,54 @@ def test_uvcal_get_methods(gain_data): uvc.get_gains(10) -@pytest.mark.parametrize("file_type", ["calfits", "calh5"]) +@pytest.mark.parametrize("file_type", ["calfits", "calh5", "ms"]) def test_write_read_optional_attrs(gain_data, tmp_path, file_type): + if file_type == "ms": + pytest.importorskip("casacore") + # read a test file cal_in = gain_data # set some optional parameters + cal_in.pol_convention = "sum" + with check_warnings( + UserWarning, + match="gain_scale should be set if pol_convention is set. When " + "calibrating data with `uvcalibrate`, pol_convention will be ignored if " + "gain_scale is not set.", + ): + cal_in.check() + cal_in.gain_scale = "Jy" # write outfile = str(tmp_path / ("test." + file_type)) write_method = "write_" + file_type + if file_type == "ms": + write_method += "_cal" + warn_msg = ( + "key CASA_Version in extra_keywords is longer than 8 characters. It " + "will be truncated to 8 if written to a calfits file format." + ) + warn_type = UserWarning + else: + warn_msg = "" + warn_type = None with check_warnings(None): getattr(cal_in, write_method)(outfile) # read and compare # also check that passing a single file in a list works properly - with check_warnings(None): - cal_in2 = UVCal.from_file([outfile]) + with check_warnings(warn_type, match=warn_msg): + cal_in2 = UVCal.from_file([outfile], file_type=file_type) + + # some things are different for ms and it's ok. reset those + if file_type == "ms": + cal_in2.scan_number_array = None + cal_in2.scan_number_array = None + cal_in2.extra_keywords = cal_in.extra_keywords + cal_in2.history = cal_in.history + assert cal_in == cal_in2 @@ -3375,6 +3406,11 @@ def test_init_from_uvdata(multi_spw, uvcalibrate_data): uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3443,6 +3479,11 @@ def test_init_from_uvdata_setfreqs(multi_spw, uvcalibrate_data): uvc_new.history = uvc2.history + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3511,6 +3552,11 @@ def test_init_from_uvdata_settimes(metadata_only, uvcalibrate_data): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3560,6 +3606,11 @@ def test_init_from_uvdata_setjones(uvcalibrate_data): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3616,6 +3667,11 @@ def test_init_single_pol(uvcalibrate_data, pol): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3666,6 +3722,11 @@ def test_init_from_uvdata_circular_pol(uvcalibrate_data): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3745,6 +3806,9 @@ def test_init_from_uvdata_sky(uvcalibrate_data, fhd_cal_raw): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new == uvc2 @@ -3840,6 +3904,11 @@ def test_init_from_uvdata_delay(multi_spw, set_frange, uvcalibrate_data): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 @@ -3933,6 +4002,11 @@ def test_init_from_uvdata_wideband(multi_spw, set_frange, uvcalibrate_data): uvc_new.time_array = uvc2.time_array uvc_new.set_lsts_from_time_array() + assert uvc_new.pol_convention != uvc2.pol_convention + uvc_new.pol_convention = uvc2.pol_convention + assert uvc_new.gain_scale != uvc2.gain_scale + uvc_new.gain_scale = uvc2.gain_scale + assert uvc_new == uvc2 diff --git a/tests/uvdata/test_miriad.py b/tests/uvdata/test_miriad.py index 77180775f..cafa18568 100644 --- a/tests/uvdata/test_miriad.py +++ b/tests/uvdata/test_miriad.py @@ -1176,6 +1176,8 @@ def test_roundtrip_optional_params(uv_in_paper, tmp_path): uv_in, uv_out, testfile = uv_in_paper uv_in.telescope.x_orientation = "east" + uv_in.pol_convention = "sum" + uv_in.vis_units = "Jy" uv_in.reorder_blts() _write_miriad(uv_in, testfile, clobber=True) diff --git a/tests/uvdata/test_ms.py b/tests/uvdata/test_ms.py index 7df6ca6ee..8584a264d 100644 --- a/tests/uvdata/test_ms.py +++ b/tests/uvdata/test_ms.py @@ -946,6 +946,25 @@ def test_antenna_diameter_handling(hera_uvh5, tmp_path): assert uv_obj2 == uv_obj +@pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") +def test_ms_optional_parameters(nrao_uv, tmp_path): + uv_obj = nrao_uv + + uv_obj.telescope.x_orientation = "east" + uv_obj.pol_convention = "sum" + uv_obj.vis_units = "Jy" + + test_file = os.path.join(tmp_path, "dish_diameter_out.ms") + uv_obj.write_ms(test_file, force_phase=True) + + uv_obj2 = UVData.from_file(test_file) + + uv_obj2._consolidate_phase_center_catalogs( + reference_catalog=uv_obj.phase_center_catalog + ) + assert uv_obj2 == uv_obj + + def test_no_source(sma_mir, tmp_path): uv = UVData() uv2 = UVData() diff --git a/tests/uvdata/test_uvdata.py b/tests/uvdata/test_uvdata.py index 695412c66..b8f9392bf 100644 --- a/tests/uvdata/test_uvdata.py +++ b/tests/uvdata/test_uvdata.py @@ -88,6 +88,7 @@ def uvdata_props(): "filename", "blts_are_rectangular", "time_axis_faster_than_bls", + "pol_convention", ] extra_parameters = ["_" + prop for prop in extra_properties] @@ -11939,3 +11940,18 @@ def test_get_ants_rectangular(hera_uvh5): hera_uvh5.reorder_blts(order="time", minor_order="baseline") ants1 = np.sort(hera_uvh5.get_ants()) assert np.all(ants1 == ants) + + +def test_pol_convention_warnings(hera_uvh5): + hera_uvh5.vis_units = "Jy" + + hera_uvh5.pol_convention = "badconvention" + with pytest.raises(ValueError): + hera_uvh5.check() + + hera_uvh5.vis_units = "uncalib" + hera_uvh5.pol_convention = "sum" + with pytest.raises( + ValueError, match="pol_convention is set but the data is uncalibrated" + ): + hera_uvh5.check() diff --git a/tests/uvdata/test_uvfits.py b/tests/uvdata/test_uvfits.py index a07f07570..0fa8983c4 100644 --- a/tests/uvdata/test_uvfits.py +++ b/tests/uvdata/test_uvfits.py @@ -702,13 +702,14 @@ def test_readwriteread_no_lst(tmp_path, casa_uvfits): @pytest.mark.filterwarnings("ignore:The uvw_array does not match the expected values") -def test_readwriteread_x_orientation(tmp_path, casa_uvfits): +def test_uvfits_optional_params(tmp_path, casa_uvfits): uv_in = casa_uvfits uv_out = UVData() write_file = str(tmp_path / "outtest_casa.uvfits") - # check that if x_orientation is set, it's read back out properly + # check that if optional params are set, they are read back out properly uv_in.telescope.x_orientation = "east" + uv_in.telescope.pol_convention = "sum" uv_in.write_uvfits(write_file) uv_out.read(write_file) diff --git a/tests/uvdata/test_uvh5.py b/tests/uvdata/test_uvh5.py index 4405f15cc..470d05640 100644 --- a/tests/uvdata/test_uvh5.py +++ b/tests/uvdata/test_uvh5.py @@ -330,6 +330,7 @@ def test_uvh5_optional_parameters(casa_uvfits, tmp_path): # set optional parameters uv_in.telescope.x_orientation = "east" + uv_in.pol_conventions = "avg" uv_in.telescope.antenna_diameters = ( np.ones_like(uv_in.telescope.antenna_numbers) * 1.0 )