diff --git a/CHANGELOG.md b/CHANGELOG.md index 297c022489..05a752893b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Added a `check_surface_based_positions` positions method for verifying antenna +positions are near surface of whatever celestial body their positions are referenced to +(either the Earth or Moon, currently). + +### Changed +- Increased the tolerance to 75 mas (equivalent to 5 ms time error) for a warning about +values in `lst_array` not conforming to expectations for `UVData`, `UVCal`, and `UVFlag` +(was 1 mas) inside of `check`. Additionally, added a keyword to `check` enable the +tolerance value to be user-specified. +- Changed the behavior of checking of telescope location to look at the combination of +`antenna_positions` and `telescope_location` together for `UVData`, `UVCal`, and `UVFlag`. +Additionally, failing this check results in a warning (was an error). + ## [2.4.1] - 2023-10-13 ### Added diff --git a/pyuvdata/parameter.py b/pyuvdata/parameter.py index 3c09521dd3..7dfa32abb9 100644 --- a/pyuvdata/parameter.py +++ b/pyuvdata/parameter.py @@ -895,8 +895,6 @@ class LocationParameter(UVParameter): """ - _range_dict = {"itrs": (6.35e6, 6.39e6), "mcmf": (1717100.0, 1757100.0)} - def __init__( self, name, @@ -905,7 +903,7 @@ def __init__( spoof_val=None, description="", frame="itrs", - acceptable_range=-1, + acceptable_range=None, tols=1e-3, ): super(LocationParameter, self).__init__( @@ -921,21 +919,6 @@ def __init__( ) self.frame = frame - # If acceptable_range is set, set it now. Has to be done after frame is set - # because setting the frame changes the acceptable_range. - if acceptable_range != -1: - self.acceptable_range = acceptable_range - - def __setattr__(self, name, value): - """Ensure that acceptable_range is set properly when frame is changed.""" - if name == "frame": - if value in self._range_dict.keys(): - self.acceptable_range = self._range_dict[value] - else: - self.acceptable_range = None - - return super().__setattr__(name, value) - def lat_lon_alt(self): """Get value in (latitude, longitude, altitude) tuple in radians.""" if self.value is None: diff --git a/pyuvdata/tests/test_parameter.py b/pyuvdata/tests/test_parameter.py index 3af7e87a40..0a9fa01369 100644 --- a/pyuvdata/tests/test_parameter.py +++ b/pyuvdata/tests/test_parameter.py @@ -397,6 +397,17 @@ def test_location_set_lat_lon_alt_degrees_none(): assert param1.value is None +def test_location_acceptability(): + """Test check_acceptability with LocationParameters""" + val = np.array([0.5, 0.5, 0.5]) + param1 = uvp.LocationParameter("p1", value=val, acceptable_range=[0, 1]) + assert param1.check_acceptability()[0] + + val += 0.5 + param1 = uvp.LocationParameter("p1", value=val, acceptable_range=[0, 1]) + assert not param1.check_acceptability()[0] + + def test_location_acceptable_none(): param1 = uvp.LocationParameter(name="p2", value=1, acceptable_range=None) @@ -408,33 +419,6 @@ def test_location_acceptable_none(): assert param1.check_acceptability()[0] -def test_location_mcmf_acceptability(): - loc = np.array([1717200.0, 0.0, 0.0]) - param1 = uvp.LocationParameter(name="p2", value=loc, frame="mcmf") - assert param1.acceptable_range == (1717100.0, 1757100.0) - assert param1.check_acceptability()[0] - - # test that the acceptable range is changed when the frame is changed - param1.frame = "itrs" - assert param1.acceptable_range == (6.35e6, 6.39e6) - assert not param1.check_acceptability()[0] - - param1.frame = "foo" - assert param1.acceptable_range is None - assert param1.check_acceptability()[0] - - param1.frame = "mcmf" - assert param1.acceptable_range == (1717100.0, 1757100.0) - assert param1.check_acceptability()[0] - - # check that if you set the acceptable range on init that it is used - param1 = uvp.LocationParameter( - name="p2", value=loc, acceptable_range=(1717100.0, 1717150.0), frame="mcmf" - ) - assert param1.acceptable_range == (1717100.0, 1717150.0) - assert not param1.check_acceptability()[0] - - @pytest.mark.parametrize( "sky2", [ diff --git a/pyuvdata/tests/test_utils.py b/pyuvdata/tests/test_utils.py index 6e789b8efe..8848886556 100644 --- a/pyuvdata/tests/test_utils.py +++ b/pyuvdata/tests/test_utils.py @@ -4261,3 +4261,78 @@ def test_uvw_track_generator_moon(): # Check that the total lengths all match 1 assert np.allclose((gen_results["uvw"] ** 2.0).sum(1), 2.0) + + +@pytest.mark.parametrize("err_state", ["err", "warn", "none"]) +@pytest.mark.parametrize("tel_loc", ["Center", "Moon", "Earth", "Space"]) +@pytest.mark.parametrize("check_frame", ["Moon", "Earth"]) +@pytest.mark.parametrize("del_tel_loc", [False, None, True]) +def test_check_surface_based_positions(err_state, tel_loc, check_frame, del_tel_loc): + tel_loc_dict = { + "Center": np.array([0, 0, 0]), + "Moon": np.array([0, 0, 1.737e6]), + "Earth": np.array([0, 6.37e6, 0]), + "Space": np.array([4.22e7, 0, 0]), + } + tel_frame_dict = {"Moon": "mcmf", "Earth": "itrs"} + + ant_pos = np.array( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ) + if del_tel_loc: + ant_pos += tel_loc_dict[tel_loc] + + fail_type = err_msg = err_type = None + err_check = uvtest.check_warnings + if (tel_loc != check_frame) and (err_state != "none"): + if tel_loc == "Center": + fail_type = "below" + elif tel_loc == "Space": + fail_type = "above" + else: + fail_type = "above" if tel_loc == "Earth" else "below" + + if fail_type is not None: + err_msg = ( + f"{tel_frame_dict[check_frame]} position vector magnitudes must be " + f"on the order of the radius of {check_frame} -- they appear to lie well " + f"{fail_type} this." + ) + if err_state == "err": + err_type = ValueError + err_check = pytest.raises + else: + err_type = UserWarning + + with err_check(err_type, match=err_msg): + status = uvutils.check_surface_based_positions( + telescope_loc=None if (del_tel_loc) else tel_loc_dict[tel_loc], + antenna_positions=None if (del_tel_loc is None) else ant_pos, + telescope_frame=tel_frame_dict[check_frame], + raise_error=err_state == "err", + raise_warning=err_state == "warn", + ) + + assert (err_state == "err") or (status == (tel_loc == check_frame)) + + +@pytest.mark.skipif(not hasmoon, reason="lunarsky not installed") +@pytest.mark.parametrize("tel_loc", ["Earth", "Moon"]) +@pytest.mark.parametrize("check_frame", ["Earth", "Moon"]) +def test_check_surface_based_positions_earthmoonloc(tel_loc, check_frame): + frame = "mcmf" if (check_frame == "Moon") else "itrs" + + if tel_loc == "Earth": + loc = EarthLocation.from_geodetic(0, 0, 0) + else: + loc = MoonLocation.from_selenodetic(0, 0, 0) + + if tel_loc == check_frame: + assert uvutils.check_surface_based_positions( + telescope_loc=loc, telescope_frame=frame + ) + else: + with pytest.raises(ValueError, match=(f"{frame} position vector")): + uvutils.check_surface_based_positions( + telescope_loc=loc, telescope_frame=frame + ) diff --git a/pyuvdata/utils.py b/pyuvdata/utils.py index 5c3f7349df..0b4edeb0e9 100644 --- a/pyuvdata/utils.py +++ b/pyuvdata/utils.py @@ -75,6 +75,11 @@ # standard angle tolerance: 1 mas in radians. RADIAN_TOL = 1 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0) +# standard lst time tolerance: 5 ms (75 mas in radians), based on an expected RMS +# accuracy of 1 ms at 7 days out from issuance of Bulletin A (which are issued once a +# week with rapidly determined parameters and forecasted values of DUT1), the exact +# formula for which is t_err = 0.00025 (MJD-)**0.75 (in secs). +LST_RAD_TOL = 2 * np.pi * 5e-3 / (86400.0) # fmt: off # polarization constants @@ -115,6 +120,11 @@ "rr": ["r", "r"], "ll": ["l", "l"], "rl": ["r", "l"], "lr": ["l", "r"]} +_range_dict = { + "itrs": (6.35e6, 6.39e6, "Earth"), "mcmf": (1717100.0, 1757100.0, "Moon") +} + + # fmt: on @@ -4078,6 +4088,89 @@ def check_lsts_against_times( ) +def check_surface_based_positions( + *, + telescope_loc=None, + telescope_frame="itrs", + antenna_positions=None, + raise_error=True, + raise_warning=True, +): + """ + Check that antenna positions are consistent with ground-based values. + + Check that the antenna position, telescope location, or combination of both produces + locations that are consistent with surface-based positions. If supplying both + antenna position and telescope location, the check will be run against the sum total + of both. For the Earth, the permitted range of values is betwen 6350 and 6390 km, + whereas for theMoon the range is 1717.1 to 1757.1 km. + + telescope_loc : tuple or EarthLocation or MoonLocation + Telescope location, specified as a 3-element tuple (specifying geo/selenocentric + position in meters) or as an astropy EarthLocation (or lunarsky MoonLocation). + telescope_frame : str, optional + Reference frame for latitude/longitude/altitude. Options are itrs (default) or + mcmf. Only used if telescope_loc is not an EarthLocation or MoonLocation. + antenna_positions : ndarray of float + List of antenna positions relative to array center in ECEF coordinates, + required if not providing `uvw_array`. Shape is (Nants, 3). If no telescope_loc + is specified, these values will be assumed to be relative to geocenter. + raise_error : bool + If True, an error is raised if telescope_loc and/or telescope_loc do not conform + to expectations for a surface-based telescope. Default is True. + raise_warning : bool + If True, a warning is raised if telescope_loc and/or telescope_loc do not + conform to expectations for a surface-based telescope. Default is True, only + used if `raise_error` is set to False. + + Returns + ------- + valid : bool + If True, the antenna_positions and/or telescope_loc conform to expectations for + a surface-based telescope. Otherwise returns false. + + """ + if antenna_positions is None: + antenna_positions = np.zeros((1, 3)) + + if isinstance(telescope_loc, EarthLocation) or ( + hasmoon and isinstance(telescope_loc, MoonLocation) + ): + antenna_positions = antenna_positions + ( + telescope_loc.x.to("m").value, + telescope_loc.y.to("m").value, + telescope_loc.z.to("m").value, + ) + elif telescope_loc is not None: + antenna_positions = antenna_positions + telescope_loc + + low_lim, hi_lim, world = _range_dict[telescope_frame] + + err_type = None + if np.any(np.sum(antenna_positions**2.0, axis=1) < low_lim**2.0): + err_type = "below" + elif np.any(np.sum(antenna_positions**2.0, axis=1) > hi_lim**2.0): + err_type = "above" + + if err_type is None: + return True + + err_msg = ( + f"{telescope_frame} position vector magnitudes must be on the order of " + f"the radius of {world} -- they appear to lie well {err_type} this." + ) + + # If desired, raise an error + if raise_error: + raise ValueError(err_msg) + + # Otherwise, if desired, raise a warning instead + if raise_warning: + warnings.warn(err_msg) + + return False + + def uvw_track_generator( *, lon_coord=None, diff --git a/pyuvdata/uvcal/tests/test_uvcal.py b/pyuvdata/uvcal/tests/test_uvcal.py index 2b6451a1ee..ff187ecb1d 100644 --- a/pyuvdata/uvcal/tests/test_uvcal.py +++ b/pyuvdata/uvcal/tests/test_uvcal.py @@ -3990,13 +3990,7 @@ def test_init_from_uvdata( # of precision in the processing pipeline. assert uvc_new._time_array == uvc2._time_array uvc_new.time_array = uvc2.time_array - with uvtest.check_warnings( - UserWarning, - match="The lst_array is not self-consistent with the time_array and " - "telescope location. Consider recomputing with the " - "`set_lsts_from_time_array` method.", - ): - uvc_new.check() + uvc_new.check() uvc_new.set_lsts_from_time_array() diff --git a/pyuvdata/uvcal/uvcal.py b/pyuvdata/uvcal/uvcal.py index 72cc9f1fe8..31ced3c1bb 100644 --- a/pyuvdata/uvcal/uvcal.py +++ b/pyuvdata/uvcal/uvcal.py @@ -102,11 +102,7 @@ def __init__(self): "telescope_location_lat_lon_alt_degrees properties" ) self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - acceptable_range=(6.35e6, 6.39e6), - tols=1e-3, - required=True, + "telescope_location", description=desc, tols=1e-3, required=True ) desc = ( @@ -1194,7 +1190,11 @@ def _check_freq_spacing(self, raise_errors=True): ) def check( - self, check_extra=True, run_check_acceptability=True, check_freq_spacing=False + self, + check_extra=True, + run_check_acceptability=True, + check_freq_spacing=False, + lst_tol=uvutils.LST_RAD_TOL, ): """ Add some extra checks on top of checks on UVBase class. @@ -1212,6 +1212,15 @@ def check( Option to check if frequencies are evenly spaced and the spacing is equal to their channel_width. This is not required for UVCal objects in general but is required to write to calfits files. + lst_tol : float or None + Tolerance level at which to test LSTs against their expected values. If + provided as a float, must be in units of radians. If set to None, the + default precision tolerance from the `lst_array` parameter is used (1 mas). + Default value is 75 mas, which is set by the predictive uncertainty in IERS + calculations of DUT1 (RMS is of order 1 ms, with with a 5-sigma threshold + for detection is used to prevent false issues from being reported), which + for some observatories sets the precision with which these values are + written. Returns ------- @@ -1309,6 +1318,14 @@ def check( self._check_freq_spacing() if run_check_acceptability: + # Check antenna positions + uvutils.check_surface_based_positions( + antenna_positions=self.antenna_positions, + telescope_loc=self.telescope_location, + telescope_frame=self._telescope_location.frame, + raise_error=False, + ) + lat, lon, alt = self.telescope_location_lat_lon_alt_degrees uvutils.check_lsts_against_times( jd_array=self.time_array, @@ -1316,7 +1333,7 @@ def check( latitude=lat, longitude=lon, altitude=alt, - lst_tols=self._lst_array.tols, + lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], frame=self._telescope_location.frame, ) diff --git a/pyuvdata/uvdata/miriad.py b/pyuvdata/uvdata/miriad.py index 8f76e77eec..a98ed5e11c 100644 --- a/pyuvdata/uvdata/miriad.py +++ b/pyuvdata/uvdata/miriad.py @@ -456,7 +456,12 @@ def _load_antpos(self, uv, sorted_unique_ants=None, correct_lat_lon=True): rel_ecef_antpos = ecef_antpos else: self.telescope_location = np.mean(ecef_antpos[good_antpos, :], axis=0) - valid_location = self._telescope_location.check_acceptability()[0] + valid_location = uvutils.check_surface_based_positions( + telescope_loc=self.telescope_location, + telescope_frame=self._telescope_location.frame, + raise_error=False, + raise_warning=False, + ) # check to see if this could be a valid telescope_location if valid_location: diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py index 36ed4fa0eb..0aa1736992 100644 --- a/pyuvdata/uvdata/tests/test_uvdata.py +++ b/pyuvdata/uvdata/tests/test_uvdata.py @@ -5514,8 +5514,13 @@ def test_telescope_loc_xyz_check(paper_uvh5, tmp_path): uv.read(fname, run_check=False, use_future_array_shapes=True) # try to read without checks: assert it fails - with pytest.raises( - ValueError, match="UVParameter _telescope_location has unacceptable values." + with uvtest.check_warnings( + UserWarning, + [ + "The uvw_array does not match the expected", + "itrs position vector magnitudes must be on the order " + "of the radius of Earth -- they appear to lie well below this.", + ], ): uv.read(fname, use_future_array_shapes=True) diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py index f4b1eb5514..89f60338b1 100644 --- a/pyuvdata/uvdata/uvdata.py +++ b/pyuvdata/uvdata/uvdata.py @@ -377,11 +377,7 @@ def __init__(self): "telescope_location_lat_lon_alt_degrees properties." ) self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - acceptable_range=(6.35e6, 6.39e6), - frame="itrs", - tols=1e-3, + "telescope_location", description=desc, frame="itrs", tols=1e-3 ) self._history = uvp.UVParameter( @@ -3208,6 +3204,7 @@ def check( allow_flip_conj=False, check_autos=False, fix_autos=False, + lst_tol=uvutils.LST_RAD_TOL, ): """ Add some extra checks on top of checks on UVBase class. @@ -3240,6 +3237,15 @@ def check( fix_autos : bool If auto-correlations with imaginary values are found, fix those values so that they are real-only in data_array. Default is True. + lst_tol : float or None + Tolerance level at which to test LSTs against their expected values. If + provided as a float, must be in units of radians. If set to None, the + default precision tolerance from the `lst_array` parameter is used (1 mas). + Default value is 75 mas, which is set by the predictive uncertainty in IERS + calculations of DUT1 (RMS is of order 1 ms, with with a 5-sigma threshold + for detection is used to prevent false issues from being reported), which + for some observatories sets the precision with which these values are + written. Returns ------- @@ -3371,27 +3377,37 @@ def check( ) if run_check_acceptability: - # check that the uvws make sense given the antenna positions - # make a metadata only copy of this object to properly calculate uvws - temp_obj = self.copy(metadata_only=True) + # Check antenna positions + uvutils.check_surface_based_positions( + antenna_positions=self.antenna_positions, + telescope_loc=self.telescope_location, + telescope_frame=self._telescope_location.frame, + raise_error=False, + ) lat, lon, alt = self.telescope_location_lat_lon_alt_degrees + # Check the LSTs against what we expect given up-to-date IERS data uvutils.check_lsts_against_times( jd_array=self.time_array, lst_array=self.lst_array, latitude=lat, longitude=lon, altitude=alt, - lst_tols=self._lst_array.tols, + lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], frame=self._telescope_location.frame, ) + # create a metadata copy to do operations on + temp_obj = self.copy(metadata_only=True) + with warnings.catch_warnings(): warnings.simplefilter("ignore") logger.debug("Setting UVWs from antenna positions...") temp_obj.set_uvws_from_antenna_positions() logger.debug("... Done Setting UVWs") + # check that the uvws make sense given the antenna positions + # make a metadata only copy of this object to properly calculate uvws if not np.allclose(temp_obj.uvw_array, self.uvw_array, atol=1): max_diff = np.max(np.abs(temp_obj.uvw_array - self.uvw_array)) if allow_flip_conj and np.allclose( @@ -3421,7 +3437,7 @@ def check( # check auto and cross-corrs have sensible uvws logger.debug("Checking autos...") - autos = np.isclose(self.ant_1_array - self.ant_2_array, 0.0) + autos = self.ant_1_array == self.ant_2_array if not np.all( np.isclose( self.uvw_array[autos], @@ -3517,15 +3533,12 @@ def check( np.isclose( # this line used to use np.linalg.norm but it turns out # squaring and sqrt is slightly more efficient unless the array - # is "very large". - np.sqrt( - self.uvw_array[~autos, 0] ** 2 - + self.uvw_array[~autos, 1] ** 2 - + self.uvw_array[~autos, 2] ** 2 - ), + # is "very large". Square the tols is equivalent to getting the + # sqrt of the uvw magnitude, but much faster. + np.sum(self.uvw_array[~autos] ** 2, axis=1), 0.0, - rtol=self._uvw_array.tols[0], - atol=self._uvw_array.tols[1], + rtol=self._uvw_array.tols[0] ** 2, + atol=self._uvw_array.tols[1] ** 2, ) ): raise ValueError( diff --git a/pyuvdata/uvdata/uvfits.py b/pyuvdata/uvdata/uvfits.py index 24e314872f..b096a04734 100644 --- a/pyuvdata/uvdata/uvfits.py +++ b/pyuvdata/uvdata/uvfits.py @@ -70,7 +70,7 @@ def _get_parameter_data( latitude=latitude, longitude=longitude, altitude=altitude, - lst_tols=self._lst_array.tols, + lst_tols=(0, uvutils.LST_RAD_TOL), frame=self._telescope_location.frame, ) diff --git a/pyuvdata/uvdata/uvh5.py b/pyuvdata/uvdata/uvh5.py index 7352a9c048..5261acdc8c 100644 --- a/pyuvdata/uvdata/uvh5.py +++ b/pyuvdata/uvdata/uvh5.py @@ -934,7 +934,7 @@ def _read_header_with_fast_meta( latitude=lat, longitude=lon, altitude=alt, - lst_tols=(0, uvutils.RADIAN_TOL), + lst_tols=(0, uvutils.LST_RAD_TOL), frame=self._telescope_location.frame, ) diff --git a/pyuvdata/uvflag/tests/test_uvflag.py b/pyuvdata/uvflag/tests/test_uvflag.py index 47b454406c..528a6d6524 100644 --- a/pyuvdata/uvflag/tests/test_uvflag.py +++ b/pyuvdata/uvflag/tests/test_uvflag.py @@ -917,20 +917,14 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) "baseline", ["telescope_location"], UserWarning, - [ - "telescope_location are not set or are being overwritten. Using known", - "The lst_array is not self-consistent with the time_array", - ], + ["telescope_location are not set or are being overwritten. Using known"], "reset_telescope_params", ), ( "baseline", ["antenna_names"], UserWarning, - [ - "antenna_names are not set or are being overwritten. Using known", - "The lst_array is not self-consistent with the time_array", - ], + ["antenna_names are not set or are being overwritten. Using known"], "reset_telescope_params", ), ( @@ -948,29 +942,17 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) "baseline", ["antenna_numbers"], UserWarning, - [ - "antenna_numbers are not set or are being overwritten. Using known", - "The lst_array is not self-consistent with the time_array", - ], + ["antenna_numbers are not set or are being overwritten. Using known"], "reset_telescope_params", ), ( "baseline", ["antenna_positions"], UserWarning, - [ - "antenna_positions are not set or are being overwritten. Using known", - "The lst_array is not self-consistent with the time_array", - ], - "reset_telescope_params", - ), - ( - "baseline", - ["Nants_telescope"], - UserWarning, - "The lst_array is not self-consistent with the time_array", + ["antenna_positions are not set or are being overwritten. Using known"], "reset_telescope_params", ), + ("baseline", ["Nants_telescope"], None, [], "reset_telescope_params"), ( "waterfall", ["Nants_telescope", "telescope_name", "antenna_numbers"], @@ -1019,8 +1001,7 @@ def test_read_write_loop_missing_shapes(uvdata_obj, test_outfile, future_shapes) UserWarning, [ "Nants_telescope, antenna_names, antenna_numbers, antenna_positions " - "are not set or are being overwritten. Using known values for HERA.", - "The lst_array is not self-consistent with the time_array", + "are not set or are being overwritten. Using known values for HERA." ], "reset_telescope_params", ), @@ -1164,7 +1145,7 @@ def test_read_write_loop_missing_telescope_info( if "telescope_name" in param_list: run_check = False - with uvtest.check_warnings(warn_type, match=msg): + with uvtest.check_warnings(warn_type, match=None if warn_type is None else msg): uvf2 = UVFlag(test_outfile, use_future_array_shapes=True, run_check=run_check) if uv_mod is None: @@ -1191,7 +1172,7 @@ def test_read_write_loop_missing_telescope_info( if "Nants_telescope" in param_list and "telescope_name" not in param_list: with uvtest.check_warnings( UserWarning, - match=[msg] + match=([] if warn_type is None else [msg]) + [ "Telescope_name parameter is set to foo, which overrides the telescope " "name in the file (HERA)." @@ -1568,11 +1549,8 @@ def test_read_multiple_files( uvf = UVFlag(uv, use_future_array_shapes=write_future_shapes) uvf.write(test_outfile, clobber=True) - warn_msg = [ - "The lst_array is not self-consistent with the time_array and telescope " - "location. Consider recomputing with the `set_lsts_from_time_array` method." - ] * 2 - warn_type = [UserWarning] * 2 + warn_msg = [] + warn_type = [] if not read_future_shapes: warn_msg += [_future_array_shapes_warning] * 2 warn_type += [DeprecationWarning] * 2 @@ -1857,12 +1835,10 @@ def test_add_frequency(): with uvtest.check_warnings( UserWarning, - match=[ + match=( "One object has the flex_spw_id_array set and one does not. Combined " - "object will have it set.", - "The lst_array is not self-consistent with the time_array and telescope " - "location. Consider recomputing with the `set_lsts_from_time_array` method", - ], + "object will have it set." + ), ): uv3 = uv1.__add__(uv2, axis="frequency") assert np.array_equal( @@ -1915,13 +1891,10 @@ def test_add_frequency_multi_spw(split_spw): assert uv2.Nfreqs == uv_full.Nfreqs // 2 with uvtest.check_warnings( - [DeprecationWarning, UserWarning] * 2, + [DeprecationWarning] * 2, match=[ "flex_spw_id_array is not set. It will be required starting in " - "version 3.0", - "The lst_array is not self-consistent with the time_array and " - "telescope location. Consider recomputing with the " - "`set_lsts_from_time_array` method.", + "version 3.0" ] * 2, ): diff --git a/pyuvdata/uvflag/uvflag.py b/pyuvdata/uvflag/uvflag.py index 1675637754..db4a472661 100644 --- a/pyuvdata/uvflag/uvflag.py +++ b/pyuvdata/uvflag/uvflag.py @@ -481,10 +481,7 @@ def __init__( ) self._telescope_location = uvp.LocationParameter( - "telescope_location", - description=desc, - acceptable_range=(6.35e6, 6.39e6), - tols=1e-3, + "telescope_location", description=desc, tols=1e-3 ) self._history = uvp.UVParameter( @@ -929,7 +926,12 @@ def _set_type_waterfall(self): if not self.future_array_shapes: self._freq_array.form = ("Nfreqs",) - def check(self, check_extra=True, run_check_acceptability=True): + def check( + self, + check_extra=True, + run_check_acceptability=True, + lst_tol=uvutils.LST_RAD_TOL, + ): """ Add some extra checks on top of checks on UVBase class. @@ -942,6 +944,15 @@ def check(self, check_extra=True, run_check_acceptability=True): If true, check all parameters, otherwise only check required parameters. run_check_acceptability : bool Option to check if values in parameters are acceptable. + lst_tol : float or None + Tolerance level at which to test LSTs against their expected values. If + provided as a float, must be in units of radians. If set to None, the + default precision tolerance from the `lst_array` parameter is used (1 mas). + Default value is 75 mas, which is set by the predictive uncertainty in IERS + calculations of DUT1 (RMS is of order 1 ms, with with a 5-sigma threshold + for detection is used to prevent false issues from being reported), which + for some observatories sets the precision with which these values are + written. Returns ------- @@ -1022,6 +1033,14 @@ def check(self, check_extra=True, run_check_acceptability=True): ) if run_check_acceptability: + # Check antenna positions + uvutils.check_surface_based_positions( + antenna_positions=self.antenna_positions, + telescope_loc=self.telescope_location, + telescope_frame=self._telescope_location.frame, + raise_error=False, + ) + lat, lon, alt = self.telescope_location_lat_lon_alt_degrees uvutils.check_lsts_against_times( jd_array=self.time_array, @@ -1029,7 +1048,7 @@ def check(self, check_extra=True, run_check_acceptability=True): latitude=lat, longitude=lon, altitude=alt, - lst_tols=self._lst_array.tols, + lst_tols=self._lst_array.tols if lst_tol is None else [0, lst_tol], frame=self._telescope_location.frame, )