Repository: RadioAstronomySoftwareGroup/pyuvdata
Pull request #785 (2020-03-11 18:58:19)

Resample time factor
<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->
Add support for averaging by an integer factor in time.


## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. If this PR closes an issue, put the word 'closes' before the issue link to auto-close the issue when the PR is merged. -->
Based on discussion in issue #773, this may behave more closely to human expectations than setting a minimum integration time.

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation change (documentation changes only)
- [ ] Version change
- [ ] Build or continuous integration change


## Checklist:
<!--- You may remove the checklists that don't apply to your change type(s) or just leave them empty -->
<!--- Go over all the following points, and replace the space with an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] I have read the [contribution guide](https://github.com/RadioAstronomySoftwareGroup/pyuvdata/blob/master/.github/CONTRIBUTING.md).
- [x] My code follows the code style of this project.

New feature checklist:
- [x] I have added or updated the docstrings associated with my feature using the [numpy docstring format](https://numpydoc.readthedocs.io/en/latest/format.html).
- [x] I have updated the tutorial to highlight my new feature (if appropriate).
- [x] I have added tests to cover my new feature.
- [x] All new and existing tests pass.
- [ ] I have updated the [CHANGELOG](https://github.com/RadioAstronomySoftwareGroup/pyuvdata/blob/master/CHANGELOG.md).


codecov[bot]: # [Codecov](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=h1) Report
> Merging [#785](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=desc) into [master](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/commit/3c00116f14781fe5edb3856c9680805c40f79fec&el=desc) will **decrease** coverage by `0.00%`.
> The diff coverage is `97.91%`.

[![Impacted file tree graph](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785/graphs/tree.svg?width=650&height=150&src=pr&token=4QykcCeB1H)](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #785      +/-   ##
==========================================
- Coverage   98.94%   98.93%   -0.01%     
==========================================
  Files          26       26              
  Lines        9464     9494      +30     
==========================================
+ Hits         9364     9393      +29     
- Misses        100      101       +1     
```


| [Impacted Files](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyuvdata/uvdata/uvdata.py](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785/diff?src=pr&el=tree#diff-cHl1dmRhdGEvdXZkYXRhL3V2ZGF0YS5weQ==) | `99.94% <97.91%> (-0.06%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=footer). Last update [3c00116...e114ddf](https://codecov.io/gh/RadioAstronomySoftwareGroup/pyuvdata/pull/785?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 41b12a1e..7b022b11 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -382,6 +382,11 @@ Use the ``frequency_average`` method to average along the frequency axis.
 
 a) Averaging (Downsampling) in time
 ***********************************
+Use either the `n_times_to_avg` keyword to specify an integer factor to average
+by or `min_int_time` to specify a minimum final integration time. Specifying
+`min_int_time` is most appropriate when the integration time varies, e.g. if
+the data have had baseline-dependent averaging applied.
+
 ::
 
   >>> import os
@@ -391,12 +396,13 @@ a) Averaging (Downsampling) in time
   >>> uv_object = UVData()
   >>> datafile = os.path.join(DATA_PATH, "zen.2458661.23480.HH.uvh5")
   >>> uv_object.read(datafile)
+  >>> uv_object2 = uv_object.copy()
   >>> print("Range of integration times: ", np.amin(uv_object.integration_time),
   ...       "-", np.amax(uv_object.integration_time))
   Range of integration times:  1.879048192 - 1.879048192
 
-  >>> min_integration_time = np.amax(uv_object.integration_time) * 2.0
-  >>> uv_object.downsample_in_time(min_integration_time)
+  # first use n_times_to_avg to average by a factor of 2 in time.
+  >>> uv_object.downsample_in_time(n_times_to_avg=2)
   Data are in drift mode, phasing before resampling.
   Unphasing back to drift mode.
 
@@ -404,6 +410,17 @@ a) Averaging (Downsampling) in time
   ...       "-", np.amax(uv_object.integration_time))
   Range of integration times after downsampling:  3.758096384 - 3.758096384
 
+  # Now use min_int_time to average by a factor of 2 in time.
+  >>> min_integration_time = np.amax(uv_object2.integration_time) * 2.0
+  >>> uv_object2.downsample_in_time(min_int_time=min_integration_time)
+  Data are in drift mode, phasing before resampling.
+  Unphasing back to drift mode.
+
+  >>> print("Range of integration times after downsampling: ", np.amin(uv_object2.integration_time),
+  ...       "-", np.amax(uv_object2.integration_time))
+  Range of integration times after downsampling:  3.758096384 - 3.758096384
+
+
 b) Upsampling in time
 *********************
 ::
diff --git a/pyuvdata/uvdata/uvdata.py b/pyuvdata/uvdata/uvdata.py
index d6c30aa2..db817e06 100644
--- a/pyuvdata/uvdata/uvdata.py
+++ b/pyuvdata/uvdata/uvdata.py
@@ -4718,7 +4718,8 @@ class UVData(UVBase):
 
     def downsample_in_time(
         self,
-        min_int_time,
+        min_int_time=None,
+        n_times_to_avg=None,
         blt_order="time",
         minor_order="baseline",
         keep_ragged=True,
@@ -4726,21 +4727,28 @@ class UVData(UVBase):
         allow_drift=False,
     ):
         """
-        Resample to a longer integration time.
+        Average to a longer integration time.
 
-        This method will resample a UVData object such that nearly all data
-        samples have an integration time greater than or equal to the `min_int_time`.
-        Note that if the integrations for a baseline do not divide evenly into
-        the specified `min_int_time`, the final integrations for that baseline
-        in the output may have integration times less than `min_int_time`.
-        This behavior can be controlled with the `keep_ragged` argument. The new
-        samples are averages of the original samples (not interpolations).
+        This method will average a UVData object either by an integer factor
+        (by setting `n_times_to_avg`) or by a factor that can differ by
+        baseline-time sample such that after averaging, the samples have an
+        integration time greater than or equal to the `min_int_time` (up to the
+        tolerance on the integration_time).
+
+        Note that if the integrations for a baseline do not divide evenly by the
+        `n_times_to_avg` or into the specified `min_int_time`, the final
+        integrations for that baseline may have integration times less than
+        `min_int_time` or be composed of fewer input integrations than `n_times_to_avg`.
+        This behavior can be controlled with the `keep_ragged` argument.
+        The new samples are averages of the original samples (not interpolations).
 
         Parameters
         ----------
         min_int_time : float
-            Minimum integration time to downsample the UVData integration_time to
+            Minimum integration time to average the UVData integration_time to
             in seconds.
+        n_times_to_avg : int
+            Number of time integrations to average together.
         blt_order : str
             Major baseline ordering for output object. Default is "time". See the
             documentation on the `reorder_blts` method for more details.
@@ -4748,98 +4756,127 @@ class UVData(UVBase):
             Minor baseline ordering for output object. Default is "baseline".
         keep_ragged : bool
             When averaging baselines that do not evenly divide into min_int_time,
-            keep_ragged controls whether to keep the (summed) integrations
-            corresponding to the remaining samples (keep_ragged=True), or
-            discard them (keep_ragged=False).
+            or that have a number of integrations that do not evenly divide by
+            n_times_to_avg, keep_ragged controls whether to keep the (averaged)
+            integrations corresponding to the remaining samples (keep_ragged=True),
+            or discard them (keep_ragged=False).
         summing_correlator_mode : bool
             Option to integrate the flux from the original samples rather than
             average the flux to emulate the behavior in some correlators (e.g. HERA).
         allow_drift : bool
-            Option to allow resampling of drift mode data. If this is False,
+            Option to allow averaging of drift mode data. If this is False,
             drift mode data will be phased before resampling and then unphased
             after resampling. Phasing and unphasing can introduce small errors,
-            but resampling in drift mode may result in unexpected behavior.
+            but averaging in drift mode may result in more decoherence.
 
         Returns
         -------
         None
 
         """
-        # check that min_int_time is sensible given integration_time
-        max_integration_time = np.amax(self.integration_time)
-        sensible_max = 1e2 * max_integration_time
-        if min_int_time > sensible_max:
-            raise ValueError(
-                "Increasing the integration time by more than a "
-                "factor of 100 is not supported. Also note that "
-                "min_int_time should be in seconds."
-            )
+        if min_int_time is None and n_times_to_avg is None:
+            raise ValueError("Either min_int_time or n_times_to_avg must be set.")
 
-        # first figure out where integration_time is shorter than min_int_time
-        inds_to_downsample = np.nonzero(
-            (self.integration_time < min_int_time)
-            & (
-                ~np.isclose(
-                    self.integration_time,
-                    min_int_time,
-                    rtol=self._integration_time.tols[0],
-                    atol=self._integration_time.tols[1],
+        if min_int_time is not None and n_times_to_avg is not None:
+            raise ValueError("Only one of min_int_time or n_times_to_avg can be set.")
+
+        if self.Ntimes == 1:
+            raise ValueError("Only one time in this object, cannot downsample.")
+
+        if min_int_time is not None:
+            # check that min_int_time is sensible given integration_time
+            max_integration_time = np.amax(self.integration_time)
+            sensible_max = 1e2 * max_integration_time
+            if min_int_time > sensible_max:
+                raise ValueError(
+                    "Increasing the integration time by more than a "
+                    "factor of 100 is not supported. Also note that "
+                    "min_int_time should be in seconds."
                 )
-            )
-        )
 
-        if len(inds_to_downsample[0]) == 0:
-            warnings.warn(
-                "All values in the integration_time array are already "
-                "shorter than the value specified; doing nothing."
+            # first figure out where integration_time is shorter than min_int_time
+            inds_to_downsample = np.nonzero(
+                (self.integration_time < min_int_time)
+                & (
+                    ~np.isclose(
+                        self.integration_time,
+                        min_int_time,
+                        rtol=self._integration_time.tols[0],
+                        atol=self._integration_time.tols[1],
+                    )
+                )
             )
-            return
+
+            if len(inds_to_downsample[0]) == 0:
+                warnings.warn(
+                    "All values in the integration_time array are already "
+                    "longer than the value specified; doing nothing."
+                )
+                return
+        else:
+            if not isinstance(n_times_to_avg, (int, np.int)):
+                raise ValueError("n_times_to_avg must be an integer.")
         # If we're going to do actual work, reorder the baselines to ensure time is
         # monotonically increasing.
         # Default of reorder_blts is baseline major, time minor, which is what we want.
         self.reorder_blts()
 
-        # now re-compute inds_to_downsample, in case things have changed
-        inds_to_downsample = np.nonzero(
-            (self.integration_time < min_int_time)
-            & ~np.isclose(
-                self.integration_time,
-                min_int_time,
-                rtol=self._integration_time.tols[0],
-                atol=self._integration_time.tols[1],
+        if min_int_time is not None:
+            # now re-compute inds_to_downsample, in case things have changed
+            inds_to_downsample = np.nonzero(
+                (self.integration_time < min_int_time)
+                & ~np.isclose(
+                    self.integration_time,
+                    min_int_time,
+                    rtol=self._integration_time.tols[0],
+                    atol=self._integration_time.tols[1],
+                )
             )
-        )
+            bls_to_downsample = np.unique(self.baseline_array[inds_to_downsample])
+        else:
+            bls_to_downsample = np.unique(self.baseline_array)
 
-        # figure out how many baselines we'll end up with at the end
-        bls_to_downsample = np.unique(self.baseline_array[inds_to_downsample])
+        # figure out how many baseline times we'll end up with at the end
         n_new_samples = 0
         for bl in bls_to_downsample:
             bl_inds = np.nonzero(self.baseline_array == bl)[0]
-            running_int_time = 0.0
-            for itime, int_time in enumerate(self.integration_time[bl_inds]):
-                running_int_time += int_time
-                over_min_int_time = running_int_time > min_int_time or np.isclose(
-                    running_int_time,
-                    min_int_time,
-                    rtol=self._integration_time.tols[0],
-                    atol=self._integration_time.tols[1],
-                )
-                last_sample = itime == len(bl_inds) - 1
-                # We sum up all the samples found so far if we're over the
-                # target minimum time, or we've hit the end of the time
-                # samples for this baseline.
-                if over_min_int_time or last_sample:
-                    if last_sample and not (over_min_int_time or keep_ragged):
-                        # don't do anything -- implicitly drop these integrations
-                        continue
-                    n_new_samples += 1
-                    running_int_time = 0.0
+            int_times = self.integration_time[bl_inds]
+
+            if min_int_time is not None:
+                running_int_time = 0.0
+                for itime, int_time in enumerate(int_times):
+                    running_int_time += int_time
+                    over_min_int_time = running_int_time > min_int_time or np.isclose(
+                        running_int_time,
+                        min_int_time,
+                        rtol=self._integration_time.tols[0],
+                        atol=self._integration_time.tols[1],
+                    )
+                    last_sample = itime == len(bl_inds) - 1
+                    # We sum up all the samples found so far if we're over the
+                    # target minimum time, or we've hit the end of the time
+                    # samples for this baseline.
+                    if over_min_int_time or last_sample:
+                        if last_sample and not (over_min_int_time or keep_ragged):
+                            # don't do anything -- implicitly drop these integrations
+                            continue
+                        n_new_samples += 1
+                        running_int_time = 0.0
+            else:
+                n_bl_times = self.time_array[bl_inds].size
+                n_sample_temp = np.sum(n_bl_times / n_times_to_avg)
+                if keep_ragged and not np.isclose(
+                    n_sample_temp, np.floor(n_sample_temp)
+                ):
+                    n_new_samples += np.ceil(n_sample_temp).astype(int)
+                else:
+                    n_new_samples += np.floor(n_sample_temp).astype(int)
 
             # figure out if there are any time gaps in the data
             # meaning that the time differences are larger than the integration times
             # time_array is in JD, need to convert to seconds for the diff
             dtime = np.ediff1d(self.time_array[bl_inds]) * 24 * 3600
-            int_times = self.integration_time[bl_inds]
+            int_times = int_times
             if len(np.unique(int_times)) == 1:
                 # this baseline has all the same integration times
                 if len(np.unique(dtime)) > 1:
@@ -4916,12 +4953,15 @@ class UVData(UVBase):
             for itime, int_time in enumerate(self.integration_time[bl_inds]):
                 running_int_time += int_time
                 n_sum += 1
-                over_min_int_time = running_int_time > min_int_time or np.isclose(
-                    running_int_time,
-                    min_int_time,
-                    rtol=self._integration_time.tols[0],
-                    atol=self._integration_time.tols[1],
-                )
+                if min_int_time is not None:
+                    over_min_int_time = running_int_time > min_int_time or np.isclose(
+                        running_int_time,
+                        min_int_time,
+                        rtol=self._integration_time.tols[0],
+                        atol=self._integration_time.tols[1],
+                    )
+                else:
+                    over_min_int_time = n_sum >= n_times_to_avg
                 last_sample = itime == len(bl_inds) - 1
                 # We sum up all the samples found so far if we're over the
                 # target minimum time, or we've hit the end of the time
@@ -5004,7 +5044,10 @@ class UVData(UVBase):
         )
 
         # harmonize temporary arrays with existing ones
-        inds_to_keep = np.nonzero(self.integration_time >= min_int_time)
+        if min_int_time is not None:
+            inds_to_keep = np.nonzero(self.integration_time >= min_int_time)
+        else:
+            inds_to_keep = np.nonzero(np.zeros_like(self.integration_time, dtype=bool))
         self._harmonize_resample_arrays(
             inds_to_keep,
             temp_baseline,
@@ -5026,10 +5069,16 @@ class UVData(UVBase):
         self.check()
 
         # add to the history
-        history_update_string = (
-            " Downsampled data to {:f} second integration "
-            "time using pyuvdata.".format(min_int_time)
-        )
+        if min_int_time is not None:
+            history_update_string = (
+                " Downsampled data to {:f} second integration "
+                "time using pyuvdata.".format(min_int_time)
+            )
+        else:
+            history_update_string = (
+                " Downsampled data by a factor of {} in "
+                "time using pyuvdata.".format(n_times_to_avg)
+            )
         self.history = self.history + history_update_string
 
         return
diff --git a/setup.cfg b/setup.cfg
index be594bb1..838936ff 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,6 @@
 [metadata]
 description-file = README.md
 
-[aliases]
-test=pytest
-
 [tool:pytest]
 addopts = --ignore=scripts
 


Test patch:
diff --git a/pyuvdata/uvdata/tests/test_uvdata.py b/pyuvdata/uvdata/tests/test_uvdata.py
index ebe9bfe2..b8bdb16e 100644
--- a/pyuvdata/uvdata/tests/test_uvdata.py
+++ b/pyuvdata/uvdata/tests/test_uvdata.py
@@ -805,14 +805,13 @@ def test_unknown_phase_unphase_hera_errors(
 def test_phase_rephase_hera_errors(uv1_2_set_uvws, phase_func, phase_kwargs, err_msg):
     uv_phase, uv_raw = uv1_2_set_uvws
 
-    # Set phase type to unkown on some tests, ignore on others.
     uv_phase.phase(0.0, 0.0, epoch="J2000")
     # if this is phase_to_time, use this index set in the dictionary and
     # assign the value of the time_array associated with that index
     # this is a little hacky, but we cannot acces uv_phase.time_array in the
     # parametrize
     if phase_func == "phase_to_time":
-        phase_kwargs["time"] = uv_phase.time_array[phase_kwargs["time"]]
+        phase_kwargs["time"] = uv_phase.time_array[int(phase_kwargs["time"])]
 
     with pytest.raises(ValueError) as cm:
         getattr(uv_phase, phase_func)(**phase_kwargs)
@@ -5299,6 +5298,7 @@ def test_downsample_in_time(resample_in_time_file):
     uv_object.phase_to_time(Time(uv_object.time_array[0], format="jd"))
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_data_size = uv_object.data_array.size
@@ -5311,7 +5311,7 @@ def test_downsample_in_time(resample_in_time_file):
     # change the target integration time
     min_integration_time = original_int_time * 2.0
     uv_object.downsample_in_time(
-        min_integration_time, blt_order="baseline", minor_order="time"
+        min_int_time=min_integration_time, blt_order="baseline", minor_order="time"
     )
 
     # Should have half the size of the data array and all the new integration time
@@ -5327,7 +5327,14 @@ def test_downsample_in_time(resample_in_time_file):
     out_ns = uv_object.get_nsamples(0, 1)
     assert np.isclose((init_ns[0, 0, 0] + init_ns[1, 0, 0]) / 2.0, out_ns[0, 0, 0])
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=2, blt_order="baseline", minor_order="time"
+    )
+    # histories are different when n_times_to_avg is set vs min_int_time
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
@@ -5338,7 +5345,6 @@ def test_downsample_in_time_partial_flags(resample_in_time_file):
     uv_object.phase_to_time(Time(uv_object.time_array[0], format="jd"))
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
-
     # save some values for later
     init_wf = uv_object.get_data(0, 1)
     original_int_time = np.amax(uv_object.integration_time)
@@ -5354,8 +5360,10 @@ def test_downsample_in_time_partial_flags(resample_in_time_file):
     # and the output should not be flagged.
     inds01 = uv_object.antpair2ind(0, 1)
     uv_object.flag_array[inds01[0], 0, 0, 0] = True
+    uv_object2 = uv_object.copy()
+
     uv_object.downsample_in_time(
-        min_integration_time, blt_order="baseline", minor_order="time"
+        min_int_time=min_integration_time, blt_order="baseline", minor_order="time"
     )
     out_wf = uv_object.get_data(0, 1)
     assert np.isclose(init_wf[1, 0, 0], out_wf[0, 0, 0])
@@ -5367,7 +5375,13 @@ def test_downsample_in_time_partial_flags(resample_in_time_file):
     # check that there are still no flags
     assert np.nonzero(uv_object.flag_array)[0].size == 0
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=2, blt_order="baseline", minor_order="time"
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
@@ -5378,6 +5392,7 @@ def test_downsample_in_time_totally_flagged(resample_in_time_file):
     uv_object.phase_to_time(Time(uv_object.time_array[0], format="jd"))
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_wf = uv_object.get_data(0, 1)
@@ -5394,8 +5409,10 @@ def test_downsample_in_time_totally_flagged(resample_in_time_file):
     # should be flagged
     inds01 = uv_object.antpair2ind(0, 1)
     uv_object.flag_array[inds01[:2], 0, 0, 0] = True
+    uv_object2 = uv_object.copy()
+
     uv_object.downsample_in_time(
-        min_integration_time, blt_order="baseline", minor_order="time"
+        min_int_time=min_integration_time, blt_order="baseline", minor_order="time"
     )
     out_wf = uv_object.get_data(0, 1)
     assert np.isclose((init_wf[0, 0, 0] + init_wf[1, 0, 0]) / 2.0, out_wf[0, 0, 0])
@@ -5408,7 +5425,13 @@ def test_downsample_in_time_totally_flagged(resample_in_time_file):
     out_flag = uv_object.get_flags(0, 1)
     assert out_flag[0, 0, 0]
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=2, blt_order="baseline", minor_order="time"
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
@@ -5419,6 +5442,7 @@ def test_downsample_in_time_uneven_samples(resample_in_time_file):
     uv_object.phase_to_time(Time(uv_object.time_array[0], format="jd"))
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_wf = uv_object.get_data(0, 1)
@@ -5430,7 +5454,7 @@ def test_downsample_in_time_uneven_samples(resample_in_time_file):
     # number of samples
     min_integration_time = original_int_time * 3.0
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         keep_ragged=False,
@@ -5446,23 +5470,32 @@ def test_downsample_in_time_uneven_samples(resample_in_time_file):
         )
     )
 
+    # make sure integration time is correct
+    # in this case, all integration times should be the target one
+    assert np.all(np.isclose(uv_object.integration_time, min_integration_time))
+
     # as usual, the new data should be the average of the input data (3 points now)
     out_wf = uv_object.get_data(0, 1)
     assert np.isclose(np.mean(init_wf[0:3, 0, 0]), out_wf[0, 0, 0])
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=3, blt_order="baseline", minor_order="time", keep_ragged=False
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
 @pytest.mark.filterwarnings("ignore:The enu array in ECEF_from_ENU")
-def test_downsample_in_time_uneven_samples_discard_ragged(resample_in_time_file):
-    """Test the downsample_in_time method with uneven downsampling and
-    discarding the ragged samples.
-    """
+def test_downsample_in_time_uneven_samples_keep_ragged(resample_in_time_file):
+    """Test downsample_in_time with uneven downsampling and keep_ragged=True."""
     uv_object = resample_in_time_file
     uv_object.phase_to_time(Time(uv_object.time_array[0], format="jd"))
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_wf = uv_object.get_data(0, 1)
@@ -5476,21 +5509,23 @@ def test_downsample_in_time_uneven_samples_discard_ragged(resample_in_time_file)
 
     # test again with keep_ragged=False
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
-        keep_ragged=False,
+        keep_ragged=True,
     )
 
-    # make sure integration time is correct
-    # in this case, all integration times should be the target one
-    assert np.all(np.isclose(uv_object.integration_time, min_integration_time))
-
     # as usual, the new data should be the average of the input data
     out_wf = uv_object.get_data(0, 1)
     assert np.isclose(np.mean(init_wf[0:3, 0, 0]), out_wf[0, 0, 0])
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=3, blt_order="baseline", minor_order="time", keep_ragged=True
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
@@ -5513,7 +5548,7 @@ def test_downsample_in_time_summing_correlator_mode(resample_in_time_file):
     # change the target integration time
     min_integration_time = original_int_time * 2.0
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         summing_correlator_mode=True,
@@ -5564,7 +5599,7 @@ def test_downsample_in_time_summing_correlator_mode_partial_flags(
     inds01 = uv_object.antpair2ind(0, 1)
     uv_object.flag_array[inds01[0], 0, 0, 0] = True
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         summing_correlator_mode=True,
@@ -5611,7 +5646,7 @@ def test_downsample_in_time_summing_correlator_mode_totally_flagged(
     inds01 = uv_object.antpair2ind(0, 1)
     uv_object.flag_array[inds01[:2], 0, 0, 0] = True
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         summing_correlator_mode=True,
@@ -5654,7 +5689,7 @@ def test_downsample_in_time_summing_correlator_mode_uneven_samples(
     # number of samples
     min_integration_time = original_int_time * 3.0
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         keep_ragged=False,
@@ -5705,7 +5740,7 @@ def test_downsample_in_time_summing_correlator_mode_uneven_samples_drop_ragged(
     # test again with keep_ragged=False
     min_integration_time = original_int_time * 3.0
     uv_object.downsample_in_time(
-        min_integration_time,
+        min_int_time=min_integration_time,
         blt_order="baseline",
         minor_order="time",
         keep_ragged=False,
@@ -5751,7 +5786,9 @@ def test_partial_downsample_in_time(resample_in_time_file):
 
     # change the target integration time
     min_integration_time = np.amax(uv_object.integration_time)
-    uv_object.downsample_in_time(min_integration_time, blt_order="baseline")
+    uv_object.downsample_in_time(
+        min_int_time=min_integration_time, blt_order="baseline"
+    )
 
     # Should have all the new integration time
     # (for this file with 20 integrations and a factor of 2 downsampling)
@@ -5784,6 +5821,7 @@ def test_downsample_in_time_drift(resample_in_time_file):
 
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_data_size = uv_object.data_array.size
@@ -5796,7 +5834,7 @@ def test_downsample_in_time_drift(resample_in_time_file):
     # change the target integration time
     min_integration_time = original_int_time * 2.0
     uv_object.downsample_in_time(
-        min_integration_time, blt_order="baseline", allow_drift=True
+        min_int_time=min_integration_time, blt_order="baseline", allow_drift=True
     )
 
     # Should have half the size of the data array and all the new integration time
@@ -5815,7 +5853,13 @@ def test_downsample_in_time_drift(resample_in_time_file):
     # check that there are no flags
     assert np.nonzero(uv_object.flag_array)[0].size == 0
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=2, blt_order="baseline", allow_drift=True
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 @pytest.mark.filterwarnings("ignore:The xyz array in ENU_from_ECEF")
@@ -5826,6 +5870,7 @@ def test_downsample_in_time_drift_no_phasing(resample_in_time_file):
 
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
+    uv_object2 = uv_object.copy()
 
     # save some values for later
     init_data_size = uv_object.data_array.size
@@ -5840,7 +5885,7 @@ def test_downsample_in_time_drift_no_phasing(resample_in_time_file):
 
     # try again with allow_drift=False
     uv_object.downsample_in_time(
-        min_integration_time, blt_order="baseline", allow_drift=False,
+        min_int_time=min_integration_time, blt_order="baseline", allow_drift=False,
     )
 
     # Should have half the size of the data array and all the new integration time
@@ -5863,7 +5908,13 @@ def test_downsample_in_time_drift_no_phasing(resample_in_time_file):
     # check that there are no flags
     assert np.nonzero(uv_object.flag_array)[0].size == 0
 
-    return
+    # Compare doing it with n_times_to_avg
+    uv_object2.downsample_in_time(
+        n_times_to_avg=2, blt_order="baseline", minor_order="time"
+    )
+    assert uv_object.history != uv_object2.history
+    uv_object2.history = uv_object.history
+    assert uv_object == uv_object2
 
 
 def test_downsample_in_time_errors(resample_in_time_file):
@@ -5873,23 +5924,49 @@ def test_downsample_in_time_errors(resample_in_time_file):
     # reorder to make sure we get the right value later
     uv_object.reorder_blts(order="baseline", minor_order="time")
 
+    # raise an error if set neither min_int_time and n_times_to_avg
+    with pytest.raises(
+        ValueError, match="Either min_int_time or n_times_to_avg must be set."
+    ):
+        uv_object.downsample_in_time()
+
+    # raise an error if set both min_int_time and n_times_to_avg
+    with pytest.raises(
+        ValueError, match="Only one of min_int_time or n_times_to_avg can be set."
+    ):
+        uv_object.downsample_in_time(
+            min_int_time=2 * np.amin(uv_object.integration_time), n_times_to_avg=2
+        )
+    # raise an error if only one time
+    uv_object2 = uv_object.copy()
+    uv_object2.select(times=uv_object2.time_array[0])
+    with pytest.raises(
+        ValueError, match="Only one time in this object, cannot downsample."
+    ):
+        uv_object2.downsample_in_time(n_times_to_avg=2)
+
     # raise an error for a too-large integration time
     max_integration_time = 1e3 * np.amax(uv_object.integration_time)
-    with pytest.raises(ValueError) as cm:
-        uv_object.downsample_in_time(max_integration_time)
-    assert str(cm.value).startswith("Increasing the integration time by more than")
+    with pytest.raises(
+        ValueError, match="Increasing the integration time by more than"
+    ):
+        uv_object.downsample_in_time(min_int_time=max_integration_time)
 
     # catch a warning for doing no work
     uv_object2 = uv_object.copy()
     max_integration_time = 0.5 * np.amin(uv_object.integration_time)
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        [max_integration_time],
-        message="All values in the integration_time array are " "already longer",
-    )
+    with pytest.warns(
+        UserWarning, match="All values in the integration_time array are already longer"
+    ):
+        uv_object.downsample_in_time(min_int_time=max_integration_time)
+
     assert uv_object == uv_object2
     del uv_object2
 
+    # raise an error if n_times_to_avg is not an integer
+    with pytest.raises(ValueError, match="n_times_to_avg must be an integer."):
+        uv_object.downsample_in_time(n_times_to_avg=2.5)
+
     # save some values for later
     init_data_size = uv_object.data_array.size
     init_wf = uv_object.get_data(0, 1)
@@ -5904,11 +5981,10 @@ def test_downsample_in_time_errors(resample_in_time_file):
     uv_object.time_array[inds01[-1]] += initial_int_time / (24 * 3600)
     uv_object.Ntimes += 1
     min_integration_time = 2 * np.amin(uv_object.integration_time)
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        [min_integration_time],
-        message=["There is a gap in the times of baseline (0, 1)"],
-    )
+    times_01 = uv_object.get_times(0, 1)
+    assert np.unique(np.diff(times_01)).size > 1
+    with pytest.warns(UserWarning, match=("There is a gap in the times of baseline")):
+        uv_object.downsample_in_time(min_int_time=min_integration_time)
 
     # Should have half the size of the data array and all the new integration time
     # (for this file with 20 integrations and a factor of 2 downsampling)
@@ -5946,12 +6022,11 @@ def test_downsample_in_time_int_time_mismatch_warning(resample_in_time_file):
     # not matching the time delta between integrations
     uv_object.integration_time *= 0.5
     min_integration_time = 2 * np.amin(uv_object.integration_time)
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        [min_integration_time],
-        message=["The time difference between integrations is " "not the same"],
-        nwarnings=10,
-    )
+    with pytest.warns(
+        UserWarning, match="The time difference between integrations is not the same"
+    ) as record:
+        uv_object.downsample_in_time(min_int_time=min_integration_time)
+    assert len(record) == 10
 
     # Should have half the size of the data array and all the new integration time
     # (for this file with 20 integrations and a factor of 2 downsampling)
@@ -5994,9 +6069,11 @@ def test_downsample_in_time_varying_integration_time(resample_in_time_file):
     uv_object.integration_time[inds01[-2:]] += initial_int_time
     uv_object.Ntimes = np.unique(uv_object.time_array).size
     min_integration_time = 2 * np.amin(uv_object.integration_time)
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time, [min_integration_time], nwarnings=0
-    )
+    # check that there are no warnings about inconsistencies between
+    # integration_time & time_array
+    with pytest.warns(None) as record:
+        uv_object.downsample_in_time(min_int_time=min_integration_time)
+    assert len(record) == 0
 
     # Should have all the new integration time
     # (for this file with 20 integrations and a factor of 2 downsampling)
@@ -6033,11 +6110,10 @@ def test_downsample_in_time_varying_integration_time_warning(resample_in_time_fi
     initial_int_time = uv_object.integration_time[inds01[0]]
     uv_object.integration_time[inds01[-2:]] += initial_int_time
     min_integration_time = 2 * np.amin(uv_object.integration_time)
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        [min_integration_time],
-        message="The time difference between integrations is " "different than",
-    )
+    with pytest.warns(
+        UserWarning, match="The time difference between integrations is different than"
+    ):
+        uv_object.downsample_in_time(min_int_time=min_integration_time)
 
     # Should have all the new integration time
     # (for this file with 20 integrations and a factor of 2 downsampling)
@@ -6099,7 +6175,7 @@ def test_upsample_downsample_in_time(resample_in_time_file):
     assert uv_object.Nblts == new_Nblts
 
     uv_object.downsample_in_time(
-        np.amin(uv_object2.integration_time), blt_order="baseline"
+        min_int_time=np.amin(uv_object2.integration_time), blt_order="baseline"
     )
 
     # increase tolerance on LST if iers.conf.auto_max_age is set to None, as we
@@ -6123,22 +6199,23 @@ def test_upsample_downsample_in_time(resample_in_time_file):
 
     # check that calling downsample again with the same min_integration_time
     # gives warning and does nothing
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        func_args=[np.amin(uv_object2.integration_time)],
-        func_kwargs={"blt_order": "baseline"},
-        message="All values in the integration_time array are " "already shorter",
-    )
+    with pytest.warns(
+        UserWarning, match="All values in the integration_time array are already longer"
+    ):
+        uv_object.downsample_in_time(
+            min_int_time=np.amin(uv_object2.integration_time), blt_order="baseline"
+        )
     assert uv_object.Nblts == uv_object2.Nblts
 
     # check that calling upsample again with the almost the same min_integration_time
     # gives warning and does nothing
-    uvtest.checkWarnings(
-        uv_object.downsample_in_time,
-        func_args=[np.amin(uv_object2.integration_time) + small_number],
-        func_kwargs={"blt_order": "baseline"},
-        message="All values in the integration_time array are " "already shorter",
-    )
+    with pytest.warns(
+        UserWarning, match="All values in the integration_time array are already longer"
+    ):
+        uv_object.upsample_in_time(
+            np.amin(uv_object2.integration_time) + small_number, blt_order="baseline"
+        )
+
     assert uv_object.Nblts == uv_object2.Nblts
 
     return
@@ -6400,10 +6477,17 @@ def test_resample_in_time_only_upsample(bda_test_file):
 
 @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline")
 def test_downsample_in_time_mwa():
-    """Test resample in time works with MWA data."""
+    """
+    Test resample in time works with numerical weirdnesses.
+
+    In particular, when min_int_time is not quite an integer mulitple of
+    integration_time. This text broke with a prior bug (see issue 773).
+    """
     filename = os.path.join(DATA_PATH, "mwa_integration_time.uvh5")
     uv = UVData()
     uv.read(filename)
+    uv.phase_to_time(np.mean(uv.time_array))
+    uv_object2 = uv.copy()
 
     # all data within 5 milliseconds of 2 second integrations
     assert np.allclose(uv.integration_time, 2, atol=5e-3)
@@ -6412,6 +6496,16 @@ def test_downsample_in_time_mwa():
 
     assert np.all(uv.integration_time > (min_int_time - 5e-3))
 
+    # Now do the human expected thing:
+    init_data = uv_object2.get_data((61, 58))
+    uv_object2.downsample_in_time(n_times_to_avg=2, keep_ragged=False)
+
+    assert uv_object2.Ntimes == 5
+
+    out_data = uv_object2.get_data((61, 58))
+
+    assert np.isclose(np.mean(init_data[0:2, 0, 0]), out_data[0, 0, 0])
+
 
 @pytest.mark.filterwarnings("ignore:There is a gap in the times of baseline")
 def test_resample_in_time_warning():