bhazelton: There are special keywords on github that will close the related issue when this PR is merged if you use them properly. It has to go in the initial (top) PR description and it needs to follow the pattern `closes #___` or `fixes #___`.
bhazelton: I just enabled circleci builds for pull requests from forks (I didn't realize it wasn't enabled until now), so when you push another commit it should actually trigger the circleci builds.
mkolopanis: This will need to be rebased for those github actions to pass now.
mkolopanis: ah the hera_qm failure here is my fault from a QM pr we just merged in. I offloaded a lot of command line arguments to the setup.cfg but that basically requires pytest-cov to run tests. that could be fixed by adding `pytest-cov` to the hera_qm environemnt yaml.

hera_cal is a bit confusing though. It is not downloading `uvtools` anymore via pip? @adampbeardsley do you know if something recently changed in hera_cal that would affect this?
ntk688: Ok I rebased this, and it now has an updated changelog and a brief demonstration in the "adding data" section of the tutorial. 
adampbeardsley: There's this:
https://github.com/HERA-Team/hera_cal/commit/03a1fee8228b21bcf4d0242be524293eabed34d8
But that was three months ago.
The only recent change I know of is that Josh changed the environment to use the most recent pyuvdata. But I don't think that should matter here.
mkolopanis: Ah i think it was a uvtools thing. I made a note of it in #728. The workaround is to manually install basemap because pip is having trouble finding it now? Thanks for digging though.
bhazelton: I'm not sure why the coverage isn't reporting, must have to do with this being a fork, but I can't find a place to fix it in the settings on codecov. I downloaded your branch and ran `test_cover_noinstall.sh` from the scripts folder, which makes output similar to codecov and I found that there are 4 new uncovered lines, line numbers 1464, 1487, 1489 and 1515. The first and last of those will only get called for an inlace sum or diff, the middle two are for a ValueError, we test that Errors are raised as expected using something like this (stolen from another test, you'll need to update for your error):
```    with pytest.raises(ValueError) as cm:
        uvdata_data.uv_object.check()
    assert str(cm.value).startswith('All antennas in ant_1_array must be in antenna_numbers')
```
ntk688: I had some of those error tests, but when I went to investigate I realized those lines weren't being called because of a goofy indenting mistake. So I fixed that and added the in place checks and now test_cover_noinstall.sh shows no uncovered lines in the code I added. Codecov still not going through though...
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa1aabe0..342f2bd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
 ## [Unreleased]
 
 ### Added
+- `sum_vis` and `diff_vis` for summing or differencing visibilities in the data_array.
 - `read_mwa_corr_fits` for reading in MWA correlator gpubox files and applying cable corrections.
 - `eq_coeffs` for storing equalization coefficients and `remove_eq_coeffs` for removing them.
 - `utils.apply_uvflag` for applying UVFlag objects to UVData objects
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 75bcee09..3cf237fe 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -690,7 +690,28 @@ and added to the previous.
   >>> filenames = ['tutorial1.uvfits', 'tutorial2.uvfits', 'tutorial3.uvfits']
   >>> uv.read(filenames)
 
-e) Fast concatenation
+e) Summing and differencing visibilities
+*****************************************
+Simple summing and differencing of visibilities can be done with the ``sum_vis`` and ``diff_vis`` methods.
+::
+
+  >>> from pyuvdata import UVData
+  >>> import copy
+  >>> filename = 'pyuvdata/data/day2_TDEM0003_10s_norx_1src_1spw.uvfits'
+  >>> uv1 = UVData()
+  >>> uv1.read(filename)
+  >>> uv2 = copy.deepcopy(uv1)
+
+  # sum visibilities
+  >>> uv1 = uv1.sum_vis(uv2)
+
+  # diff visibilities
+  >>> uv1 = uv1.diff_vis(uv2)
+
+  # in place option
+  >>> uv1.sum_vis(uv2, inplace=True)
+
+f) Fast concatenation
 *******************************
 As an alternative to the ``__add__`` operation, the ``fast_concat`` method can
 be used. The user specifies a UVData object to combine with the existing one,
diff --git a/pyuvdata/uvdata.py b/pyuvdata/uvdata.py
index 81941090..71ec7c09 100644
--- a/pyuvdata/uvdata.py
+++ b/pyuvdata/uvdata.py
@@ -1424,6 +1424,106 @@ class UVData(UVBase):
             self.check(check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability)
 
+    def sum_vis(self, other, run_check=True, check_extra=True,
+                run_check_acceptability=True, inplace=False, difference=False):
+        """
+        Sums visibilities between two UVData objects.
+
+        Parameters
+        ----------
+        other : UVData object
+            Another UVData object which will be added to self.
+        run_check : bool
+            Option to check for the existence and proper shapes of parameters
+            after combining objects.
+        check_extra : bool
+            Option to check optional parameters as well as required ones.
+        run_check_acceptability : bool
+            Option to check acceptable range of the values of parameters after
+            combining objects.
+        inplace : bool
+            If True, overwrite self as we go, otherwise create a third object
+            as the sum of the two.
+        difference : bool
+            If True, differences the visibilities of the two UVData objects
+            rather than summing them.
+
+        Returns
+        ------
+        UVData Object
+            If inplace parameter is False.
+
+        Raises
+        ------
+        ValueError
+            If other is not a UVData object, or if self and other
+            are not compatible.
+
+        """
+
+        if inplace:
+            this = self
+        else:
+            this = copy.deepcopy(self)
+
+        # Check that both objects are UVData and valid
+        this.check(check_extra=check_extra,
+                   run_check_acceptability=run_check_acceptability)
+        if not issubclass(other.__class__, this.__class__):
+            if not issubclass(this.__class__, other.__class__):
+                raise ValueError('Only UVData (or subclass) objects can be '
+                                 'added to a UVData (or subclass) object')
+        other.check(check_extra=check_extra,
+                    run_check_acceptability=run_check_acceptability)
+
+        # Define the parameters that need to be the same for objects to be
+        # summed or diffed.
+        compatibility_params = list(this.__iter__())
+        compatibility_params.remove('_data_array')
+        compatibility_params.remove('_history')
+
+        # Check each metadata element in compatibility_params
+        for a in compatibility_params:
+            params_match = (getattr(this, a) == getattr(other, a))
+            if not params_match:
+                msg = 'UVParameter ' + \
+                    a[1:] + ' does not match. Cannot combine objects.'
+                raise ValueError(msg)
+
+        # Do the summing / differencing
+        if difference:
+            this.data_array = this.data_array - other.data_array
+            history_update_string = ' Visibilities differenced using pyuvdata.'
+        else:
+            this.data_array = this.data_array + other.data_array
+            history_update_string = ' Visibilities summed using pyuvdata.'
+
+        this.history = uvutils._combine_histories(this.history, other.history)
+        this.history += history_update_string
+
+        # Check final object is self-consistent
+        if run_check:
+            this.check(check_extra=check_extra,
+                       run_check_acceptability=run_check_acceptability)
+
+        if not inplace:
+            return this
+
+    def diff_vis(self, other, run_check=True, check_extra=True,
+                 run_check_acceptability=True,
+                 inplace=False):
+
+        if inplace:
+            self.sum_vis(other, difference=True, run_check=True,
+                         check_extra=check_extra,
+                         run_check_acceptability=run_check_acceptability,
+                         inplace=inplace)
+        else:
+            return self.sum_vis(other, difference=True, run_check=True,
+                                check_extra=check_extra,
+                                run_check_acceptability=run_check_acceptability,
+                                inplace=inplace)
+
     def __add__(self, other, run_check=True, check_extra=True,
                 run_check_acceptability=True, inplace=False):
         """

method to add matched visibilities between 2 objects
e.g. to combine simulated EoR/foreground/noise visibilities.

Needed for pyuvsim: RadioAstronomySoftwareGroup/pyuvsim#28
diff --git a/pyuvdata/tests/test_uvdata.py b/pyuvdata/tests/test_uvdata.py
index 5b55dbe3..67e686db 100644
--- a/pyuvdata/tests/test_uvdata.py
+++ b/pyuvdata/tests/test_uvdata.py
@@ -1642,6 +1642,45 @@ def test_reorder_blts():
     assert str(cm.value).startswith('minor_order can only be one of')
 
 
+@pytest.mark.filterwarnings("ignore:Telescope EVLA is not")
+def test_sum_vis():
+    # check sum_vis
+    uv_full = UVData()
+    testfile = os.path.join(DATA_PATH, 'day2_TDEM0003_10s_norx_1src_1spw.uvfits')
+    uv_full.read_uvfits(testfile)
+
+    uv_half = copy.deepcopy(uv_full)
+    uv_half.data_array = uv_full.data_array / 2
+    uv_summed = uv_half.sum_vis(uv_half)
+
+    assert np.array_equal(uv_summed.data_array, uv_full.data_array)
+    assert uvutils._check_histories(uv_half.history + ' Visibilities summed '
+                                    'using pyuvdata.', uv_summed.history)
+
+    # check diff_vis
+    uv_diffed = uv_full.diff_vis(uv_half)
+
+    assert np.array_equal(uv_diffed.data_array, uv_half.data_array)
+    assert uvutils._check_histories(uv_full.history + ' Visibilities '
+                                    'differenced using pyuvdata.',
+                                    uv_diffed.history)
+
+    # check in place
+    uv_summed.diff_vis(uv_half, inplace=True)
+    assert np.array_equal(uv_summed.data_array, uv_half.data_array)
+
+    # check error messages
+    with pytest.raises(ValueError) as cm:
+        uv_full.sum_vis('foo')
+    assert str(cm.value).startswith('Only UVData (or subclass) objects can be')
+
+    uv_full.instrument = 'foo'
+    with pytest.raises(ValueError) as cm:
+        uv_full.sum_vis(uv_half, inplace=True)
+    assert str(cm.value).startswith('UVParameter instrument '
+                                    'does not match')
+
+
 @pytest.mark.filterwarnings("ignore:Telescope EVLA is not")
 def test_add():
     uv_full = UVData()

pip install -e .[dev]
parse_log_pytest
requirements.txt
coverage pytest-cov setuptools_scm pytest-cases
apt-get update apt-get install -y gcc g++
3.6
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astropy==4.1
attrs==22.2.0
cached-property==1.5.2
certifi==2021.5.30
coverage==6.2
decopatch==1.4.10
h5py==3.1.0
importlib-metadata==4.8.3
iniconfig==1.1.1
makefun==1.15.6
numpy==1.19.5
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
pytest-cases==3.8.6
pytest-cov==4.0.0
-e git+https://github.com/RadioAstronomySoftwareGroup/pyuvdata.git@96be086324ba8f35815dd590429c6415411c15ea#egg=pyuvdata
scipy==1.5.4
setuptools-scm==6.4.2
six==1.17.0
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: pyuvdata
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astropy==4.1
      - attrs==22.2.0
      - cached-property==1.5.2
      - coverage==6.2
      - decopatch==1.4.10
      - h5py==3.1.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - makefun==1.15.6
      - numpy==1.19.5
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cases==3.8.6
      - pytest-cov==4.0.0
      - scipy==1.5.4
      - setuptools-scm==6.4.2
      - six==1.17.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/pyuvdata

pyuvdata/tests/test_uvdata.py::test_sum_vis
pyuvdata/tests/test_uvdata.py::test_add_drift pyuvdata/tests/test_uvdata.py::test_break_add pyuvdata/tests/test_uvdata.py::test_set_uvws_from_antenna_pos pyuvdata/tests/test_uvdata.py::test_downsample_in_time_errors pyuvdata/tests/test_uvdata.py::test_downsample_in_time_int_time_mismatch_warning pyuvdata/tests/test_uvdata.py::test_downsample_in_time_varying_integration_time pyuvdata/tests/test_uvdata.py::test_downsample_in_time_varying_integration_time_warning
pyuvdata/tests/test_uvdata.py::test_parameter_iter pyuvdata/tests/test_uvdata.py::test_required_parameter_iter pyuvdata/tests/test_uvdata.py::test_extra_parameter_iter pyuvdata/tests/test_uvdata.py::test_unexpected_parameters pyuvdata/tests/test_uvdata.py::test_unexpected_attributes pyuvdata/tests/test_uvdata.py::test_properties pyuvdata/tests/test_uvdata.py::test_baseline_to_antnums pyuvdata/tests/test_uvdata.py::test_baseline_to_antnums_vectorized pyuvdata/tests/test_uvdata.py::test_antnums_to_baselines pyuvdata/tests/test_uvdata.py::test_known_telescopes pyuvdata/tests/test_uvdata.py::test_HERA_diameters pyuvdata/tests/test_uvdata.py::test_generic_read pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA[phase_kwargs0] pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA[phase_kwargs1] pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA[phase_kwargs2] pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA_one_bl pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA_antpos pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA_zenith_timestamp pyuvdata/tests/test_uvdata.py::test_phase_to_time_jd_input pyuvdata/tests/test_uvdata.py::test_phase_to_time_error pyuvdata/tests/test_uvdata.py::test_unphase_drift_data_error pyuvdata/tests/test_uvdata.py::test_unknown_phase_unphaseHERA_errors[unphase_to_drift-phase_kwargs0-The pyuvdata/tests/test_uvdata.py::test_unknown_phase_unphaseHERA_errors[phase-phase_kwargs1-The pyuvdata/tests/test_uvdata.py::test_unknown_phase_unphaseHERA_errors[phase_to_time-phase_kwargs2-The pyuvdata/tests/test_uvdata.py::test_phase_rephaseHERA_errors[phase-phase_kwargs0-The pyuvdata/tests/test_uvdata.py::test_phase_rephaseHERA_errors[phase_to_time-phase_kwargs1-The pyuvdata/tests/test_uvdata.py::test_phase_unphaseHERA_bad_frame pyuvdata/tests/test_uvdata.py::test_phasing pyuvdata/tests/test_uvdata.py::test_set_phase_unknown pyuvdata/tests/test_uvdata.py::test_select_blts pyuvdata/tests/test_uvdata.py::test_select_antennas pyuvdata/tests/test_uvdata.py::test_select_bls pyuvdata/tests/test_uvdata.py::test_select_times pyuvdata/tests/test_uvdata.py::test_select_frequencies pyuvdata/tests/test_uvdata.py::test_select_freq_chans pyuvdata/tests/test_uvdata.py::test_select_polarizations pyuvdata/tests/test_uvdata.py::test_select pyuvdata/tests/test_uvdata.py::test_select_not_inplace pyuvdata/tests/test_uvdata.py::test_conjugate_bls pyuvdata/tests/test_uvdata.py::test_reorder_pols pyuvdata/tests/test_uvdata.py::test_reorder_blts pyuvdata/tests/test_uvdata.py::test_add pyuvdata/tests/test_uvdata.py::test_fast_concat pyuvdata/tests/test_uvdata.py::test_fast_concat_errors pyuvdata/tests/test_uvdata.py::test_key2inds pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols_fringe pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols_bl_fringe pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols_missing_data pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols_bls pyuvdata/tests/test_uvdata.py::test_key2inds_conj_all_pols_missing_data_bls pyuvdata/tests/test_uvdata.py::test_smart_slicing pyuvdata/tests/test_uvdata.py::test_get_data pyuvdata/tests/test_uvdata.py::test_get_flags pyuvdata/tests/test_uvdata.py::test_get_nsamples pyuvdata/tests/test_uvdata.py::test_antpair2ind pyuvdata/tests/test_uvdata.py::test_get_times pyuvdata/tests/test_uvdata.py::test_antpairpol_iter pyuvdata/tests/test_uvdata.py::test_get_ants pyuvdata/tests/test_uvdata.py::test_get_ENU_antpos pyuvdata/tests/test_uvdata.py::test_telescope_loc_XYZ_check pyuvdata/tests/test_uvdata.py::test_get_pols pyuvdata/tests/test_uvdata.py::test_get_pols_x_orientation pyuvdata/tests/test_uvdata.py::test_deprecated_x_orientation pyuvdata/tests/test_uvdata.py::test_get_feedpols pyuvdata/tests/test_uvdata.py::test_parse_ants pyuvdata/tests/test_uvdata.py::test_select_with_ant_str pyuvdata/tests/test_uvdata.py::test_deprecated_redundancy_funcs pyuvdata/tests/test_uvdata.py::test_get_antenna_redundancies pyuvdata/tests/test_uvdata.py::test_redundancy_contract_expand pyuvdata/tests/test_uvdata.py::test_redundancy_contract_expand_nblts_not_nbls_times_ntimes pyuvdata/tests/test_uvdata.py::test_compress_redundancy_metadata_only pyuvdata/tests/test_uvdata.py::test_redundancy_missing_groups pyuvdata/tests/test_uvdata.py::test_quick_redundant_vs_redundant_test_array pyuvdata/tests/test_uvdata.py::test_redundancy_finder_when_nblts_not_nbls_times_ntimes pyuvdata/tests/test_uvdata.py::test_overlapping_data_add pyuvdata/tests/test_uvdata.py::test_lsts_from_time_with_only_unique pyuvdata/tests/test_uvdata.py::test_copy pyuvdata/tests/test_uvdata.py::test_upsample_in_time pyuvdata/tests/test_uvdata.py::test_upsample_in_time_with_flags pyuvdata/tests/test_uvdata.py::test_upsample_in_time_noninteger_resampling pyuvdata/tests/test_uvdata.py::test_upsample_in_time_errors pyuvdata/tests/test_uvdata.py::test_upsample_in_time_summing_correlator_mode pyuvdata/tests/test_uvdata.py::test_upsample_in_time_summing_correlator_mode_with_flags pyuvdata/tests/test_uvdata.py::test_upsample_in_time_summing_correlator_mode_nonint_resampling pyuvdata/tests/test_uvdata.py::test_partial_upsample_in_time pyuvdata/tests/test_uvdata.py::test_upsample_in_time_drift pyuvdata/tests/test_uvdata.py::test_upsample_in_time_drift_no_phasing pyuvdata/tests/test_uvdata.py::test_downsample_in_time pyuvdata/tests/test_uvdata.py::test_downsample_in_time_partial_flags pyuvdata/tests/test_uvdata.py::test_downsample_in_time_totally_flagged pyuvdata/tests/test_uvdata.py::test_downsample_in_time_uneven_samples pyuvdata/tests/test_uvdata.py::test_downsample_in_time_uneven_samples_discard_ragged pyuvdata/tests/test_uvdata.py::test_downsample_in_time_summing_correlator_mode pyuvdata/tests/test_uvdata.py::test_downsample_in_time_summing_correlator_mode_partial_flags pyuvdata/tests/test_uvdata.py::test_downsample_in_time_summing_correlator_mode_totally_flagged pyuvdata/tests/test_uvdata.py::test_downsample_in_time_summing_correlator_mode_uneven_samples pyuvdata/tests/test_uvdata.py::test_downsample_in_time_summing_correlator_mode_uneven_samples_drop_ragged pyuvdata/tests/test_uvdata.py::test_partial_downsample_in_time pyuvdata/tests/test_uvdata.py::test_downsample_in_time_drift pyuvdata/tests/test_uvdata.py::test_downsample_in_time_drift_no_phasing pyuvdata/tests/test_uvdata.py::test_upsample_downsample_in_time pyuvdata/tests/test_uvdata.py::test_upsample_downsample_in_time_odd_resample pyuvdata/tests/test_uvdata.py::test_upsample_downsample_in_time_metadata_only pyuvdata/tests/test_uvdata.py::test_resample_in_time pyuvdata/tests/test_uvdata.py::test_resample_in_time_downsample_only pyuvdata/tests/test_uvdata.py::test_resample_in_time_only_upsample
docs/tutorial.rst pyuvdata/uvdata.py CHANGELOG.md
docs/tutorial.rst pyuvdata/uvdata.py CHANGELOG.md