.timesys is not None:
            header["timesys"] = np.string_(self.timesys)
        if self.x_orientation is not None:
            header["x_orientation"] = np.string_(self.x_orientation)
        if self.blt_order is not None:
            header["blt_order"] = np.string_(", ".join(self.blt_order))
        if self.antenna_diameters is not None:
            header["antenna_diameters"] = self.antenna_diameters
        if self.uvplane_reference_time is not None:
            header["uvplane_reference_time"] = self.uvplane_reference_time
        if self.eq_coeffs is not None:
            header["eq_coeffs"] = self.eq_coeffs
        if self.eq_coeffs_convention is not None:
            header["eq_coeffs_convention"] = np.string_(self.eq_coeffs_convention)
        if self.flex_spw_id_array is not None:
            header["flex_spw_id_array"] = self.flex_spw_id_array
        if self.flex_spw_polarization_array is not None:
            header["flex_spw_polarization_array"] = self.flex_spw_polarization_array

        if self.blts_are_rectangular is not None:
            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

        # write out extra keywords if it exists and has elements
        if self.extra_keywords:
            extra_keywords = header.create_group("extra_keywords")
            for k in self.extra_keywords.keys():
                if isinstance(self.extra_keywords[k], str):
                    extra_keywords[k] = np.string_(self.extra_keywords[k])
                elif self.extra_keywords[k] is None:
                    # save as empty/null dataset
                    extra_keywords[k] = h5py.Empty("f")
                else:
                    extra_keywords[k] = self.extra_keywords[k]

        # write out history
        header["history"] = np.string_(self.history)

        return

    def write_uvh5(
        self,
        filename,
        clobber=False,
        chunks=True,
        data_compression=None,
        flags_compression="lzf",
        nsample_compression="lzf",
        data_write_dtype=None,
        add_to_history=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write an in-memory UVData object to a UVH5 file.

        Parameters
        ----------
        filename : str
            The UVH5 file to write to.
        clobber : bool
            Option to overwrite the file if it already exists.
        chunks : tuple or bool
            h5py.create_dataset chunks keyword. Tuple for chunk shape,
            True for auto-chunking, None for no chunking. Default is True.
        data_compression : str
            HDF5 filter to apply when writing the data_array. Default is None
            (no filter/compression). In addition to the normal HDF5 filter values, the
            user may specify "bitshuffle" which will set the compression to `32008` for
            bitshuffle and will set the `compression_opts` to `(0, 2)` to allow
            bitshuffle to automatically determine the block size and to use the LZF
            filter after bitshuffle. Using `bitshuffle` requires having the
            `hdf5plugin` package installed.  Dataset must be chunked to use compression.
        flags_compression : str
            HDF5 filter to apply when writing the flags_array. Default is the
            LZF filter. Dataset must be chunked.
        nsample_compression : str
            HDF5 filter to apply when writing the nsample_array. Default is the
            LZF filter. Dataset must be chunked.
        data_write_dtype : numpy dtype
            The datatype of output visibility data. If 'None', then the same
            datatype as data_array will be used. The user may specify 'c8' for
            single-precision floats or 'c16' for double-presicion. Otherwise, a
            numpy dtype object must be specified with an 'r' field and an 'i'
            field for real and imaginary parts, respectively. See uvh5.py for
            an example of defining such a datatype.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            before writing the file.
        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 before
            writing the file.
        strict_uvw_antpos_check : bool
            Option to raise an error rather than a warning if the check that
            uvws match antenna positions does not pass.
        check_autos : bool
            Check whether any auto-correlations have non-zero imaginary values in
            data_array (which should not mathematically exist). Default is True.
        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 False.

        Returns
        -------
        None

        Raises
        ------
        IOError
            If the file located at `filename` already exists and clobber=False,
            an IOError is raised.

        Notes
        -----
        The HDF5 library allows for the application of "filters" when writing
        data, which can provide moderate to significant levels of compression
        for the datasets in question.  Testing has shown that for some typical
        cases of UVData objects (empty/sparse flag_array objects, and/or uniform
        nsample_arrays), the built-in LZF filter provides significant
        compression for minimal computational overhead.

        Note that for typical HERA data files written after mid-2020, the
        bitshuffle filter was applied to the data_array. Because of the lack of
        portability, it is not included as an option here; in the future, it may
        be added. Note that as long as bitshuffle is installed on the system in
        a way that h5py can find it, no action needs to be taken to _read_ a
        data_array encoded with bitshuffle (or an error will be raised).
        """
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )

        if os.path.exists(filename):
            if clobber:
                print("File exists; clobbering")
            else:
                raise IOError("File exists; skipping")

        revert_fas = False
        if not self.future_array_shapes:
            # We force using future array shapes here to always write version 1.* files.
            # We capture the current state so that it can be reverted later if needed.
            revert_fas = True
            self.use_future_array_shapes()

        data_compression, data_compression_opts = _get_compression(data_compression)

        # open file for writing
        with h5py.File(filename, "w") as f:
            # write header
            header = f.create_group("Header")
            self._write_header(header)

            # write out data, flags, and nsample arrays
            dgrp = f.create_group("Data")
            if data_write_dtype is None:
                if self.data_array.dtype == "complex64":
                    data_write_dtype = "c8"
                else:
                    data_write_dtype = "c16"
            if data_write_dtype not in ("c8", "c16"):
                _check_uvh5_dtype(data_write_dtype)
                visdata = dgrp.create_dataset(
                    "visdata",
                    self.data_array.shape,
                    chunks=chunks,
                    compression=data_compression,
                    compression_opts=data_compression_opts,
                    dtype=data_write_dtype,
                )
                indices = (np.s_[:], np.s_[:], np.s_[:])
                _write_complex_astype(self.data_array, visdata, indices)
            else:
                visdata = dgrp.create_dataset(
                    "visdata",
                    chunks=chunks,
                    data=self.data_array,
                    compression=data_compression,
                    compression_opts=data_compression_opts,
                    dtype=data_write_dtype,
                )
            dgrp.create_dataset(
                "flags",
                chunks=chunks,
                data=self.flag_array,
                compression=flags_compression,
            )
            dgrp.create_dataset(
                "nsamples",
                chunks=chunks,
                data=self.nsample_array.astype(np.float32),
                compression=nsample_compression,
            )

        if revert_fas:
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore",
                    message="This method will be removed in version 3.0 when the "
                    "current array shapes are no longer supported.",
                )
                self.use_current_array_shapes()

        return

    def initialize_uvh5_file(
        self,
        filename,
        clobber=False,
        chunks=True,
        data_compression=None,
        flags_compression="lzf",
        nsample_compression="lzf",
        data_write_dtype=None,
    ):
        """
        Initialize a UVH5 file on disk to be written to in parts.

        Parameters
        ----------
        filename : str
            The UVH5 file to write to.
        clobber : bool
            Option to overwrite the file if it already exists.
        chunks : tuple or bool
            h5py.create_dataset chunks keyword. Tuple for chunk shape,
            True for auto-chunking, None for no chunking. Default is True.
        data_compression : str
            HDF5 filter to apply when writing the data_array. Default is None
            (no filter/compression). In addition to the normal HDF5 filter values, the
            user may specify "bitshuffle" which will set the compression to `32008` for
            bitshuffle and will set the `compression_opts` to `(0, 2)` to allow
            bitshuffle to automatically determine the block size and to use the LZF
            filter after bitshuffle. Using `bitshuffle` requires having the
            `hdf5plugin` package installed.  Dataset must be chunked to use compression.
        flags_compression : str
            HDF5 filter to apply when writing the flags_array. Default is the
            LZF filter. Dataset must be chunked.
        nsample_compression : str
            HDF5 filter to apply when writing the nsample_array. Default is the
            LZF filter. Dataset must be chunked.
        data_write_dtype : str or numpy dtype
            The datatype of output visibility data. If 'None', then double-
            precision floats will be used. The user may specify 'c8' for
            single-precision floats or 'c16' for double-presicion. Otherwise, a
            numpy dtype object must be specified with an 'r' field and an 'i'
            field for real and imaginary parts, respectively. See uvh5.py for
            an example of defining such a datatype.

        Returns
        -------
        None

        Raises
        ------
        IOError
            If the file located at `filename` already exists and clobber=False,
            an IOError is raised.

        Notes
        -----
        When partially writing out data, this function should be called first to
        initialize the file on disk. The data is then actually written by
        calling the write_uvh5_part method, with the same filename as the one
        specified in this function. See the tutorial for a worked example.

        The HDF5 library allows for the application of "filters" when writing
        data, which can provide moderate to significant levels of compression
        for the datasets in question.  Testing has shown that for some typical
        cases of UVData objects (empty/sparse flag_array objects, and/or uniform
        nsample_arrays), the built-in LZF filter provides significant
        compression for minimal computational overhead.

        Note that for typical HERA data files written after mid-2018, the
        bitshuffle filter was applied to the data_array. Because of the lack of
        portability, it is not included as an option here; in the future, it may
        be added. Note that as long as bitshuffle is installed on the system in
        a way that h5py can find it, no action needs to be taken to _read_ a
        data_array encoded with bitshuffle (or an error will be raised).
        """
        if os.path.exists(filename):
            if clobber:
                print("File exists; clobbering")
            else:
                raise IOError("File exists; skipping")

        data_compression, data_compression_opts = _get_compression(data_compression)

        revert_fas = False
        if not self.future_array_shapes:
            # We force using future array shapes here to always write version 1.* files.
            # We capture the current state so that it can be reverted later if needed.
            revert_fas = True
            self.use_future_array_shapes()

        # write header and empty arrays to file
        with h5py.File(filename, "w") as f:
            # write header
            header = f.create_group("Header")
            self._write_header(header)

            # initialize the data groups on disk
            data_size = (self.Nblts, self.Nfreqs, self.Npols)
            dgrp = f.create_group("Data")
            if data_write_dtype is None:
                # we don't know what kind of data we'll get--default to double-precision
                data_write_dtype = "c16"
            if data_write_dtype not in ("c8", "c16"):
                # make sure the data type is correct
                _check_uvh5_dtype(data_write_dtype)
            dgrp.create_dataset(
                "visdata",
                data_size,
                chunks=chunks,
                dtype=data_write_dtype,
                compression=data_compression,
                compression_opts=data_compression_opts,
            )
            dgrp.create_dataset(
                "flags",
                data_size,
                chunks=chunks,
                dtype="b1",
                compression=flags_compression,
            )
            dgrp.create_dataset(
                "nsamples",
                data_size,
                chunks=chunks,
                dtype="f4",
                compression=nsample_compression,
            )

        if revert_fas:
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore",
                    message="This method will be removed in version 3.0 when the "
                    "current array shapes are no longer supported.",
                )
                self.use_current_array_shapes()

        return

    def _check_header(
        self, filename, run_check_acceptability=True, background_lsts=True
    ):
        """
        Check that the metadata in a file header matches the object's metadata.

        Parameters
        ----------
        header : h5py datagroup
            A reference to an h5py data group that contains the header
            information. For UVH5 files conforming to the spec, this should be
            "/Header".
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file.
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.

        Returns
        -------
        None

        Notes
        -----
        This function creates a new UVData object an reads in the header
        information saved on disk to compare with the object in memory. Note
        that this adds some small memory overhead, but this amount is typically
        much smaller than the size of the data.
        """
        uvd_file = UVH5()
        with h5py.File(filename, "r") as f:
            header = f["/Header"]
            uvd_file._read_header(
                header,
                run_check_acceptability=run_check_acceptability,
                background_lsts=background_lsts,
            )

        # temporarily remove data, flag, and nsample arrays, so we only check metadata
        if self.data_array is not None:
            data_array = self.data_array
            self.data_array = None
            replace_data = True
        else:
            replace_data = False
        if self.flag_array is not None:
            flag_array = self.flag_array
            self.flag_array = None
            replace_flags = True
        else:
            replace_flags = False
        if self.nsample_array is not None:
            nsample_array = self.nsample_array
            self.nsample_array = None
            replace_nsamples = True
        else:
            replace_nsamples = False

        # also ignore filename attribute
        uvd_file.filename = self.filename
        uvd_file._filename.form = self._filename.form

        if self != uvd_file:
            raise AssertionError(
                "The object metadata in memory and metadata on disk are different"
            )
        else:
            # clean up after ourselves
            if replace_data:
                self.data_array = data_array
            if replace_flags:
                self.flag_array = flag_array
            if replace_nsamples:
                self.nsample_array = nsample_array
            del uvd_file
        return

    def write_uvh5_part(
        self,
        filename,
        data_array,
        flag_array,
        nsample_array,
        check_header=True,
        antenna_nums=None,
        antenna_names=None,
        ant_str=None,
        bls=None,
        frequencies=None,
        freq_chans=None,
        times=None,
        time_range=None,
        lsts=None,
        lst_range=None,
        polarizations=None,
        blt_inds=None,
        phase_center_ids=None,
        catalog_names=None,
        run_check_acceptability=True,
        add_to_history=None,
    ):
        """
        Write out a part of a UVH5 file that has been previously initialized.

        Parameters
        ----------
        filename : str
            The file on disk to write data to. It must already exist,
            and is assumed to have been initialized with initialize_uvh5_file.
        data_array : array of float
            The data to write to disk. A check is done to ensure that
            the dimensions of the data passed in conform to the ones specified by
            the "selection" arguments.
        flag_array : array of bool
            The flags array to write to disk. A check is done to ensure
            that the dimensions of the data passed in conform to the ones specified
            by the "selection" arguments.
        nsample_array : array of float
            The nsample array to write to disk. A check is done to ensure
            that the dimensions fo the data passed in conform to the ones specified
            by the "selection" arguments.
        check_header : bool
            Option to check that the metadata present in the header
            on disk matches that in the object.
        run_check_acceptability : bool
            If check_header, additional option to check
            acceptable range of the values of parameters after reading in the file.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when writing data into
            the object (antenna positions and names for the excluded antennas
            will be retained). This cannot be provided if antenna_names is
            also provided.
        antenna_names : array_like of str, optional
            The antennas names to include when writing data into
            the object (antenna positions and names for the excluded antennas
            will be retained). This cannot be provided if antenna_nums is
            also provided.
        bls : list of tuples, optional
            A list of antenna number tuples (e.g. [(0, 1), (3, 2)]) or a list of
            baseline 3-tuples (e.g. [(0, 1, 'xx'), (2, 3, 'yy')]) specifying baselines
            to write to the file. For length-2 tuples, the ordering of the numbers
            within the tuple does not matter. For length-3 tuples, the polarization
            string is in the order of the two antennas. If length-3 tuples are provided,
            the polarizations argument below must be None.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when writing data into the object.
            Can be 'auto', 'cross', 'all', or combinations of antenna numbers
            and polarizations (e.g. '1', '1_2', '1x_2y').
            See tutorial for more examples of valid strings and
            the behavior of different forms for ant_str.
            If '1x_2y,2y_3y' is passed, both polarizations 'xy' and 'yy' will
            be written for both baselines (1, 2) and (2, 3) to reflect a valid
            pyuvdata object.
            An ant_str cannot be passed in addition to any of the above antenna
            args or the polarizations arg.
        frequencies : array_like of float, optional
            The frequencies to include when writing data to the file.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when writing data to the file.
        times : array_like of float, optional
            The times in Julian Day to include when writing data to the file.
        time_range : array_like of float, optional
            The time range in Julian Date to include when writing data to the
            file, must be length 2. Some of the times in the object should fall
            between the first and last elements. Cannot be used with `times`.
        lsts : array_like of float, optional
            The local sidereal times (LSTs) to keep in the object, each value
            passed here should exist in the lst_array. Cannot be used with
            `times`, `time_range`, or `lst_range`.
        lst_range : array_like of float, optional
            The local sidereal time (LST) range in radians to keep in the
            object, must be of length 2. Some of the LSTs in the object should
            fall between the first and last elements. If the second value is
            smaller than the first, the LSTs are treated as having phase-wrapped
            around LST = 2*pi = 0, and the LSTs kept on the object will run from
            the larger value, through 0, and end at the smaller value.
        polarizations : array_like of int, optional
            The polarizations to include when writing data to the file.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when writing data to the file.
            This is not commonly used.
        phase_center_ids : array_like of int, optional
            Phase center IDs to include when reading data into the object (effectively
            a selection on baseline-times). Cannot be used with catalog_names.
        catalog_names : str or array-like of str
            The names of the phase centers (sources) to include when reading data into
            the object, which should match exactly in spelling and capitalization.
            Cannot be used with phase_center_ids.
        add_to_history : str
            String to append to history before write out. Default is no appending.

        Returns
        -------
        None

        Raises
        ------
        AssertionError
            An AsserionError is raised if: (1) the location specified by
            `filename` does not exist; (2) the data_array, flag_array, and
            nsample_array do not all have the same shape; (3) the shape of the
            data arrays do not correspond to the sizes specified by the
            properties to write out.

        Notes
        -----
        When partially writing out data, this function should be called after
        calling initialize_uvh5_file. The same filename is passed in, with an
        optional check to ensure that the object's metadata in-memory matches
        the header on-disk. See the tutorial for a worked example.
        """
        # check that the file already exists
        if not os.path.exists(filename):
            raise AssertionError(
                "{0} does not exists; please first initialize it with "
                "initialize_uvh5_file".format(filename)
            )

        revert_fas = False
        if not self.future_array_shapes:
            # We force using future array shapes here to always write version 1.* files.
            # We capture the current state so that it can be reverted later if needed.
            revert_fas = True
            self.use_future_array_shapes()

        if check_header:
            self._check_header(
                filename, run_check_acceptability=run_check_acceptability
            )

        # figure out which "full file" indices to write data to
        blt_inds, freq_inds, pol_inds, _ = self._select_preprocess(
            antenna_nums,
            antenna_names,
            ant_str,
            bls,
            frequencies,
            freq_chans,
            times,
            time_range,
            lsts,
            lst_range,
            polarizations,
            blt_inds,
            phase_center_ids,
            catalog_names,
        )

        # make sure that the dimensions of the data to write are correct
        if data_array.shape != flag_array.shape:
            raise AssertionError("data_array and flag_array must have the same shape")
        if data_array.shape != nsample_array.shape:
            raise AssertionError(
                "data_array and nsample_array must have the same shape"
            )

        # check what part of each dimension to grab
        # we can use numpy slice objects to index the h5py indices
        if blt_inds is not None:
            Nblts = len(blt_inds)

            # test if blts are regularly spaced
            if len(set(np.ediff1d(blt_inds))) <= 1:
                blt_reg_spaced = True
                blt_start = blt_inds[0]
                blt_end = blt_inds[-1] + 1
                if len(blt_inds) == 1:
                    d_blt = 1
                else:
                    d_blt = blt_inds[1] - blt_inds[0]
                blt_inds = np.s_[blt_start:blt_end:d_blt]
            else:
                blt_reg_spaced = False
        else:
            Nblts = self.Nblts
            blt_reg_spaced = True
            blt_inds = np.s_[:]
        if freq_inds is not None:
            Nfreqs = len(freq_inds)

            # test if frequencies are regularly spaced
            if len(set(np.ediff1d(freq_inds))) <= 1:
                freq_reg_spaced = True
                freq_start = freq_inds[0]
                freq_end = freq_inds[-1] + 1
                if len(freq_inds) == 1:
                    d_freq = 1
                else:
                    d_freq = freq_inds[1] - freq_inds[0]
                freq_inds = np.s_[freq_start:freq_end:d_freq]
            else:
                freq_reg_spaced = False
        else:
            Nfreqs = self.Nfreqs
            freq_reg_spaced = True
            freq_inds = np.s_[:]
        if pol_inds is not None:
            Npols = len(pol_inds)

            # test if pols are regularly spaced
            if len(set(np.ediff1d(pol_inds))) <= 1:
                pol_reg_spaced = True
                pol_start = pol_inds[0]
                pol_end = pol_inds[-1] + 1
                if len(pol_inds) == 1:
                    d_pol = 1
                else:
                    d_pol = pol_inds[1] - pol_inds[0]
                pol_inds = np.s_[pol_start:pol_end:d_pol]
            else:
                pol_reg_spaced = False
        else:
            Npols = self.Npols
            pol_reg_spaced = True
            pol_inds = np.s_[:]

        # check for proper size of input arrays
        proper_shape = (Nblts, Nfreqs, Npols)
        if data_array.shape != proper_shape:
            if revert_fas and data_array.shape == (Nblts, 1, Nfreqs, Npols):
                data_array = data_array[:, 0, :, :]
                flag_array = flag_array[:, 0, :, :]
                nsample_array = nsample_array[:, 0, :, :]
            else:
                raise AssertionError(
                    "data_array has shape {0}; was expecting {1}".format(
                        data_array.shape, proper_shape
                    )
                )

        # actually write the data
        with h5py.File(filename, "r+") as f:
            dgrp = f["/Data"]
            visdata_dset = dgrp["visdata"]
            flags_dset = dgrp["flags"]
            nsamples_dset = dgrp["nsamples"]
            visdata_dtype = visdata_dset.dtype
            if visdata_dtype not in ("complex64", "complex128"):
                custom_dtype = True
            else:
                custom_dtype = False

            # check if we can do fancy indexing
            # as long as at least 2 out of 3 axes can be written as slices,
            # we can be fancy
            n_reg_spaced = np.count_nonzero(
                [blt_reg_spaced, freq_reg_spaced, pol_reg_spaced]
            )
            if n_reg_spaced >= 2:
                if custom_dtype:
                    indices = (blt_inds, freq_inds, pol_inds)
                    _write_complex_astype(data_array, visdata_dset, indices)
                else:
                    visdata_dset[blt_inds, freq_inds, pol_inds] = data_array
                flags_dset[blt_inds, freq_inds, pol_inds] = flag_array
                nsamples_dset[blt_inds, freq_inds, pol_inds] = nsample_array
            elif n_reg_spaced == 1:
                # figure out which axis is regularly spaced
                if blt_reg_spaced:
                    for ifreq, freq_idx in enumerate(freq_inds):
                        for ipol, pol_idx in enumerate(pol_inds):
                            if custom_dtype:
                                indices = (blt_inds, freq_idx, pol_idx)
                                _write_complex_astype(
                                    data_array[:, ifreq, ipol], visdata_dset, indices
                                )
                            else:
                                visdata_dset[blt_inds, freq_idx, pol_idx] = data_array[
                                    :, ifreq, ipol
                                ]
                            flags_dset[blt_inds, freq_idx, pol_idx] = flag_array[
                                :, ifreq, ipol
                            ]
                            nsamples_dset[blt_inds, freq_idx, pol_idx] = nsample_array[
                                :, ifreq, ipol
                            ]
                elif freq_reg_spaced:
                    for iblt, blt_idx in enumerate(blt_inds):
                        for ipol, pol_idx in enumerate(pol_inds):
                            if custom_dtype:
                                indices = (blt_idx, freq_inds, pol_idx)
                                _write_complex_astype(
                                    data_array[iblt, :, ipol], visdata_dset, indices
                                )
                            else:
                                visdata_dset[blt_idx, freq_inds, pol_idx] = data_array[
                                    iblt, :, ipol
                                ]
                            flags_dset[blt_idx, freq_inds, pol_idx] = flag_array[
                                iblt, :, ipol
                            ]
                            nsamples_dset[blt_idx, freq_inds, pol_idx] = nsample_array[
                                iblt, :, ipol
                            ]
                else:  # pol_reg_spaced
                    for iblt, blt_idx in enumerate(blt_inds):
                        for ifreq, freq_idx in enumerate(freq_inds):
                            if custom_dtype:
                                indices = (blt_idx, freq_idx, pol_inds)
                                _write_complex_astype(
                                    data_array[iblt, ifreq, :], visdata_dset, indices
                                )
                            else:
                                visdata_dset[blt_idx, freq_idx, pol_inds] = data_array[
                                    iblt, ifreq, :
                                ]
                            flags_dset[blt_idx, freq_idx, pol_inds] = flag_array[
                                iblt, ifreq, :
                            ]
                            nsamples_dset[blt_idx, freq_idx, pol_inds] = nsample_array[
                                iblt, ifreq, :
                            ]
            else:
                # all axes irregularly spaced
                # perform a triple loop -- probably very slow!
                for iblt, blt_idx in enumerate(blt_inds):
                    for ifreq, freq_idx in enumerate(freq_inds):
                        for ipol, pol_idx in enumerate(pol_inds):
                            if custom_dtype:
                                indices = (blt_idx, freq_idx, pol_idx)
                                _write_complex_astype(
                                    data_array[iblt, ifreq, ipol], visdata_dset, indices
                                )
                            else:
                                visdata_dset[blt_idx, freq_idx, pol_idx] = data_array[
                                    iblt, ifreq, ipol
                                ]
                            flags_dset[blt_idx, freq_idx, pol_idx] = flag_array[
                                iblt, ifreq, ipol
                            ]
                            nsamples_dset[blt_idx, freq_idx, pol_idx] = nsample_array[
                                iblt, ifreq, ipol
                            ]

            # append to history if desired
            if add_to_history is not None:
                history = np.string_(self.history) + np.string_(add_to_history)
                if "history" in f["Header"]:
                    # erase dataset first b/c it has fixed-length string datatype
                    del f["Header"]["history"]
                f["Header"]["history"] = np.string_(history)

        if revert_fas:
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore",
                    message="This method will be removed in version 3.0 when the "
                    "current array shapes are no longer supported.",
                )
                self.use_current_array_shapes()

        return


# pyuvdata/uvflag/__init__.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Init file for UVFlag."""
from .uvflag import *  # noqa


# pyuvdata/uvflag/uvflag.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Primary container for radio interferometer flag manipulation."""
import copy
import os
import pathlib
import threading
import warnings

import h5py
import numpy as np

from .. import parameter as uvp
from .. import telescopes as uvtel
from .. import utils as uvutils
from ..uvbase import UVBase
from ..uvcal import UVCal
from ..uvdata import UVData

__all__ = ["UVFlag", "flags2waterfall", "and_rows_cols", "lst_from_uv"]


_future_array_shapes_warning = (
    "The shapes of several attributes will be changing in the future to remove the "
    "deprecated spectral window axis. You can call the `use_future_array_shapes` "
    "method to convert to the future array shapes now or set the parameter of the same "
    "name on this method to both convert to the future array shapes and silence this "
    "warning."
)


def and_rows_cols(waterfall):
    """Perform logical and over rows and cols of a waterfall.

    For a 2D flag waterfall, flag pixels only if fully flagged along
    time and/or frequency

    Parameters
    ----------
    waterfall : 2D boolean array of shape (Ntimes, Nfreqs)

    Returns
    -------
    wf : 2D array
        A 2D array (size same as input) where only times/integrations
        that were fully flagged are flagged.

    """
    wf = np.zeros_like(waterfall, dtype=np.bool_)
    Ntimes, Nfreqs = waterfall.shape
    wf[:, (np.sum(waterfall, axis=0) / Ntimes) == 1] = True
    wf[(np.sum(waterfall, axis=1) / Nfreqs) == 1] = True
    return wf


def lst_from_uv(uv):
    """Calculate the lst_array for a UVData or UVCal object.

    Parameters
    ----------
    uv : a UVData or UVCal object.
        Object from which lsts are calculated

    Returns
    -------
    lst_array: array of float
        lst_array corresponding to time_array and at telescope location.
        Units are radian.

    """
    warnings.warn(
        "The lst_from_uv function is deprecated. Use the `set_lsts_from_time_array` "
        "method on the input object. This will become an error in version 2.4.",
        category=DeprecationWarning,
    )

    if not isinstance(uv, (UVCal, UVData)):
        raise ValueError(
            "Function lst_from_uv can only operate on UVCal or UVData object."
        )

    uv.set_lsts_from_time_array()
    return copy.deepcopy(uv.lst_array)


def flags2waterfall(uv, flag_array=None, keep_pol=False):
    """Convert a flag array to a 2D waterfall of dimensions (Ntimes, Nfreqs).

    Averages over baselines and polarizations (in the case of visibility data),
    or antennas and jones parameters (in case of calibrationd data).

    Parameters
    ----------
    uv : A UVData or UVCal object
        Object defines the times and frequencies, and supplies the
        flag_array to convert (if flag_array not specified)
    flag_array :  Optional,
        flag array to convert instead of uv.flag_array.
        Must have same dimensions as uv.flag_array.
    keep_pol : bool
        Option to keep the polarization axis intact.

    Returns
    -------
    waterfall : 2D array or 3D array
        Waterfall of averaged flags, for example fraction of baselines
        which are flagged for every time and frequency (in case of UVData input)
        Size is (Ntimes, Nfreqs) or (Ntimes, Nfreqs, Npols).

    """
    if not isinstance(uv, (UVData, UVCal)):
        raise ValueError(
            "flags2waterfall() requires a UVData or UVCal object as "
            "the first argument."
        )
    if flag_array is None:
        flag_array = uv.flag_array
    if uv.flag_array.shape != flag_array.shape:
        raise ValueError("Flag array must align with UVData or UVCal object.")

    if isinstance(uv, UVCal):
        mean_axis = [0]
        if not uv.future_array_shapes:
            mean_axis.append(1)
        if not keep_pol:
            if uv.future_array_shapes:
                mean_axis.append(3)
            else:
                mean_axis.append(4)

        mean_axis = tuple(mean_axis)
        if keep_pol:
            waterfall = np.swapaxes(np.mean(flag_array, axis=mean_axis), 0, 1)
        else:
            waterfall = np.mean(flag_array, axis=mean_axis).T
    else:
        mean_axis = [0]
        if not uv.future_array_shapes:
            mean_axis.append(1)
        if not keep_pol:
            if uv.future_array_shapes:
                mean_axis.append(2)
            else:
                mean_axis.append(3)

        mean_axis = tuple(mean_axis)
        if keep_pol:
            waterfall = np.zeros((uv.Ntimes, uv.Nfreqs, uv.Npols))
            for i, t in enumerate(np.unique(uv.time_array)):
                waterfall[i, :] = np.mean(
                    flag_array[uv.time_array == t], axis=mean_axis
                )
        else:
            waterfall = np.zeros((uv.Ntimes, uv.Nfreqs))
            for i, t in enumerate(np.unique(uv.time_array)):
                waterfall[i, :] = np.mean(
                    flag_array[uv.time_array == t], axis=mean_axis
                )

    return waterfall


class UVFlag(UVBase):
    """Object to handle flag arrays and waterfalls for interferometric datasets.

    Supports reading/writing, and stores all relevant information to combine
    flags and apply to data.
    Initialization of the UVFlag object requires some parameters. Metadata is
    copied from indata object. If indata is subclass of UVData or UVCal,
    the weights_array will be set to all ones.
    Lists or tuples are iterated through, treating each entry with an
    individual UVFlag init.

    Parameters
    ----------
    indata : UVData, UVCal, str, pathlib.Path, list of compatible combination
        Input to initialize UVFlag object. If str, assumed to be path to previously
        saved UVFlag object. UVData and UVCal objects cannot be directly combined,
        unless waterfall is True.
    mode : {"metric", "flag"}, optional
        The mode determines whether the object has a floating point metric_array
        or a boolean flag_array.
    copy_flags : bool, optional
        Whether to copy flags from indata to new UVFlag object
    waterfall : bool, optional
        Whether to immediately initialize as a waterfall object, with flag/metric
        axes: time, frequency, polarization.
    history : str, optional
        History string to attach to object.
    extra_keywords : dict, optional
        A dictionary of metadata values not explicitly specified by another
        parameter.
    label: str, optional
        String used for labeling the object (e.g. 'FM').
    use_future_array_shapes : bool
        Option to convert to the future planned array shapes before the changes go
        into effect by removing the spectral window axis.
    run_check : bool
        Option to check for the existence and proper shapes of parameters
        after creating UVFlag object.
    check_extra : bool
        Option to check optional parameters as well as required ones (the
        default is True, meaning the optional parameters will be checked).
    run_check_acceptability : bool
        Option to check acceptable range of the values of parameters after
        creating UVFlag object.

    Attributes
    ----------
    UVParameter objects :
        For full list see the UVFlag Parameters Documentation.
        (https://pyuvdata.readthedocs.io/en/latest/uvflag_parameters.html)
        Some are always required, some are required for certain phase_types
        and others are always optional.


    """

    def __init__(
        self,
        indata=None,
        mode="metric",
        copy_flags=False,
        waterfall=False,
        history="",
        label="",
        use_future_array_shapes=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Initialize the object."""
        # standard angle tolerance: 10 mas in radians.
        # Should perhaps be decreased to 1 mas in the future
        radian_tol = 10 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0)

        desc = (
            "The mode determines whether the object has a "
            "floating point metric_array or a boolean flag_array. "
            'Options: {"metric", "flag"}. Default is "metric".'
        )
        self._mode = uvp.UVParameter(
            "mode",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=["metric", "flag"],
        )

        desc = (
            "String used for labeling the object (e.g. 'FM'). "
            "Default is empty string."
        )
        self._label = uvp.UVParameter(
            "label", description=desc, form="str", expected_type=str
        )

        desc = (
            "The type of object defines the form of some arrays "
            "and also how metrics/flags are combined. "
            "Accepted types:'waterfall', 'baseline', 'antenna'"
        )
        self._type = uvp.UVParameter(
            "type",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=["antenna", "baseline", "waterfall"],
        )

        self._Ntimes = uvp.UVParameter(
            "Ntimes", description="Number of times", expected_type=int
        )
        desc = "Number of baselines. Only Required for 'baseline' type objects."
        self._Nbls = uvp.UVParameter(
            "Nbls", description=desc, expected_type=int, required=False
        )
        self._Nblts = uvp.UVParameter(
            "Nblts",
            description="Number of baseline-times "
            "(i.e. number of spectra). Not necessarily "
            "equal to Nbls * Ntimes",
            expected_type=int,
        )
        self._Nspws = uvp.UVParameter(
            "Nspws",
            description="Number of spectral windows "
            "(ie non-contiguous spectral chunks).",
            expected_type=int,
            required=False,  # will be required starting in version 2.4
        )
        self._Nfreqs = uvp.UVParameter(
            "Nfreqs", description="Number of frequency channels", expected_type=int
        )
        self._Npols = uvp.UVParameter(
            "Npols", description="Number of polarizations", expected_type=int
        )

        desc = (
            "Floating point metric information, only available in metric mode. "
            "The shape depends on the `type` parameter and on the "
            "`future_array_shapes` parameter. For 'baseline' type objects, "
            "the shape is (Nblts, 1, Nfreq, Npols) or (Nblts, Nfreqs, Npols) if "
            "future_array_shapes=True. For 'antenna' type objects, the shape is "
            "(Nants_data, 1, Nfreqs, Ntimes, Npols) or "
            "(Nants_data, Nfreqs, Ntimes, Npols) if future_array_shapes=True. "
            "For 'waterfall' type objects, the shape is (Nfreq, Ntimes, Npols)."
        )
        self._metric_array = uvp.UVParameter(
            "metric_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=float,
            required=False,
        )

        desc = (
            "Boolean flag, True is flagged, only available in flag mode. "
            "The shape depends on the `type` parameter and on the "
            "`future_array_shapes` parameter. For 'baseline' type objects, "
            "the shape is (Nblts, 1, Nfreq, Npols) or (Nblts, Nfreqs, Npols) if "
            "future_array_shapes=True. For 'antenna' type objects, the shape is "
            "(Nants_data, 1, Nfreqs, Ntimes, Npols) or "
            "(Nants_data, Nfreqs, Ntimes, Npols) if future_array_shapes=True. "
            "For 'waterfall' type objects, the shape is (Nfreq, Ntimes, Npols)."
        )
        self._flag_array = uvp.UVParameter(
            "flag_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=bool,
            required=False,
        )

        desc = (
            "Floating point weight information, only available in metric mode."
            "The shape depends on the `type` parameter and on the "
            "`future_array_shapes` parameter. For 'baseline' type objects, "
            "the shape is (Nblts, 1, Nfreq, Npols) or (Nblts, Nfreqs, Npols) if "
            "future_array_shapes=True. For 'antenna' type objects, the shape is "
            "(Nants_data, 1, Nfreqs, Ntimes, Npols) or "
            "(Nants_data, Nfreqs, Ntimes, Npols) if future_array_shapes=True. "
            "For 'waterfall' type objects, the shape is (Nfreq, Ntimes, Npols)."
        )
        self._weights_array = uvp.UVParameter(
            "weights_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=float,
        )

        desc = (
            "Floating point weight information about sum of squares of weights "
            "when weighted data is converted from baseline to waterfall type."
            "Only available in metric mode, the shape is (Nfreq, Ntimes, Npols)."
        )
        # TODO: should this be set to None when converting back to baseline or antenna?
        # If not, should the shape be adjusted?
        self._weights_square_array = uvp.UVParameter(
            "weights_square_array",
            description=desc,
            form=("Ntimes", "Nfreqs", "Npols"),
            expected_type=float,
            required=False,
        )

        desc = (
            "Array of times in Julian Date, center of integration. The shape depends "
            "on the `type` parameter. For 'baseline' type object, shape is (Nblts), "
            "for 'antenna' and 'waterfall' type objects, shape is (Ntimes)."
        )
        self._time_array = uvp.UVParameter(
            "time_array",
            description=desc,
            form=("Nblts",),
            expected_type=float,
            tols=1e-3 / (60.0 * 60.0 * 24.0),
        )  # 1 ms in days

        desc = (
            "Array of lsts radians, center of integration. The shape depends "
            "on the `type` parameter. For 'baseline' type object, shape is (Nblts), "
            "for 'antenna' and 'waterfall' type objects, shape is (Ntimes)."
        )
        self._lst_array = uvp.UVParameter(
            "lst_array",
            description=desc,
            form=("Nblts",),
            expected_type=float,
            tols=radian_tol,
        )

        desc = (
            "Array of first antenna numbers, shape (Nblts). Only available for "
            "'baseline' type objects."
        )
        self._ant_1_array = uvp.UVParameter(
            "ant_1_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            required=False,
        )
        desc = (
            "Array of second antenna numbers, shape (Nblts). Only available for "
            "'baseline' type objects."
        )
        self._ant_2_array = uvp.UVParameter(
            "ant_2_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            required=False,
        )

        desc = (
            "Array of antenna numbers, shape (Nants_data), only available for "
            "'antenna' type objects. "
        )
        self._ant_array = uvp.UVParameter(
            "ant_array",
            description=desc,
            expected_type=int,
            form=("Nants_data",),
            required=False,
        )

        desc = (
            "Array of baseline indices, shape (Nblts). "
            "Only available for 'baseline' type objects. "
            "type = int; baseline = 2048 * ant1 + ant2 + 2^16"
        )
        self._baseline_array = uvp.UVParameter(
            "baseline_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            required=False,
        )

        desc = (
            "Array of frequencies in Hz, center of the channel. Shape (1, Nfreqs) or "
            "(Nfreqs,) if type is 'waterfall' or if future_array_shapes=True."
        )
        self._freq_array = uvp.UVParameter(
            "freq_array",
            description=desc,
            form=(1, "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )  # mHz

        desc = "Width of frequency channels (Hz). Shape (Nfreqs,), type = float."
        self._channel_width = uvp.UVParameter(
            "channel_width",
            description=desc,
            form=("Nfreqs",),
            expected_type=float,
            tols=1e-3,
            required=False,  # will be required starting in version 2.4
        )  # 1 mHz

        self._spw_array = uvp.UVParameter(
            "spw_array",
            description="Array of spectral window numbers, shape (Nspws).",
            form=("Nspws",),
            expected_type=int,
            required=False,  # will be required starting in version 2.4
        )

        desc = (
            "Required if Nspws > 1 and will always be required starting in "
            "version 3.0. Maps individual channels along the "
            "frequency axis to individual spectral windows, as listed in the "
            "spw_array. Shape (Nfreqs), type = int."
        )
        self._flex_spw_id_array = uvp.UVParameter(
            "flex_spw_id_array",
            description=desc,
            form=("Nfreqs",),
            expected_type=int,
            required=False,
        )

        desc = (
            "Array of polarization integers, shape (Npols). "
            "AIPS Memo 117 says: pseudo-stokes 1:4 (pI, pQ, pU, pV);  "
            "circular -1:-4 (RR, LL, RL, LR); linear -5:-8 (XX, YY, XY, YX). "
            "NOTE: AIPS Memo 117 actually calls the pseudo-Stokes polarizations "
            '"Stokes", but this is inaccurate as visibilities cannot be in '
            "true Stokes polarizations for physical antennas. We adopt the "
            "term pseudo-Stokes to refer to linear combinations of instrumental "
            "visibility polarizations (e.g. pI = xx + yy)."
        )
        self._polarization_array = uvp.UVParameter(
            "polarization_array",
            description=desc,
            expected_type=int,
            acceptable_vals=list(np.arange(-8, 0)) + list(np.arange(1, 5)),
            form=("Npols",),
        )

        self._telescope_name = uvp.UVParameter(
            "telescope_name",
            description="Name of telescope or array (string).",
            form="str",
            expected_type=str,
            required=False,  # will be required starting in version 2.4
        )

        self._telescope_location = uvp.LocationParameter(
            "telescope_location",
            description=desc,
            acceptable_range=(6.35e6, 6.39e6),
            tols=1e-3,
            required=False,  # will be required starting in version 2.4
        )

        self._history = uvp.UVParameter(
            "history",
            description="String of history, units English",
            form="str",
            expected_type=str,
        )

        desc = (
            "Any user supplied extra keywords, type=dict."
            "Use the special key 'comment' for long multi-line string comments."
            "Default is an empty dictionary."
        )
        self._extra_keywords = uvp.UVParameter(
            "extra_keywords",
            required=False,
            description=desc,
            value={},
            spoof_val={},
            expected_type=dict,
        )

        desc = "Flag indicating that this object is using the future array shapes."
        self._future_array_shapes = uvp.UVParameter(
            "future_array_shapes", description=desc, expected_type=bool, value=False
        )

        # ---antenna information ---
        desc = (
            "Number of antennas in the array.  Only available for 'baseline' type "
            "objects, used for calculating baseline numbers. "
            "May be larger than the number of antennas with data."
        )
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", description=desc, expected_type=int, required=False
        )
        desc = (
            "Number of antennas with data present. "
            "Only available for 'baseline' or 'antenna' type objects."
            "May be smaller than the number of antennas in the array"
        )
        self._Nants_data = uvp.UVParameter(
            "Nants_data", description=desc, expected_type=int, required=False
        )

        desc = (
            "List of antenna names, shape (Nants_telescope), with numbers given by "
            "antenna_numbers (which can be matched to ant_1_array and ant_2_array for "
            "baseline type or ant_array for antenna type objects). Required for "
            "baseline or antenna type objects. There must be one entry here for each "
            "unique entry in ant_1_array and ant_2_array (for baseline type) or "
            "ant_array (for antenna type), but there may be extras as well. "
        )
        self._antenna_names = uvp.UVParameter(
            "antenna_names",
            description=desc,
            form=("Nants_telescope",),
            expected_type=str,
            required=False,  # will be required starting in version 2.4
        )

        desc = (
            "List of integer antenna numbers corresponding to antenna_names, "
            "shape (Nants_telescope). Required for baseline or antenna type objects. "
            "There must be one entry here for each unique entry in ant_1_array and "
            "ant_2_array (for baseline type) or ant_array (for antenna type), but "
            "there may be extras as well. Note that these are not indices -- they do "
            "not need to start at zero or be continuous."
        )
        self._antenna_numbers = uvp.UVParameter(
            "antenna_numbers",
            description=desc,
            form=("Nants_telescope",),
            expected_type=int,
            required=False,  # will be required starting in version 2.4
        )

        desc = (
            "Array giving coordinates of antennas relative to "
            "telescope_location (ITRF frame), shape (Nants_telescope, 3), "
            "units meters. See the tutorial page in the documentation "
            "for an example of how to convert this to topocentric frame."
        )
        self._antenna_positions = uvp.UVParameter(
            "antenna_positions",
            description=desc,
            form=("Nants_telescope", 3),
            expected_type=float,
            tols=1e-3,  # 1 mm
            required=False,  # will be required starting in version 2.4
        )

        #  --extra information ---
        desc = (
            "Orientation of the physical dipole corresponding to what is "
            'labelled as the x polarization. Options are "east" '
            '(indicating east/west orientation) and "north" (indicating '
            "north/south orientation)"
        )
        self._x_orientation = uvp.UVParameter(
            "x_orientation",
            description=desc,
            required=False,
            expected_type=str,
            acceptable_vals=["east", "north"],
        )

        desc = (
            "List of strings containing the unique basenames (not the full path) of "
            "input files."
        )
        self._filename = uvp.UVParameter(
            "filename", required=False, description=desc, expected_type=str
        )

        # initialize the underlying UVBase properties
        super(UVFlag, self).__init__()

        self.history = ""  # Added to at the end

        self.label = ""  # Added to at the end
        if isinstance(indata, (list, tuple)):
            self.__init__(
                indata[0],
                mode=mode,
                copy_flags=copy_flags,
                waterfall=waterfall,
                history=history,
                label=label,
                use_future_array_shapes=use_future_array_shapes,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
            if len(indata) > 1:
                for i in indata[1:]:
                    fobj = UVFlag(
                        i,
                        mode=mode,
                        copy_flags=copy_flags,
                        waterfall=waterfall,
                        history=history,
                        use_future_array_shapes=use_future_array_shapes,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                    )
                    self.__add__(
                        fobj,
                        run_check=run_check,
                        inplace=True,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                    )
                del fobj

        elif issubclass(indata.__class__, (str, pathlib.Path)):
            # Given a path, read indata
            self.read(
                indata,
                history,
                use_future_array_shapes=use_future_array_shapes,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
        elif issubclass(indata.__class__, UVData):
            self.from_uvdata(
                indata,
                mode=mode,
                copy_flags=copy_flags,
                waterfall=waterfall,
                history=history,
                label=label,
                use_future_array_shapes=use_future_array_shapes,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )

        elif issubclass(indata.__class__, UVCal):
            self.from_uvcal(
                indata,
                mode=mode,
                copy_flags=copy_flags,
                waterfall=waterfall,
                history=history,
                label=label,
                use_future_array_shapes=use_future_array_shapes,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )

        elif indata is not None:
            raise ValueError(
                "input to UVFlag.__init__ must be one of: "
                "list, tuple, string, pathlib.Path, UVData, or UVCal."
            )

    @property
    def _data_params(self):
        """List of strings giving the data-like parameters."""
        if not hasattr(self, "mode") or self.mode is None:
            return None
        elif self.mode == "flag":
            return ["flag_array"]
        elif self.mode == "metric":
            if self.weights_square_array is None:
                return ["metric_array", "weights_array"]
            else:
                return ["metric_array", "weights_array", "weights_square_array"]
        else:
            raise ValueError(
                "Invalid mode. Mode must be one of "
                + ", ".join(["{}"] * len(self._mode.acceptable_vals)).format(
                    *self._mode.acceptable_vals
                )
            )

    @property
    def data_like_parameters(self):
        """Return iterator of defined parameters which are data-like."""
        for key in self._data_params:
            if hasattr(self, key):
                yield getattr(self, key)

    @property
    def pol_collapsed(self):
        """Determine if this object has had pols collapsed."""
        if not hasattr(self, "polarization_array") or self.polarization_array is None:
            return False
        elif isinstance(self.polarization_array.item(0), str):
            return True
        else:
            return False

    def _check_pol_state(self):
        if self.pol_collapsed:
            # collapsed pol objects have a different type for
            # the polarization array.
            self._polarization_array.expected_type = str
            self._polarization_array.acceptable_vals = None
        else:
            self._polarization_array.expected_type = uvp._get_generic_type(int)
            self._polarization_array.acceptable_vals = list(np.arange(-8, 0)) + list(
                np.arange(1, 5)
            )

    def _set_future_array_shapes(self):
        """
        Set future_array_shapes to True and adjust required parameters.

        This method should not be called directly by users; instead it is called
        by file-reading methods and `use_future_array_shapes` to indicate the
        `future_array_shapes` is True and define expected parameter shapes.

        """
        self.future_array_shapes = True
        self._freq_array.form = ("Nfreqs",)
        data_like_params = ["metric_array", "weights_array", "flag_array"]

        if self.type == "baseline":
            for param_name in data_like_params:
                getattr(self, "_" + param_name).form = ("Nblts", "Nfreqs", "Npols")
        elif self.type == "antenna":
            for param_name in data_like_params:
                getattr(self, "_" + param_name).form = (
                    "Nants_data",
                    "Nfreqs",
                    "Ntimes",
                    "Npols",
                )

    def use_future_array_shapes(self):
        """
        Change the array shapes of this object to match the planned future shapes.

        This method sets allows users to convert to the planned array shapes changes
        before the changes go into effect. This method sets the `future_array_shapes`
        parameter on this object to True.

        """
        if self.future_array_shapes:
            return

        self._set_future_array_shapes()
        if not self.type == "waterfall":
            # remove the length-1 spw axis for all data-like parameters
            for param_name in self._data_params:
                if param_name == "weights_square_array":
                    continue
                setattr(self, param_name, (getattr(self, param_name))[:, 0])

            # remove the length-1 spw axis for the freq_array
            self.freq_array = self.freq_array[0, :]

    def use_current_array_shapes(self):
        """
        Change the array shapes of this object to match the current future shapes.

        This method sets allows users to convert back to the current array shapes.
        This method sets the `future_array_shapes` parameter on this object to False.
        """
        warnings.warn(
            "This method will be removed in version 3.0 when the current array shapes "
            "are no longer supported.",
            DeprecationWarning,
        )

        if not self.future_array_shapes:
            return

        data_like_params = ["metric_array", "weights_array", "flag_array"]

        self.future_array_shapes = False
        if not self.type == "waterfall":
            if self.type == "baseline":
                for param_name in data_like_params:
                    getattr(self, "_" + param_name).form = (
                        "Nblts",
                        1,
                        "Nfreqs",
                        "Npols",
                    )
            elif self.type == "antenna":
                for param_name in data_like_params:
                    getattr(self, "_" + param_name).form = (
                        "Nants_data",
                        1,
                        "Nfreqs",
                        "Ntimes",
                        "Npols",
                    )

            for param_name in self._data_params:
                if param_name == "weights_square_array":
                    continue
                setattr(
                    self, param_name, (getattr(self, param_name))[:, np.newaxis, :, :]
                )

            self._freq_array.form = (1, "Nfreqs")
            self.freq_array = self.freq_array[np.newaxis, :]

    def _set_mode_flag(self):
        """Set the mode and required parameters consistent with a flag object."""
        self.mode = "flag"
        self._flag_array.required = True
        self._metric_array.required = False
        self._weights_array.required = False
        if self.weights_square_array is not None:
            self.weights_square_array = None

        return

    def _set_mode_metric(self):
        """Set the mode and required parameters consistent with a metric object."""
        self.mode = "metric"
        self._flag_array.required = False
        self._metric_array.required = True
        self._weights_array.required = True

        if self.weights_array is None and self.metric_array is not None:
            self.weights_array = np.ones_like(self.metric_array, dtype=float)

        return

    def _set_type_antenna(self):
        """Set the type and required propertis consistent with an antenna object."""
        self.type = "antenna"
        self._ant_array.required = True
        self._baseline_array.required = False
        self._ant_1_array.required = False
        self._ant_2_array.required = False
        self._Nants_telescope.required = False
        self._Nants_data.required = True
        self._Nbls.required = False
        self._Nblts.required = False
        self._Nspws.required = True  # this should be removed in version 2.4

        if self.future_array_shapes:
            self._metric_array.form = ("Nants_data", "Nfreqs", "Ntimes", "Npols")
            self._flag_array.form = ("Nants_data", "Nfreqs", "Ntimes", "Npols")
            self._weights_array.form = ("Nants_data", "Nfreqs", "Ntimes", "Npols")
        else:
            self._metric_array.form = ("Nants_data", 1, "Nfreqs", "Ntimes", "Npols")
            self._flag_array.form = ("Nants_data", 1, "Nfreqs", "Ntimes", "Npols")
            self._weights_array.form = ("Nants_data", 1, "Nfreqs", "Ntimes", "Npols")
            self._freq_array.form = (1, "Nfreqs")

        self._time_array.form = ("Ntimes",)
        self._lst_array.form = ("Ntimes",)

    def _set_type_baseline(self):
        """Set the type and required propertis consistent with a baseline object."""
        self.type = "baseline"
        self._ant_array.required = False
        self._baseline_array.required = True
        self._ant_1_array.required = True
        self._ant_2_array.required = True
        self._Nants_telescope.required = True
        self._Nants_data.required = True
        self._Nbls.required = True
        self._Nblts.required = True
        self._Nspws.required = True  # this should be removed in version 2.4

        if self.time_array is not None:
            self.Nblts = len(self.time_array)

        if self.future_array_shapes:
            self._metric_array.form = ("Nblts", "Nfreqs", "Npols")
            self._flag_array.form = ("Nblts", "Nfreqs", "Npols")
            self._weights_array.form = ("Nblts", "Nfreqs", "Npols")

        else:
            self._metric_array.form = ("Nblts", 1, "Nfreqs", "Npols")
            self._flag_array.form = ("Nblts", 1, "Nfreqs", "Npols")
            self._weights_array.form = ("Nblts", 1, "Nfreqs", "Npols")
            self._freq_array.form = (1, "Nfreqs")

        self._time_array.form = ("Nblts",)
        self._lst_array.form = ("Nblts",)

    def _set_type_waterfall(self):
        """Set the type and required propertis consistent with a waterfall object."""
        self.type = "waterfall"
        self._ant_array.required = False
        self._baseline_array.required = False
        self._ant_1_array.required = False
        self._ant_2_array.required = False
        self._Nants_telescope.required = False
        self._Nants_data.required = False
        self._Nbls.required = False
        self._Nblts.required = False
        self._Nspws.required = False  # this should be removed in version 2.4

        self._metric_array.form = ("Ntimes", "Nfreqs", "Npols")
        self._flag_array.form = ("Ntimes", "Nfreqs", "Npols")
        self._weights_array.form = ("Ntimes", "Nfreqs", "Npols")

        self._time_array.form = ("Ntimes",)
        self._lst_array.form = ("Ntimes",)

        if not self.future_array_shapes:
            self._freq_array.form = ("Nfreqs",)

    def check(self, check_extra=True, run_check_acceptability=True):
        """
        Add some extra checks on top of checks on UVBase class.

        Check that required parameters exist. Check that parameters have
        appropriate shapes and optionally that the values are acceptable.

        Parameters
        ----------
        check_extra : bool
            If true, check all parameters, otherwise only check required parameters.
        run_check_acceptability : bool
            Option to check if values in parameters are acceptable.

        Returns
        -------
        bool
            True if check passes

        Raises
        ------
        ValueError
            if parameter shapes or types are wrong or do not have acceptable
            values (if run_check_acceptability is True)

        """
        # set the flex_spw_id_array to required if Nspws > 1
        if self.Nspws is not None and self.Nspws > 1:
            self._flex_spw_id_array.required = True
        else:
            self._flex_spw_id_array.required = False

        # Issue a deprecation warnings for parameters that are not set but will be
        # required in v2.4
        params_to_warn = [
            "telescope_name",
            "telescope_location",
            "channel_width",
            "spw_array",
            "Nspws",
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
        ]
        for param in params_to_warn:
            if getattr(self, param) is None:
                warnings.warn(
                    f"The {param} is not set. It will be a required "
                    "parameter starting in pyuvdata version 2.4",
                    category=DeprecationWarning,
                )

        # first run the basic check from UVBase
        super().check(check_extra, run_check_acceptability)

        # Check internal consistency of numbers which don't explicitly correspond
        # to the shape of another array.
        if self.type == "baseline":
            if self.Nants_data != int(
                np.union1d(self.ant_1_array, self.ant_2_array).size
            ):
                raise ValueError(
                    "Nants_data must be equal to the number of unique "
                    "values in ant_1_array and ant_2_array"
                )

            if self.Nbls != len(np.unique(self.baseline_array)):
                raise ValueError(
                    "Nbls must be equal to the number of unique "
                    "baselines in the baseline_array"
                )

            if self.Ntimes != len(np.unique(self.time_array)):
                raise ValueError(
                    "Ntimes must be equal to the number of unique "
                    "times in the time_array"
                )

            if self.antenna_numbers is not None:
                if not set(np.unique(self.ant_1_array)).issubset(self.antenna_numbers):
                    raise ValueError(
                        "All antennas in ant_1_array must be in antenna_numbers."
                    )
                if not set(np.unique(self.ant_2_array)).issubset(self.antenna_numbers):
                    raise ValueError(
                        "All antennas in ant_2_array must be in antenna_numbers."
                    )
        elif self.type == "antenna":
            if self.antenna_numbers is not None:
                if not all(ant in self.antenna_numbers for ant in self.ant_array):
                    raise ValueError(
                        "All antennas in ant_array must be in antenna_numbers."
                    )

        if self.flex_spw_id_array is None:
            warnings.warn(
                "flex_spw_id_array is not set. It will be required starting in version "
                "3.0",
                DeprecationWarning,
            )
        else:
            # Check that all values in flex_spw_id_array are entries in the spw_array
            if not np.all(np.isin(self.flex_spw_id_array, self.spw_array)):
                raise ValueError(
                    "All values in the flex_spw_id_array must exist in the spw_array."
                )
        return True

    def clear_unused_attributes(self):
        """Remove unused attributes.

        Useful when changing type or mode or to save memory.
        Will set all non-required attributes to None, except x_orientation,
        extra_keywords, weights_square_array and filename.

        """
        optional_attrs_to_keep = [
            "telescope_name",
            "telescope_location",
            "channel_width",
            "spw_array",
            "Nspws",
            "flex_spw_id_array",  # TODO remove this from this list in version 3.0
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
            "Nants_telescope",
            "x_orientation",
            "weights_square_array",
            "extra_keywords",
            "filename",
        ]
        for p in self:
            attr = getattr(self, p)
            if (
                not attr.required
                and attr.value is not None
                and attr.name not in optional_attrs_to_keep
            ):
                attr.value = None
                setattr(self, p, attr)

    def __eq__(self, other, check_history=True, check_extra=True):
        """Check Equality of two UVFlag objects.

        Parameters
        ----------
        other: UVFlag
            object to check against
        check_history : bool
            Include the history keyword when comparing UVFlag objects.
        check_extra : bool
            Include non-required parameters when comparing UVFlag objects.

        """
        if check_history:
            return super(UVFlag, self).__eq__(other, check_extra=check_extra)

        else:
            # initial check that the classes are the same
            # then strip the histories
            if isinstance(other, self.__class__):
                _h1 = self.history
                self.history = None

                _h2 = other.history
                other.history = None

                truth = super(UVFlag, self).__eq__(other, check_extra=check_extra)

                self.history = _h1
                other.history = _h2

                return truth
            else:
                print("Classes do not match")
                return False

    def __ne__(self, other, check_history=True, check_extra=True):
        """Not Equal."""
        return not self.__eq__(
            other, check_history=check_history, check_extra=check_extra
        )

    def _set_lsts_helper(self):
        latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees
        unique_times, inverse_inds = np.unique(self.time_array, return_inverse=True)
        unique_lst_array = uvutils.get_lst_for_time(
            unique_times, latitude, longitude, altitude
        )
        self.lst_array = unique_lst_array[inverse_inds]
        return

    def set_lsts_from_time_array(self, background=False):
        """Set the lst_array based from the time_array.

        Parameters
        ----------
        background : bool, False
            When set to True, start the calculation on a threading.Thread in the
            background and return the thread to the user.

        Returns
        -------
        proc : None or threading.Thread instance
            When background is set to True, a thread is returned which must be
            joined before the lst_array exists on the UVData object.

        """
        if not background:
            self._set_lsts_helper()
            return
        else:
            proc = threading.Thread(target=self._set_lsts_helper)
            proc.start()
            return proc

    def set_telescope_params(self, overwrite=False, warn=True):
        """
        Set telescope related parameters.

        If the telescope_name is in the known_telescopes, set any missing
        telescope-associated parameters (e.g. telescope location) to the value
        for the known telescope.

        Parameters
        ----------
        overwrite : bool
            Option to overwrite existing telescope-associated parameters with
            the values from the known telescope.

        Raises
        ------
        ValueError
            if the telescope_name is not in known telescopes
        """
        telescope_obj = uvtel.get_telescope(self.telescope_name)
        if telescope_obj is not False:
            params_set = []
            telescope_params = list(telescope_obj.__iter__())
            # ensure that the Nants_telescope comes first so shapes work out below
            telescope_params.remove("_Nants_telescope")
            telescope_params.insert(0, "_Nants_telescope")
            for p in telescope_params:
                telescope_param = getattr(telescope_obj, p)
                if p in self:
                    self_param = getattr(self, p)
                else:
                    continue
                if telescope_param.value is not None and (
                    overwrite is True or self_param.value is None
                ):
                    telescope_shape = telescope_param.expected_shape(telescope_obj)
                    self_shape = self_param.expected_shape(self)
                    if telescope_shape == self_shape:
                        params_set.append(self_param.name)
                        prop_name = self_param.name
                        setattr(self, prop_name, getattr(telescope_obj, prop_name))
                    else:
                        # Note dropped handling for antenna diameters that appears in
                        # UVData because they don't exist on UVFlag.
                        warnings.warn(
                            f"{self_param.name} is not set but cannot be set using "
                            f"known values for {telescope_obj.telescope_name} "
                            "because the expected shapes don't match."
                        )

            if len(params_set) > 0:
                params_set_str = ", ".join(params_set)
                if warn:
                    warnings.warn(
                        f"{params_set_str} are not set or being overwritten. Using "
                        f"known values for {telescope_obj.telescope_name}."
                    )
        else:
            raise ValueError(
                f"Telescope {self.telescope_name} is not in known_telescopes."
            )

    def antpair2ind(self, ant1, ant2):
        """Get blt indices for given (ordered) antenna pair.

        Parameters
        ----------
        ant1 : int or array_like of int
            Number of the first antenna
        ant2 : int or array_like of int
            Number of the second antenna

        Returns
        -------
        int or array_like of int
            baseline number(s) corresponding to the input antenna number

        """
        if self.type != "baseline":
            raise ValueError(
                "UVFlag object of type " + self.type + " does not "
                "contain antenna pairs to index."
            )
        return np.where((self.ant_1_array == ant1) & (self.ant_2_array == ant2))[0]

    def baseline_to_antnums(self, baseline):
        """Get the antenna numbers corresponding to a given baseline number.

        Parameters
        ----------
        baseline : int
            baseline number

        Returns
        -------
        tuple
            Antenna numbers corresponding to baseline.

        """
        assert self.type == "baseline", "Must be 'baseline' type UVFlag object."
        return uvutils.baseline_to_antnums(baseline, self.Nants_telescope)

    def antnums_to_baseline(self, ant1, ant2, attempt256=False):
        """
        Get the baseline number corresponding to two given antenna numbers.

        Parameters
        ----------
        ant1 : int or array_like of int
            first antenna number
        ant2 : int or array_like of int
            second antenna number
        attempt256 : bool
            Option to try to use the older 256 standard used in many uvfits files
            (will use 2048 standard if there are more than 256 antennas).

        Returns
        -------
        int or array of int
            baseline number corresponding to the two antenna numbers.
        """
        assert self.type == "baseline", "Must be 'baseline' type UVFlag object."
        return uvutils.antnums_to_baseline(
            ant1, ant2, self.Nants_telescope, attempt256=attempt256
        )

    def get_baseline_nums(self):
        """Return numpy array of unique baseline numbers in data."""
        assert self.type == "baseline", "Must be 'baseline' type UVFlag object."
        return np.unique(self.baseline_array)

    def get_antpairs(self):
        """Return list of unique antpair tuples (ant1, ant2) in data."""
        assert self.type == "baseline", "Must be 'baseline' type UVFlag object."
        return list(zip(*self.baseline_to_antnums(self.get_baseline_nums())))

    def get_ants(self):
        """
        Get the unique antennas that have data associated with them.

        Returns
        -------
        ndarray of int
            Array of unique antennas with data associated with them.
        """
        if self.type == "baseline":
            return np.unique(np.append(self.ant_1_array, self.ant_2_array))
        elif self.type == "antenna":
            return np.unique(self.ant_array)
        elif self.type == "waterfall":
            raise ValueError("A waterfall type UVFlag object has no sense of antennas.")

    def get_pols(self):
        """
        Get the polarizations in the data.

        Returns
        -------
        list of str
            list of polarizations (as strings) in the data.
        """
        return uvutils.polnum2str(
            self.polarization_array, x_orientation=self.x_orientation
        )

    def parse_ants(self, ant_str, print_toggle=False):
        """
        Get antpair and polarization from parsing an aipy-style ant string.

        Used to support the select function. This function is only useable when
        the UVFlag type is 'baseline'. Generates two lists of antenna pair tuples
        and polarization indices based on parsing of the string ant_str. If no
        valid polarizations (pseudo-Stokes params, or combinations of [lr] or
        [xy]) or antenna numbers are found in ant_str, ant_pairs_nums and
        polarizations are returned as None.

        Parameters
        ----------
        ant_str : str
            String containing antenna information to parse. Can be 'all',
            'auto', 'cross', or combinations of antenna numbers and polarization
            indicators 'l' and 'r' or 'x' and 'y'.  Minus signs can also be used
            in front of an antenna number or baseline to exclude it from being
            output in ant_pairs_nums. If ant_str has a minus sign as the first
            character, 'all,' will be added to the beginning of the string.
            See the tutorial for examples of valid strings and their behavior.
        print_toggle : bool
            Boolean for printing parsed baselines for a visual user check.

        Returns
        -------
        ant_pairs_nums : list of tuples of int or None
            List of tuples containing the parsed pairs of antenna numbers, or
            None if ant_str is 'all' or a pseudo-Stokes polarizations.
        polarizations : list of int or None
            List of desired polarizations or None if ant_str does not contain a
            polarization specification.

        """
        if self.type != "baseline":
            raise ValueError(
                "UVFlag objects can only call 'parse_ants' function "
                "if type is 'baseline'."
            )
        return uvutils.parse_ants(
            self,
            ant_str=ant_str,
            print_toggle=print_toggle,
            x_orientation=self.x_orientation,
        )

    def collapse_pol(
        self,
        method="quadmean",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Collapse the polarization axis using a given method.

        If the original UVFlag object has more than one polarization,
        the resulting polarization_array will be a single element array with a
        comma separated string encoding the original polarizations.

        Parameters
        ----------
        method : str, {"quadmean", "absmean", "mean", "or", "and"}
            How to collapse the dimension(s).
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after collapsing polarizations.
        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
            collapsing polarizations.

        """
        method = method.lower()
        if self.mode == "flag":
            darr = self.flag_array
        else:
            darr = self.metric_array
        if len(self.polarization_array) > 1:
            if self.mode == "metric":
                _weights = self.weights_array
            else:
                _weights = np.ones_like(darr)
            # Collapse pol dimension. But note we retain a polarization axis.
            d, w = uvutils.collapse(
                darr, method, axis=-1, weights=_weights, return_weights=True
            )
            darr = np.expand_dims(d, axis=d.ndim)

            if self.mode == "metric":
                self.weights_array = np.expand_dims(w, axis=w.ndim)

            self.polarization_array = np.array(
                [",".join(map(str, self.polarization_array))], dtype=np.str_
            )

            self.Npols = len(self.polarization_array)
            self._check_pol_state()
        else:
            warnings.warn(
                "Cannot collapse polarization axis when only one pol present."
            )
            return
        if ((method == "or") or (method == "and")) and (self.mode == "flag"):
            self.flag_array = darr
        else:
            self.metric_array = darr
            self._set_mode_metric()
        self.clear_unused_attributes()
        self.history += "Pol axis collapse. "

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def to_waterfall(
        self,
        method="quadmean",
        keep_pol=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        return_weights_square=False,
    ):
        """Convert an 'antenna' or 'baseline' type object to waterfall.

        Parameters
        ----------
        method : str, {"quadmean", "absmean", "mean", "or", "and"}
            How to collapse the dimension(s).
        keep_pol : bool
            Whether to also collapse the polarization dimension
            If keep_pol is False, and the original UVFlag object has more
            than one polarization, the resulting polarization_array
            will be a single element array with a comma separated string
            encoding the original polarizations.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to waterfall type.
        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
            converting to waterfall type.
        return_weights_square: bool
            Option to compute the sum of the squares of the weights when
            collapsing baseline object to waterfall. Not used if type is not
            baseline to begin with. Fills an optional parameter if so.

        """
        method = method.lower()
        if self.type == "waterfall" and (
            keep_pol or (len(self.polarization_array) == 1)
        ):
            warnings.warn("This object is already a waterfall. Nothing to change.")
            return
        if (not keep_pol) and (len(self.polarization_array) > 1):
            self.collapse_pol(method)

        if self.mode == "flag":
            darr = self.flag_array
        else:
            darr = self.metric_array

        if self.type == "antenna":
            if self.future_array_shapes:
                collapse_axes = (0,)
            else:
                collapse_axes = (0, 1)
            d, w = uvutils.collapse(
                darr,
                method,
                axis=collapse_axes,
                weights=self.weights_array,
                return_weights=True,
            )
            darr = np.swapaxes(d, 0, 1)
            if self.mode == "metric":
                self.weights_array = np.swapaxes(w, 0, 1)
        elif self.type == "baseline":
            Nt = len(np.unique(self.time_array))
            Nf = self.freq_array.size
            Np = len(self.polarization_array)
            d = np.zeros((Nt, Nf, Np))
            w = np.zeros((Nt, Nf, Np))
            if return_weights_square:
                ws = np.zeros((Nt, Nf, Np))
            for i, t in enumerate(np.unique(self.time_array)):
                ind = self.time_array == t
                if self.mode == "metric":
                    _weights = self.weights_array[ind, :, :]
                else:
                    _weights = np.ones_like(darr[ind, :, :], dtype=float)
                if return_weights_square:
                    d[i, :, :], w[i, :, :], ws[i, :, :] = uvutils.collapse(
                        darr[ind, :, :],
                        method,
                        axis=0,
                        weights=_weights,
                        return_weights=True,
                        return_weights_square=return_weights_square,
                    )
                else:
                    d[i, :, :], w[i, :, :] = uvutils.collapse(
                        darr[ind, :, :],
                        method,
                        axis=0,
                        weights=_weights,
                        return_weights=True,
                        return_weights_square=return_weights_square,
                    )
            darr = d
            if self.mode == "metric":
                self.weights_array = w
                if return_weights_square:
                    self.weights_square_array = ws
            self.time_array, ri = np.unique(self.time_array, return_index=True)
            self.lst_array = self.lst_array[ri]
        if ((method == "or") or (method == "and")) and (self.mode == "flag"):
            # If using a boolean operation (AND/OR) and in flag mode, stay in flag
            # flags should be bool, but somehow it is cast as float64
            # is reacasting to bool like this best?
            self.flag_array = darr.astype(bool)
        else:
            # Otherwise change to (or stay in) metric
            self.metric_array = darr
            self._set_mode_metric()
        self.freq_array = self.freq_array.flatten()
        self._set_type_waterfall()
        self.history += 'Collapsed to type "waterfall". '  # + self.pyuvdata_version_str

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        self.clear_unused_attributes()
        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def to_baseline(
        self,
        uv,
        force_pol=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Convert a UVFlag object of type "waterfall" or "antenna" to type "baseline".

        Broadcasts the flag array to all baselines.
        This function does NOT apply flags to uv (see utils.apply_uvflag for that).

        Parameters
        ----------
        uv : UVData or UVFlag object
            Object with type baseline to match.
        force_pol : bool
            If True, will use 1 pol to broadcast to any other pol.
            Otherwise, will require polarizations match.
            For example, this keyword is useful if one flags on all
            pols combined, and wants to broadcast back to individual pols.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to baseline type.
        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
            converting to baseline type.

        """
        if self.type == "baseline":
            return
        if not (
            issubclass(uv.__class__, UVData)
            or (isinstance(uv, UVFlag) and uv.type == "baseline")
        ):
            raise ValueError(
                "Must pass in UVData object or UVFlag object of type "
                '"baseline" to match.'
            )

        # write it out this rather than comparing the UVParameters because
        # future_array_shapes might be different. In the future, when shapes are not
        # variable, this can be done by comparing the UVParameters.
        if self.Nfreqs != uv.Nfreqs or not np.allclose(
            np.squeeze(self.freq_array),
            np.squeeze(uv.freq_array),
            rtol=self._freq_array.tols[0],
            atol=self._freq_array.tols[1],
        ):
            raise ValueError(
                "The freq_array on uv is not the same as the freq_array on this "
                f"object. The value on this object is {self.freq_array}; the value "
                f"on uv is {uv.freq_array}"
            )

        warn_compatibility_params = [
            "telescope_name",
            "telescope_location",
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
            "channel_width",
            "spw_array",
        ]
        if self.Nspws is not None and self.Nspws > 1:
            # TODO: make this always be in the compatibility list in version 3.0
            warn_compatibility_params.append("flex_spw_id_array")

        for param in warn_compatibility_params:
            if (
                issubclass(uv.__class__, UVData)
                and param == "channel_width"
                and not (uv.future_array_shapes or uv.flex_spw)
            ):
                if not np.allclose(
                    self.channel_width,
                    np.full(uv.Nfreqs, uv.channel_width),
                    rtol=self._channel_width.tols[0],
                    atol=self._channel_width.tols[1],
                ):
                    warnings.warn(
                        "channel_width is not the same this object and on uv. The "
                        f"value on this object is {self.channel_width}; the value on "
                        f"uv is {uv.channel_width}. This will become an error in "
                        "version 2.4.",
                        DeprecationWarning,
                    )
            else:
                # compare the UVParameter objects to properly handle tolerances
                this_param = getattr(self, "_" + param)
                uv_param = getattr(uv, "_" + param)
                if this_param.value is not None and this_param != uv_param:
                    warnings.warn(
                        f"{param} is not the same this object and on uv. The value on "
                        f"this object is {this_param.value}; the value on uv is "
                        f"{uv_param.value}. This will become an error in version 2.4.",
                        DeprecationWarning,
                    )

        # Deal with polarization
        if force_pol and self.polarization_array.size == 1:
            # Use single pol for all pols, regardless
            self.polarization_array = uv.polarization_array
            # Broadcast arrays
            if self.mode == "flag":
                self.flag_array = self.flag_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            else:
                self.metric_array = self.metric_array.repeat(
                    self.polarization_array.size, axis=-1
                )
                self.weights_array = self.weights_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            self.Npols = len(self.polarization_array)
            self._check_pol_state()

        # Now the pol axes should match regardless of force_pol.
        if not np.array_equal(uv.polarization_array, self.polarization_array):
            if self.polarization_array.size == 1:
                raise ValueError(
                    "Polarizations do not match. Try keyword force_pol"
                    + " if you wish to broadcast to all polarizations."
                )
            else:
                raise ValueError("Polarizations could not be made to match.")
        if self.type == "waterfall":
            # Populate arrays
            if self.mode == "flag":
                if (
                    issubclass(uv.__class__, UVData)
                    and uv.future_array_shapes != self.future_array_shapes
                ):
                    if uv.future_array_shapes:
                        arr = np.zeros_like(uv.flag_array[:, np.newaxis, :, :])
                    else:
                        arr = np.zeros_like(uv.flag_array[:, 0, :, :])
                else:
                    arr = np.zeros_like(uv.flag_array)
                sarr = self.flag_array
            elif self.mode == "metric":
                if (
                    issubclass(uv.__class__, UVData)
                    and uv.future_array_shapes != self.future_array_shapes
                ):
                    if uv.future_array_shapes:
                        arr = np.zeros_like(
                            uv.flag_array[:, np.newaxis, :, :], dtype=np.float64
                        )
                        warr = np.zeros_like(
                            uv.flag_array[:, np.newaxis, :, :], dtype=np.float64
                        )
                    else:
                        arr = np.zeros_like(uv.flag_array[:, 0, :, :], dtype=np.float64)
                        warr = np.zeros_like(
                            uv.flag_array[:, 0, :, :], dtype=np.float64
                        )
                else:
                    arr = np.zeros_like(uv.flag_array, dtype=np.float64)
                    warr = np.zeros_like(uv.flag_array, dtype=np.float64)
                sarr = self.metric_array
            for i, t in enumerate(np.unique(self.time_array)):
                ti = np.where(
                    np.isclose(
                        uv.time_array,
                        t,
                        rtol=max(self._time_array.tols[0], uv._time_array.tols[0]),
                        atol=max(self._time_array.tols[1], uv._time_array.tols[1]),
                    )
                )
                if self.future_array_shapes:
                    arr[ti] = sarr[i][np.newaxis, :, :]
                    if self.mode == "metric":
                        warr[ti] = self.weights_array[i][np.newaxis, :, :]
                else:
                    arr[ti] = sarr[i][np.newaxis, np.newaxis, :, :]
                    if self.mode == "metric":
                        warr[ti] = self.weights_array[i][np.newaxis, np.newaxis, :, :]
            if self.mode == "flag":
                self.flag_array = arr
            elif self.mode == "metric":
                self.metric_array = arr
                self.weights_array = warr
        elif self.type == "antenna":
            if self.mode == "metric":
                raise NotImplementedError(
                    "Cannot currently convert from antenna type, metric mode to "
                    "baseline type UVFlag object."
                )
            ants_data = np.unique(uv.ant_1_array.tolist() + uv.ant_2_array.tolist())
            new_ants = np.setdiff1d(ants_data, self.ant_array)
            if new_ants.size > 0:
                self.ant_array = np.append(self.ant_array, new_ants).tolist()
                # make new flags of the same shape but with first axis the
                # size of the new ants
                flag_shape = list(self.flag_array.shape)
                flag_shape[0] = new_ants.size
                new_flags = np.full(flag_shape, True, dtype=bool)
                self.flag_array = np.append(self.flag_array, new_flags, axis=0)

            if self.future_array_shapes:
                baseline_flags = np.full(
                    (uv.Nblts, self.Nfreqs, self.Npols), True, dtype=bool
                )
            else:
                baseline_flags = np.full(
                    (uv.Nblts, 1, self.Nfreqs, self.Npols), True, dtype=bool
                )

            for blt_index, bl in enumerate(uv.baseline_array):
                uvf_t_index = np.nonzero(
                    np.isclose(
                        uv.time_array[blt_index],
                        self.time_array,
                        rtol=max(self._time_array.tols[0], uv._time_array.tols[0]),
                        atol=max(self._time_array.tols[1], uv._time_array.tols[1]),
                    )
                )[0]
                if uvf_t_index.size > 0:
                    # if the time is found in the uvflag object time_array
                    # input the or'ed data from each antenna
                    ant1, ant2 = uv.baseline_to_antnums(bl)
                    ant1_index = np.nonzero(np.array(self.ant_array) == ant1)
                    ant2_index = np.nonzero(np.array(self.ant_array) == ant2)
                    if self.future_array_shapes:
                        or_flag = np.logical_or(
                            self.flag_array[ant1_index, :, uvf_t_index, :],
                            self.flag_array[ant2_index, :, uvf_t_index, :],
                        )
                    else:
                        or_flag = np.logical_or(
                            self.flag_array[ant1_index, :, :, uvf_t_index, :],
                            self.flag_array[ant2_index, :, :, uvf_t_index, :],
                        )

                    baseline_flags[blt_index] = or_flag.copy()

            self.flag_array = baseline_flags

        # Check the frequency array for shape, broadcast to (1, Nfreqs) if needed
        if not self.future_array_shapes:
            self.freq_array = np.atleast_2d(self.freq_array)

        if self.Nspws is None:
            self.Nspws = uv.Nspws
            self.spw_array = uv.spw_array
            if uv.flex_spw_id_array is not None:
                self.flex_spw_id_array = uv.flex_spw_id_array

        self.baseline_array = uv.baseline_array
        self.Nbls = np.unique(self.baseline_array).size
        self.ant_1_array = uv.ant_1_array
        self.ant_2_array = uv.ant_2_array
        self.Nants_data = int(np.union1d(self.ant_1_array, self.ant_2_array).size)

        self.time_array = uv.time_array
        self.lst_array = uv.lst_array
        self.Nblts = self.time_array.size

        if self.telescope_name is None and self.telescope_location is None:
            self.telescope_name = uv.telescope_name
            self.telescope_location = uv.telescope_location

        if (
            self.antenna_numbers is None
            and self.antenna_names is None
            and self.antenna_positions is None
        ):
            self.antenna_numbers = uv.antenna_numbers
            self.antenna_names = uv.antenna_names
            self.antenna_positions = uv.antenna_positions
            self.Nants_telescope = uv.Nants_telescope

        self._set_type_baseline()
        self.clear_unused_attributes()

        self.history += 'Broadcast to type "baseline". '

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def to_antenna(
        self,
        uv,
        force_pol=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Convert a UVFlag object of type "waterfall" to type "antenna".

        Broadcasts the flag array to all antennas.
        This function does NOT apply flags to uv (see utils.apply_uvflag for that).

        Parameters
        ----------
        uv : UVCal or UVFlag object
            object of type antenna to match.
        force_pol : bool
            If True, will use 1 pol to broadcast to any other pol.
            Otherwise, will require polarizations match.
            For example, this keyword is useful if one flags on all
            pols combined, and wants to broadcast back to individual pols.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to antenna type.
        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
            converting to antenna type.

        """
        if self.type == "antenna":
            return
        if not (
            issubclass(uv.__class__, UVCal)
            or (isinstance(uv, UVFlag) and uv.type == "antenna")
        ):
            raise ValueError(
                "Must pass in UVCal object or UVFlag object of type "
                '"antenna" to match.'
            )
        if self.type != "waterfall":
            raise ValueError(
                'Cannot convert from type "' + self.type + '" to "antenna".'
            )

        # write it out this rather than comparing the UVParameters because
        # future_array_shapes might be different. In the future, when shapes are not
        # variable, this can be done by comparing the UVParameters.
        if self.Nfreqs != uv.Nfreqs or not np.allclose(
            np.squeeze(self.freq_array),
            np.squeeze(uv.freq_array),
            rtol=self._freq_array.tols[0],
            atol=self._freq_array.tols[1],
        ):
            raise ValueError(
                "The freq_array on uv is not the same as the freq_array on this "
                f"object. The value on this object is {self.freq_array}; the value "
                f"on uv is {uv.freq_array}"
            )

        warn_compatibility_params = [
            "telescope_name",
            "telescope_location",
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
            "channel_width",
            "spw_array",
        ]
        if self.Nspws is not None and self.Nspws > 1:
            # TODO: make this always be in the compatibility list in version 3.0
            warn_compatibility_params.append("flex_spw_id_array")
        for param in warn_compatibility_params:
            if (
                issubclass(uv.__class__, UVCal)
                and param == "channel_width"
                and not (uv.future_array_shapes or uv.flex_spw)
            ):
                if not np.allclose(
                    self.channel_width,
                    np.full(uv.Nfreqs, uv.channel_width),
                    rtol=self._channel_width.tols[0],
                    atol=self._channel_width.tols[1],
                ):
                    warnings.warn(
                        "channel_width is not the same this object and on uv. The "
                        f"value on this object is {self.channel_width}; the value on "
                        f"uv is {uv.channel_width}. This will become an error in "
                        "version 2.4.",
                        DeprecationWarning,
                    )
            else:
                # compare the UVParameter objects to properly handle tolerances
                this_param = getattr(self, "_" + param)
                uv_param = getattr(uv, "_" + param)
                if this_param.value is not None and this_param != uv_param:
                    warnings.warn(
                        f"{param} is not the same this object and on uv. The value on "
                        f"this object is {this_param.value}; the value on uv is "
                        f"{uv_param.value}. This will become an error in version 2.4.",
                        DeprecationWarning,
                    )

        # Deal with polarization
        if issubclass(uv.__class__, UVCal):
            polarr = uv.jones_array
        else:
            polarr = uv.polarization_array
        if force_pol and self.polarization_array.size == 1:
            # Use single pol for all pols, regardless
            self.polarization_array = polarr
            # Broadcast arrays
            if self.mode == "flag":
                self.flag_array = self.flag_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            else:
                self.metric_array = self.metric_array.repeat(
                    self.polarization_array.size, axis=-1
                )
                self.weights_array = self.weights_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            self.Npols = len(self.polarization_array)
            self._check_pol_state()

        # Now the pol axes should match regardless of force_pol.
        if not np.array_equal(polarr, self.polarization_array):
            if self.polarization_array.size == 1:
                raise ValueError(
                    "Polarizations do not match. Try keyword force_pol"
                    + "if you wish to broadcast to all polarizations."
                )
            else:
                raise ValueError("Polarizations could not be made to match.")
        # Populate arrays
        if self.mode == "flag":
            if self.future_array_shapes:
                self.flag_array = np.swapaxes(self.flag_array, 0, 1)[
                    np.newaxis, :, :, :
                ]
            else:
                self.flag_array = np.swapaxes(self.flag_array, 0, 1)[
                    np.newaxis, np.newaxis, :, :, :
                ]
            self.flag_array = self.flag_array.repeat(len(uv.ant_array), axis=0)
        elif self.mode == "metric":
            if self.future_array_shapes:
                self.metric_array = np.swapaxes(self.metric_array, 0, 1)[
                    np.newaxis, :, :, :
                ]
                self.weights_array = np.swapaxes(self.weights_array, 0, 1)[
                    np.newaxis, :, :, :
                ]
            else:
                self.metric_array = np.swapaxes(self.metric_array, 0, 1)[
                    np.newaxis, np.newaxis, :, :, :
                ]
                self.weights_array = np.swapaxes(self.weights_array, 0, 1)[
                    np.newaxis, np.newaxis, :, :, :
                ]
            self.metric_array = self.metric_array.repeat(len(uv.ant_array), axis=0)
            self.weights_array = self.weights_array.repeat(len(uv.ant_array), axis=0)
        self.ant_array = uv.ant_array
        self.Nants_data = len(uv.ant_array)
        # Check the frequency array for Nspws, otherwise broadcast to 1,Nfreqs
        if not self.future_array_shapes:
            self.freq_array = np.atleast_2d(self.freq_array)

        if self.telescope_name is None and self.telescope_location is None:
            self.telescope_name = uv.telescope_name
            self.telescope_location = uv.telescope_location

        if (
            self.antenna_numbers is None
            and self.antenna_names is None
            and self.antenna_positions is None
        ):
            self.antenna_numbers = uv.antenna_numbers
            self.antenna_names = uv.antenna_names
            self.antenna_positions = uv.antenna_positions
            self.Nants_telescope = uv.Nants_telescope

        if self.Nspws is None:
            self.Nspws = uv.Nspws
            self.spw_array = uv.spw_array
            if uv.flex_spw_id_array is not None:
                self.flex_spw_id_array = uv.flex_spw_id_array

        self._set_type_antenna()
        self.history += 'Broadcast to type "antenna". '

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def to_flag(
        self,
        threshold=np.inf,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Convert to flag mode.

        This function is NOT SMART. Removes metric_array and creates a
        flag_array from a simple threshold on the metric values.

        Parameters
        ----------
        threshold : float
            Metric value over which the corresponding flag is
            set to True. Default is np.inf, which results in flags of all False.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to flag mode.
        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
            converting to flag mode.

        """
        if self.mode == "flag":
            return
        elif self.mode == "metric":
            self.flag_array = np.where(self.metric_array >= threshold, True, False)
            self._set_mode_flag()
        else:
            raise ValueError(
                "Unknown UVFlag mode: " + self.mode + ". Cannot convert to flag."
            )
        self.history += 'Converted to mode "flag". '
        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str
        self.clear_unused_attributes()

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def to_metric(
        self,
        convert_wgts=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Convert to metric mode.

        This function is NOT SMART. Simply recasts flag_array as float
        and uses this as the metric array.

        Parameters
        ----------
        convert_wgts : bool
            if True convert self.weights_array to ones
            unless a column or row is completely flagged, in which case
            convert those pixels to zero. This is used when reinterpretting
            flags as metrics to calculate flag fraction. Zero weighting
            completely flagged rows/columns prevents those from counting
            against a threshold along the other dimension.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to metric mode.
        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
            converting to metric mode.

        """
        if self.mode == "metric":
            return
        elif self.mode == "flag":
            self.metric_array = self.flag_array.astype(np.float64)
            self._set_mode_metric()

            if convert_wgts:
                self.weights_array = np.ones_like(self.weights_array)
                if self.type == "waterfall":
                    for i in range(self.Npols):
                        self.weights_array[:, :, i] *= ~and_rows_cols(
                            self.flag_array[:, :, i]
                        )
                elif self.type == "baseline":
                    for i in range(self.Npols):
                        for ap in self.get_antpairs():
                            inds = self.antpair2ind(*ap)
                            if self.future_array_shapes:
                                self.weights_array[inds, :, i] *= ~and_rows_cols(
                                    self.flag_array[inds, :, i]
                                )
                            else:
                                self.weights_array[inds, 0, :, i] *= ~and_rows_cols(
                                    self.flag_array[inds, 0, :, i]
                                )
                elif self.type == "antenna":
                    for i in range(self.Npols):
                        for j in range(self.weights_array.shape[0]):
                            if self.future_array_shapes:
                                self.weights_array[j, :, :, i] *= ~and_rows_cols(
                                    self.flag_array[j, :, :, i]
                                )
                            else:
                                self.weights_array[j, 0, :, :, i] *= ~and_rows_cols(
                                    self.flag_array[j, 0, :, :, i]
                                )
        else:
            raise ValueError(
                "Unknown UVFlag mode: " + self.mode + ". Cannot convert to metric."
            )
        self.history += 'Converted to mode "metric". '

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str
        self.clear_unused_attributes()

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def __add__(
        self,
        other,
        inplace=False,
        axis="time",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Add two UVFlag objects together along a given axis.

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        axis : str
            Axis along which to combine UVFlag objects.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two 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 two objects.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data.

        Returns
        -------
        uvf : UVFlag
            If inplace==False, return new UVFlag object.

        """
        # Handle in place
        if inplace:
            this = self
        else:
            this = self.copy()

        # Check that objects are compatible
        if not isinstance(other, this.__class__):
            raise ValueError("Only UVFlag objects can be added to a UVFlag object")
        if this.type != other.type:
            raise ValueError(
                "UVFlag object of type " + other.type + " cannot be "
                "added to object of type " + this.type + "."
            )
        if this.mode != other.mode:
            raise ValueError(
                "UVFlag object of mode " + other.mode + " cannot be "
                "added to object of mode " + this.type + "."
            )

        # check that both objects have the same array shapes
        if this.future_array_shapes != other.future_array_shapes:
            raise ValueError(
                "Both objects must have the same `future_array_shapes` parameter. "
                "Use the `use_future_array_shapes` or `use_current_array_shapes` "
                "methods to convert them."
            )

        this_has_spw_id = this.flex_spw_id_array is not None
        other_has_spw_id = other.flex_spw_id_array is not None
        if this_has_spw_id != other_has_spw_id:
            warnings.warn(
                "One object has the flex_spw_id_array set and one does not. Combined "
                "object will have it set."
            )

        # Update filename parameter
        this.filename = uvutils._combine_filenames(this.filename, other.filename)
        if this.filename is not None:
            this._filename.form = (len(this.filename),)

        # Simplify axis referencing
        axis = axis.lower()
        type_nums = {"waterfall": 0, "baseline": 1, "antenna": 2}
        if self.future_array_shapes:
            axis_nums = {
                "time": [0, 0, 2],
                "baseline": [None, 0, None],
                "antenna": [None, None, 0],
                "frequency": [1, 1, 1],
                "polarization": [2, 2, 3],
                "pol": [2, 2, 3],
                "jones": [2, 2, 3],
            }
        else:
            axis_nums = {
                "time": [0, 0, 3],
                "baseline": [None, 0, None],
                "antenna": [None, None, 0],
                "frequency": [1, 2, 2],
                "polarization": [2, 3, 4],
                "pol": [2, 3, 4],
                "jones": [2, 3, 4],
            }
        if axis not in axis_nums.keys():
            raise ValueError(f"Axis not recognized, must be one of {axis_nums.keys()}")

        ax = axis_nums[axis][type_nums[self.type]]

        warn_compatibility_params = [
            "telescope_name",
            "telescope_location",
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
        ]

        if axis != "frequency":
            warn_compatibility_params.extend(
                ["freq_array", "channel_width", "spw_array"]
            )
            if self.flex_spw_id_array is not None:
                # TODO: make this always be in the compatibility list in version 3.0
                warn_compatibility_params.append("flex_spw_id_array")
        if axis not in ["polarization", "pol", "jones"]:
            warn_compatibility_params.extend(["polarization_array"])
        if axis != "time":
            warn_compatibility_params.extend(["time_array", "lst_array"])
        if axis != "antenna" and self.type == "antenna":
            warn_compatibility_params.extend(["ant_array"])
        if axis != "baseline" and self.type == "baseline":
            warn_compatibility_params.extend(
                ["baseline_array", "ant_1_array", "ant_2_array"]
            )

        nants_telescope_unmatched = []
        for param in warn_compatibility_params:
            # compare the UVParameter objects to properly handle tolerances
            this_param = getattr(self, "_" + param)
            other_param = getattr(other, "_" + param)
            if this_param.value is not None and this_param != other_param:
                warnings.warn(
                    f"{param} is not the same the two objects. The value on this "
                    f"object is {this_param.value}; the value on the other object is "
                    f"{other_param.value}. This will become an error in version 2.4.",
                    DeprecationWarning,
                )
                if param in ["antenna_numbers", "antenna_names", "antenna_positions"]:
                    nants_telescope_unmatched.append(param)

        if axis == "time":
            this.time_array = np.concatenate([this.time_array, other.time_array])
            this.lst_array = np.concatenate([this.lst_array, other.lst_array])
            if this.type == "baseline":
                this.baseline_array = np.concatenate(
                    [this.baseline_array, other.baseline_array]
                )
                this.ant_1_array = np.concatenate([this.ant_1_array, other.ant_1_array])
                this.ant_2_array = np.concatenate([this.ant_2_array, other.ant_2_array])
                this.Nants_data = int(
                    np.union1d(this.ant_1_array, this.ant_2_array).size
                )

            this.Ntimes = np.unique(this.time_array).size
            this.Nblts = len(this.time_array)

        elif axis == "baseline":
            if self.type != "baseline":
                raise ValueError(
                    "Flag object of type " + self.type + " cannot be "
                    "concatenated along baseline axis."
                )
            this.time_array = np.concatenate([this.time_array, other.time_array])
            this.lst_array = np.concatenate([this.lst_array, other.lst_array])
            this.baseline_array = np.concatenate(
                [this.baseline_array, other.baseline_array]
            )
            this.ant_1_array = np.concatenate([this.ant_1_array, other.ant_1_array])
            this.ant_2_array = np.concatenate([this.ant_2_array, other.ant_2_array])
            this.Nants_data = int(np.union1d(this.ant_1_array, this.ant_2_array).size)

            this.Nbls = np.unique(this.baseline_array).size
            this.Nblts = len(this.baseline_array)

        elif axis == "antenna":
            if self.type != "antenna":
                raise ValueError(
                    "Flag object of type " + self.type + " cannot be "
                    "concatenated along antenna axis."
                )
            this.ant_array = np.concatenate([this.ant_array, other.ant_array])
            this.Nants_data = len(this.ant_array)
        elif axis == "frequency":
            this.freq_array = np.concatenate(
                [this.freq_array, other.freq_array], axis=-1
            )
            if this.channel_width is not None and other.channel_width is not None:
                this.channel_width = np.concatenate(
                    [this.channel_width, other.channel_width]
                )
            elif this.channel_width is not None or other.channel_width is not None:
                warnings.warn(
                    "channel_width is None on one object and an array on the other. "
                    "It will be set to None on the combined object. This will become "
                    "an error in version 2.4",
                    DeprecationWarning,
                )
                this.channel_width = None

            # handle multiple spws
            if this.Nspws > 1 or other.Nspws > 1 or this._spw_array != other._spw_array:
                if this.flex_spw_id_array is None:
                    this.flex_spw_id_array = np.full(
                        this.Nfreqs, this.spw_array[0], dtype=int
                    )
                if other.flex_spw_id_array is None:
                    other.flex_spw_id_array = np.full(
                        other.Nfreqs, other.spw_array[0], dtype=int
                    )

                this.flex_spw_id_array = np.concatenate(
                    [this.flex_spw_id_array, other.flex_spw_id_array]
                )
                this.spw_array = np.concatenate([this.spw_array, other.spw_array])
                # We want to preserve per-spw information based on first appearance
                # in the concatenated array.
                unique_index = np.sort(
                    np.unique(this.flex_spw_id_array, return_index=True)[1]
                )
                this.spw_array = this.flex_spw_id_array[unique_index]
                this.Nspws = len(this.spw_array)
            else:
                if this_has_spw_id or other_has_spw_id:
                    this.flex_spw_id_array = np.full(
                        this.freq_array.size, this.spw_array[0], dtype=int
                    )

            this.Nfreqs = np.unique(this.freq_array.flatten()).size

        elif axis in ["polarization", "pol", "jones"]:
            if this.pol_collapsed:
                raise NotImplementedError(
                    "Two UVFlag objects with their "
                    "polarizations collapsed cannot be "
                    "added along the polarization axis "
                    "at this time."
                )
            this.polarization_array = np.concatenate(
                [this.polarization_array, other.polarization_array]
            )
            this.Npols = len(this.polarization_array)

        if len(nants_telescope_unmatched) > 0 and (
            axis in ["baseline", "antenna"]
            or (axis == "time" and self.type == "baseline", "antenna")
        ):
            # this handles the case where antenna_numbers/names/positions do not match
            # but we added objects across related axes. The following code ensures we
            # at least have all the unique values so that the check doesn't error
            warnings.warn(
                f"Parameters {nants_telescope_unmatched} are different on the two "
                "objects but are related to the axis they are being combined along. "
                "We will keep the unique antenna_numbers, but if there are different "
                "names or positions for those numbers on the two objects we will keep "
                "the values from the first object. This will become an error in "
                "version 2.4",
                DeprecationWarning,
            )
            this.antenna_numbers = np.concatenate(
                (this.antenna_numbers, other.antenna_numbers)
            )
            this.antenna_names = np.concatenate(
                (this.antenna_names, other.antenna_names)
            )
            this.antenna_positions = np.concatenate(
                (this.antenna_positions, other.antenna_positions), axis=0
            )
            this.antenna_numbers, ri = np.unique(
                this.antenna_numbers, return_index=True
            )
            this.antenna_names = this.antenna_names[ri]
            this.antenna_positions = this.antenna_positions[ri]
            this.Nants_telescope = this.antenna_numbers.size

        for attr in this._data_params:
            # Check that 'other' also has the attribute filled
            if getattr(other, attr) is not None:
                setattr(
                    this,
                    attr,
                    np.concatenate(
                        [getattr(this, attr), getattr(other, attr)], axis=ax
                    ),
                )
            # May 21, 2020 - should only happen for weights_square_array attr
            else:
                raise ValueError(
                    f"{attr} optional parameter is missing from second UVFlag"
                    f" object. To concatenate two {this.mode} objects, they"
                    " must both contain the same optional parameters set."
                )

        this.history += "Data combined along " + axis + " axis. "
        if not uvutils._check_history_version(this.history, this.pyuvdata_version_str):
            this.history += this.pyuvdata_version_str

        this.Ntimes = np.unique(this.time_array).size

        if run_check:
            this.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )
        if not inplace:
            return this

    def __iadd__(
        self,
        other,
        axis="time",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """In place add.

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        axis : str
            Axis along which to combine UVFlag objects.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two 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 two objects.

        """
        self.__add__(
            other,
            inplace=True,
            axis=axis,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
        )
        return self

    def __or__(
        self,
        other,
        inplace=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Combine two UVFlag objects in "flag" mode by "OR"-ing their flags.

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two 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 two objects.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data.

        Returns
        -------
        uvf : UVFlag
            If inplace==False, return new UVFlag object.

        """
        if (self.mode != "flag") or (other.mode != "flag"):
            raise ValueError(
                'UVFlag object must be in "flag" mode to use "or" function.'
            )

        # Handle in place
        if inplace:
            this = self
        else:
            this = self.copy()
        this.flag_array += other.flag_array
        if other.history not in this.history:
            this.history += "Flags OR'd with: " + other.history

        if not uvutils._check_history_version(this.history, this.pyuvdata_version_str):
            this.history += this.pyuvdata_version_str

        if run_check:
            this.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )
        if not inplace:
            return this

    def __ior__(
        self, other, run_check=True, check_extra=True, run_check_acceptability=True
    ):
        """Perform an inplace logical or.

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two 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 two objects.

        """
        self.__or__(
            other,
            inplace=True,
            run_check=True,
            check_extra=True,
            run_check_acceptability=True,
        )
        return self

    def combine_metrics(
        self,
        others,
        method="quadmean",
        inplace=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Combine metric arrays between different UVFlag objects together.

        Parameters
        ----------
        others : UVFlag or list of UVFlags
            Other UVFlag objects to combine metrics with this one.
        method : str, {"quadmean", "absmean", "mean", "or", "and"}
            Method to combine metrics.
        inplace : bool, optional
            Perform combination in place.

        Returns
        -------
        uvf : UVFlag
            If inplace==False, return new UVFlag object with combined metrics.

        """
        # Ensure others is iterable (in case of single UVFlag object)
        # cannot use uvutils._get_iterable because the object itself is iterable
        if not isinstance(others, (list, tuple, np.ndarray)):
            others = [others]

        if np.any([not isinstance(other, UVFlag) for other in others]):
            raise ValueError('"others" must be UVFlag or list of UVFlag objects')
        if (self.mode != "metric") or np.any(
            [other.mode != "metric" for other in others]
        ):
            raise ValueError(
                'UVFlag object and "others" must be in "metric" mode '
                'to use "add_metrics" function.'
            )
        if inplace:
            this = self
        else:
            this = self.copy()
        method = method.lower()
        darray = np.expand_dims(this.metric_array, 0)
        warray = np.expand_dims(this.weights_array, 0)
        for other in others:
            if this.metric_array.shape != other.metric_array.shape:
                raise ValueError("UVFlag metric array shapes do not match.")
            darray = np.vstack([darray, np.expand_dims(other.metric_array, 0)])
            warray = np.vstack([warray, np.expand_dims(other.weights_array, 0)])
        darray, warray = uvutils.collapse(
            darray, method, weights=warray, axis=0, return_weights=True
        )
        this.metric_array = darray
        this.weights_array = warray
        this.history += "Combined metric arrays. "

        if not uvutils._check_history_version(this.history, this.pyuvdata_version_str):
            this.history += this.pyuvdata_version_str

        if run_check:
            this.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )
        if not inplace:
            return this

    def _select_preprocess(
        self,
        antenna_nums,
        ant_str,
        bls,
        frequencies,
        freq_chans,
        times,
        polarizations,
        blt_inds,
        ant_inds,
    ):
        """Build up blt_inds, freq_inds, pol_inds and history_update_string for select.

        Parameters
        ----------
        antenna_nums : array_like of int, optional
            The antennas numbers to keep in the object (antenna positions and
            names for the removed antennas will be retained unless
            `keep_all_metadata` is False).
        bls : list of tuple, optional
            A list of antenna number tuples (e.g. [(0,1), (3,2)]) or a list of
            baseline 3-tuples (e.g. [(0,1,'xx'), (2,3,'yy')]) specifying baselines
            to keep in the object. For length-2 tuples, the ordering of the numbers
            within the tuple does not matter. For length-3 tuples, the polarization
            string is in the order of the two antennas. If length-3 tuples are
            provided, `polarizations` must be None.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to keep in the object.  Can be 'auto', 'cross', 'all',
            or combinations of antenna numbers and polarizations (e.g. '1',
            '1_2', '1x_2y').  See tutorial for more examples of valid strings and
            the behavior of different forms for ant_str.
            If '1x_2y,2y_3y' is passed, both polarizations 'xy' and 'yy' will
            be kept for both baselines (1, 2) and (2, 3) to return a valid
            pyuvdata object.
            An ant_str cannot be passed in addition to any of `antenna_nums`,
            `bls` args or the `polarizations` parameters,
            if it is a ValueError will be raised.
        frequencies : array_like of float, optional
            The frequencies to keep in the object, each value passed here should
            exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to keep in the object.
        times : array_like of float, optional
            The times to keep in the object, each value passed here should
            exist in the time_array.
        polarizations : array_like of int or str, optional
            The polarizations numbers to keep in the object, each value passed
            here should exist in the polarization_array. If passing strings, the
            canonical polarization strings (e.g. "xx", "rr") are supported and if the
            `x_orientation` attribute is set, the physical dipole strings
            (e.g. "nn", "ee") are also supported.
        blt_inds : array_like of int, optional
            The baseline-time indices to keep in the object. This is
            not commonly used.
        ant_inds : array_like of int, optional
            The antenna indices to keep in the object. This is
            not commonly used.

        Returns
        -------
        blt_inds : list of int
            list of baseline-time indices to keep. Can be None (to keep everything).
        ant_inds : list of int
            list of antenna number indices to keep. Can be None
            (keep all; only valid for "antenna" mode).
        freq_inds : list of int
            list of frequency indices to keep. Can be None (to keep everything).
        pol_inds : list of int
            list of polarization indices to keep. Can be None (to keep everything).
        history_update_string : str
            string to append to the end of the history.

        """
        # build up history string as we go
        history_update_string = "  Downselected to specific "
        n_selects = 0

        if self.type == "waterfall":
            if antenna_nums is not None:
                raise ValueError(
                    "Cannot select on antenna_nums with waterfall type "
                    "UVFlag objects."
                )
            if bls is not None:
                raise ValueError(
                    "Cannot select on bls with waterfall type UVFlag objects."
                )

        if ant_str is not None:
            if not (antenna_nums is None and bls is None and polarizations is None):
                raise ValueError(
                    "Cannot provide ant_str with antenna_nums, bls, or polarizations."
                )
            else:
                bls, polarizations = self.parse_ants(ant_str)
                if bls is not None and len(bls) == 0:
                    raise ValueError(
                        f"There is no data matching ant_str={ant_str} in this object."
                    )

        # Antennas, times and blt_inds all need to be combined into a set of
        # blts indices to keep.

        # test for blt_inds presence before adding inds from antennas & times
        if blt_inds is not None:
            blt_inds = uvutils._get_iterable(blt_inds)
            if np.array(blt_inds).ndim > 1:
                blt_inds = np.array(blt_inds).flatten()
            if self.type == "baseline":
                history_update_string += "baseline-times"
            else:
                history_update_string += "times"
            n_selects += 1

        if antenna_nums is not None:
            antenna_nums = uvutils._get_iterable(antenna_nums)
            if np.array(antenna_nums).ndim > 1:
                antenna_nums = np.array(antenna_nums).flatten()
            if n_selects > 0:
                history_update_string += ", antennas"
            else:
                history_update_string += "antennas"
            n_selects += 1

            if self.type == "baseline":
                inds1 = np.zeros(0, dtype=np.int64)
                inds2 = np.zeros(0, dtype=np.int64)
                for ant in antenna_nums:
                    if ant in self.ant_1_array or ant in self.ant_2_array:
                        wh1 = np.where(self.ant_1_array == ant)[0]
                        wh2 = np.where(self.ant_2_array == ant)[0]
                        if len(wh1) > 0:
                            inds1 = np.append(inds1, list(wh1))
                        if len(wh2) > 0:
                            inds2 = np.append(inds2, list(wh2))
                    else:
                        raise ValueError(
                            "Antenna number {a} is not present in the "
                            "ant_1_array or ant_2_array".format(a=ant)
                        )
                ant_blt_inds = set(inds1).intersection(inds2)

            if self.type == "antenna":
                ant_blt_inds = None
                ant_inds = np.zeros(0, dtype=np.int64)
                for ant in antenna_nums:
                    if ant in self.ant_array:
                        wh = np.nonzero(self.ant_array == ant)[0]
                        if len(wh) > 0:
                            ant_inds = np.append(ant_inds, list(wh))
                    else:
                        raise ValueError(
                            "Antenna number {a} is not present in the "
                            "ant_array".format(a=ant)
                        )

        else:
            ant_blt_inds = None

        if bls is not None:
            if self.type != "baseline":
                raise ValueError(
                    'Only "baseline" mode UVFlag objects may select'
                    " along the baseline axis"
                )
            if isinstance(bls, tuple) and (len(bls) == 2 or len(bls) == 3):
                bls = [bls]
            if not all(isinstance(item, tuple) for item in bls):
                raise ValueError(
                    "bls must be a list of tuples of antenna numbers "
                    "(optionally with polarization)."
                )
            if not all(
                [isinstance(item[0], (int, np.integer)) for item in bls]
                + [isinstance(item[1], (int, np.integer)) for item in bls]
            ):
                raise ValueError(
                    "bls must be a list of tuples of integer antenna numbers "
                    "(optionally with polarization)."
                )
            if all(len(item) == 3 for item in bls):
                if polarizations is not None:
                    raise ValueError(
                        "Cannot provide length-3 tuples and also specify polarizations."
                    )
                if not all(isinstance(item[2], str) for item in bls):
                    raise ValueError(
                        "The third element in each bl must be a polarization string"
                    )

            if n_selects > 0:
                history_update_string += ", baselines"
            else:
                history_update_string += "baselines"

            n_selects += 1
            bls_blt_inds = np.zeros(0, dtype=np.int64)
            bl_pols = set()
            for bl in bls:
                if not (bl[0] in self.ant_1_array or bl[0] in self.ant_2_array):
                    raise ValueError(
                        "Antenna number {a} is not present in the "
                        "ant_1_array or ant_2_array".format(a=bl[0])
                    )
                if not (bl[1] in self.ant_1_array or bl[1] in self.ant_2_array):
                    raise ValueError(
                        "Antenna number {a} is not present in the "
                        "ant_1_array or ant_2_array".format(a=bl[1])
                    )
                wh1 = np.where(
                    np.logical_and(self.ant_1_array == bl[0], self.ant_2_array == bl[1])
                )[0]
                wh2 = np.where(
                    np.logical_and(self.ant_1_array == bl[1], self.ant_2_array == bl[0])
                )[0]
                if len(wh1) > 0:
                    bls_blt_inds = np.append(bls_blt_inds, list(wh1))
                    if len(bl) == 3:
                        bl_pols.add(bl[2])
                elif len(wh2) > 0:
                    bls_blt_inds = np.append(bls_blt_inds, list(wh2))
                    if len(bl) == 3:
                        bl_pols.add(uvutils.conj_pol(bl[2]))
                else:
                    raise ValueError(
                        "Antenna pair {p} does not have any data "
                        "associated with it.".format(p=bl)
                    )
            if len(bl_pols) > 0:
                polarizations = list(bl_pols)

            if ant_blt_inds is not None:
                # Use intersection (and) to join antenna_names/nums & ant_pairs_nums
                ant_blt_inds = set(ant_blt_inds).intersection(bls_blt_inds)
            else:
                ant_blt_inds = bls_blt_inds

        if ant_blt_inds is not None:
            if blt_inds is not None:
                # Use intersection (and) to join
                # antenna_names/nums/ant_pairs_nums with blt_inds
                blt_inds = set(blt_inds).intersection(ant_blt_inds)
            else:
                blt_inds = ant_blt_inds

        if times is not None:
            times = uvutils._get_iterable(times)
            if np.array(times).ndim > 1:
                times = np.array(times).flatten()

            if n_selects > 0:
                if (
                    self.type != "baseline" and "times" not in history_update_string
                ) or self.type == "baseline":
                    history_update_string += ", times"
            else:
                history_update_string += "times"

            n_selects += 1

            time_blt_inds = np.zeros(0, dtype=np.int64)
            for jd in times:
                if jd in self.time_array:
                    time_blt_inds = np.append(
                        time_blt_inds, np.where(self.time_array == jd)[0]
                    )
                else:
                    raise ValueError(
                        "Time {t} is not present in the time_array".format(t=jd)
                    )

            if blt_inds is not None:
                # Use intesection (and) to join
                # antenna_names/nums/ant_pairs_nums/blt_inds with times
                blt_inds = set(blt_inds).intersection(time_blt_inds)
            else:
                blt_inds = time_blt_inds

        if blt_inds is not None:
            if len(blt_inds) == 0:
                raise ValueError("No baseline-times were found that match criteria")

            if self.type == "baseline":
                compare_length = self.Nblts
            else:
                compare_length = self.Ntimes

            if max(blt_inds) >= compare_length:
                raise ValueError("blt_inds contains indices that are too large")
            if min(blt_inds) < 0:
                raise ValueError("blt_inds contains indices that are negative")

            blt_inds = sorted(set(blt_inds))

        if freq_chans is not None:
            freq_chans = uvutils._get_iterable(freq_chans)
            if np.array(freq_chans).ndim > 1:
                freq_chans = np.array(freq_chans).flatten()
            if frequencies is None:
                frequencies = np.squeeze(self.freq_array)[freq_chans]

            else:
                frequencies = uvutils._get_iterable(frequencies)
                frequencies = np.sort(
                    list(
                        set(frequencies) | set(np.squeeze(self.freq_array)[freq_chans])
                    )
                )

        if frequencies is not None:
            frequencies = uvutils._get_iterable(frequencies)
            if np.array(frequencies).ndim > 1:
                frequencies = np.array(frequencies).flatten()
            if n_selects > 0:
                history_update_string += ", frequencies"
            else:
                history_update_string += "frequencies"
            n_selects += 1

            freq_inds = np.zeros(0, dtype=np.int64)
            # this works because we only allow one SPW. This will have to be
            # reworked when we support more.
            if self.type != "waterfall" and not self.future_array_shapes:
                freq_arr_use = self.freq_array[0, :]
            else:
                freq_arr_use = self.freq_array
            for f in frequencies:
                if f in freq_arr_use:
                    freq_inds = np.append(freq_inds, np.where(freq_arr_use == f)[0])
                else:
                    raise ValueError(
                        "Frequency {f} is not present in the freq_array".format(f=f)
                    )

            freq_inds = sorted(set(freq_inds))
        else:
            freq_inds = None

        if polarizations is not None:
            polarizations = uvutils._get_iterable(polarizations)
            if np.array(polarizations).ndim > 1:
                polarizations = np.array(polarizations).flatten()
            if n_selects > 0:
                history_update_string += ", polarizations"
            else:
                history_update_string += "polarizations"
            n_selects += 1

            pol_inds = np.zeros(0, dtype=np.int64)
            for p in polarizations:
                if isinstance(p, str):
                    p_num = uvutils.polstr2num(p, x_orientation=self.x_orientation)
                else:
                    p_num = p
                if p_num in self.polarization_array:
                    pol_inds = np.append(
                        pol_inds, np.where(self.polarization_array == p_num)[0]
                    )
                else:
                    raise ValueError(
                        "Polarization {p} is not present in the "
                        "polarization_array".format(p=p)
                    )

            pol_inds = sorted(set(pol_inds))
        else:
            pol_inds = None

        history_update_string += " using pyuvdata."

        return blt_inds, ant_inds, freq_inds, pol_inds, history_update_string

    def _select_metadata(
        self, blt_inds, ant_inds, freq_inds, pol_inds, history_update_string
    ):
        """Perform select on everything except the data-sized arrays.

        Parameters
        ----------
        blt_inds : list of int
            list of baseline-time indices to keep. Can be None (to keep everything).
        freq_inds : list of int
            list of frequency indices to keep. Can be None (to keep everything).
        pol_inds : list of int
            list of polarization indices to keep. Can be None (to keep everything).
        history_update_string : str
            string to append to the end of the history.
        keep_all_metadata : bool
            Option to keep metadata for antennas that are no longer in the dataset.

        """
        if blt_inds is not None:
            if self.type == "baseline":
                self.Nblts = len(blt_inds)
                self.baseline_array = self.baseline_array[blt_inds]
                self.Nbls = len(np.unique(self.baseline_array))
                self.ant_1_array = self.ant_1_array[blt_inds]
                self.ant_2_array = self.ant_2_array[blt_inds]
                self.Nants_data = int(
                    np.union1d(self.ant_1_array, self.ant_2_array).size
                )

            self.time_array = self.time_array[blt_inds]
            self.lst_array = self.lst_array[blt_inds]
            self.Ntimes = len(np.unique(self.time_array))

        if self.type == "antenna":
            if ant_inds is not None:
                self.ant_array = self.ant_array[ant_inds]
                self.Nants_data = int(len(self.ant_array))

        if freq_inds is not None:
            self.Nfreqs = len(freq_inds)
            if self.type != "waterfall" and not self.future_array_shapes:
                self.freq_array = self.freq_array[:, freq_inds]
            else:
                self.freq_array = self.freq_array[freq_inds]
            if self.channel_width is not None:
                self.channel_width = self.channel_width[freq_inds]
            if self.flex_spw_id_array is not None:
                self.flex_spw_id_array = self.flex_spw_id_array[freq_inds]

            if self.Nspws > 1:
                self.spw_array = self.spw_array[
                    np.where(np.isin(self.spw_array, self.flex_spw_id_array))[0]
                ]
                self.Nspws = self.spw_array.size

        if pol_inds is not None:
            self.Npols = len(pol_inds)
            self.polarization_array = self.polarization_array[pol_inds]

        self.history = self.history + history_update_string

    def select(
        self,
        antenna_nums=None,
        ant_inds=None,
        bls=None,
        ant_str=None,
        frequencies=None,
        freq_chans=None,
        times=None,
        polarizations=None,
        blt_inds=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        inplace=True,
    ):
        """
        Downselect data to keep on the object along various axes.

        Axes that can be selected along depend on the current type of the object.
        However some axis may always be selected upon, these include frequencies,
        times and polarizations.
        In "baseline" and "antenna" modes, antenna numbers may be selected.
        In "baseline" mode, antenna pairs may be selected.
        Specific baseline-time indices can also be selected in "baseline" mode,
        but this is not commonly used.
        The history attribute on the object will be updated to identify the
        operations performed.

        Parameters
        ----------
        antenna_nums : array_like of int, optional
            The antennas numbers to keep in the object (antenna positions and
            names for the removed antennas will be retained unless
            `keep_all_metadata` is False). This cannot be provided if
            `antenna_names` is also provided.
        bls : list of tuple, optional
            A list of antenna number tuples (e.g. [(0,1), (3,2)]) or a list of
            baseline 3-tuples (e.g. [(0,1,'xx'), (2,3,'yy')]) specifying baselines
            to keep in the object. For length-2 tuples, the ordering of the numbers
            within the tuple does not matter. For length-3 tuples, the polarization
            string is in the order of the two antennas. If length-3 tuples are
            provided, `polarizations` must be None.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to keep in the object.  Can be 'auto', 'cross', 'all',
            or combinations of antenna numbers and polarizations (e.g. '1',
            '1_2', '1x_2y').  See tutorial for more examples of valid strings and
            the behavior of different forms for ant_str.
            If '1x_2y,2y_3y' is passed, both polarizations 'xy' and 'yy' will
            be kept for both baselines (1, 2) and (2, 3) to return a valid
            pyuvdata object.
            An ant_str cannot be passed in addition to any of `antenna_nums`,
            `antenna_names`, `bls` args or the `polarizations` parameters,
            if it is a ValueError will be raised.
        frequencies : array_like of float, optional
            The frequencies to keep in the object, each value passed here should
            exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to keep in the object.
        times : array_like of float, optional
            The times to keep in the object, each value passed here should
            exist in the time_array.
        polarizations : array_like of int or str, optional
            The polarizations numbers to keep in the object, each value passed
            here should exist in the polarization_array. If passing strings, the
            canonical polarization strings (e.g. "xx", "rr") are supported and if the
            `x_orientation` attribute is set, the physical dipole strings
            (e.g. "nn", "ee") are also supported.
        blt_inds : array_like of int, optional
            The baseline-time indices to keep in the object. This is
            not commonly used.
        ant_inds : array_like of int, optional
            The antenna indices to keep in the object. This is
            not commonly used.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after downselecting data on this object.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            downselecting data on this object.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data.

        Returns
        -------
        UVData object or None
            None is returned if inplace is True, otherwise a new UVData object
            with just the selected data is returned

        Raises
        ------
        ValueError
            If any of the parameters are set to inappropriate values.

        """
        if inplace:
            uv_object = self
        else:
            uv_object = self.copy()

        (
            blt_inds,
            ant_inds,
            freq_inds,
            pol_inds,
            history_update_string,
        ) = uv_object._select_preprocess(
            antenna_nums=antenna_nums,
            ant_str=ant_str,
            bls=bls,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            polarizations=polarizations,
            blt_inds=blt_inds,
            ant_inds=ant_inds,
        )

        # do select operations on everything except data_array, flag_array
        # and nsample_array
        uv_object._select_metadata(
            blt_inds, ant_inds, freq_inds, pol_inds, history_update_string
        )

        if blt_inds is not None:
            if self.type == "baseline":
                for param_name, param in zip(
                    self._data_params, uv_object.data_like_parameters
                ):
                    setattr(uv_object, param_name, param[blt_inds])
            if self.type == "waterfall":
                for param_name, param in zip(
                    self._data_params, uv_object.data_like_parameters
                ):
                    setattr(uv_object, param_name, param[blt_inds])
            if self.type == "antenna":
                if self.future_array_shapes:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, blt_inds, :])
                else:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, :, blt_inds, :])

        if ant_inds is not None and self.type == "antenna":
            for param_name, param in zip(
                self._data_params, uv_object.data_like_parameters
            ):
                setattr(uv_object, param_name, param[ant_inds])

        if freq_inds is not None:
            if self.type == "baseline":
                if self.future_array_shapes:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, freq_inds, :])
                else:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, freq_inds, :])
            if self.type == "waterfall":
                for param_name, param in zip(
                    self._data_params, uv_object.data_like_parameters
                ):
                    setattr(uv_object, param_name, param[:, freq_inds, :])
            if self.type == "antenna":
                if self.future_array_shapes:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, freq_inds, :, :])
                else:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, freq_inds, :, :])

        if pol_inds is not None:
            if self.type == "baseline":
                if self.future_array_shapes:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, pol_inds])
                else:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, :, pol_inds])
            if self.type == "waterfall":
                for param_name, param in zip(
                    self._data_params, uv_object.data_like_parameters
                ):
                    setattr(uv_object, param_name, param[:, :, pol_inds])
            if self.type == "antenna":
                if self.future_array_shapes:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, :, pol_inds])
                else:
                    for param_name, param in zip(
                        self._data_params, uv_object.data_like_parameters
                    ):
                        setattr(uv_object, param_name, param[:, :, :, :, pol_inds])

        # check if object is uv_object-consistent
        if run_check:
            uv_object.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

        if not inplace:
            return uv_object

    def read(
        self,
        filename,
        history="",
        use_future_array_shapes=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Read in flag/metric data from a HDF5 file.

        Parameters
        ----------
        filename : str or pathlib.Path
            The file name to read.
        history : str
            History string to append to UVFlag history attribute.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after reading data.
        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
            reading data.

        """
        # make sure we have an empty object.
        self.__init__()
        if isinstance(filename, (tuple, list)):
            self.read(filename[0], use_future_array_shapes=use_future_array_shapes)
            if len(filename) > 1:
                for f in filename[1:]:
                    f2 = UVFlag(
                        f,
                        history=history,
                        use_future_array_shapes=use_future_array_shapes,
                    )
                    self += f2
                del f2

        else:
            if not os.path.exists(filename):
                raise IOError(filename + " not found.")

            # update filename attribute
            basename = os.path.basename(filename)
            self.filename = [basename]
            self._filename.form = (1,)

            # Open file for reading
            with h5py.File(filename, "r") as f:
                header = f["/Header"]

                self.type = header["type"][()].decode("utf8")
                if self.type == "antenna":
                    self._set_type_antenna()
                elif self.type == "baseline":
                    self._set_type_baseline()
                elif self.type == "waterfall":
                    self._set_type_waterfall()
                else:
                    raise ValueError(
                        "File cannot be read. Received type "
                        "parameter: {receive} but "
                        "must be within acceptable values: "
                        "{expect}".format(
                            receive=self.type,
                            expect=(", ").join(self._type.acceptable_vals),
                        )
                    )

                self.mode = header["mode"][()].decode("utf8")

                if self.mode == "metric":
                    self._set_mode_metric()
                elif self.mode == "flag":
                    self._set_mode_flag()
                else:
                    raise ValueError(
                        "File cannot be read. Received mode "
                        "parameter: {receive} but "
                        "must be within acceptable values: "
                        "{expect}".format(
                            receive=self.mode,
                            expect=(", ").join(self._mode.acceptable_vals),
                        )
                    )

                if "x_orientation" in header.keys():
                    self.x_orientation = header["x_orientation"][()].decode("utf8")

                self.time_array = header["time_array"][()]
                if "Ntimes" in header.keys():
                    self.Ntimes = int(header["Ntimes"][()])
                else:
                    self.Ntimes = np.unique(self.time_array).size

                self.lst_array = header["lst_array"][()]

                # read data arrays to figure out if the file has future shapes or not
                future_shapes_ndim = {"antenna": 4, "baseline": 3}
                dgrp = f["/Data"]
                if self.mode == "metric":
                    self.metric_array = dgrp["metric_array"][()]
                    if self.type != "waterfall":
                        if self.metric_array.ndim == future_shapes_ndim[self.type]:
                            self._set_future_array_shapes()

                    self.weights_array = dgrp["weights_array"][()]
                    if "weights_square_array" in dgrp:
                        self.weights_square_array = dgrp["weights_square_array"][()]

                elif self.mode == "flag":
                    self.flag_array = dgrp["flag_array"][()]
                    if self.type != "waterfall":
                        if self.flag_array.ndim == future_shapes_ndim[self.type]:
                            self._set_future_array_shapes()

                self.freq_array = header["freq_array"][()]
                # older save files will not have this spws axis
                # at least_2d will preserve shape of 2d arrays and
                # promote 1D to (1, Nfreqs)
                if self.type != "waterfall" and not self.future_array_shapes:
                    self.freq_array = np.atleast_2d(self.freq_array)
                elif self.freq_array.ndim > 1:
                    self.freq_array = np.squeeze(self.freq_array)

                if "Nfreqs" in header.keys():
                    self.Nfreqs = int(header["Nfreqs"][()])
                else:
                    self.Nfreqs = np.unique(self.freq_array).size

                if "channel_width" in header.keys():
                    self.channel_width = header["channel_width"][()]
                else:
                    # older files do not have the channel_width parameter. Guess it from
                    # the freq array spacing.
                    msg = (
                        "channel_width not available in file, computing it from the "
                        "freq_array spacing."
                    )
                    freq_delta = np.diff(np.squeeze(self.freq_array))
                    if uvutils._test_array_constant_spacing(
                        self.freq_array, tols=self._freq_array.tols
                    ):
                        self.channel_width = np.full(self.Nfreqs, freq_delta[0])
                    else:
                        msg += (
                            " The freq_array does not have equal spacing, so the last "
                            "channel_width is set equal to the channel width below it."
                        )
                        self.channel_width = np.concatenate(
                            (freq_delta, np.array([freq_delta[-1]]))
                        )
                    warnings.warn(msg)

                if "spw_array" in header.keys():
                    self.spw_array = header["spw_array"][()]
                else:
                    self.spw_array = np.array([0])

                if "Nspws" in header.keys():
                    self.Nspws = int(header["Nspws"][()])
                else:
                    self.Nspws = self.spw_array.size

                if "flex_spw_id_array" in header.keys():
                    self.flex_spw_id_array = header["flex_spw_id_array"][()]
                elif self.Nspws == 1:
                    # set it by default
                    self.flex_spw_id_array = np.full(
                        self.Nfreqs, self.spw_array[0], dtype=int
                    )

                if "telescope_name" in header.keys():
                    self.telescope_name = header["telescope_name"][()].decode("utf8")

                if "telescope_location" in header.keys():
                    self.telescope_location = header["telescope_location"][()]

                self.history = header["history"][()].decode("utf8")

                self.history += history

                if not uvutils._check_history_version(
                    self.history, self.pyuvdata_version_str
                ):
                    self.history += self.pyuvdata_version_str

                # get extra_keywords
                if "extra_keywords" in header.keys():
                    self.extra_keywords = {}
                    for key in header["extra_keywords"].keys():
                        if header["extra_keywords"][key].dtype.type in (
                            np.string_,
                            np.object_,
                        ):
                            self.extra_keywords[key] = bytes(
                                header["extra_keywords"][key][()]
                            ).decode("utf8")
                        else:
                            self.extra_keywords[key] = header["extra_keywords"][key][()]
                else:
                    self.extra_keywords = {}

                if "label" in header.keys():
                    self.label = header["label"][()].decode("utf8")

                polarization_array = header["polarization_array"][()]
                if isinstance(polarization_array[0], np.string_):
                    polarization_array = np.asarray(polarization_array, dtype=np.str_)
                self.polarization_array = polarization_array
                self._check_pol_state()

                if "Npols" in header.keys():
                    self.Npols = int(header["Npols"][()])
                else:
                    self.Npols = len(self.polarization_array)

                if self.type == "baseline":
                    self.ant_1_array = header["ant_1_array"][()]
                    self.ant_2_array = header["ant_2_array"][()]

                    self.baseline_array = self.antnums_to_baseline(
                        self.ant_1_array, self.ant_2_array
                    )

                    if "Nblts" in header.keys():
                        self.Nblts = int(header["Nblts"][()])
                    else:
                        self.Nblts = len(self.baseline_array)

                    self.Nbls = np.unique(self.baseline_array).size

                    if "Nants_data" in header.keys():
                        self.Nants_data = int(header["Nants_data"][()])

                        n_ants_detected = int(
                            np.union1d(self.ant_1_array, self.ant_2_array).size
                        )
                        if self.Nants_data != n_ants_detected:
                            warnings.warn(
                                "Nants_data in file does not match number of antennas "
                                "with data. Resetting Nants_data."
                            )
                            self.Nants_data = n_ants_detected
                    else:
                        self.Nants_data = int(
                            np.union1d(self.ant_1_array, self.ant_2_array).size
                        )

                elif self.type == "antenna":
                    self.ant_array = header["ant_array"][()]
                    if "Nants_data" in header.keys():
                        self.Nants_data = int(header["Nants_data"][()])
                    else:
                        self.Nants_data = len(self.ant_array)

                if "Nants_telescope" in header.keys():
                    self.Nants_telescope = int(header["Nants_telescope"][()])

                if "antenna_numbers" in header.keys():
                    self.antenna_numbers = header["antenna_numbers"][()]

                if "antenna_names" in header.keys():
                    self.antenna_names = np.array(
                        [bytes(n).decode("utf8") for n in header["antenna_names"][:]]
                    )

                if "antenna_positions" in header.keys():
                    self.antenna_positions = header["antenna_positions"][()]

                if self.telescope_name is None:
                    warnings.warn(
                        "telescope_name not available in file, so telescope related "
                        "parameters cannot be set."
                    )
                elif (
                    self.telescope_location is None
                    or self.antenna_numbers is None
                    or self.antenna_names is None
                    or self.antenna_positions is None
                ):
                    if (
                        self.antenna_numbers is None
                        and self.antenna_names is None
                        and self.antenna_positions is None
                    ):
                        self.Nants_telescope = None
                    self.set_telescope_params()

                if self.antenna_numbers is None and self.type in [
                    "baseline",
                    "antenna",
                ]:
                    msg = "antenna_numbers not in file"
                    if (
                        self.Nants_telescope is None
                        or self.Nants_telescope == self.Nants_data
                    ):
                        if self.type == "baseline":
                            msg += ", setting based on ant_1_array and ant_2_array."
                            self.antenna_numbers = np.unique(
                                np.union1d(self.ant_1_array, self.ant_2_array)
                            )
                        else:
                            msg += ", setting based on ant_array."
                            self.antenna_numbers = np.unique(self.ant_array)
                    else:
                        if self.type == "baseline":
                            msg += (
                                ", cannot be set based on ant_1_array and ant_2_array "
                                "because Nants_telescope is greater than Nants_data."
                            )
                        else:
                            msg += (
                                ", cannot be set based on ant_array because "
                                "Nants_telescope is greater than Nants_data."
                            )
                    warnings.warn(msg)

                if self.antenna_names is None and self.antenna_numbers is not None:
                    warnings.warn(
                        "antenna_names not in file, setting based on antenna_numbers"
                    )
                    self.antenna_names = self.antenna_numbers.astype(str)

                if self.Nants_telescope is None:
                    if self.antenna_numbers is not None:
                        self.Nants_telescope = self.antenna_numbers.size
                    elif self.antenna_names is not None:
                        self.Nants_telescope = self.antenna_names.size
                    elif self.antenna_positions is not None:
                        self.Nants_telescope = (self.antenna_positions.shape)[0]

            self.clear_unused_attributes()

            if use_future_array_shapes != self.future_array_shapes:
                if use_future_array_shapes:
                    self.use_future_array_shapes()
                else:
                    with warnings.catch_warnings():
                        warnings.filterwarnings(
                            "ignore",
                            message="This method will be removed in version 3.0 when "
                            "the current array shapes are no longer supported.",
                        )
                        self.use_current_array_shapes()

            if not use_future_array_shapes:
                warnings.warn(_future_array_shapes_warning, DeprecationWarning)

            if run_check:
                self.check(
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                )

    def write(self, filename, clobber=False, data_compression="lzf"):
        """Write a UVFlag object to a hdf5 file.

        Parameters
        ----------
        filename : str
            The file to write to.
        clobber : bool
            Option to overwrite the file if it already exists.
        data_compression : str
            HDF5 filter to apply when writing the data_array.
            If no compression is wanted, set to None.

        """
        if os.path.exists(filename):
            if clobber:
                print("File " + filename + " exists; clobbering")
            else:
                raise ValueError("File " + filename + " exists; skipping")

        with h5py.File(filename, "w") as f:
            header = f.create_group("Header")

            # write out metadata
            if self.future_array_shapes:
                # this is Version 1.0
                header["version"] = np.string_("1.0")
            else:
                header["version"] = np.string_("0.1")

            header["type"] = np.string_(self.type)
            header["mode"] = np.string_(self.mode)

            if self.telescope_name is not None:
                header["telescope_name"] = np.string_(self.telescope_name)
            if self.telescope_location is not None:
                header["telescope_location"] = self.telescope_location

            header["Ntimes"] = self.Ntimes
            header["time_array"] = self.time_array
            header["lst_array"] = self.lst_array

            header["freq_array"] = self.freq_array
            header["Nfreqs"] = self.Nfreqs
            header["channel_width"] = self.channel_width

            header["Nspws"] = self.Nspws
            header["spw_array"] = self.spw_array
            if self.flex_spw_id_array is not None:
                header["flex_spw_id_array"] = self.flex_spw_id_array

            header["Npols"] = self.Npols

            if self.x_orientation is not None:
                header["x_orientation"] = np.string_(self.x_orientation)

            if isinstance(self.polarization_array.item(0), str):
                polarization_array = np.asarray(
                    self.polarization_array, dtype=np.string_
                )
            else:
                polarization_array = self.polarization_array
            header["polarization_array"] = polarization_array

            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            # write out extra keywords if it exists and has elements
            if self.extra_keywords:
                extra_keywords = header.create_group(
                    "extra_keywords"
                )  # create spot in header
                for k in self.extra_keywords.keys():
                    if isinstance(self.extra_keywords[k], str):
                        extra_keywords[k] = np.string_(self.extra_keywords[k])
                    else:
                        extra_keywords[k] = self.extra_keywords[k]

            header["history"] = np.string_(self.history)

            header["label"] = np.string_(self.label)

            if self.type == "baseline":
                header["Nblts"] = self.Nblts
                header["ant_1_array"] = self.ant_1_array
                header["ant_2_array"] = self.ant_2_array
                header["Nants_data"] = self.Nants_data

            elif self.type == "antenna":
                header["ant_array"] = self.ant_array
                header["Nants_data"] = self.Nants_data

            header["Nants_telescope"] = self.Nants_telescope
            if self.antenna_names is not None:
                header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes")
            if self.antenna_numbers is not None:
                header["antenna_numbers"] = self.antenna_numbers
            if self.antenna_positions is not None:
                header["antenna_positions"] = self.antenna_positions

            dgrp = f.create_group("Data")
            if self.mode == "metric":
                dgrp.create_dataset(
                    "metric_array",
                    chunks=True,
                    data=self.metric_array,
                    compression=data_compression,
                )
                dgrp.create_dataset(
                    "weights_array",
                    chunks=True,
                    data=self.weights_array,
                    compression=data_compression,
                )
                if self.weights_square_array is not None:
                    dgrp.create_dataset(
                        "weights_square_array",
                        chunks=True,
                        data=self.weights_square_array,
                        compression=data_compression,
                    )
            elif self.mode == "flag":
                dgrp.create_dataset(
                    "flag_array",
                    chunks=True,
                    data=self.flag_array,
                    compression=data_compression,
                )

    def from_uvdata(
        self,
        indata,
        mode="metric",
        copy_flags=False,
        waterfall=False,
        history="",
        label="",
        use_future_array_shapes=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Construct a UVFlag object from a UVData object.

        Parameters
        ----------
        indata : UVData
            Input to initialize UVFlag object.
        mode : {"metric", "flag"}, optional
            The mode determines whether the object has a floating point metric_array
            or a boolean flag_array.
        copy_flags : bool, optional
            Whether to copy flags from indata to new UVFlag object
        waterfall : bool, optional
            Whether to immediately initialize as a waterfall object, with flag/metric
            axes: time, frequency, polarization.
        history : str, optional
            History string to attach to object.
        label: str, optional
            String used for labeling the object (e.g. 'FM').
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after creating UVFlag object.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            creating UVFlag object.

        """
        if not issubclass(indata.__class__, UVData):
            raise ValueError(
                "from_uvdata can only initialize a UVFlag object from an input "
                "UVData object or a subclass of a UVData object."
            )

        if mode.lower() == "metric":
            self._set_mode_metric()
        elif mode.lower() == "flag":
            self._set_mode_flag()
        else:
            raise ValueError(
                "Input mode must be within acceptable values: "
                "{}".format((", ").join(self._mode.acceptable_vals))
            )

        if use_future_array_shapes:
            self._set_future_array_shapes()

        self.Nfreqs = indata.Nfreqs
        self.polarization_array = copy.deepcopy(indata.polarization_array)
        self.Npols = indata.Npols
        self.Nants_telescope = indata.Nants_telescope
        self.Ntimes = indata.Ntimes

        if indata.future_array_shapes or indata.flex_spw:
            self.channel_width = copy.deepcopy(indata.channel_width)
        else:
            self.channel_width = np.full(self.Nfreqs, indata.channel_width)

        self.telescope_name = indata.telescope_name
        self.telescope_location = indata.telescope_location
        self.antenna_names = copy.deepcopy(indata.antenna_names)
        self.antenna_numbers = copy.deepcopy(indata.antenna_numbers)
        self.antenna_positions = copy.deepcopy(indata.antenna_positions)
        self.Nspws = indata.Nspws
        self.spw_array = copy.deepcopy(indata.spw_array)
        if indata.flex_spw_id_array is not None:
            self.flex_spw_id_array = copy.deepcopy(indata.flex_spw_id_array)

        if waterfall:
            self._set_type_waterfall()
            self.history += 'Flag object with type "waterfall" created. '
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            self.time_array, ri = np.unique(indata.time_array, return_index=True)
            if indata.future_array_shapes:
                self.freq_array = copy.deepcopy(indata.freq_array)
            else:
                self.freq_array = indata.freq_array[0, :]
            self.lst_array = indata.lst_array[ri]
            if copy_flags:
                raise NotImplementedError(
                    "Cannot copy flags when initializing waterfall UVFlag from "
                    "UVData or UVCal."
                )
            else:
                if self.mode == "flag":
                    self.flag_array = np.zeros(
                        (self.Ntimes, self.Nfreqs, self.Npols), np.bool_
                    )
                elif self.mode == "metric":
                    self.metric_array = np.zeros((self.Ntimes, self.Nfreqs, self.Npols))

        else:
            self._set_type_baseline()
            self.history += 'Flag object with type "baseline" created. '
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            self.baseline_array = copy.deepcopy(indata.baseline_array)
            self.Nbls = indata.Nbls
            self.Nblts = indata.Nblts
            self.ant_1_array = copy.deepcopy(indata.ant_1_array)
            self.ant_2_array = copy.deepcopy(indata.ant_2_array)
            self.Nants_data = indata.Nants_data

            self.time_array = copy.deepcopy(indata.time_array)
            self.lst_array = copy.deepcopy(indata.lst_array)

            if self.future_array_shapes == indata.future_array_shapes:
                # match on future shape
                self.freq_array = copy.deepcopy(indata.freq_array)
            elif indata.future_array_shapes:
                # input is future shaped, self is not
                self.freq_array = indata.freq_array[np.newaxis, :]
            else:
                # input is not future shaped, self is
                self.freq_array = indata.freq_array[0, :]

            if copy_flags:
                if self.future_array_shapes == indata.future_array_shapes:
                    self.flag_array = copy.deepcopy(indata.flag_array)
                elif indata.future_array_shapes:
                    self.flag_array = indata.flag_array[:, np.newaxis, :, :]
                else:
                    self.flag_array = indata.flag_array[:, 0, :, :]
                self.history += (
                    " Flags copied from " + str(indata.__class__) + " object."
                )
                if self.mode == "metric":
                    warnings.warn(
                        'Copying flags to type=="baseline" results in mode=="flag".'
                    )
                    self._set_mode_flag()
            else:
                if self.future_array_shapes:
                    array_shape = (self.Nblts, self.Nfreqs, self.Npols)
                else:
                    array_shape = (self.Nblts, 1, self.Nfreqs, self.Npols)
                if self.mode == "flag":
                    self.flag_array = np.zeros(array_shape, dtype=np.bool_)
                elif self.mode == "metric":
                    self.metric_array = np.zeros(array_shape, dtype=np.float64)

        self.filename = indata.filename
        self._filename.form = indata._filename.form

        if indata.x_orientation is not None:
            self.x_orientation = indata.x_orientation

        if self.mode == "metric":
            self.weights_array = np.ones(self.metric_array.shape)

        if indata.extra_keywords is not None:
            self.extra_keywords = copy.deepcopy(indata.extra_keywords)

        if history not in self.history:
            self.history += history
        self.label += label

        self.clear_unused_attributes()

        if not use_future_array_shapes:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )
        return

    def from_uvcal(
        self,
        indata,
        mode="metric",
        copy_flags=False,
        waterfall=False,
        history="",
        label="",
        use_future_array_shapes=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """Construct a UVFlag object from a UVCal object.

        Parameters
        ----------
        indata : UVData
            Input to initialize UVFlag object.
        mode : {"metric", "flag"}, optional
            The mode determines whether the object has a floating point metric_array
            or a boolean flag_array.
        copy_flags : bool, optional
            Whether to copy flags from indata to new UVFlag object
        waterfall : bool, optional
            Whether to immediately initialize as a waterfall object, with flag/metric
            axes: time, frequency, polarization.
        history : str, optional
            History string to attach to object.
        label: str, optional
            String used for labeling the object (e.g. 'FM').
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after creating UVFlag object.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            creating UVFlag object.

        """
        if not issubclass(indata.__class__, UVCal):
            raise ValueError(
                "from_uvcal can only initialize a UVFlag object from an input "
                "UVCal object or a subclass of a UVCal object."
            )

        if indata.wide_band:
            raise ValueError(
                "from_uvcal can only initialize a UVFlag object from a non-wide-band "
                "UVCal object."
            )

        if mode.lower() == "metric":
            self._set_mode_metric()
        elif mode.lower() == "flag":
            self._set_mode_flag()
        else:
            raise ValueError(
                "Input mode must be within acceptable values: "
                "{}".format((", ").join(self._mode.acceptable_vals))
            )

        if use_future_array_shapes:
            self._set_future_array_shapes()

        self.Nfreqs = indata.Nfreqs
        self.polarization_array = copy.deepcopy(indata.jones_array)
        self.Npols = indata.Njones
        self.Nants_telescope = indata.Nants_telescope
        self.Ntimes = indata.Ntimes
        self.time_array = copy.deepcopy(indata.time_array)
        self.lst_array = copy.deepcopy(indata.lst_array)

        if indata.future_array_shapes or indata.flex_spw:
            self.channel_width = copy.deepcopy(indata.channel_width)
        else:
            self.channel_width = np.full(self.Nfreqs, indata.channel_width)

        self.telescope_name = indata.telescope_name
        self.telescope_location = indata.telescope_location
        self.antenna_names = copy.deepcopy(indata.antenna_names)
        self.antenna_numbers = copy.deepcopy(indata.antenna_numbers)
        self.antenna_positions = copy.deepcopy(indata.antenna_positions)
        self.Nspws = indata.Nspws
        self.spw_array = copy.deepcopy(indata.spw_array)
        if indata.flex_spw_id_array is not None:
            self.flex_spw_id_array = copy.deepcopy(indata.flex_spw_id_array)

        if waterfall:
            self._set_type_waterfall()
            self.history += 'Flag object with type "waterfall" created. '
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            if indata.future_array_shapes:
                self.freq_array = copy.deepcopy(indata.freq_array)
            else:
                self.freq_array = indata.freq_array[0, :]
            if copy_flags:
                raise NotImplementedError(
                    "Cannot copy flags when "
                    "initializing waterfall UVFlag "
                    "from UVData or UVCal."
                )
            else:
                if self.mode == "flag":
                    self.flag_array = np.zeros(
                        (self.Ntimes, self.Nfreqs, self.Npols), np.bool_
                    )
                elif self.mode == "metric":
                    self.metric_array = np.zeros((self.Ntimes, self.Nfreqs, self.Npols))

        else:
            self._set_type_antenna()
            self.history += 'Flag object with type "antenna" created. '
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str
            self.ant_array = copy.deepcopy(indata.ant_array)
            self.Nants_data = len(self.ant_array)

            if self.future_array_shapes == indata.future_array_shapes:
                # match on future shape
                self.freq_array = copy.deepcopy(indata.freq_array)
            elif indata.future_array_shapes:
                # input is future shaped, self is not
                self.freq_array = indata.freq_array[np.newaxis, :]
            else:
                # input is not future shaped, self is
                self.freq_array = indata.freq_array[0, :]

            if copy_flags:
                if self.future_array_shapes == indata.future_array_shapes:
                    self.flag_array = copy.deepcopy(indata.flag_array)
                elif indata.future_array_shapes:
                    self.flag_array = indata.flag_array[:, np.newaxis, :, :]
                else:
                    self.flag_array = indata.flag_array[:, 0, :, :]
                self.history += (
                    " Flags copied from " + str(indata.__class__) + " object."
                )
                if self.mode == "metric":
                    warnings.warn(
                        'Copying flags to type=="antenna" results in mode=="flag".'
                    )
                    self._set_mode_flag()
            else:
                if self.future_array_shapes:
                    array_shape = (
                        self.Nants_data,
                        self.Nfreqs,
                        self.Ntimes,
                        self.Npols,
                    )
                else:
                    array_shape = (
                        self.Nants_data,
                        1,
                        self.Nfreqs,
                        self.Ntimes,
                        self.Npols,
                    )
                if self.mode == "flag":
                    self.flag_array = np.zeros(array_shape, dtype=np.bool_)
                elif self.mode == "metric":
                    self.metric_array = np.zeros(array_shape, dtype=np.float64)

        if self.mode == "metric":
            self.weights_array = np.ones(self.metric_array.shape)

        self.filename = indata.filename
        self._filename.form = indata._filename.form

        if indata.x_orientation is not None:
            self.x_orientation = indata.x_orientation

        if history not in self.history:
            self.history += history
        self.label += label

        self.clear_unused_attributes()

        if not use_future_array_shapes:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )
        return


# setup.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

import glob
import io
import os
import platform
import sys
from sysconfig import get_config_var

import numpy
from Cython.Build import cythonize
from packaging.version import parse
from setuptools import Extension, find_namespace_packages, setup

# add pyuvdata to our path in order to use the branch_scheme function
sys.path.append("pyuvdata")
from branch_scheme import branch_scheme  # noqa

with io.open("README.md", "r", encoding="utf-8") as readme_file:
    readme = readme_file.read()


def is_platform_mac():
    return sys.platform == "darwin"


def is_platform_windows():
    return sys.platform == "win32"


# For mac, ensure extensions are built for macos 10.9 when compiling on a
# 10.9 system or above, overriding distuitls behaviour which is to target
# the version that python was built for. This may be overridden by setting
# MACOSX_DEPLOYMENT_TARGET before calling setup.py
# implementation based on pandas, see https://github.com/pandas-dev/pandas/issues/23424
if is_platform_mac():
    if "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
        current_system = parse(platform.mac_ver()[0])
        python_target = parse(get_config_var("MACOSX_DEPLOYMENT_TARGET"))
        if python_target < parse("10.9") and current_system >= parse("10.9"):
            os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.9"

# define the cython compile args, depending on platform
if is_platform_windows():
    extra_compile_args = ["/Ox", "/openmp"]
    extra_link_args = ["/openmp"]
elif is_platform_mac():
    extra_compile_args = ["-O3"]
    extra_link_args = []
else:
    extra_compile_args = ["-O3", "-fopenmp"]
    extra_link_args = ["-fopenmp"]

global_c_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")]

miriad_extension = Extension(
    "pyuvdata._miriad",
    sources=[
        "pyuvdata/uvdata/src/miriad_wrap.pyx",
        "pyuvdata/uvdata/src/uvio.c",
        "pyuvdata/uvdata/src/hio.c",
        "pyuvdata/uvdata/src/pack.c",
        "pyuvdata/uvdata/src/bug.c",
        "pyuvdata/uvdata/src/dio.c",
        "pyuvdata/uvdata/src/headio.c",
        "pyuvdata/uvdata/src/maskio.c",
    ],
    define_macros=global_c_macros,
    include_dirs=["pyuvdata/uvdata/src/", numpy.get_include()],
)

corr_fits_extension = Extension(
    "pyuvdata._corr_fits",
    sources=["pyuvdata/uvdata/corr_fits.pyx"],
    define_macros=global_c_macros,
    include_dirs=[numpy.get_include()],
    extra_compile_args=extra_compile_args,
    extra_link_args=extra_link_args,
)

utils_extension = Extension(
    "pyuvdata._utils",
    sources=["pyuvdata/utils.pyx"],
    define_macros=global_c_macros,
    include_dirs=[numpy.get_include()],
    extra_compile_args=extra_compile_args,
)

uvbeam_extension = Extension(
    "pyuvdata._uvbeam",
    sources=["pyuvdata/uvbeam/uvbeam.pyx"],
    define_macros=global_c_macros,
    include_dirs=[numpy.get_include()],
    extra_compile_args=extra_compile_args,
)

extensions = [corr_fits_extension, utils_extension, uvbeam_extension]

# don't build miriad on windows
if not is_platform_windows():
    extensions.append(miriad_extension)

astroquery_reqs = ["astroquery>=0.4.4"]
casa_reqs = ["python-casacore>=3.3.1"]
cst_reqs = ["pyyaml>=5.1"]
hdf5_compression_reqs = ["hdf5plugin>=3.1.0"]
healpix_reqs = ["astropy_healpix>=0.6"]
lunar_reqs = ["lunarsky>=0.2.1"]
novas_reqs = ["novas", "novas_de405"]
all_optional_reqs = (
    astroquery_reqs
    + casa_reqs
    + cst_reqs
    + hdf5_compression_reqs
    + healpix_reqs
    + lunar_reqs
    + novas_reqs
)
test_reqs = all_optional_reqs + [
    "pytest>=6.2",
    "pytest-xdist",
    "pytest-cases>=3.6.9",
    "pytest-cov",
    "cython",
    "coverage",
    "pre-commit",
]
doc_reqs = ["sphinx", "pypandoc"]

setup_args = {
    "name": "pyuvdata",
    "author": "Radio Astronomy Software Group",
    "url": "https://github.com/RadioAstronomySoftwareGroup/pyuvdata",
    "license": "BSD",
    "description": "an interface for astronomical interferometeric datasets in python",
    "long_description": readme,
    "long_description_content_type": "text/markdown",
    "package_dir": {"pyuvdata": "pyuvdata"},
    "packages": find_namespace_packages(),
    "ext_modules": cythonize(extensions, language_level=3),
    "scripts": [fl for fl in glob.glob("scripts/*") if not os.path.isdir(fl)],
    "use_scm_version": {"local_scheme": branch_scheme},
    "include_package_data": True,
    "install_requires": [
        "astropy>=5.0.4",
        "h5py>=3.1",
        "numpy>=1.20",
        "pyerfa>=2.0",
        "scipy>=1.5",
        "setuptools_scm!=7.0.0,!=7.0.1,!=7.0.2",
    ],
    "extras_require": {
        "astroquery": astroquery_reqs,
        "casa": casa_reqs,
        "cst": cst_reqs,
        "hdf5_compression": hdf5_compression_reqs,
        "healpix": healpix_reqs,
        "lunar": lunar_reqs,
        "novas": novas_reqs,
        "all": all_optional_reqs,
        "test": test_reqs,
        "doc": doc_reqs,
        "dev": test_reqs + doc_reqs,
    },
    "classifiers": [
        "Development Status :: 5 - Production/Stable",
        "Intended Audience :: Science/Research",
        "License :: OSI Approved :: BSD License",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Topic :: Scientific/Engineering :: Astronomy",
    ],
    "keywords": "radio astronomy interferometry",
}

if __name__ == "__main__":
    setup(**setup_args)


# pyuvdata/uvcal/tests/__init__.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License


# pyuvdata/uvcal/tests/conftest.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2021 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""pytest fixtures for UVCal tests."""
import os

import numpy as np
import pytest

import pyuvdata.tests as uvtest
import pyuvdata.uvcal.tests.test_fhd_cal as test_fhd_cal
from pyuvdata import UVCal
from pyuvdata.data import DATA_PATH


@pytest.fixture(scope="session")
def gain_data_main():
    """Read in gain calfits file."""
    gainfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.gain.calfits")
    gain_object = UVCal.from_file(gainfile, use_future_array_shapes=True)
    gain_object.freq_range = None

    yield gain_object

    del gain_object


@pytest.fixture(scope="function")
def gain_data(gain_data_main):
    """Make function level gain uvcal object."""
    gain_object = gain_data_main.copy()

    yield gain_object

    del gain_object


@pytest.fixture(scope="session")
def delay_data_main():
    """Read in delay calfits file, add input flag array."""
    delayfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.delay.calfits")
    with uvtest.check_warnings(
        UserWarning,
        match=[
            "telescope_location is not set. Using known values for HERA.",
            "antenna_positions is not set. Using known values for HERA.",
            "When converting a delay-style cal to future array shapes the flag_array"
            " (and input_flag_array if it exists) must drop the frequency axis",
        ],
    ):
        delay_object = UVCal.from_file(delayfile, use_future_array_shapes=True)

    # yield the data for testing, then del after tests finish
    yield delay_object

    del delay_object


@pytest.fixture(scope="function")
def delay_data(delay_data_main):
    """Make function level delay uvcal object."""
    delay_object = delay_data_main.copy()

    yield delay_object

    del delay_object


@pytest.fixture(scope="session")
def delay_data_inputflag_main(delay_data_main):
    """Add an input flag array to delay object."""
    delay_object = delay_data_main.copy()

    # add an input flag array for testing
    delay_object.input_flag_array = np.zeros(
        delay_object._input_flag_array.expected_shape(delay_object), dtype=bool
    )

    # yield the data for testing, then del after tests finish
    yield delay_object

    del delay_object


@pytest.fixture(scope="function")
def delay_data_inputflag(delay_data_inputflag_main):
    """Make function level delay uvcal object."""
    delay_object = delay_data_inputflag_main.copy()

    yield delay_object

    del delay_object


@pytest.fixture(scope="session")
def fhd_cal_raw_main():
    """Read in raw FHD cal."""
    with uvtest.check_warnings(
        UserWarning, "Telescope location derived from obs lat/lon/alt"
    ):
        fhd_cal = UVCal.from_file(
            test_fhd_cal.cal_testfile,
            obs_file=test_fhd_cal.obs_testfile,
            layout_file=test_fhd_cal.layout_testfile,
            settings_file=test_fhd_cal.settings_testfile,
            raw=True,
            use_future_array_shapes=True,
        )

    yield fhd_cal

    del fhd_cal


@pytest.fixture(scope="function")
def fhd_cal_raw(fhd_cal_raw_main):
    """Make function raw FHD cal object."""
    fhd_cal = fhd_cal_raw_main.copy()

    yield fhd_cal

    del fhd_cal


@pytest.fixture(scope="session")
def fhd_cal_fit_main():
    """Read in fit FHD cal."""
    with uvtest.check_warnings(
        UserWarning, "Telescope location derived from obs lat/lon/alt"
    ):
        fhd_cal = UVCal.from_file(
            test_fhd_cal.cal_testfile,
            obs_file=test_fhd_cal.obs_testfile,
            layout_file=test_fhd_cal.layout_testfile,
            settings_file=test_fhd_cal.settings_testfile,
            raw=False,
            use_future_array_shapes=True,
        )

    yield fhd_cal

    del fhd_cal


@pytest.fixture(scope="function")
def fhd_cal_fit(fhd_cal_fit_main):
    """Make function fit FHD cal object."""
    fhd_cal = fhd_cal_fit_main.copy()

    yield fhd_cal

    del fhd_cal


# pyuvdata/uvcal/tests/test_calfits.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Tests for calfits object

"""
import os

import numpy as np
import pytest
from astropy.io import fits

import pyuvdata.tests as uvtest
import pyuvdata.utils as uvutils
from pyuvdata import UVCal
from pyuvdata.data import DATA_PATH
from pyuvdata.uvcal.uvcal import _future_array_shapes_warning

pytestmark = pytest.mark.filterwarnings(
    "ignore:telescope_location is not set. Using known values",
    "ignore:antenna_positions is not set. Using known values",
)


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_readwriteread(future_shapes, caltype, gain_data, delay_data, tmp_path):
    """
    Omnical/Firstcal fits loopback test.

    Read in calfits file, write out new calfits file, read back in and check for
    object equality.
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    if not future_shapes:
        cal_in.use_current_array_shapes()

    write_file = str(tmp_path / "outtest.fits")
    cal_in.write_calfits(write_file, clobber=True)
    if not future_shapes:
        warn_type = DeprecationWarning
        warn_msg = _future_array_shapes_warning
    elif caltype == "delay":
        warn_type = UserWarning
        warn_msg = "When converting a delay-style cal to future array shapes the"
    else:
        warn_type = None
        warn_msg = ""

    with uvtest.check_warnings(warn_type, match=warn_msg):
        cal_out = UVCal.from_file(write_file, use_future_array_shapes=future_shapes)

    assert cal_out.filename == [os.path.basename(write_file)]

    assert cal_in == cal_out

    return


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
def test_write_inttime_equal_timediff(future_shapes, gain_data, delay_data, tmp_path):
    """
    test writing out object with integration times close to time diffs
    """
    cal_in = gain_data

    time_diffs = np.diff(cal_in.time_array)

    gain_data.integration_time = np.full(
        cal_in.Ntimes, np.mean(time_diffs) * (24.0 * 60.0**2)
    )

    if not future_shapes:
        cal_in.use_current_array_shapes()

    write_file = str(tmp_path / "outtest.fits")
    cal_in.write_calfits(write_file, clobber=True)

    if not future_shapes:
        warn_type = DeprecationWarning
    else:
        warn_type = None

    with uvtest.check_warnings(warn_type, match=_future_array_shapes_warning):
        cal_out = UVCal.from_file(write_file, use_future_array_shapes=future_shapes)

    assert cal_in == cal_out

    return


@pytest.mark.parametrize(
    "filein,caltype",
    [
        ("zen.2457698.40355.xx.gain.calfits", "gain"),
        ("zen.2457698.40355.xx.delay.calfits", "delay"),
    ],
)
def test_read_metadata_only(filein, caltype, gain_data, delay_data):
    """
    check that metadata only reads work
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    # check that metadata only reads work
    cal2 = cal_in.copy(metadata_only=True)
    testfile = os.path.join(DATA_PATH, filein)
    cal3 = UVCal.from_file(testfile, read_data=False, use_future_array_shapes=True)
    assert cal2 == cal3

    return


def test_readwriteread_no_freq_range(gain_data, tmp_path):
    # test without freq_range parameter
    cal_in = gain_data
    write_file = str(tmp_path / "outtest_omnical.fits")

    cal_in.freq_range = None
    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out

    return


def test_readwriteread_no_time_range(tmp_path):
    # test without time_range parameter
    testfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.gain.calfits")
    write_file = str(tmp_path / "outtest_omnical.fits")

    cal_in = UVCal.from_file(testfile, use_future_array_shapes=True)
    cal_in.time_range = None
    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out

    return


def test_error_unknown_cal_type(delay_data, tmp_path):
    """
    Test an error is raised when writing an unknown cal type.
    """
    cal_in = delay_data
    write_file = str(tmp_path / "outtest_firstcal.fits")

    with uvtest.check_warnings(
        DeprecationWarning,
        match="Setting the cal_type to 'unknown' is deprecated. This will become an "
        "error in version 2.5",
    ):
        cal_in._set_unknown_cal_type()
    with pytest.raises(ValueError, match="unknown calibration type"):
        cal_in.write_calfits(write_file, run_check=False, clobber=True)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"flag": "CDELT2"}, "Jones values are different in FLAGS"),
        ({"flag": "CDELT3"}, "Time values are different in FLAGS"),
        ({"flag": "CRVAL5"}, "Spectral window values are different in FLAGS"),
        ({"totqual": "CDELT1"}, "Jones values are different in TOTQLTY"),
        ({"totqual": "CDELT2"}, "Time values are different in TOTQLTY"),
        ({"totqual": "CRVAL4"}, "Spectral window values are different in TOTQLTY"),
    ],
)
def test_fits_header_errors_delay(delay_data, tmp_path, header_dict, error_msg):
    # change values for various axes in flag and total quality hdus to not
    # match primary hdu
    cal_in = delay_data
    write_file = str(tmp_path / "outtest_firstcal.fits")
    write_file2 = str(tmp_path / "outtest_firstcal2.fits")

    # Create filler jones info
    cal_in.jones_array = np.array([-5, -6, -7, -8])
    cal_in.Njones = 4
    cal_in.flag_array = np.zeros(cal_in._flag_array.expected_shape(cal_in), dtype=bool)
    cal_in.delay_array = np.ones(
        cal_in._delay_array.expected_shape(cal_in), dtype=np.float64
    )
    cal_in.quality_array = np.zeros(cal_in._quality_array.expected_shape(cal_in))

    # add total_quality_array so that can be tested as well
    cal_in.total_quality_array = np.zeros(
        cal_in._total_quality_array.expected_shape(cal_in)
    )

    # write file
    cal_in.write_calfits(write_file, clobber=True)

    unit = list(header_dict.keys())[0]
    keyword = header_dict[unit]

    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        ant_hdu = fname[hdunames["ANTENNAS"]]
        flag_hdu = fname[hdunames["FLAGS"]]
        flag_hdr = flag_hdu.header
        totqualhdu = fname[hdunames["TOTQLTY"]]
        totqualhdr = totqualhdu.header

        if unit == "flag":
            flag_hdr[keyword] *= 2
        elif unit == "totqual":
            totqualhdr[keyword] *= 2

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        hdulist = fits.HDUList([prihdu, ant_hdu])
        flag_hdu = fits.ImageHDU(data=flag_hdu.data, header=flag_hdr)
        hdulist.append(flag_hdu)
        totqualhdu = fits.ImageHDU(data=totqualhdu.data, header=totqualhdr)
        hdulist.append(totqualhdu)

        hdulist.writeto(write_file2, overwrite=True)
        hdulist.close()

    with pytest.raises(ValueError, match=error_msg):
        UVCal.from_file(write_file2, use_future_array_shapes=True)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"totqual": "CDELT1"}, "Jones values are different in TOTQLTY"),
        ({"totqual": "CDELT2"}, "Time values are different in TOTQLTY"),
        ({"totqual": "CDELT3"}, "Frequency values are different in TOTQLTY"),
        ({"totqual": "CRVAL4"}, "Spectral window values are different in TOTQLTY"),
    ],
)
def test_fits_header_errors_gain(gain_data, tmp_path, header_dict, error_msg):
    # repeat for gain type file
    cal_in = gain_data
    write_file = str(tmp_path / "outtest_omnical.fits")
    write_file2 = str(tmp_path / "outtest_omnical2.fits")

    # Create filler jones info
    cal_in.jones_array = np.array([-5, -6, -7, -8])
    cal_in.Njones = 4
    cal_in.flag_array = np.zeros(cal_in._flag_array.expected_shape(cal_in), dtype=bool)
    cal_in.gain_array = np.ones(
        cal_in._gain_array.expected_shape(cal_in), dtype=np.complex64
    )
    cal_in.quality_array = np.zeros(cal_in._quality_array.expected_shape(cal_in))

    # add total_quality_array so that can be tested as well
    cal_in.total_quality_array = np.zeros(
        cal_in._total_quality_array.expected_shape(cal_in)
    )

    # write file
    cal_in.write_calfits(write_file, clobber=True)

    unit = list(header_dict.keys())[0]
    keyword = header_dict[unit]

    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        ant_hdu = fname[hdunames["ANTENNAS"]]
        totqualhdu = fname[hdunames["TOTQLTY"]]
        totqualhdr = totqualhdu.header

        if unit == "totqual":
            totqualhdr[keyword] *= 2

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        hdulist = fits.HDUList([prihdu, ant_hdu])
        totqualhdu = fits.ImageHDU(data=totqualhdu.data, header=totqualhdr)
        hdulist.append(totqualhdu)

        hdulist.writeto(write_file2, overwrite=True)
        hdulist.close()

    with pytest.raises(ValueError, match=error_msg):
        UVCal.from_file(write_file2, use_future_array_shapes=True)

    return


def test_latlonalt_noxyz(gain_data, tmp_path):
    cal_in = gain_data
    write_file = str(tmp_path / "outtest.fits")
    write_file2 = str(tmp_path / "outtest_noxyz.fits")

    cal_in.write_calfits(write_file)

    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        ant_hdu = fname[hdunames["ANTENNAS"]]

        primary_hdr.pop("ARRAYX")
        primary_hdr.pop("ARRAYY")
        primary_hdr.pop("ARRAYZ")

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        hdulist = fits.HDUList([prihdu, ant_hdu])

        hdulist.writeto(write_file2, overwrite=True)

    cal_out = UVCal.from_file(write_file2, use_future_array_shapes=True)
    assert cal_out == cal_in


@pytest.mark.parametrize(
    "kwd1,kwd2,val1,val2",
    [
        ["keyword1", "keyword2", True, False],
        ["keyword1", "keyword2", np.int64(5), 7],
        ["keyword1", "keyword2", np.int64(5.3), 6.9],
        ["keyword1", "keyword2", np.complex64(5.3 + 1.2j), 6.9 + 4.6j],
        [
            "keyword1",
            "comment",
            "short comment",
            "this is a very long comment that will "
            "be broken into several lines\nif "
            "everything works properly.",
        ],
    ],
)
def test_extra_keywords(gain_data, kwd1, kwd2, val1, val2, tmp_path):
    cal_in = gain_data
    testfile = str(tmp_path / "outtest_extrakws.fits")

    # check handling of boolean keywords
    cal_in.extra_keywords[kwd1] = val1
    cal_in.extra_keywords[kwd2] = val2
    cal_in.write_calfits(testfile, clobber=True)
    cal_out = UVCal.from_file(testfile, use_future_array_shapes=True)

    assert cal_in == cal_out

    return


@pytest.mark.parametrize(
    "ex_val,error_msg",
    [
        ({"testdict": {"testkey": 23}}, "Extra keyword testdict is of"),
        ({"testlist": [12, 14, 90]}, "Extra keyword testlist is of"),
        ({"testarr": np.array([12, 14, 90])}, "Extra keyword testarr is of"),
    ],
)
def test_extra_keywords_errors(gain_data, tmp_path, ex_val, error_msg):
    cal_in = gain_data
    testfile = str(tmp_path / "outtest_extrakwd_err.fits")

    # check for warnings & errors with extra_keywords that are dicts, lists or arrays
    keyword = list(ex_val.keys())[0]
    val = ex_val[keyword]
    cal_in.extra_keywords[keyword] = val
    with uvtest.check_warnings(
        UserWarning, match=f"{keyword} in extra_keywords is a list, array or dict"
    ):
        cal_in.check()
    with pytest.raises(TypeError, match=error_msg):
        cal_in.write_calfits(testfile, run_check=False)

    return


def test_extra_keywords_warnings(gain_data, tmp_path):
    cal_in = gain_data
    testfile = str(tmp_path / "outtest_extrakwd_warn.fits")

    # check for warnings with extra_keywords keys that are too long
    cal_in.extra_keywords["test_long_key"] = True
    with uvtest.check_warnings(
        UserWarning,
        match="key test_long_key in extra_keywords is longer than 8 characters",
    ):
        cal_in.check()
    with uvtest.check_warnings(
        UserWarning, "key test_long_key in extra_keywords is longer than 8 characters"
    ):
        cal_in.write_calfits(testfile, run_check=False, clobber=True)

    return


@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_input_flag_array(caltype, gain_data, delay_data, tmp_path):
    """
    Test when data file has input flag array.

    Currently we do not have a testfile, so we will artifically create one
    and check for internal consistency.
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    write_file = str(tmp_path / "outtest_input_flags.fits")
    cal_in.input_flag_array = np.zeros(
        cal_in._input_flag_array.expected_shape(cal_in), dtype=bool
    )
    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out


@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_jones(caltype, gain_data, delay_data, tmp_path):
    """
    Test when data file has more than one element in Jones matrix.

    Currently we do not have a testfile, so we will artifically create one
    and check for internal consistency.
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    write_file = str(tmp_path / "outtest_jones.fits")

    # Create filler jones info
    cal_in.jones_array = np.array([-5, -6, -7, -8])
    cal_in.Njones = 4
    cal_in.flag_array = np.zeros(cal_in._flag_array.expected_shape(cal_in), dtype=bool)
    if caltype == "gain":
        cal_in.gain_array = np.ones(
            cal_in._gain_array.expected_shape(cal_in), dtype=np.complex64
        )
    else:
        cal_in.delay_array = np.ones(
            cal_in._delay_array.expected_shape(cal_in), dtype=np.float64
        )
    cal_in.quality_array = np.zeros(cal_in._quality_array.expected_shape(cal_in))

    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out


@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_readwriteread_total_quality_array(caltype, gain_data, delay_data, tmp_path):
    """
    Test when data file has a total quality array.

    Currently we have no such file, so we will artificially create one and
    check for internal consistency.
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    write_file = str(tmp_path / "outtest_total_quality_array.fits")

    # Create filler total quality array
    cal_in.total_quality_array = np.zeros(
        cal_in._total_quality_array.expected_shape(cal_in)
    )

    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out
    del cal_in
    del cal_out


@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_total_quality_array_size(caltype, gain_data, delay_data):
    """
    Test that total quality array defaults to the proper size
    """
    if caltype == "gain":
        cal_in = gain_data
    else:
        cal_in = delay_data

    # Create filler total quality array
    cal_in.total_quality_array = np.zeros(
        cal_in._total_quality_array.expected_shape(cal_in)
    )

    if caltype == "gain":
        proper_shape = (cal_in.Nfreqs, cal_in.Ntimes, cal_in.Njones)
    else:
        proper_shape = (1, cal_in.Ntimes, cal_in.Njones)
    assert cal_in.total_quality_array.shape == proper_shape
    del cal_in


def test_write_time_precision(gain_data, tmp_path):
    """
    Test that times are being written to appropriate precision (see issue 311).
    """
    cal_in = gain_data

    write_file = str(tmp_path / "outtest_time_prec.fits")
    # overwrite time array to break old code
    dt = cal_in.integration_time / (24.0 * 60.0 * 60.0)
    t0 = cal_in.time_array[0] + dt * 3
    cal_in.time_array = dt * np.arange(cal_in.Ntimes) + t0
    if cal_in.lst_array is not None:
        cal_in.set_lsts_from_time_array()
    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out


def test_read_noversion_history(gain_data, tmp_path):
    """
    Test that version info gets added to the history if it's missing
    """
    cal_in = gain_data

    write_file = str(tmp_path / "outtest_nover.fits")
    write_file2 = str(tmp_path / "outtest_nover2.fits")

    cal_in.write_calfits(write_file, clobber=True)

    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        ant_hdu = fname[hdunames["ANTENNAS"]]

        primary_hdr["HISTORY"] = ""

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        hdulist = fits.HDUList([prihdu, ant_hdu])

        hdulist.writeto(write_file2, overwrite=True)
        hdulist.close()

    cal_out = UVCal.from_file(write_file2, use_future_array_shapes=True)
    assert cal_in == cal_out


@pytest.mark.filterwarnings("ignore:Selected frequencies are not contiguous")
def test_write_freq_spacing_not_channel_width(gain_data, tmp_path):
    cal_in = gain_data

    write_file = str(tmp_path / "outtest_freqspace.fits")

    # select every other frequency -- then evenly spaced but doesn't match channel width
    cal_in.select(freq_chans=np.arange(0, 10, 2))

    cal_in.write_calfits(write_file, clobber=True)
    cal_out = UVCal.from_file(write_file, use_future_array_shapes=True)
    assert cal_in == cal_out


# pyuvdata/uvcal/tests/test_fhd_cal.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Tests for FHD_cal object."""
import os

import numpy as np
import pytest

import pyuvdata.tests as uvtest
from pyuvdata import UVCal
from pyuvdata.data import DATA_PATH
from pyuvdata.uvcal.uvcal import _future_array_shapes_warning

# set up FHD files
testdir = os.path.join(DATA_PATH, "fhd_cal_data/")
testfile_prefix = "1061316296_"
obs_testfile = os.path.join(testdir, testfile_prefix + "obs.sav")
cal_testfile = os.path.join(testdir, testfile_prefix + "cal.sav")
settings_testfile = os.path.join(testdir, testfile_prefix + "settings.txt")
settings_testfile_nodiffuse = os.path.join(
    testdir, testfile_prefix + "nodiffuse_settings.txt"
)
layout_testfile = os.path.join(testdir, testfile_prefix + "layout.sav")

testdir2 = os.path.join(DATA_PATH, "fhd_cal_data/set2")
obs_file_multi = [obs_testfile, os.path.join(testdir2, testfile_prefix + "obs.sav")]
cal_file_multi = [cal_testfile, os.path.join(testdir2, testfile_prefix + "cal.sav")]
layout_file_multi = [layout_testfile, layout_testfile]
settings_file_multi = [
    settings_testfile,
    os.path.join(testdir2, testfile_prefix + "settings.txt"),
]


@pytest.mark.parametrize("raw", [True, False])
def test_read_fhdcal_write_read_calfits(raw, fhd_cal_raw, fhd_cal_fit, tmp_path):
    """
    FHD cal to calfits loopback test.

    Read in FHD cal files, write out as calfits, read back in and check for
    object equality.
    """
    if raw:
        fhd_cal = fhd_cal_raw
    else:
        fhd_cal = fhd_cal_fit

    filelist = [cal_testfile, obs_testfile, layout_testfile, settings_testfile]

    assert fhd_cal.filename == sorted(os.path.basename(file) for file in filelist)
    assert np.max(fhd_cal.gain_array) < 2.0

    outfile = str(tmp_path / "outtest_FHDcal_1061311664.calfits")
    fhd_cal.write_calfits(outfile, clobber=True)
    calfits_cal = UVCal.from_file(outfile, use_future_array_shapes=True)
    assert fhd_cal == calfits_cal


@pytest.mark.filterwarnings("ignore:Telescope location derived from obs lat/lon/alt")
@pytest.mark.parametrize("raw", [True, False])
def test_read_fhdcal_metadata(raw, fhd_cal_raw, fhd_cal_fit):
    """
    Test FHD cal metadata only read.
    """
    if raw:
        fhd_cal_full = fhd_cal_raw
    else:
        fhd_cal_full = fhd_cal_fit

    with uvtest.check_warnings(
        [DeprecationWarning, UserWarning],
        match=[
            _future_array_shapes_warning,
            "Telescope location derived from obs lat/lon/alt",
        ],
    ):
        fhd_cal = UVCal.from_file(
            cal_testfile,
            obs_file=obs_testfile,
            layout_file=layout_testfile,
            settings_file=settings_testfile,
            raw=raw,
            read_data=False,
        )
    fhd_cal.use_future_array_shapes()

    fhd_cal2 = fhd_cal_full.copy(metadata_only=True)

    # this file set has a mismatch in Nsources between the cal file & settings
    # file for some reason. I think it's just an issue with the files chosen
    assert fhd_cal.Nsources != fhd_cal2.Nsources
    fhd_cal.Nsources = fhd_cal2.Nsources

    # there is a loss in precision for float auto scale values in the
    # settings file vs the cal file
    # first check that they are similar (extract from the string they are packed in)
    assert np.allclose(
        np.asarray(fhd_cal.extra_keywords["AUTOSCAL"][1:-1].split(", "), dtype=float),
        np.asarray(fhd_cal2.extra_keywords["AUTOSCAL"][1:-1].split(", "), dtype=float),
    )
    # replace the strings to prevent errors
    fhd_cal.extra_keywords["autoscal".upper()] = fhd_cal2.extra_keywords[
        "autoscal".upper()
    ]

    assert fhd_cal == fhd_cal2

    # test that no diffuse is properly picked up from the settings file when
    # read_data is False
    fhd_cal = UVCal.from_file(
        cal_testfile,
        obs_file=obs_testfile,
        layout_file=layout_testfile,
        settings_file=settings_testfile_nodiffuse,
        raw=raw,
        read_data=False,
        use_future_array_shapes=True,
    )

    assert fhd_cal.diffuse_model is None

    return


@pytest.mark.filterwarnings("ignore:Telescope location derived from obs lat/lon/alt")
def test_read_fhdcal_multimode():
    """
    Read cal with multiple mode_fit values.
    """
    fhd_cal = UVCal.from_file(
        os.path.join(testdir, testfile_prefix + "multimode_cal.sav"),
        obs_file=obs_testfile,
        layout_file=layout_testfile,
        settings_file=os.path.join(testdir, testfile_prefix + "multimode_settings.txt"),
        raw=False,
        use_future_array_shapes=True,
    )
    assert fhd_cal.extra_keywords["MODE_FIT"] == "[90, 150, 230, 320, 400, 524]"

    fhd_cal2 = fhd_cal.copy(metadata_only=True)

    # check metadata only read
    fhd_cal = UVCal.from_file(
        os.path.join(testdir, testfile_prefix + "multimode_cal.sav"),
        obs_file=obs_testfile,
        layout_file=layout_testfile,
        settings_file=os.path.join(testdir, testfile_prefix + "multimode_settings.txt"),
        raw=False,
        read_data=False,
        use_future_array_shapes=True,
    )
    # this file set has a mismatch in Nsources between the cal file & settings
    # file for some reason. I think it's just an issue with the files chosen
    assert fhd_cal.Nsources != fhd_cal2.Nsources
    fhd_cal.Nsources = fhd_cal2.Nsources

    # there is a loss in precision for float auto scale values in the
    # settings file vs the cal file
    assert (
        fhd_cal.extra_keywords["autoscal".upper()]
        != fhd_cal2.extra_keywords["autoscal".upper()]
    )
    fhd_cal.extra_keywords["autoscal".upper()] = fhd_cal2.extra_keywords[
        "autoscal".upper()
    ]
    assert fhd_cal == fhd_cal2

    return


@pytest.mark.filterwarnings("ignore:Telescope location derived from obs lat/lon/alt")
@pytest.mark.parametrize(
    "extra_history",
    [
        "Some extra history for testing\n",
        ["Some extra history for testing", "And some more history as well"],
    ],
)
def test_extra_history(extra_history, tmp_path):
    """Test that setting the extra_history keyword works."""
    fhd_cal = UVCal.from_file(
        cal_testfile,
        obs_file=obs_testfile,
        layout_file=layout_testfile,
        settings_file=settings_testfile,
        extra_history=extra_history,
        use_future_array_shapes=True,
    )

    outfile = str(tmp_path / "outtest_FHDcal_1061311664.calfits")
    fhd_cal.write_calfits(outfile, clobber=True)
    calfits_cal = UVCal.from_file(outfile, use_future_array_shapes=True)
    assert fhd_cal == calfits_cal
    for line in extra_history:
        assert line in fhd_cal.history

    return


@pytest.mark.filterwarnings("ignore:Telescope location derived from obs lat/lon/alt")
def test_flags_galaxy(tmp_path):
    """Test files with time, freq and tile flags and galaxy models behave."""
    testdir = os.path.join(DATA_PATH, "fhd_cal_data/flag_set")
    obs_testfile_flag = os.path.join(testdir, testfile_prefix + "obs.sav")
    cal_testfile_flag = os.path.join(testdir, testfile_prefix + "cal.sav")
    settings_testfile_flag = os.path.join(testdir, testfile_prefix + "settings.txt")
    layout_testfile_flag = os.path.join(testdir, testfile_prefix + "layout.sav")

    with uvtest.check_warnings(
        UserWarning,
        match=[
            "tile_names from obs structure does not match",
            "Telescope location derived from obs lat/lon/alt",
        ],
    ):
        fhd_cal = UVCal.from_file(
            cal_testfile_flag,
            obs_file=obs_testfile_flag,
            layout_file=layout_testfile_flag,
            settings_file=settings_testfile_flag,
            use_future_array_shapes=True,
        )

    outfile = str(tmp_path / "outtest_FHDcal_1061311664.calfits")
    fhd_cal.write_calfits(outfile, clobber=True)
    calfits_cal = UVCal.from_file(outfile, use_future_array_shapes=True)
    assert fhd_cal == calfits_cal


def test_unknown_telescope():
    with uvtest.check_warnings(
        UserWarning,
        match=[
            "Telescope foo is not in known_telescopes.",
            "Telescope location derived from obs lat/lon/alt",
        ],
    ):
        fhd_cal = UVCal.from_file(
            cal_testfile,
            obs_file=os.path.join(testdir, testfile_prefix + "telescopefoo_obs.sav"),
            layout_file=layout_testfile,
            settings_file=settings_testfile,
            use_future_array_shapes=True,
        )
    assert fhd_cal.telescope_name == "foo"


@pytest.mark.parametrize(
    "cal_file,obs_file,layout_file,settings_file,nfiles",
    [
        [cal_testfile, obs_testfile, layout_testfile, settings_testfile, 1],
        [cal_file_multi, obs_file_multi, layout_file_multi, settings_file_multi, 2],
    ],
)
def test_break_read_fhdcal(cal_file, obs_file, layout_file, settings_file, nfiles):
    """Try various cases of missing files."""
    # check useful error message for metadata only read with no settings file
    with pytest.raises(
        ValueError, match="A settings_file must be provided if read_data is False."
    ):
        fhd_cal = UVCal.from_file(
            cal_file,
            obs_file=obs_file,
            layout_file=layout_file,
            read_data=False,
            use_future_array_shapes=True,
        )

    message_list = [
        "No settings file",
        "Telescope location derived from obs lat/lon/alt",
    ]
    if nfiles > 1:
        message_list *= 2
        message_list.append("UVParameter diffuse_model does not match")

    with uvtest.check_warnings(UserWarning, message_list):
        fhd_cal = UVCal.from_file(
            cal_file,
            obs_file=obs_file,
            layout_file=layout_file,
            use_future_array_shapes=True,
        )

    # Check only pyuvdata version history with no settings file
    expected_history = "\n" + fhd_cal.pyuvdata_version_str
    if nfiles > 1:
        expected_history += " Combined data along time axis using pyuvdata."
    assert fhd_cal.history == expected_history

    message_list = [
        "No layout file, antenna_postions will not be defined."
    ] * nfiles + ["UVParameter diffuse_model does not match"] * (nfiles - 1)

    warning_list = [UserWarning] * (2 * nfiles - 1)

    if nfiles > 1:
        warning_list += [DeprecationWarning]
        message_list += [
            "Reading multiple files from file specific read methods is deprecated. Use "
            "the generic `UVCal.read` method instead. This will become an error in "
            "version 2.5"
        ]

    with pytest.raises(
        ValueError, match="Required UVParameter _antenna_positions has not been set."
    ):
        with uvtest.check_warnings(warning_list, match=message_list):
            fhd_cal.read_fhd_cal(
                cal_file,
                obs_file,
                settings_file=settings_file,
                use_future_array_shapes=True,
            )

    with pytest.raises(
        ValueError, match="A settings_file must be provided if read_data is False."
    ):
        with uvtest.check_warnings(
            [DeprecationWarning],
            match="Reading multiple files from file specific read methods is "
            "deprecated. Use the generic `UVCal.read` method instead. This will become "
            "an error in version 2.5",
        ):
            fhd_cal.read_fhd_cal(
                cal_file,
                obs_file,
                layout_file=layout_file,
                read_data=False,
                use_future_array_shapes=True,
            )


@pytest.mark.parametrize(
    ["concat_method", "read_method"],
    [["__add__", "read"], ["__add__", "read_fhd_cal"], ["fast_concat", "read"]],
)
def test_read_multi(tmp_path, concat_method, read_method):
    """Test reading in multiple files."""
    warn_type = [UserWarning] * 3
    msg = [
        "UVParameter diffuse_model does not match",
        "Telescope location derived from obs lat/lon/alt values does not match the "
        "location in the layout file.",
        "Telescope location derived from obs lat/lon/alt values does not match the "
        "location in the layout file.",
    ]

    if concat_method == "fast_concat":
        with uvtest.check_warnings(warn_type, match=msg):
            fhd_cal = UVCal.from_file(
                cal_file_multi,
                axis="time",
                obs_file=obs_file_multi,
                settings_file=settings_file_multi,
                layout_file=layout_file_multi,
                use_future_array_shapes=True,
            )
    else:
        if read_method == "read_fhd_cal":
            warn_type += [DeprecationWarning]
            msg += [
                "Reading multiple files from file specific read methods is deprecated. "
                "Use the generic `UVCal.read` method instead."
            ]
        fhd_cal = UVCal()
        with uvtest.check_warnings(warn_type, match=msg):
            getattr(fhd_cal, read_method)(
                cal_file_multi,
                obs_file=obs_file_multi,
                settings_file=settings_file_multi,
                layout_file=layout_file_multi,
                use_future_array_shapes=True,
            )

    outfile = str(tmp_path / "outtest_FHDcal_1061311664.calfits")
    fhd_cal.write_calfits(outfile, clobber=True)
    calfits_cal = UVCal.from_file(outfile, use_future_array_shapes=True)
    assert fhd_cal == calfits_cal


@pytest.mark.parametrize(
    "cal_file,obs_file,layout_file,settings_file,message",
    [
        [
            cal_file_multi[0],
            obs_file_multi,
            layout_file_multi,
            settings_file_multi,
            "Number of obs_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi[0],
            layout_file_multi,
            settings_file_multi,
            "Number of obs_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi,
            layout_file_multi[0],
            settings_file_multi,
            "Number of layout_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi,
            layout_file_multi,
            settings_file_multi[0],
            "Number of settings_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi + obs_file_multi,
            layout_file_multi,
            settings_file_multi,
            "Number of obs_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi,
            layout_file_multi + layout_file_multi,
            settings_file_multi,
            "Number of layout_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            obs_file_multi,
            layout_file_multi,
            settings_file_multi + settings_file_multi,
            "Number of settings_files must match number of cal_files",
        ],
        [
            cal_file_multi[0],
            obs_file_multi[0],
            layout_file_multi,
            settings_file_multi,
            "Number of layout_files must match number of cal_files",
        ],
        [
            cal_file_multi[0],
            obs_file_multi[0],
            layout_file_multi[0],
            settings_file_multi,
            "Number of settings_files must match number of cal_files",
        ],
        [
            cal_file_multi,
            None,
            layout_file_multi,
            settings_file_multi,
            "obs_file parameter must be set for FHD files.",
        ],
    ],
)
@pytest.mark.parametrize("read_method", ["read", "read_fhd_cal"])
def test_break_read_multi(
    cal_file, obs_file, layout_file, settings_file, message, read_method
):
    """Test errors for different numbers of files."""
    cal = UVCal()
    if read_method == "read_fhd_cal":
        warn_type = DeprecationWarning
        msg = [
            "Reading multiple files from file specific read methods is deprecated. "
            "Use the generic `UVCal.read` method instead."
        ]
    else:
        warn_type = None
        msg = ""
    if obs_file is None and read_method == "read_fhd_cal":
        message = "Number of obs_files must match number of cal_files"
    with pytest.raises(ValueError, match=message):
        with uvtest.check_warnings(warn_type, match=msg):
            getattr(cal, read_method)(
                cal_file,
                obs_file=obs_file,
                layout_file=layout_file,
                settings_file=settings_file,
                use_future_array_shapes=True,
            )


# pyuvdata/uvcal/tests/test_uvcal.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Tests for uvcal object.

"""
import copy
import itertools
import os

import numpy as np
import pytest
from astropy.io import fits
from astropy.table import Table

import pyuvdata.tests as uvtest
import pyuvdata.utils as uvutils
from pyuvdata import UVCal
from pyuvdata.data import DATA_PATH
from pyuvdata.uvcal.uvcal import _future_array_shapes_warning

pytestmark = pytest.mark.filterwarnings(
    "ignore:telescope_location is not set. Using known values",
    "ignore:antenna_positions is not set. Using known values",
)


@pytest.fixture(scope="function")
def uvcal_data():
    """Set up some uvcal iter tests."""
    required_properties = [
        "Nfreqs",
        "Njones",
        "Ntimes",
        "Nspws",
        "Nants_data",
        "Nants_telescope",
        "wide_band",
        "antenna_names",
        "antenna_numbers",
        "ant_array",
        "telescope_name",
        "freq_array",
        "channel_width",
        "spw_array",
        "flex_spw",
        "jones_array",
        "time_array",
        "integration_time",
        "gain_convention",
        "flag_array",
        "quality_array",
        "cal_type",
        "cal_style",
        "x_orientation",
        "future_array_shapes",
        "history",
    ]
    required_parameters = ["_" + prop for prop in required_properties]

    extra_properties = [
        "telescope_location",
        "antenna_positions",
        "lst_array",
        "gain_array",
        "delay_array",
        "sky_field",
        "sky_catalog",
        "ref_antenna_name",
        "Nsources",
        "baseline_range",
        "diffuse_model",
        "input_flag_array",
        "time_range",
        "freq_range",
        "flex_spw_id_array",
        "observer",
        "git_origin_cal",
        "git_hash_cal",
        "total_quality_array",
        "extra_keywords",
        "gain_scale",
        "filename",
    ]
    extra_parameters = ["_" + prop for prop in extra_properties]

    other_properties = ["pyuvdata_version_str"]

    uv_cal_object = UVCal()

    # yields the data we need but will continue to the del call after tests
    yield (
        uv_cal_object,
        required_parameters,
        required_properties,
        extra_parameters,
        extra_properties,
        other_properties,
    )

    # some post-test object cleanup
    del uv_cal_object
    return


@pytest.fixture
def multi_spw_gain(gain_data):
    gain_obj = gain_data.copy()
    gain_obj._set_flex_spw()
    gain_obj.channel_width = (
        np.zeros(gain_obj.Nfreqs, dtype=np.float64) + gain_obj.channel_width
    )
    gain_obj.Nspws = 2
    gain_obj.flex_spw_id_array = np.concatenate(
        (
            np.ones(gain_obj.Nfreqs // 2, dtype=int),
            np.full(gain_obj.Nfreqs // 2, 2, dtype=int),
        )
    )
    gain_obj.spw_array = np.array([1, 2])
    spw2_inds = np.nonzero(gain_obj.flex_spw_id_array == 2)[0]
    spw2_chan_width = gain_obj.channel_width[0] * 2
    gain_obj.freq_array[spw2_inds] = gain_obj.freq_array[
        spw2_inds[0]
    ] + spw2_chan_width * np.arange(spw2_inds.size)
    gain_obj.channel_width[spw2_inds] = spw2_chan_width
    gain_obj.check(check_freq_spacing=True)

    yield gain_obj

    del gain_obj


@pytest.fixture
def wideband_gain(gain_data):
    gain_obj = gain_data.copy()
    gain_obj._set_wide_band()

    gain_obj.spw_array = np.array([1, 2, 3])
    gain_obj.Nspws = 3
    gain_obj.gain_array = gain_obj.gain_array[:, 0:3, :, :]
    gain_obj.flag_array = gain_obj.flag_array[:, 0:3, :, :]
    gain_obj.quality_array = gain_obj.quality_array[:, 0:3, :, :]
    gain_obj.input_flag_array = np.zeros(
        gain_obj._input_flag_array.expected_shape(gain_obj)
    ).astype(np.bool_)

    gain_obj.freq_range = np.zeros((gain_obj.Nspws, 2), dtype=gain_obj.freq_array.dtype)
    gain_obj.freq_range[0, :] = gain_obj.freq_array[[0, 2]]
    gain_obj.freq_range[1, :] = gain_obj.freq_array[[2, 4]]
    gain_obj.freq_range[2, :] = gain_obj.freq_array[[4, 6]]

    gain_obj.channel_width = None
    gain_obj.freq_array = None
    gain_obj.flex_spw_id_array = None
    gain_obj.Nfreqs = 1

    with uvtest.check_warnings(
        DeprecationWarning,
        match="The input_flag_array is deprecated and will be removed in version 2.5",
    ):
        gain_obj.check(check_freq_spacing=True)

    yield gain_obj

    del gain_obj


@pytest.fixture
def multi_spw_delay(delay_data_inputflag):
    delay_obj = delay_data_inputflag.copy()
    delay_obj.Nspws = 3
    delay_obj.spw_array = np.array([1, 2, 3])

    # copy the delay array to the second SPW
    delay_obj.delay_array = np.repeat(delay_obj.delay_array, delay_obj.Nspws, axis=1)
    delay_obj.flag_array = np.repeat(delay_obj.flag_array, delay_obj.Nspws, axis=1)
    delay_obj.input_flag_array = np.repeat(
        delay_obj.input_flag_array, delay_obj.Nspws, axis=1
    )
    delay_obj.quality_array = np.repeat(
        delay_obj.quality_array, delay_obj.Nspws, axis=1
    )

    delay_obj.freq_range = np.repeat(delay_obj.freq_range, delay_obj.Nspws, axis=0)
    # Make the second & third SPWs be contiguous with a 10 MHz range
    delay_obj.freq_range[1, 0] = delay_obj.freq_range[0, 1]
    delay_obj.freq_range[1, 1] = delay_obj.freq_range[1, 0] + 10e6
    delay_obj.freq_range[2, 0] = delay_obj.freq_range[1, 1]
    delay_obj.freq_range[2, 1] = delay_obj.freq_range[1, 1] + 10e6

    with uvtest.check_warnings(
        DeprecationWarning,
        match="The input_flag_array is deprecated and will be removed in version 2.5",
    ):
        delay_obj.check()

    yield delay_obj

    del delay_obj


def extend_jones_axis(calobj, input_flag=True, total_quality=True):
    while calobj.Njones < 4:
        new_jones = np.min(calobj.jones_array) - 1
        calobj.jones_array = np.append(calobj.jones_array, new_jones)
        calobj.Njones += 1
        if not calobj.metadata_only:
            if calobj.future_array_shapes:
                calobj.flag_array = np.concatenate(
                    (calobj.flag_array, calobj.flag_array[:, :, :, [-1]]), axis=3
                )
                if calobj.cal_type == "gain":
                    calobj.gain_array = np.concatenate(
                        (calobj.gain_array, calobj.gain_array[:, :, :, [-1]]), axis=3
                    )
                else:
                    calobj.delay_array = np.concatenate(
                        (calobj.delay_array, calobj.delay_array[:, :, :, [-1]]), axis=3
                    )
                if calobj.input_flag_array is not None:
                    calobj.input_flag_array = np.concatenate(
                        (
                            calobj.input_flag_array,
                            calobj.input_flag_array[:, :, :, [-1]],
                        ),
                        axis=3,
                    )
                calobj.quality_array = np.concatenate(
                    (calobj.quality_array, calobj.quality_array[:, :, :, [-1]]), axis=3
                )
                if calobj.total_quality_array is not None:
                    calobj.total_quality_array = np.concatenate(
                        (
                            calobj.total_quality_array,
                            calobj.total_quality_array[:, :, [-1]],
                        ),
                        axis=2,
                    )
            else:
                calobj.flag_array = np.concatenate(
                    (calobj.flag_array, calobj.flag_array[:, :, :, :, [-1]]), axis=4
                )
                if calobj.cal_type == "gain":
                    calobj.gain_array = np.concatenate(
                        (calobj.gain_array, calobj.gain_array[:, :, :, :, [-1]]), axis=4
                    )
                else:
                    calobj.delay_array = np.concatenate(
                        (calobj.delay_array, calobj.delay_array[:, :, :, :, [-1]]),
                        axis=4,
                    )
                if calobj.input_flag_array is not None:
                    calobj.input_flag_array = np.concatenate(
                        (
                            calobj.input_flag_array,
                            calobj.input_flag_array[:, :, :, :, [-1]],
                        ),
                        axis=4,
                    )
                calobj.quality_array = np.concatenate(
                    (calobj.quality_array, calobj.quality_array[:, :, :, :, [-1]]),
                    axis=4,
                )
                if calobj.total_quality_array is not None:
                    calobj.total_quality_array = np.concatenate(
                        (
                            calobj.total_quality_array,
                            calobj.total_quality_array[:, :, :, [-1]],
                        ),
                        axis=3,
                    )
    if not calobj.metadata_only:
        if calobj.input_flag_array is None and input_flag:
            calobj.input_flag_array = calobj.flag_array
        if calobj.total_quality_array is None and total_quality:
            calobj.total_quality_array = np.ones(
                calobj._total_quality_array.expected_shape(calobj)
            )


def test_parameter_iter(uvcal_data):
    """Test expected parameters."""
    (uv_cal_object, required_parameters, _, extra_parameters, _, _) = uvcal_data
    all_params = []
    for prop in uv_cal_object:
        all_params.append(prop)
    for a in required_parameters + extra_parameters:
        assert a in all_params, (
            "expected attribute " + a + " not returned in object iterator"
        )


def test_required_parameter_iter(uvcal_data):
    """Test expected required parameters."""
    (uv_cal_object, required_parameters, _, _, _, _) = uvcal_data
    # at first it's a metadata_only object, so need to modify required_parameters
    required = []
    for prop in uv_cal_object.required():
        required.append(prop)
    expected_required = copy.copy(required_parameters)
    expected_required.remove("_quality_array")
    expected_required.remove("_flag_array")
    for a in expected_required:
        assert a in required, (
            "expected attribute " + a + " not returned in required iterator"
        )

    uv_cal_object.quality_array = 1
    uv_cal_object.flag_array = 1
    required = []
    for prop in uv_cal_object.required():
        required.append(prop)
    for a in required_parameters:
        assert a in required, (
            "expected attribute " + a + " not returned in required iterator"
        )


def test_unexpected_parameters(uvcal_data):
    """Test for extra parameters."""
    (uv_cal_object, required_parameters, _, extra_parameters, _, _) = uvcal_data
    expected_parameters = required_parameters + extra_parameters
    attributes = [i for i in uv_cal_object.__dict__.keys() if i[0] == "_"]
    for a in attributes:
        assert a in expected_parameters, "unexpected parameter " + a + " found in UVCal"


def test_unexpected_attributes(uvcal_data):
    """Test for extra attributes."""
    (
        uv_cal_object,
        _,
        required_properties,
        _,
        extra_properties,
        other_properties,
    ) = uvcal_data
    expected_attributes = required_properties + extra_properties + other_properties
    attributes = [i for i in uv_cal_object.__dict__.keys() if i[0] != "_"]
    for a in attributes:
        assert a in expected_attributes, "unexpected attribute " + a + " found in UVCal"


def test_properties(uvcal_data):
    """Test that properties can be get and set properly."""
    (
        uv_cal_object,
        required_parameters,
        required_properties,
        extra_parameters,
        extra_properties,
        _,
    ) = uvcal_data
    prop_dict = dict(
        list(
            zip(
                required_properties + extra_properties,
                required_parameters + extra_parameters,
            )
        )
    )
    for k, v in prop_dict.items():
        rand_num = np.random.rand()
        setattr(uv_cal_object, k, rand_num)
        this_param = getattr(uv_cal_object, v)
        try:
            assert rand_num == this_param.value
        except AssertionError:
            print("setting {prop_name} to a random number failed".format(prop_name=k))
            raise


def test_equality(gain_data):
    """Basic equality test"""
    assert gain_data == gain_data


def test_check(gain_data):
    """Test that parameter checks run properly"""
    assert gain_data.check()


def test_check_flag_array(gain_data):
    gain_data.flag_array = np.ones((gain_data.flag_array.shape), dtype=int)

    with pytest.raises(
        ValueError, match="UVParameter _flag_array is not the appropriate type."
    ):
        gain_data.check()


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_future_array_shape(caltype, gain_data, delay_data_inputflag):
    """Convert to future shapes and check. Convert back and test for equality."""

    if caltype == "gain":
        calobj = gain_data
        calobj.total_quality_array = np.ones(
            (calobj._total_quality_array.expected_shape(calobj))
        )
        calobj.input_flag_array = np.zeros(
            calobj._input_flag_array.expected_shape(calobj)
        ).astype(np.bool_)
    else:
        calobj = delay_data_inputflag
        calobj.total_quality_array = np.ones(
            (calobj._total_quality_array.expected_shape(calobj))
        )
        calobj.input_flag_array = np.zeros(
            calobj._input_flag_array.expected_shape(calobj)
        ).astype(np.bool_)

    calobj2 = calobj.copy()
    # test the no-op
    calobj2.use_future_array_shapes()
    assert calobj == calobj2

    with uvtest.check_warnings(
        DeprecationWarning,
        match="This method will be removed in version 3.0 when the current array "
        "shapes are no longer supported.",
    ):
        calobj.use_current_array_shapes()
    with uvtest.check_warnings(
        DeprecationWarning,
        match="The input_flag_array is deprecated and will be removed in version 2.5",
    ):
        calobj.check()

    calobj3 = calobj.copy()
    # test the no-op
    calobj.use_current_array_shapes()
    assert calobj3 == calobj

    warn_type = [DeprecationWarning]
    warn_msg = ["The input_flag_array is deprecated and will be removed in version 2.5"]

    if caltype == "delay":
        warn_type += [UserWarning]
        warn_msg += [
            "When converting a delay-style cal to future array shapes the "
            "flag_array (and input_flag_array if it exists) must drop the "
            "frequency axis so that it will be the same shape as the delay_array."
        ]
    with uvtest.check_warnings(warn_type, match=warn_msg):
        calobj.use_future_array_shapes()
        calobj.check()

    assert calobj == calobj2


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_future_array_shape_errors(
    caltype,
    gain_data,
    delay_data_inputflag,
    multi_spw_gain,
    multi_spw_delay,
    wideband_gain,
):
    if caltype == "gain":
        calobj = gain_data
        calobj2 = calobj.copy()

        calobj_multi_spw = multi_spw_gain

        calobj_wideband = wideband_gain
        calobj_wideband.select(spws=1)
        with pytest.raises(
            ValueError,
            match="Cannot use current array shapes if cal_style is not 'delay' and "
            "wide_band is True.",
        ):
            with uvtest.check_warnings(
                DeprecationWarning,
                "This method will be removed in version 3.0 when the current array "
                "shapes are no longer supported.",
            ):
                calobj_wideband.use_current_array_shapes()

    else:
        calobj = delay_data_inputflag

        calobj_multi_spw = multi_spw_delay

        with pytest.raises(
            ValueError, match="Cannot use current array shapes if Nspws > 1."
        ):
            with uvtest.check_warnings(
                DeprecationWarning,
                match="This method will be removed in version 3.0 when the current "
                "array shapes are no longer supported.",
            ):
                calobj_multi_spw.use_current_array_shapes()

    calobj.integration_time[-1] = calobj.integration_time[0] * 2.0
    if caltype == "delay":
        calobj.Nfreqs = 2
        with uvtest.check_warnings(
            DeprecationWarning,
            match=[
                "Nfreqs will be required to be 1 for wide_band cals",
                "The input_flag_array is deprecated and will be removed in version 2.5",
            ],
        ):
            calobj.check()
        calobj.Nfreqs = 1
    else:
        calobj.check()

    with pytest.raises(
        ValueError, match="integration_time parameter contains multiple unique values"
    ):
        with uvtest.check_warnings(
            UserWarning,
            match="When converting a delay-style cal to future array shapes",
        ):
            calobj.use_current_array_shapes()

    with pytest.raises(
        ValueError, match="The integration times are variable. The calfits format"
    ):
        with uvtest.check_warnings(
            DeprecationWarning,
            match="Nfreqs will be required to be 1 for wide_band cals",
        ):
            calobj.write_calfits("foo")

    if caltype == "gain":
        calobj2.channel_width[-1] = calobj2.channel_width[0] * 2.0
        calobj2.check()

        with pytest.raises(
            ValueError, match="channel_width parameter contains multiple unique values"
        ):
            with uvtest.check_warnings(
                UserWarning,
                match="When converting a delay-style cal to future array shapes",
            ):
                calobj2.use_current_array_shapes()

        with pytest.raises(ValueError, match="The frequencies are not evenly spaced"):
            calobj2._check_freq_spacing()


def test_unknown_telescopes(gain_data, tmp_path):
    calobj = gain_data

    write_file = str(tmp_path / "test.calfits")
    write_file2 = str(tmp_path / "test2.calfits")
    calobj.write_calfits(write_file)
    with fits.open(write_file, memmap=True) as hdu_list:
        hdunames = uvutils._fits_indexhdus(hdu_list)
        primary_hdu = hdu_list[0]
        primary_hdr = primary_hdu.header.copy()
        del primary_hdr["ARRAYX"]
        del primary_hdr["ARRAYY"]
        del primary_hdr["ARRAYZ"]
        del primary_hdr["LAT"]
        del primary_hdr["LON"]
        del primary_hdr["ALT"]
        primary_hdr["TELESCOP"] = "foo"
        primary_hdu.header = primary_hdr

        ant_hdu = hdu_list[hdunames["ANTENNAS"]]

        table = Table(ant_hdu.data)
        del table["ANTXYZ"]
        ant_hdu = fits.BinTableHDU(table)
        ant_hdu.header["EXTNAME"] = "ANTENNAS"

        hdulist = fits.HDUList([primary_hdu, ant_hdu])

        hdulist.writeto(write_file2)
        hdulist.close()

    with pytest.raises(
        ValueError, match="Required UVParameter _antenna_positions has not been set."
    ):
        with uvtest.check_warnings(
            [UserWarning], match=["Telescope foo is not in known_telescopes"]
        ):
            UVCal.from_file(write_file2, use_future_array_shapes=True)
    with uvtest.check_warnings(
        [UserWarning], match=["Telescope foo is not in known_telescopes"]
    ):
        UVCal.from_file(write_file2, use_future_array_shapes=True, run_check=False)


def test_nants_data_telescope_larger(gain_data):
    # make sure it's okay for Nants_telescope to be strictly greater than Nants_data
    gain_data.Nants_telescope += 1
    # add dummy information for "new antenna" to pass object check
    gain_data.antenna_names = np.concatenate((gain_data.antenna_names, ["dummy_ant"]))
    gain_data.antenna_numbers = np.concatenate((gain_data.antenna_numbers, [20]))
    gain_data.antenna_positions = np.concatenate(
        (gain_data.antenna_positions, np.zeros((1, 3), dtype=float))
    )
    assert gain_data.check()


def test_ant_array_not_in_antnums(gain_data):
    # make sure an error is raised if antennas with data not in antenna_numbers
    # remove antennas from antenna_names & antenna_numbers by hand
    gain_data.antenna_names = gain_data.antenna_names[1:]
    gain_data.antenna_numbers = gain_data.antenna_numbers[1:]
    gain_data.antenna_positions = gain_data.antenna_positions[1:, :]
    gain_data.Nants_telescope = gain_data.antenna_numbers.size
    with pytest.raises(ValueError) as cm:
        gain_data.check()
    assert str(cm.value).startswith(
        "All antennas in ant_array must be in antenna_numbers"
    )


def test_set_gain(gain_data, delay_data):
    delay_data._set_gain()
    assert delay_data._gain_array.required
    assert not delay_data._delay_array.required
    assert delay_data._gain_array.form == delay_data._flag_array.form
    assert delay_data._gain_array.form == delay_data._quality_array.form


def test_set_delay(gain_data):
    gain_data._set_delay()
    assert gain_data._delay_array.required
    assert not gain_data._gain_array.required
    assert gain_data._gain_array.form == gain_data._flag_array.form
    assert gain_data._delay_array.form == gain_data._quality_array.form


def test_set_unknown(gain_data):
    with uvtest.check_warnings(
        DeprecationWarning,
        match="Setting the cal_type to 'unknown' is deprecated. This will become an "
        "error in version 2.5",
    ):
        gain_data._set_unknown_cal_type()

    with uvtest.check_warnings(
        DeprecationWarning,
        match="The 'unknown' cal_type is deprecated and will be removed in version "
        "2.5",
    ):
        gain_data.check()

    assert not gain_data._delay_array.required
    assert not gain_data._gain_array.required
    assert gain_data._gain_array.form == gain_data._flag_array.form
    assert gain_data._gain_array.form == gain_data._quality_array.form


def test_set_sky(gain_data):
    gain_data._set_sky()
    assert gain_data._sky_field.required
    assert gain_data._sky_catalog.required
    assert gain_data._ref_antenna_name.required


def test_set_redundant(gain_data):
    gain_data._set_redundant()
    assert not gain_data._sky_field.required
    assert not gain_data._sky_catalog.required
    assert not gain_data._ref_antenna_name.required


def test_convert_filetype(gain_data):
    # error testing
    with pytest.raises(ValueError, match="filetype must be calfits."):
        gain_data._convert_to_filetype("uvfits")


def test_error_metadata_only_write(gain_data, tmp_path):
    calobj = gain_data.copy(metadata_only=True)

    out_file = os.path.join(tmp_path, "outtest.calfits")
    with pytest.raises(ValueError, match="Cannot write out metadata only objects to a"):
        calobj.write_calfits(out_file)


def test_flexible_spw(gain_data):
    calobj = gain_data

    # check that this check passes on non-flex_spw objects
    calobj._check_flex_spw_contiguous()

    # check warning if flex_spw_id_array is not set
    calobj.flex_spw_id_array = None
    with uvtest.check_warnings(
        DeprecationWarning,
        match="flex_spw_id_array is not set. It will be required starting in version "
        "3.0 for non-wide-band objects",
    ):
        calobj.check()

    # first just make one spw and check that object still passes check
    calobj._set_flex_spw()
    calobj.channel_width = (
        np.zeros(calobj.Nfreqs, dtype=np.float64) + calobj.channel_width
    )
    calobj.flex_spw_id_array = np.zeros(calobj.Nfreqs, dtype=int)
    calobj.check()

    # now make two
    calobj.Nspws = 2
    calobj.spw_array = np.array([1, 2])

    calobj.flex_spw_id_array = np.concatenate(
        (
            np.ones(calobj.Nfreqs // 2, dtype=int),
            np.full(calobj.Nfreqs // 2, 5, dtype=int),
        )
    )
    with pytest.raises(
        ValueError,
        match="All values in the flex_spw_id_array must exist in the spw_array.",
    ):
        calobj.check()

    calobj.flex_spw_id_array = np.concatenate(
        (
            np.ones(calobj.Nfreqs // 2, dtype=int),
            np.full(calobj.Nfreqs // 2, 2, dtype=int),
        )
    )
    calobj.check()

    calobj._check_flex_spw_contiguous()

    # now mix them up
    calobj.Nspws = 2
    calobj.flex_spw_id_array = np.concatenate(
        (
            np.ones(2, dtype=int),
            np.full(2, 2, dtype=int),
            np.ones(2, dtype=int),
            np.full(2, 2, dtype=int),
            np.ones(2, dtype=int),
        )
    )
    calobj.spw_array = np.array([1, 2])
    calobj.check()

    with pytest.raises(
        ValueError, match="Channels from different spectral windows are interspersed"
    ):
        calobj._check_flex_spw_contiguous()


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("convention", ["minus", "plus"])
@pytest.mark.parametrize("same_freqs", [True, False])
def test_convert_to_gain(future_shapes, convention, same_freqs, delay_data_inputflag):
    delay_obj = delay_data_inputflag
    delay_obj.Nfreqs = 30
    delay_obj.freq_array = np.arange(delay_obj.Nfreqs) * 1e6 + 1e8
    delay_obj.channel_width = np.full(delay_obj.Nfreqs, 1e6)
    with uvtest.check_warnings(
        DeprecationWarning,
        match=[
            "Nfreqs will be required to be 1 for wide_band cals (including all "
            "delay cals) starting in version 3.0",
            "The input_flag_array is deprecated and will be removed in version 2.5",
        ],
    ):
        delay_obj.check()
    freq_array = copy.deepcopy(delay_obj.freq_array)
    channel_width = delay_obj.channel_width
    if not same_freqs:
        # try with different number and same number but different values
        if convention == "minus":
            freq_array = freq_array[0 : (delay_obj.Nfreqs // 2)]
            channel_width = channel_width[0 : (delay_obj.Nfreqs // 2)]
        else:
            freq_array[2] = freq_array[2] + 1e6

    # test passing a 1 element array for channel width
    if not future_shapes:
        delay_obj.use_current_array_shapes()
        if convention == "minus":
            channel_width = np.asarray([delay_obj.channel_width])
        else:
            channel_width = delay_obj.channel_width
    else:
        assert channel_width.size == freq_array.size

    new_gain_obj = delay_obj.copy()
    tqa_size = new_gain_obj.delay_array.shape[1:]
    new_gain_obj.total_quality_array = np.ones(tqa_size)

    new_gain_obj2 = new_gain_obj.copy()

    if not future_shapes and not same_freqs:
        with uvtest.check_warnings(
            [UserWarning, DeprecationWarning],
            match=[
                "Existing flag array has a frequency axis of length > 1 but "
                "frequencies do not match freq_array. The existing flag array "
                "(and input_flag_array if it exists) will be collapsed using "
                "the `pyuvdata.utils.and_collapse` function which will only "
                "flag an antpol-time if all of the frequecies are flagged for "
                "that antpol-time. Then it will be broadcast to all the new "
                "frequencies. To preserve the original flag information, "
                "create a UVFlag object from this cal object before this "
                "operation.",
                "The input_flag_array is deprecated and will be removed in version 2.5",
            ],
        ):
            new_gain_obj.convert_to_gain(
                freq_array=freq_array,
                channel_width=channel_width,
                delay_convention=convention,
            )
    else:
        new_gain_obj.convert_to_gain(
            freq_array=freq_array,
            channel_width=channel_width,
            delay_convention=convention,
        )
    assert np.isclose(
        np.max(np.absolute(new_gain_obj.gain_array)),
        1.0,
        rtol=new_gain_obj._gain_array.tols[0],
        atol=new_gain_obj._gain_array.tols[1],
    )
    assert np.isclose(
        np.min(np.absolute(new_gain_obj.gain_array)),
        1.0,
        rtol=new_gain_obj._gain_array.tols[0],
        atol=new_gain_obj._gain_array.tols[1],
    )

    if convention == "minus":
        conv = -1
    else:
        conv = 1
    if future_shapes:
        assert np.allclose(
            np.angle(new_gain_obj.gain_array[:, 10, :, :]) % (2 * np.pi),
            (conv * 2 * np.pi * delay_obj.delay_array[:, 0, :, :] * freq_array[10])
            % (2 * np.pi),
            rtol=new_gain_obj._gain_array.tols[0],
            atol=new_gain_obj._gain_array.tols[1],
        )
        assert np.allclose(
            delay_obj.quality_array,
            new_gain_obj.quality_array[:, 10, :, :],
            rtol=new_gain_obj._quality_array.tols[0],
            atol=new_gain_obj._quality_array.tols[1],
        )

    else:
        assert np.allclose(
            np.angle(new_gain_obj.gain_array[:, :, 10, :, :]) % (2 * np.pi),
            (conv * 2 * np.pi * delay_obj.delay_array[:, :, 0, :, :] * freq_array[10])
            % (2 * np.pi),
            rtol=new_gain_obj._gain_array.tols[0],
            atol=new_gain_obj._gain_array.tols[1],
        )
        assert np.allclose(
            delay_obj.quality_array,
            new_gain_obj.quality_array[:, :, 10, :, :],
            rtol=new_gain_obj._quality_array.tols[0],
            atol=new_gain_obj._quality_array.tols[1],
        )

    assert new_gain_obj.history == (
        delay_obj.history + "  Converted from delays to gains using pyuvdata."
    )

    if same_freqs:
        with uvtest.check_warnings(
            DeprecationWarning,
            match=[
                "In version 3.0 and later freq_array and channel_width will be",
                "The input_flag_array is deprecated and will be removed in version 2.5",
            ],
        ):
            new_gain_obj2.convert_to_gain(delay_convention=convention)

        assert new_gain_obj == new_gain_obj2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
def test_convert_to_gain_errors(gain_data, delay_data_inputflag, multi_spw_delay):
    delay_obj = delay_data_inputflag
    gain_obj = gain_data

    delay_obj.Nfreqs = 30
    delay_obj.freq_array = np.arange(delay_obj.Nfreqs) * 1e6 + 1e8
    delay_obj.channel_width = np.full(delay_obj.Nfreqs, 1e6)
    delay_obj_current = delay_obj.copy()
    delay_obj_current.use_current_array_shapes()

    with pytest.raises(
        ValueError, match="freq_array contains values outside the freq_range."
    ):
        delay_obj_current.convert_to_gain(
            freq_array=np.asarray([50e6, 60e6]),
            channel_width=delay_obj_current.channel_width,
        )

    with pytest.raises(
        ValueError, match="freq_array parameter must be a one dimensional array"
    ):
        delay_obj_current.convert_to_gain(
            freq_array=delay_obj_current.freq_array,
            channel_width=delay_obj_current.channel_width,
        )

    with pytest.raises(
        ValueError,
        match="This object is using the current array shapes, so the "
        "channel_width parameter must be a scalar value",
    ):
        delay_obj_current.convert_to_gain(
            freq_array=delay_obj_current.freq_array[0, :],
            channel_width=(
                np.zeros(delay_obj_current.Nfreqs, dtype=float)
                + delay_obj_current.channel_width
            ),
        )

    with pytest.raises(
        ValueError,
        match="convert_to_gain currently does not support multiple spectral windows",
    ):
        multi_spw_delay.convert_to_gain()

    with pytest.raises(
        ValueError, match="delay_convention can only be 'minus' or 'plus'"
    ):
        delay_obj_current.convert_to_gain(delay_convention="bogus")

    with pytest.raises(ValueError, match="The data is already a gain cal_type."):
        gain_obj.convert_to_gain()

    with uvtest.check_warnings(
        DeprecationWarning,
        match="Setting the cal_type to 'unknown' is deprecated. This will become an "
        "error in version 2.5",
    ):
        gain_obj._set_unknown_cal_type()
    with pytest.raises(ValueError, match="cal_type is unknown, cannot convert to gain"):
        gain_obj.convert_to_gain()

    delay_obj_current.freq_array = None
    delay_obj_current.channel_width = None
    with pytest.raises(
        ValueError, match="freq_array and channel_width must be provided"
    ):
        delay_obj_current.convert_to_gain()

    with pytest.raises(
        ValueError,
        match="This object is using the future array shapes, so the "
        "channel_width parameter be an array shaped like the freq_array",
    ):
        delay_obj.convert_to_gain(
            freq_array=delay_obj.freq_array, channel_width=delay_obj.channel_width[0]
        )


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning)
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select_antennas(
    caltype, future_shapes, gain_data, delay_data_inputflag, tmp_path
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    old_history = calobj.history
    ants_to_keep = np.array([65, 96, 9, 97, 89, 22, 20, 72])
    calobj2.select(antenna_nums=ants_to_keep)

    assert len(ants_to_keep) == calobj2.Nants_data
    for ant in ants_to_keep:
        assert ant in calobj2.ant_array
    for ant in calobj2.ant_array:
        assert ant in ants_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific antennas using pyuvdata.",
        calobj2.history,
    )

    # now test using antenna_names to specify antennas to keep
    ants_to_keep = np.array(sorted(ants_to_keep))
    ant_names = []
    for a in ants_to_keep:
        ind = np.where(calobj.antenna_numbers == a)[0][0]
        ant_names.append(calobj.antenna_names[ind])

    calobj3 = calobj.select(antenna_names=ant_names, inplace=False)

    assert calobj2 == calobj3

    # check for errors associated with antennas not included in data, bad names
    # or providing numbers and names
    with pytest.raises(
        ValueError,
        match=f"Antenna number {np.max(calobj.ant_array)+1} "
        "is not present in the array",
    ):
        calobj.select(antenna_nums=np.max(calobj.ant_array) + np.arange(1, 3))

    with pytest.raises(
        ValueError, match="Antenna name test1 is not present in the antenna_names array"
    ):
        calobj.select(antenna_names=["test1"])
    with pytest.raises(
        ValueError, match="Only one of antenna_nums and antenna_names can be provided."
    ):
        calobj.select(antenna_nums=ants_to_keep, antenna_names=ant_names)

    # check that write_calfits works with Nants_data < Nants_telescope
    write_file_calfits = str(tmp_path / "select_test.calfits")
    calobj2.write_calfits(write_file_calfits, clobber=True)

    # check that reading it back in works too
    new_calobj = UVCal.from_file(
        write_file_calfits, use_future_array_shapes=future_shapes
    )
    assert calobj2 == new_calobj

    # check that total_quality_array is handled properly when present
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )
    warn_type = [UserWarning]
    msg = ["Cannot preserve total_quality_array"]
    if caltype == "delay":
        warn_type += [DeprecationWarning]
        msg += ["The input_flag_array is deprecated and will be removed in version 2.5"]
    with uvtest.check_warnings(warn_type, match=msg):
        calobj.select(antenna_names=ant_names, inplace=True)
    assert calobj.total_quality_array is None


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select_times(
    future_shapes, caltype, gain_data, delay_data_inputflag, tmp_path
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    old_history = calobj.history
    times_to_keep = calobj.time_array[2:5]

    calobj2.select(times=times_to_keep)

    assert len(times_to_keep) == calobj2.Ntimes
    for t in times_to_keep:
        assert t in calobj2.time_array
    for t in np.unique(calobj2.time_array):
        assert t in times_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific times using pyuvdata.",
        calobj2.history,
    )

    write_file_calfits = str(tmp_path / "select_test.calfits")
    # test writing calfits with only one time
    calobj2 = calobj.copy()
    times_to_keep = calobj.time_array[[1]]
    calobj2.select(times=times_to_keep)
    calobj2.write_calfits(write_file_calfits, clobber=True)

    # check for errors associated with times not included in data
    pytest.raises(
        ValueError,
        calobj.select,
        times=[np.min(calobj.time_array) - calobj.integration_time],
    )

    # check for warnings and errors associated with unevenly spaced times
    calobj2 = calobj.copy()
    warn_type = [UserWarning]
    msg = ["Selected times are not evenly spaced"]
    if caltype == "delay":
        warn_type += [DeprecationWarning]
        msg += ["The input_flag_array is deprecated and will be removed in version 2.5"]
    with uvtest.check_warnings(warn_type, match=msg):
        calobj2.select(times=calobj2.time_array[[0, 2, 3]])
    pytest.raises(ValueError, calobj2.write_calfits, write_file_calfits)


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.filterwarnings("ignore:Nfreqs will be required to be 1 for wide_band")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select_frequencies(
    future_shapes, caltype, gain_data, delay_data_inputflag, tmp_path
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag
        calobj.freq_array = gain_data.freq_array
        calobj.channel_width = gain_data.channel_width
        calobj.Nfreqs = gain_data.Nfreqs

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    old_history = calobj.history

    if future_shapes:
        freqs_to_keep = calobj.freq_array[np.arange(4, 8)]
    else:
        freqs_to_keep = calobj.freq_array[0, np.arange(4, 8)]

    # add dummy total_quality_array
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )
    calobj2.total_quality_array = np.zeros(
        calobj2._total_quality_array.expected_shape(calobj2)
    )

    calobj2.select(frequencies=freqs_to_keep)

    assert len(freqs_to_keep) == calobj2.Nfreqs
    for f in freqs_to_keep:
        assert f in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in freqs_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific frequencies using pyuvdata.",
        calobj2.history,
    )

    write_file_calfits = str(tmp_path / "select_test.calfits")
    # test writing calfits with only one frequency
    calobj2 = calobj.copy()
    if future_shapes:
        freqs_to_keep = calobj.freq_array[5]
    else:
        freqs_to_keep = calobj.freq_array[0, 5]
    calobj2.select(frequencies=freqs_to_keep)
    calobj2.write_calfits(write_file_calfits, clobber=True)

    # test writing calfits with frequencies spaced by more than the channel width
    calobj2 = calobj.copy()
    if future_shapes:
        freqs_to_keep = calobj.freq_array[[0, 2, 4, 6, 8]]
    else:
        freqs_to_keep = calobj.freq_array[0, [0, 2, 4, 6, 8]]
    warn_type = [UserWarning]
    msg = ["Selected frequencies are not contiguous."]
    if caltype == "delay":
        warn_type += [DeprecationWarning]
        msg += ["The input_flag_array is deprecated and will be removed in version 2.5"]
        if future_shapes:
            warn_type += [DeprecationWarning]
            msg += ["Nfreqs will be required to be 1 for wide_band cals"]
    with uvtest.check_warnings(warn_type, match=msg):
        calobj2.select(frequencies=freqs_to_keep)
    calobj2.write_calfits(write_file_calfits, clobber=True)

    # check for errors associated with frequencies not included in data
    pytest.raises(
        ValueError,
        calobj.select,
        frequencies=[np.max(calobj.freq_array) + calobj.channel_width],
    )

    # check for warnings and errors associated with unevenly spaced frequencies
    calobj2 = calobj.copy()
    if future_shapes:
        freqs_to_keep = calobj2.freq_array[[0, 5, 6]]
    else:
        freqs_to_keep = calobj2.freq_array[0, [0, 5, 6]]
    warn_type = [UserWarning]
    msg = ["Selected frequencies are not evenly spaced."]
    if caltype == "delay":
        warn_type += [DeprecationWarning]
        msg += ["The input_flag_array is deprecated and will be removed in version 2.5"]
        if future_shapes:
            warn_type += [DeprecationWarning]
            msg += ["Nfreqs will be required to be 1 for wide_band cals"]
    with uvtest.check_warnings(warn_type, match=msg):
        calobj2.select(frequencies=freqs_to_keep)

    with pytest.raises(
        ValueError,
        match="Frequencies are not evenly spaced or have differing values of channel",
    ):
        calobj2.write_calfits(write_file_calfits)


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:" + _future_array_shapes_warning)
@pytest.mark.parametrize("future_shapes", [True, False])
def test_select_frequencies_multispw(future_shapes, multi_spw_gain, tmp_path):
    calobj = multi_spw_gain

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()
    old_history = calobj.history

    if future_shapes:
        freqs_to_keep = calobj.freq_array[np.arange(4, 8)]
    else:
        freqs_to_keep = calobj.freq_array[0, np.arange(4, 8)]

    # add dummy total_quality_array
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )
    calobj2.total_quality_array = np.zeros(
        calobj2._total_quality_array.expected_shape(calobj2)
    )

    # add dummy input_flag_array
    calobj.input_flag_array = np.zeros(
        calobj._input_flag_array.expected_shape(calobj)
    ).astype(np.bool_)
    calobj2.input_flag_array = np.zeros(
        calobj2._input_flag_array.expected_shape(calobj2)
    ).astype(np.bool_)

    # add freq_range
    if future_shapes:
        calobj2.freq_range = np.zeros(
            (calobj2.Nspws, 2), dtype=calobj2.freq_array.dtype
        )
        for index, spw in enumerate(calobj2.spw_array):
            spw_inds = np.nonzero(calobj2.flex_spw_id_array == spw)[0]
            calobj2.freq_range[index, 0] = np.min(
                np.squeeze(calobj2.freq_array)[spw_inds]
            )
            calobj2.freq_range[index, 1] = np.max(
                np.squeeze(calobj2.freq_array)[spw_inds]
            )
        calobj2.check()

    calobj2.select(frequencies=freqs_to_keep)

    assert len(freqs_to_keep) == calobj2.Nfreqs
    for f in freqs_to_keep:
        assert f in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in freqs_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific frequencies using pyuvdata.",
        calobj2.history,
    )

    # test calfits write error
    write_file_calfits = str(tmp_path / "select_test.calfits")
    with pytest.raises(
        ValueError,
        match="The calfits format does not support multiple spectral windows",
    ):
        calobj2.write_calfits(write_file_calfits, clobber=True)

    # test that we can write to calfits if select to only one spw
    calobj2 = calobj.copy()

    if future_shapes:
        freqs_to_keep = calobj.freq_array[np.arange(5)]
    else:
        freqs_to_keep = calobj.freq_array[0, np.arange(5)]

    calobj2.select(frequencies=freqs_to_keep)
    calobj2.check()

    assert len(freqs_to_keep) == calobj2.Nfreqs
    for f in freqs_to_keep:
        assert f in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in freqs_to_keep

    assert calobj2.Nspws == 1
    calobj2.write_calfits(write_file_calfits, clobber=True)

    calobj3 = calobj.select(spws=[1], inplace=False)
    assert calobj3 == calobj2
    with uvtest.check_warnings(
        [UserWarning, DeprecationWarning],
        match=[
            "Cannot select on spws if Nspws=1.",
            "The input_flag_array is deprecated and will be removed in version 2.5",
        ],
    ):
        calobj3.select(spws=1)
    assert calobj3 == calobj2

    calobj3 = UVCal.from_file(write_file_calfits, use_future_array_shapes=future_shapes)

    calobj2.flex_spw = False
    calobj2._flex_spw_id_array.required = False
    calobj2.flex_spw_id_array = np.zeros(calobj2.Nfreqs, dtype=int)
    calobj2.spw_array = np.array([0])
    if not future_shapes:
        calobj2._channel_width.form = ()
        calobj2.channel_width = calobj2.channel_width[0]
    calobj2.check()

    assert calobj3 == calobj2


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.filterwarnings("ignore:Nfreqs will be required to be 1 for wide_band cals")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("future_shapes", [True, False])
def test_select_freq_chans(caltype, future_shapes, gain_data, delay_data_inputflag):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    old_history = calobj.history
    chans_to_keep = np.arange(4, 8)

    if caltype == "delay":
        with pytest.raises(
            ValueError,
            match="Cannot select on frequencies because this is a wide_band object "
            "with no freq_array.",
        ):
            calobj.select(freq_chans=chans_to_keep)

    if caltype == "delay":
        calobj.freq_array = gain_data.freq_array
        calobj.channel_width = gain_data.channel_width
        calobj.Nfreqs = gain_data.Nfreqs

    if not future_shapes:
        calobj.use_current_array_shapes()

    # add dummy total_quality_array
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )

    calobj2 = calobj.copy()

    calobj2.select(freq_chans=chans_to_keep)

    assert len(chans_to_keep) == calobj2.Nfreqs
    if future_shapes:
        obj_freqs = calobj.freq_array
    else:
        obj_freqs = calobj.freq_array[0, :]
    for chan in chans_to_keep:
        assert obj_freqs[chan] in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in obj_freqs

    assert uvutils._check_histories(
        old_history + "  Downselected to specific frequencies using pyuvdata.",
        calobj2.history,
    )

    # Test selecting both channels and frequencies
    if future_shapes:
        obj_freqs = calobj.freq_array
    else:
        obj_freqs = calobj.freq_array[0, :]
    freqs_to_keep = obj_freqs[np.arange(7, 10)]  # Overlaps with chans
    all_chans_to_keep = np.arange(4, 10)

    calobj2 = calobj.copy()
    calobj2.select(frequencies=freqs_to_keep, freq_chans=chans_to_keep)

    assert len(all_chans_to_keep) == calobj2.Nfreqs
    for chan in all_chans_to_keep:
        assert obj_freqs[chan] in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in obj_freqs[all_chans_to_keep]


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select_spws_wideband(caltype, multi_spw_delay, wideband_gain, tmp_path):
    if caltype == "gain":
        calobj = wideband_gain
    else:
        calobj = multi_spw_delay

    calobj2 = calobj.copy()

    old_history = calobj.history

    # add dummy total_quality_array
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )
    calobj2.total_quality_array = np.zeros(
        calobj2._total_quality_array.expected_shape(calobj2)
    )

    spws_to_keep = [2, 3]
    calobj2.select(spws=spws_to_keep)

    assert len(spws_to_keep) == calobj2.Nspws
    for spw in spws_to_keep:
        assert spw in calobj2.spw_array
    for spw in calobj2.spw_array:
        assert spw in spws_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific spectral windows using pyuvdata.",
        calobj2.history,
    )

    # check for errors associated with spws not included in data
    with pytest.raises(
        ValueError, match="SPW number 5 is not present in the spw_array"
    ):
        calobj.select(spws=[5])


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize(
    "jones_to_keep", ([-5, -6], ["xx", "yy"], ["nn", "ee"], [[-5, -6]])
)
def test_select_polarizations(
    future_shapes, caltype, jones_to_keep, gain_data, delay_data_inputflag, tmp_path
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    # add more jones terms to allow for better testing of selections
    extend_jones_axis(calobj)

    assert calobj.check()
    calobj2 = calobj.copy()

    old_history = calobj.history

    calobj2.select(jones=jones_to_keep)

    if isinstance(jones_to_keep[0], list):
        jones_to_keep = jones_to_keep[0]
    assert len(jones_to_keep) == calobj2.Njones
    for j in jones_to_keep:
        if isinstance(j, int):
            assert j in calobj2.jones_array
        else:
            assert (
                uvutils.jstr2num(j, x_orientation=calobj2.x_orientation)
                in calobj2.jones_array
            )
    for j in np.unique(calobj2.jones_array):
        if isinstance(jones_to_keep[0], int):
            assert j in jones_to_keep
        else:
            assert j in uvutils.jstr2num(
                jones_to_keep, x_orientation=calobj2.x_orientation
            )

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific jones polarization terms "
        "using pyuvdata.",
        calobj2.history,
    )

    # check for errors associated with polarizations not included in data
    pytest.raises(ValueError, calobj2.select, jones=[-3, -4])

    # check for warnings and errors associated with unevenly spaced polarizations
    warn_type = [UserWarning, DeprecationWarning]
    msg = [
        "Selected jones polarization terms are not evenly spaced",
        "The input_flag_array is deprecated and will be removed in version 2.5",
    ]
    with uvtest.check_warnings(warn_type, match=msg):
        calobj.select(jones=calobj.jones_array[[0, 1, 3]])
    write_file_calfits = os.path.join(tmp_path, "select_test.calfits")
    pytest.raises(ValueError, calobj.write_calfits, write_file_calfits)


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:Nfreqs will be required to be 1 for wide_band")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select(future_shapes, caltype, gain_data, delay_data_inputflag):
    # now test selecting along all axes at once
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag
        calobj.freq_array = gain_data.freq_array
        calobj.channel_width = gain_data.channel_width
        calobj.Nfreqs = gain_data.Nfreqs

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    old_history = calobj.history

    ants_to_keep = np.array([10, 89, 43, 9, 80, 96, 64])
    if future_shapes:
        freqs_to_keep = calobj.freq_array[np.arange(2, 5)]
    else:
        freqs_to_keep = calobj.freq_array[0, np.arange(2, 5)]
    times_to_keep = calobj.time_array[[1, 2]]
    jones_to_keep = [-5]

    calobj2.select(
        antenna_nums=ants_to_keep,
        frequencies=freqs_to_keep,
        times=times_to_keep,
        jones=jones_to_keep,
    )

    assert len(ants_to_keep) == calobj2.Nants_data
    for ant in ants_to_keep:
        assert ant in calobj2.ant_array
    for ant in calobj2.ant_array:
        assert ant in ants_to_keep

    assert len(times_to_keep) == calobj2.Ntimes
    for t in times_to_keep:
        assert t in calobj2.time_array
    for t in np.unique(calobj2.time_array):
        assert t in times_to_keep

    assert len(freqs_to_keep) == calobj2.Nfreqs
    for f in freqs_to_keep:
        assert f in calobj2.freq_array
    for f in np.unique(calobj2.freq_array):
        assert f in freqs_to_keep

    assert len(jones_to_keep) == calobj2.Njones
    for j in jones_to_keep:
        assert j in calobj2.jones_array
    for j in np.unique(calobj2.jones_array):
        assert j in jones_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific antennas, times, "
        "frequencies, jones polarization terms "
        "using pyuvdata.",
        calobj2.history,
    )


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_select_wideband(caltype, multi_spw_delay, wideband_gain):
    if caltype == "gain":
        calobj = wideband_gain
    else:
        calobj = multi_spw_delay

    calobj2 = calobj.copy()

    old_history = calobj.history

    ants_to_keep = np.array([10, 89, 43, 9, 80, 96, 64])
    spws_to_keep = [2, 3]
    times_to_keep = calobj.time_array[[1, 2]]
    jones_to_keep = [-5]

    calobj2.select(
        antenna_nums=ants_to_keep,
        spws=spws_to_keep,
        times=times_to_keep,
        jones=jones_to_keep,
    )

    assert len(ants_to_keep) == calobj2.Nants_data
    for ant in ants_to_keep:
        assert ant in calobj2.ant_array
    for ant in calobj2.ant_array:
        assert ant in ants_to_keep

    assert len(times_to_keep) == calobj2.Ntimes
    for t in times_to_keep:
        assert t in calobj2.time_array
    for t in np.unique(calobj2.time_array):
        assert t in times_to_keep

    assert len(spws_to_keep) == calobj2.Nspws
    for spw in spws_to_keep:
        assert spw in calobj2.spw_array
    for spw in np.unique(calobj2.spw_array):
        assert spw in spws_to_keep

    assert len(jones_to_keep) == calobj2.Njones
    for j in jones_to_keep:
        assert j in calobj2.jones_array
    for j in np.unique(calobj2.jones_array):
        assert j in jones_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific antennas, times, "
        "spectral windows, jones polarization terms "
        "using pyuvdata.",
        calobj2.history,
    )


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_antennas(future_shapes, caltype, gain_data, method, delay_data_inputflag):
    """Test adding antennas between two UVCal objects"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()
    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    ants1 = np.array([9, 10, 20, 22, 31, 43, 53, 64, 65, 72])
    ants2 = np.array([80, 81, 88, 89, 96, 97, 104, 105, 112])
    calobj.select(antenna_nums=ants1)
    calobj2.select(antenna_nums=ants2)
    if method == "fast_concat":
        kwargs = {"axis": "antenna", "inplace": True}
    else:
        kwargs = {}

    getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "antennas using pyuvdata. Combined "
        "data along antenna axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full

    # test for when total_quality_array is present
    calobj.select(antenna_nums=ants1)
    calobj.total_quality_array = np.zeros(
        calobj._total_quality_array.expected_shape(calobj)
    )
    warn_type = [UserWarning]
    msg = ["Total quality array detected"]
    if caltype == "delay":
        warn_type += [DeprecationWarning] * 3
        msg += [
            "The input_flag_array is deprecated and will be removed in version 2.5"
        ] * 3
    with uvtest.check_warnings(warn_type, match=msg):
        getattr(calobj, method)(calobj2, **kwargs)
    assert calobj.total_quality_array is None

    if caltype == "delay":
        # test for when input_flag_array is present in first file but not second
        calobj.select(antenna_nums=ants1)
        ifa = np.zeros(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.ones(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        tot_ifa = np.concatenate([ifa, ifa2], axis=0)
        calobj.input_flag_array = ifa
        calobj2.input_flag_array = None
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

        # test for when input_flag_array is present in second file but not first
        calobj.select(antenna_nums=ants1)
        ifa = np.ones(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.zeros(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        tot_ifa = np.concatenate([ifa, ifa2], axis=0)
        calobj.input_flag_array = None
        calobj2.input_flag_array = ifa2
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

    # Out of order - antennas
    calobj = calobj_full.copy()
    calobj2 = calobj.copy()
    calobj.select(antenna_nums=ants2)
    calobj2.select(antenna_nums=ants1)
    getattr(calobj, method)(calobj2, **kwargs)
    calobj.history = calobj_full.history
    if method == "fast_concat":
        # need to sort objects before they will be equal
        assert calobj != calobj_full
        calobj.reorder_antennas()
    assert calobj == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("metadata_only", [True, False])
def test_reorder_ants(
    future_shapes, caltype, metadata_only, gain_data, delay_data_inputflag
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy(metadata_only=metadata_only)
    if metadata_only:
        calobj = calobj2.copy()

    # this is a no-op because it's already sorted this way
    calobj2.reorder_antennas()
    ant_num_diff = np.diff(calobj2.ant_array)
    assert np.all(ant_num_diff > 0)

    calobj2.reorder_antennas("-number")
    ant_num_diff = np.diff(calobj2.ant_array)
    assert np.all(ant_num_diff < 0)

    sorted_names = np.sort(calobj.antenna_names)
    calobj.reorder_antennas("name")
    temp = np.asarray(calobj.antenna_names)
    dtype_use = temp.dtype
    name_array = np.zeros_like(calobj.ant_array, dtype=dtype_use)
    for ind, ant in enumerate(calobj.ant_array):
        name_array[ind] = calobj.antenna_names[
            np.nonzero(calobj.antenna_numbers == ant)[0][0]
        ]

    assert np.all(sorted_names == name_array)

    # test sorting with an integer array. First resort back to by number
    calobj2.reorder_antennas("number")
    sorted_nums = [int(name[3:]) for name in sorted_names]
    index_array = [np.nonzero(calobj2.ant_array == ant)[0][0] for ant in sorted_nums]
    calobj2.reorder_antennas(index_array)
    assert calobj2 == calobj


def test_reorder_ants_errors(gain_data):
    with pytest.raises(
        ValueError,
        match="order must be one of 'number', 'name', '-number', '-name' or an "
        "index array of length Nants_data",
    ):
        gain_data.reorder_antennas(order="foo")

    with pytest.raises(
        ValueError,
        match="If order is an index array, it must contain all indicies for the"
        "ant_array, without duplicates.",
    ):
        gain_data.reorder_antennas(order=gain_data.antenna_numbers.astype(float))

    with pytest.raises(
        ValueError,
        match="If order is an index array, it must contain all indicies for the"
        "ant_array, without duplicates.",
    ):
        gain_data.reorder_antennas(order=gain_data.antenna_numbers[:8])


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("metadata_only", [True, False])
def test_reorder_freqs(
    future_shapes, caltype, metadata_only, gain_data, delay_data_inputflag
):
    if caltype == "gain":
        calobj = gain_data
        # add total_quality_array
        calobj.total_quality_array = np.tile(
            np.arange(calobj.Nfreqs, dtype=float)[:, np.newaxis, np.newaxis],
            (1, calobj.Ntimes, calobj.Njones),
        )
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        if caltype == "delay":
            calobj.freq_array = gain_data.freq_array
            calobj.channel_width = gain_data.channel_width
            calobj.Nfreqs = gain_data.Nfreqs
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy(metadata_only=metadata_only)
    if metadata_only:
        calobj = calobj2.copy()

    if future_shapes and caltype == "delay":
        with uvtest.check_warnings(
            UserWarning,
            match="Cannot reorder the frequency/spw axis with only one frequency and "
            "spw. Returning the object unchanged.",
        ):
            calobj2.reorder_freqs(channel_order="-freq")
        assert calobj == calobj2
    else:
        calobj2.reorder_freqs(channel_order="-freq")
        freq_diff = np.diff(calobj2.freq_array)
        assert np.all(freq_diff < 0)

        if caltype == "gain" and not metadata_only:
            # check total quality array
            if future_shapes:
                total_quality_diff = np.diff(calobj2.total_quality_array, axis=0)
            else:
                total_quality_diff = np.diff(calobj2.total_quality_array, axis=1)
            assert np.all(total_quality_diff < 0)

        calobj.reorder_freqs(channel_order=np.flip(np.arange(calobj.Nfreqs)))
        assert calobj == calobj2


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_reorder_freqs_multi_spw(caltype, multi_spw_gain, multi_spw_delay):
    if caltype == "gain":
        calobj = multi_spw_gain
    else:
        calobj = multi_spw_delay

    if not calobj.future_array_shapes:
        calobj.use_future_array_shapes()

    calobj2 = calobj.copy()

    if caltype == "delay":
        with uvtest.check_warnings(
            [UserWarning, DeprecationWarning],
            match=[
                "channel_order and select_spws are ignored for wide-band "
                "calibration solutions",
                "The input_flag_array is deprecated and will be removed in version 2.5",
            ],
        ):
            calobj.reorder_freqs(spw_order="-number", channel_order="freq")
    else:
        # this should be a no-op
        calobj.reorder_freqs(spw_order="number", channel_order="freq")
        assert calobj2 == calobj

        calobj.reorder_freqs(spw_order="-number", channel_order="freq")
        for spw in calobj.spw_array:
            ant_num_diff = np.diff(
                calobj.freq_array[np.nonzero(calobj.flex_spw_id_array == spw)[0]]
            )
            assert np.all(ant_num_diff > 0)

    spw_diff = np.diff(calobj.spw_array)
    assert np.all(spw_diff < 0)

    calobj2.reorder_freqs(spw_order=np.flip(np.arange(calobj2.Nspws)))
    assert calobj2 == calobj

    calobj.reorder_freqs(spw_order="freq")
    spw_diff = np.diff(calobj.spw_array)
    assert np.all(spw_diff > 0)


def test_reorder_freqs_errors(gain_data, multi_spw_delay):
    with pytest.raises(
        ValueError,
        match="spw_order can only be one of 'number', '-number', "
        "'freq', '-freq', None or an index array of length Nspws",
    ):
        gain_data.reorder_freqs(spw_order="foo")

    with pytest.raises(
        ValueError,
        match="spw_order can only be one of 'number', '-number', "
        "'freq', '-freq', None or an index array of length Nspws",
    ):
        multi_spw_delay.reorder_freqs(spw_order="foo")

    with pytest.raises(
        ValueError,
        match="If spw_order is an array, it must contain all indicies for "
        "the spw_array, without duplicates.",
    ):
        multi_spw_delay.reorder_freqs(spw_order=[0, 1])

    with pytest.raises(
        ValueError,
        match="channel_order can only be one of 'freq' or '-freq' or an index "
        "array of length Nfreqs",
    ):
        gain_data.reorder_freqs(channel_order="foo")

    with pytest.raises(
        ValueError,
        match="Index array for channel_order must contain all indicies for "
        "the frequency axis, without duplicates.",
    ):
        gain_data.reorder_freqs(
            channel_order=np.arange(gain_data.Nfreqs, dtype=float) * 2
        )

    with pytest.raises(
        ValueError,
        match="Index array for channel_order must contain all indicies for "
        "the frequency axis, without duplicates.",
    ):
        gain_data.reorder_freqs(channel_order=np.arange(3))


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("metadata_only", [True, False])
def test_reorder_times(
    future_shapes, caltype, metadata_only, gain_data, delay_data_inputflag
):
    if caltype == "gain":
        calobj = gain_data
        # add total_quality_array
        calobj.total_quality_array = np.tile(
            np.arange(calobj.Ntimes, dtype=float)[np.newaxis, :, np.newaxis],
            (calobj.Nfreqs, 1, calobj.Njones),
        )
        calobj.check()
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy(metadata_only=metadata_only)
    if metadata_only:
        calobj = calobj2.copy()

    # this should be a no-op
    calobj.reorder_times()
    assert calobj == calobj2

    calobj2.reorder_times(order="-time")
    time_diff = np.diff(calobj2.time_array)
    assert np.all(time_diff < 0)

    if caltype == "gain" and not metadata_only:
        # check total quality array
        if future_shapes:
            total_quality_diff = np.diff(calobj2.total_quality_array, axis=1)
        else:
            total_quality_diff = np.diff(calobj2.total_quality_array, axis=2)
        assert np.all(total_quality_diff < 0)

    calobj.reorder_times(order=np.flip(np.arange(calobj.Ntimes)))
    assert calobj == calobj2


def test_reorder_times_errors(gain_data):
    with pytest.raises(
        ValueError,
        match="order must be one of 'time', '-time' or an index array of length Ntimes",
    ):
        gain_data.reorder_times(order="foo")

    with pytest.raises(
        ValueError,
        match="If order is an array, it must contain all indicies for the time axis, "
        "without duplicates.",
    ):
        gain_data.reorder_times(order=np.arange(gain_data.Ntimes) * 2)

    with pytest.raises(
        ValueError,
        match="If order is an array, it must contain all indicies for the time axis, "
        "without duplicates.",
    ):
        gain_data.reorder_times(order=np.arange(7))


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("metadata_only", [True, False])
def test_reorder_jones(
    future_shapes, caltype, metadata_only, gain_data, delay_data_inputflag
):
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    # all the input objects have a Njones=1, extend to get to 4
    calobj2 = calobj.copy(metadata_only=metadata_only)
    extend_jones_axis(calobj2)

    if caltype == "gain" and not metadata_only:
        # add total_quality_array
        if future_shapes:
            calobj2.total_quality_array = np.tile(
                np.arange(calobj2.Njones, dtype=float)[np.newaxis, np.newaxis, :],
                (calobj2.Nfreqs, calobj2.Ntimes, 1),
            )
        else:
            calobj2.total_quality_array = np.tile(
                np.arange(calobj2.Njones, dtype=float)[
                    np.newaxis, np.newaxis, np.newaxis, :
                ],
                (1, calobj2.Nfreqs, calobj2.Ntimes, 1),
            )

    calobj = calobj2.copy()

    # this is a no-op because it's already sorted this way
    calobj2.reorder_jones("-number")
    jnum_diff = np.diff(calobj2.jones_array)
    assert np.all(jnum_diff < 0)

    calobj2.reorder_jones("number")
    jnum_diff = np.diff(calobj2.jones_array)
    assert np.all(jnum_diff > 0)

    if caltype == "gain" and not metadata_only:
        assert calobj2.total_quality_array is not None
        # check total quality array
        if future_shapes:
            total_quality_diff = np.diff(calobj2.total_quality_array, axis=2)
        else:
            total_quality_diff = np.diff(calobj2.total_quality_array, axis=3)
        assert np.all(total_quality_diff < 0)

    # the default order is "name"
    calobj2.reorder_jones()
    name_array = np.asarray(
        uvutils.jnum2str(calobj2.jones_array, x_orientation=calobj2.x_orientation)
    )
    sorted_names = np.sort(name_array)
    assert np.all(sorted_names == name_array)

    # test sorting with an index array. Sort back to number first so indexing works
    sorted_nums = uvutils.jstr2num(sorted_names, x_orientation=calobj.x_orientation)
    index_array = [np.nonzero(calobj.jones_array == num)[0][0] for num in sorted_nums]
    calobj.reorder_jones(index_array)
    assert calobj2 == calobj


def test_reorder_jones_errors(gain_data):
    # all the input objects have a Njones=1, extend to get to 4
    calobj = gain_data.copy()
    extend_jones_axis(calobj)

    with pytest.raises(
        ValueError,
        match="order must be one of 'number', 'name', '-number', '-name' or an "
        "index array of length Njones",
    ):
        calobj.reorder_jones(order="foo")

    with pytest.raises(
        ValueError,
        match="If order is an array, it must contain all indicies for "
        "the jones axis, without duplicates.",
    ):
        calobj.reorder_jones(order=np.arange(gain_data.Njones) * 2)

    with pytest.raises(
        ValueError,
        match="If order is an array, it must contain all indicies for "
        "the jones axis, without duplicates.",
    ):
        calobj.reorder_jones(order=np.arange(2))


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:Combined Jones elements are not evenly spaced")
@pytest.mark.filterwarnings("ignore:Combined frequencies are not evenly spaced")
@pytest.mark.filterwarnings("ignore:Cannot reorder the frequency/spw axis with only")
@pytest.mark.parametrize("add_type", ["ant", "time", "freq", "jones"])
@pytest.mark.parametrize("sort_type", ["ant", "time", "freq", "jones"])
@pytest.mark.parametrize(
    ["future_shapes", "wide_band"], [[True, False], [False, False], [True, True]]
)
def test_add_different_sorting(
    add_type, sort_type, future_shapes, wide_band, gain_data, wideband_gain
):
    if wide_band:
        calobj = wideband_gain.copy()
        calobj.check()
        assert calobj.freq_range is not None
    else:
        calobj = gain_data.copy()
    # add total_quality_array and initial flag array
    calobj.input_flag_array = copy.copy(calobj.flag_array)
    if add_type != "ant":
        calobj.total_quality_array = np.random.random(
            calobj._total_quality_array.expected_shape(calobj)
        )

    # all the input objects have a Njones=1, extend to get to 4
    extend_jones_axis(calobj, total_quality=False)

    if future_shapes:
        calobj.use_future_array_shapes()

    if add_type == "ant":
        ants1 = np.array([9, 10, 20, 22, 31, 43, 53, 64, 80, 81])
        ants2 = np.array([65, 72, 88, 89, 96, 97, 104, 105, 112])
        cal1 = calobj.select(antenna_nums=ants1, inplace=False)
        cal2 = calobj.select(antenna_nums=ants2, inplace=False)
    elif add_type == "time":
        n_times2 = calobj.Ntimes // 2
        times1 = calobj.time_array[:n_times2]
        times2 = calobj.time_array[n_times2:]
        cal1 = calobj.select(times=times1, inplace=False)
        cal2 = calobj.select(times=times2, inplace=False)
    elif add_type == "freq":
        if wide_band:
            spws1 = calobj.spw_array[: calobj.Nspws // 2]
            spws2 = calobj.spw_array[calobj.Nspws // 2 :]
            cal1 = calobj.select(spws=spws1, inplace=False)
            cal2 = calobj.select(spws=spws2, inplace=False)
        else:
            cal1 = calobj.select(
                freq_chans=np.arange(calobj.Nfreqs // 2), inplace=False
            )
            cal2 = calobj.select(
                freq_chans=np.arange(calobj.Nfreqs // 2, calobj.Nfreqs), inplace=False
            )
    elif add_type == "jones":
        cal1 = calobj.select(jones=np.array([-5, -7]), inplace=False)
        cal2 = calobj.select(jones=np.array([-6, -8]), inplace=False)

    if sort_type == "ant":
        cal1.reorder_antennas("number")
        cal2.reorder_antennas("-number")
        calobj.reorder_antennas("name")
        order_check = cal1._ant_array == cal2._ant_array
    elif sort_type == "time":
        cal1.reorder_times("time")
        cal2.reorder_times("-time")
        calobj.reorder_times("time")
        order_check = cal1._time_array == cal2._time_array
    elif sort_type == "freq":
        if wide_band:
            cal1.reorder_freqs(spw_order="number")
            cal2.reorder_freqs(spw_order="-number")
            calobj.reorder_freqs(spw_order="number")
            order_check = cal1._spw_array == cal2._spw_array
        else:
            cal1.reorder_freqs(channel_order="freq")
            cal2.reorder_freqs(channel_order="-freq")
            calobj.reorder_freqs(channel_order="freq")
            order_check = cal1._freq_array == cal2._freq_array
    elif sort_type == "jones":
        cal1.reorder_jones("name")
        cal2.reorder_jones("-number")
        calobj.reorder_jones("number")
        order_check = cal1._jones_array == cal2._jones_array

    # Make sure that the order has actually been scrambled
    assert not np.all(order_check)

    # Combine the objects in both orders
    cal3 = cal1 + cal2
    cal4 = cal2 + cal1

    if sort_type == "ant":
        cal3.reorder_antennas("name")
        cal4.reorder_antennas("name")
    elif sort_type == "time":
        cal3.reorder_times("time")
        cal4.reorder_times("time")
    elif sort_type == "freq":
        if wide_band:
            cal3.reorder_freqs()
            cal4.reorder_freqs()
        else:
            cal3.reorder_freqs(channel_order="freq")
            cal4.reorder_freqs(channel_order="freq")
    elif sort_type == "jones":
        cal3.reorder_jones("number")
        cal4.reorder_jones("number")

    # Deal with the history separately, since it will be different
    assert str.startswith(cal3.history, calobj.history)
    assert str.startswith(cal4.history, calobj.history)
    calobj.history = ""
    cal3.history = ""
    cal4.history = ""

    # Finally, make sure everything else lines up
    assert cal3 == calobj
    assert cal4._ant_array == calobj._ant_array
    assert cal4._freq_array == calobj._freq_array
    assert cal4 == calobj


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_antennas_multispw(future_shapes, multi_spw_gain, method):
    """Test adding antennas between two UVCal objects"""
    calobj = multi_spw_gain

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    ants1 = np.array([9, 10, 20, 22, 31, 43, 53, 64, 65, 72])
    ants2 = np.array([80, 81, 88, 89, 96, 97, 104, 105, 112])
    calobj.select(antenna_nums=ants1)
    calobj2.select(antenna_nums=ants2)
    if method == "fast_concat":
        kwargs = {"axis": "antenna", "inplace": True}
    else:
        kwargs = {}

    getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "antennas using pyuvdata. Combined "
        "data along antenna axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_frequencies(future_shapes, gain_data, method):
    """Test adding frequencies between two UVCal objects"""
    # don't test on delays because there's no freq axis for the delay array
    calobj = gain_data

    if not future_shapes:
        calobj.use_current_array_shapes()
    calobj2 = calobj.copy()
    calobj_full = calobj.copy()
    if future_shapes:
        freqs1 = calobj.freq_array[np.arange(0, calobj.Nfreqs // 2)]
        freqs2 = calobj.freq_array[np.arange(calobj.Nfreqs // 2, calobj.Nfreqs)]
    else:
        freqs1 = calobj.freq_array[0, np.arange(0, calobj.Nfreqs // 2)]
        freqs2 = calobj.freq_array[0, np.arange(calobj.Nfreqs // 2, calobj.Nfreqs)]
    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)

    if method == "fast_concat":
        kwargs = {"axis": "freq", "inplace": True}
    else:
        kwargs = {}

    getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "frequencies using pyuvdata. Combined "
        "data along frequency axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full

    # test for when total_quality_array is present in first file but not second
    # also check for missing flex_spw_id_array and freq_range in one object
    calobj.select(frequencies=freqs1)
    calobj.flex_spw_id_array = None
    if future_shapes:
        calobj.freq_range = np.array(
            [np.min(calobj.freq_array), np.max(calobj.freq_array)]
        )[np.newaxis, :]
    else:
        calobj.freq_range = None
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.zeros(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=0)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    calobj.total_quality_array = tqa
    msg = [
        "flex_spw_id_array is not set. It will be required starting in version 3.0 "
        "for non-wide-band objects"
    ]
    if method == "fast_concat":
        msg.extend(
            [
                "Some objects have the flex_spw_id_array set and some do not. Combined "
                "object will have it set.",
                "Some objects have the freq_range set and and some do not. "
                "Combined object will not have it set.",
            ]
        )
    else:
        msg.extend(
            [
                "One object has the flex_spw_id_array set and one does not. Combined "
                "object will have it set.",
                "One object has the freq_range set and one does not. Combined "
                "object will not have it set.",
            ]
        )

    with uvtest.check_warnings(
        [DeprecationWarning, UserWarning, UserWarning], match=msg
    ):
        getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in second file but not first
    calobj = calobj_full.copy()
    calobj.select(frequencies=freqs1)
    tqa = np.zeros(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=0)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    calobj.total_quality_array = None
    calobj2.total_quality_array = tqa2
    if future_shapes:
        calobj.freq_range = np.array(
            [np.min(calobj.freq_array), np.max(calobj.freq_array)]
        )[np.newaxis, :]
        calobj2.freq_range = np.array(
            [np.min(calobj2.freq_array), np.max(calobj2.freq_array)]
        )[np.newaxis, :]

    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in both
    calobj.select(frequencies=freqs1)
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    tqa *= 2
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=0)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    calobj.total_quality_array = tqa
    calobj2.total_quality_array = tqa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when input_flag_array is present in first file but not second
    calobj = calobj_full.copy()
    calobj.input_flag_array = np.zeros(
        calobj._input_flag_array.expected_shape(calobj), dtype=bool
    )
    calobj2 = calobj.copy()
    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)
    ifa = np.zeros(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
    ifa2 = np.ones(calobj2._input_flag_array.expected_shape(calobj2)).astype(np.bool_)
    if future_shapes:
        tot_ifa = np.concatenate([ifa, ifa2], axis=1)
    else:
        tot_ifa = np.concatenate([ifa, ifa2], axis=2)
    calobj.input_flag_array = ifa
    calobj2.input_flag_array = None
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(calobj.input_flag_array, tot_ifa)

    # test for when input_flag_array is present in second file but not first
    calobj.select(frequencies=freqs1)
    ifa = np.ones(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
    ifa2 = np.zeros(calobj2._input_flag_array.expected_shape(calobj2)).astype(np.bool_)
    if future_shapes:
        tot_ifa = np.concatenate([ifa, ifa2], axis=1)
    else:
        tot_ifa = np.concatenate([ifa, ifa2], axis=2)
    calobj.input_flag_array = None
    calobj2.input_flag_array = ifa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(calobj.input_flag_array, tot_ifa)

    # Out of order - freqs
    calobj = calobj_full.copy()
    calobj2 = calobj_full.copy()
    calobj.select(frequencies=freqs2)
    calobj2.select(frequencies=freqs1)
    if method == "fast_concat":
        warn_type = UserWarning
        msg = "Combined frequencies are not evenly spaced"
    else:
        warn_type = None
        msg = ""
    with uvtest.check_warnings(warn_type, match=msg):
        getattr(calobj, method)(calobj2, **kwargs)
    calobj.history = calobj_full.history
    if method == "fast_concat":
        # need to sort object first
        calobj.reorder_freqs(channel_order="freq")
    assert calobj == calobj_full


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:One object has the freq_range set and one does not")
@pytest.mark.filterwarnings("ignore:Some objects have the freq_range set")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("split_f_ind", [3, 5])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_frequencies_multispw(future_shapes, split_f_ind, method, multi_spw_gain):
    """Test adding frequencies between two UVCal objects"""
    # don't test on delays because there's no freq axis for the delay array

    # split_f_ind=5 splits the objects in the same place as the spws split
    # (so each object has only one spw). A different value splits within an spw.

    calobj = multi_spw_gain

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    calobj_full2 = calobj.copy()
    if future_shapes:
        freqs1 = calobj.freq_array[np.arange(0, split_f_ind)]
        freqs2 = calobj.freq_array[np.arange(split_f_ind, calobj.Nfreqs)]
    else:
        freqs1 = calobj.freq_array[0, np.arange(0, split_f_ind)]
        freqs2 = calobj.freq_array[0, np.arange(split_f_ind, calobj.Nfreqs)]
    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)
    if split_f_ind == 5:
        if future_shapes:
            calobj2.freq_range = np.array(
                [np.min(calobj2.freq_array), np.max(calobj2.freq_array)]
            )[np.newaxis, :]
        else:
            calobj2.freq_range = None
        calobj_full.freq_range = None
        warn_type = UserWarning
        if method == "fast_concat":
            msg = (
                "Some objects have the freq_range set and some do not. "
                "Combined object will not have it set."
            )
        else:
            msg = (
                "One object has the freq_range set and one does not. Combined "
                "object will not have it set."
            )
    else:
        warn_type = None
        msg = ""

    if method == "fast_concat":
        kwargs = {"axis": "freq", "inplace": True}
    else:
        kwargs = {}

    with uvtest.check_warnings(warn_type, match=msg):
        getattr(calobj, method)(calobj2, **kwargs)

    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "frequencies using pyuvdata. Combined "
        "data along frequency axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full

    # test adding out of order
    calobj = calobj_full2.copy()
    calobj.select(frequencies=freqs1)
    if split_f_ind == 5:
        if future_shapes:
            calobj.freq_range = np.array(
                [np.min(calobj.freq_array), np.max(calobj.freq_array)]
            )[np.newaxis, :]

    if method == "fast_concat":
        if split_f_ind == 5:
            calobj2.fast_concat(calobj, axis="freq", inplace=True)
        else:
            with pytest.raises(
                ValueError,
                match="Channels from different spectral windows are interspersed "
                "with one another, rather than being grouped together along the "
                "frequency axis. Most file formats do not support such "
                "non-grouping of data.",
            ):
                calobj2.fast_concat(calobj, axis="freq", inplace=True)
            return
    else:
        calobj2 += calobj

    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "frequencies using pyuvdata. Combined "
        "data along frequency axis using pyuvdata.",
        calobj2.history,
    )
    calobj2.history = calobj_full.history
    if split_f_ind == 5 and future_shapes:
        assert calobj_full._freq_range != calobj2._freq_range
        calobj_full.freq_range = calobj2.freq_range

    if method == "fast_concat":
        # need to sort object first
        calobj2.reorder_freqs(channel_order="freq", spw_order="number")
    assert calobj2 == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize(
    ["axis", "method"],
    [
        ["antenna", "__add__"],
        ["spw", "__add__"],
        ["multi", "__add__"],
        ["antenna", "fast_concat"],
        ["spw", "fast_concat"],
    ],
)
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_add_spw_wideband(axis, caltype, method, multi_spw_delay, wideband_gain):
    if caltype == "delay":
        calobj_full = multi_spw_delay
    else:
        calobj_full = wideband_gain
    calobj_full.select(times=calobj_full.time_array[:2])

    spw1 = calobj_full.spw_array[0]
    spw2 = calobj_full.spw_array[1:]
    ants1 = np.array([9, 10])
    ants2 = np.array([80, 81])
    calobj_full.select(antenna_nums=np.concatenate((ants1, ants2)))

    calobj = calobj_full.copy()
    calobj2 = calobj.copy()

    if axis == "antenna":
        calobj.select(antenna_nums=ants1)
        calobj2.select(antenna_nums=ants2)
    elif axis == "spw":
        calobj.select(spws=spw1)
        calobj2.select(spws=spw2)
    elif axis == "multi":
        calobj.select(antenna_nums=ants1, spws=spw1)
        calobj2.select(antenna_nums=ants2, spws=spw2)

        # zero out missing data in reference object
        ant1_inds = np.nonzero(np.in1d(calobj_full.ant_array, ants1))[0]
        ant2_inds = np.nonzero(np.in1d(calobj_full.ant_array, ants2))[0]
        if caltype == "delay":
            calobj_full.delay_array[ant1_inds, 1:] = 0
            calobj_full.delay_array[ant2_inds, 0] = 0
        else:
            calobj_full.gain_array[ant1_inds, 1:] = 0
            calobj_full.gain_array[ant2_inds, 0] = 0
        calobj_full.quality_array[ant1_inds, 1:] = 0
        calobj_full.quality_array[ant2_inds, 0] = 0
        calobj_full.flag_array[ant1_inds, 1:] = True
        calobj_full.flag_array[ant2_inds, 0] = True
        calobj_full.input_flag_array[ant1_inds, 1:] = True
        calobj_full.input_flag_array[ant2_inds, 0] = True

    if method == "fast_concat":
        kwargs = {"axis": axis, "inplace": False}
    else:
        kwargs = {}

    calobj3 = getattr(calobj, method)(calobj2, **kwargs)

    # Check history is correct, before replacing and doing a full object check
    if axis == "multi":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific antennas, spectral "
            "windows using pyuvdata. Combined data along antenna, spectral window axis "
            "using pyuvdata.",
            calobj3.history,
        )
    elif axis == "spw":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific spectral windows using "
            "pyuvdata. Combined data along spectral window axis using pyuvdata.",
            calobj3.history,
        )
    elif axis == "antenna":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific antennas using pyuvdata. "
            "Combined data along antenna axis using pyuvdata.",
            calobj3.history,
        )
    calobj3.history = calobj_full.history
    assert calobj3 == calobj_full

    # test adding out of order
    calobj3 = getattr(calobj2, method)(calobj, **kwargs)
    if method == "fast_concat":
        if axis == "spw":
            calobj3.reorder_freqs(spw_order="number")
        else:
            calobj3.reorder_antennas()

    # Check history is correct, before replacing and doing a full object check
    if axis == "multi":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific antennas, spectral "
            "windows using pyuvdata. Combined data along antenna, spectral window axis "
            "using pyuvdata.",
            calobj3.history,
        )
    elif axis == "spw":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific spectral windows using "
            "pyuvdata. Combined data along spectral window axis using pyuvdata.",
            calobj3.history,
        )
    elif axis == "antenna":
        assert uvutils._check_histories(
            calobj_full.history + "  Downselected to specific antennas using pyuvdata. "
            "Combined data along antenna axis using pyuvdata.",
            calobj3.history,
        )
    calobj3.history = calobj_full.history
    assert calobj3 == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_times(future_shapes, caltype, method, gain_data, delay_data_inputflag):
    """Test adding times between two UVCal objects"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    n_times2 = calobj.Ntimes // 2
    times1 = calobj.time_array[:n_times2]
    times2 = calobj.time_array[n_times2:]
    calobj.select(times=times1)
    calobj2.select(times=times2)

    if method == "fast_concat":
        kwargs = {"axis": "time", "inplace": True}
    else:
        kwargs = {}
    getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "times using pyuvdata. Combined "
        "data along time axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full

    # test for when total_quality_array is present in first file but not second
    calobj.select(times=times1)
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.zeros(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    calobj.total_quality_array = tqa
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in second file but not first
    calobj.select(times=times1)
    tqa = np.zeros(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    calobj.total_quality_array = None
    calobj2.total_quality_array = tqa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in both
    calobj.select(times=times1)
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    tqa *= 2
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=1)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    calobj.total_quality_array = tqa
    calobj2.total_quality_array = tqa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    if caltype == "delay":
        # test for when input_flag_array is present in first file but not second
        calobj.select(times=times1)
        ifa = np.zeros(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.ones(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        if future_shapes:
            tot_ifa = np.concatenate([ifa, ifa2], axis=2)
        else:
            tot_ifa = np.concatenate([ifa, ifa2], axis=3)
        calobj.input_flag_array = ifa
        calobj2.input_flag_array = None
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

        # test for when input_flag_array is present in second file but not first
        calobj.select(times=times1)
        ifa = np.ones(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.zeros(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        if future_shapes:
            tot_ifa = np.concatenate([ifa, ifa2], axis=2)
        else:
            tot_ifa = np.concatenate([ifa, ifa2], axis=3)
        calobj.input_flag_array = None
        calobj2.input_flag_array = ifa2
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

    # Out of order - times
    calobj = calobj_full.copy()
    calobj2 = calobj.copy()
    calobj.select(times=times2)
    calobj2.select(times=times1)
    getattr(calobj, method)(calobj2, **kwargs)
    calobj.history = calobj_full.history
    if method == "fast_concat":
        # need to sort object first
        calobj.reorder_times()
    assert calobj == calobj_full


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_times_multispw(future_shapes, method, multi_spw_gain):
    """Test adding times between two UVCal objects"""
    calobj = multi_spw_gain

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    n_times2 = calobj.Ntimes // 2
    times1 = calobj.time_array[:n_times2]
    times2 = calobj.time_array[n_times2:]
    calobj.select(times=times1)
    calobj2.select(times=times2)
    if method == "fast_concat":
        kwargs = {"axis": "time", "inplace": True}
    else:
        kwargs = {}
    getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific "
        "times using pyuvdata. Combined "
        "data along time axis using pyuvdata.",
        calobj.history,
    )
    calobj.history = calobj_full.history
    assert calobj == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_jones(future_shapes, caltype, method, gain_data, delay_data_inputflag):
    """Test adding Jones axes between two UVCal objects"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    calobj_original = calobj.copy()
    # artificially change the Jones value to permit addition
    calobj2.jones_array[0] = -6
    if method == "fast_concat":
        kwargs = {"axis": "jones", "inplace": True}
    else:
        kwargs = {}
    getattr(calobj, method)(calobj2, **kwargs)

    # check dimensionality of resulting object
    if caltype == "gain":
        assert calobj.gain_array.shape[-1] == 2
    else:
        assert calobj.delay_array.shape[-1] == 2
    assert sorted(calobj.jones_array) == [-6, -5]

    # test for when total_quality_array is present in first file but not second
    calobj = calobj_original.copy()
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.zeros(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=3)
    calobj.total_quality_array = tqa
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in second file but not first
    calobj = calobj_original.copy()
    tqa = np.zeros(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=3)
    calobj2.total_quality_array = tqa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    # test for when total_quality_array is present in both
    calobj = calobj_original.copy()
    tqa = np.ones(calobj._total_quality_array.expected_shape(calobj))
    tqa2 = np.ones(calobj2._total_quality_array.expected_shape(calobj2))
    tqa *= 2
    if future_shapes:
        tot_tqa = np.concatenate([tqa, tqa2], axis=2)
    else:
        tot_tqa = np.concatenate([tqa, tqa2], axis=3)
    calobj.total_quality_array = tqa
    calobj2.total_quality_array = tqa2
    getattr(calobj, method)(calobj2, **kwargs)
    assert np.allclose(
        calobj.total_quality_array,
        tot_tqa,
        rtol=calobj._total_quality_array.tols[0],
        atol=calobj._total_quality_array.tols[1],
    )

    if caltype == "delay":
        # test for when input_flag_array is present in first file but not second
        calobj = calobj_original.copy()
        ifa = np.zeros(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.ones(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        if future_shapes:
            tot_ifa = np.concatenate([ifa, ifa2], axis=3)
        else:
            tot_ifa = np.concatenate([ifa, ifa2], axis=4)
        calobj.input_flag_array = ifa
        calobj2.input_flag_array = None
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

        # test for when input_flag_array is present in second file but not first
        calobj = calobj_original.copy()
        ifa = np.ones(calobj._input_flag_array.expected_shape(calobj)).astype(np.bool_)
        ifa2 = np.zeros(calobj2._input_flag_array.expected_shape(calobj2)).astype(
            np.bool_
        )
        if future_shapes:
            tot_ifa = np.concatenate([ifa, ifa2], axis=3)
        else:
            tot_ifa = np.concatenate([ifa, ifa2], axis=4)
        calobj.input_flag_array = None
        calobj2.input_flag_array = ifa2
        getattr(calobj, method)(calobj2, **kwargs)
        assert np.allclose(calobj.input_flag_array, tot_ifa)

    # Out of order - jones
    calobj = calobj_original.copy()
    calobj2 = calobj_original.copy()
    calobj.jones_array[0] = -6
    getattr(calobj, method)(calobj2, **kwargs)
    calobj2 = calobj.copy()
    calobj.select(jones=-5)
    calobj.history = calobj_original.history
    assert calobj == calobj_original
    calobj2.select(jones=-6)
    calobj2.jones_array[:] = -5
    calobj2.history = calobj_original.history
    assert calobj2 == calobj_original


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_add_jones_multispw(future_shapes, method, multi_spw_gain):
    """Test adding Jones axes between two UVCal objects"""
    calobj = multi_spw_gain

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    # artificially change the Jones value to permit addition
    calobj2.jones_array[0] = -6
    if method == "fast_concat":
        kwargs = {"axis": "jones", "inplace": True}
    else:
        kwargs = {}
    getattr(calobj, method)(calobj2, **kwargs)

    # check dimensionality of resulting object
    assert calobj.gain_array.shape[-1] == 2

    assert sorted(calobj.jones_array) == [-6, -5]


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__add__", "fast_concat"])
def test_add(caltype, method, gain_data, delay_data_inputflag):
    """Test miscellaneous aspects of add method"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    calobj2 = calobj.copy()

    # test not-in-place addition
    calobj_original = calobj.copy()
    ants1 = np.array([9, 10, 20, 22, 31, 43, 53, 64, 65, 72])
    ants2 = np.array([80, 81, 88, 89, 96, 97, 104, 105, 112])
    calobj.select(antenna_nums=ants1)
    calobj2.select(antenna_nums=ants2)

    if method == "fast_concat":
        kwargs = {"axis": "antenna", "inplace": False}
    else:
        kwargs = {}

    calobj_add = getattr(calobj, method)(calobj2, **kwargs)
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        calobj_original.history + "  Downselected to specific "
        "antennas using pyuvdata. Combined "
        "data along antenna axis using pyuvdata.",
        calobj_add.history,
    )
    calobj_add.history = calobj_original.history
    assert calobj_add == calobj_original

    # test history concatenation
    calobj.history = calobj_original.history
    if caltype == "gain":
        calobj2.history = "Some random history string OMNI_RUN:"
    else:
        calobj2.history = "Some random history string firstcal.py"
    new_cal = getattr(calobj, method)(calobj2, **kwargs)

    additional_history = "Some random history string"
    assert uvutils._check_histories(
        calobj_original.history + " Combined data along antenna axis "
        "using pyuvdata. Unique part of next object history follows.  "
        + additional_history,
        new_cal.history,
    )

    kwargs["verbose_history"] = True
    new_cal = getattr(calobj, method)(calobj2, **kwargs)
    assert uvutils._check_histories(
        calobj_original.history + " Combined data along antenna axis "
        "using pyuvdata. Next object history follows.  " + calobj2.history,
        new_cal.history,
    )


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize(
    ["ant", "freq", "time", "jones"],
    [
        [True, True, False, False],
        [False, False, True, True],
        [True, True, True, False],
        [False, True, True, True],
        [True, True, True, True],
    ],
)
@pytest.mark.parametrize("in_order", [True, False])
def test_add_multiple_axes(gain_data, ant, freq, time, jones, in_order):
    """Test addition along multiple axes"""
    calobj_full = gain_data
    calobj_full.select(
        antenna_nums=calobj_full.ant_array[:4],
        frequencies=calobj_full.freq_array[:2],
        times=calobj_full.time_array[:2],
    )

    # add more jones terms to allow for better testing of selections
    while calobj_full.Njones < 2:
        new_jones = np.min(calobj_full.jones_array) - 1
        calobj_full.jones_array = np.append(calobj_full.jones_array, new_jones)
        calobj_full.Njones += 1
        calobj_full.flag_array = np.concatenate(
            (calobj_full.flag_array, calobj_full.flag_array[:, :, :, [-1]]), axis=3
        )
        calobj_full.gain_array = np.concatenate(
            (calobj_full.gain_array, calobj_full.gain_array[:, :, :, [-1]]), axis=3
        )
        calobj_full.quality_array = np.concatenate(
            (calobj_full.quality_array, calobj_full.quality_array[:, :, :, [-1]]),
            axis=3,
        )
    # add an input_flag_array
    calobj_full.input_flag_array = calobj_full.flag_array

    calobj = calobj_full.copy()
    calobj2 = calobj_full.copy()

    ants1 = None
    ants2 = None
    freqs1 = None
    freqs2 = None
    times1 = None
    times2 = None
    jones1 = None
    jones2 = None

    if ant:
        ants1 = calobj.ant_array[calobj.Nants_data // 2 :]
        ants2 = calobj.ant_array[: calobj.Nants_data // 2]
    if freq:
        freqs1 = calobj.freq_array[: calobj.Nfreqs // 2]
        freqs2 = calobj.freq_array[calobj.Nfreqs // 2 :]
    if time:
        times1 = calobj.time_array[: calobj.Ntimes // 2]
        times2 = calobj.time_array[calobj.Ntimes // 2 :]
    if jones:
        jones1 = calobj.jones_array[: calobj.Njones // 2]
        jones2 = calobj.jones_array[calobj.Njones // 2 :]

    # perform select
    calobj.select(antenna_nums=ants1, frequencies=freqs1, times=times1, jones=jones1)
    calobj2.select(antenna_nums=ants2, frequencies=freqs2, times=times2, jones=jones2)

    if in_order:
        calobj3 = calobj + calobj2
    else:
        calobj3 = calobj2 + calobj

    # remove the missing parts from calobj_full
    if ant:
        ant1_inds = np.nonzero(np.in1d(calobj_full.ant_array, ants1))[0]
        ant2_inds = np.nonzero(np.in1d(calobj_full.ant_array, ants2))[0]
    else:
        ant1_inds = np.arange(calobj_full.Nants_data)
        ant2_inds = np.arange(calobj_full.Nants_data)
    if freq:
        freq1_inds = np.nonzero(np.in1d(calobj_full.freq_array, freqs1))[0]
        freq2_inds = np.nonzero(np.in1d(calobj_full.freq_array, freqs2))[0]
    else:
        freq1_inds = np.arange(calobj_full.Nfreqs)
        freq2_inds = np.arange(calobj_full.Nfreqs)
    if time:
        time1_inds = np.nonzero(np.in1d(calobj_full.time_array, times1))[0]
        time2_inds = np.nonzero(np.in1d(calobj_full.time_array, times2))[0]
    else:
        time1_inds = np.arange(calobj_full.Ntimes)
        time2_inds = np.arange(calobj_full.Ntimes)
    if jones:
        jones1_inds = np.nonzero(np.in1d(calobj_full.jones_array, jones1))[0]
        jones2_inds = np.nonzero(np.in1d(calobj_full.jones_array, jones2))[0]
    else:
        jones1_inds = np.arange(calobj_full.Njones)
        jones2_inds = np.arange(calobj_full.Njones)
    axis_dict = {
        "ant": {"axis": 0, 1: ant1_inds, 2: ant2_inds},
        "freq": {"axis": 1, 1: freq1_inds, 2: freq2_inds},
        "time": {"axis": 2, 1: time1_inds, 2: time2_inds},
        "jones": {"axis": 3, 1: jones1_inds, 2: jones2_inds},
    }
    axes_used = []
    if ant:
        axes_used.append("ant")
    if freq:
        axes_used.append("freq")
    if time:
        axes_used.append("time")
    if jones:
        axes_used.append("jones")
    axis_list = []
    for n_comb in range(1, len(axes_used)):
        axis_list += list(itertools.combinations(axes_used, n_comb))
    for al in axis_list:
        set_use = [1, 1, 1, 1]
        for axis in al:
            set_use[axis_dict[axis]["axis"]] = 2
        inds = np.ix_(
            axis_dict["ant"][set_use[0]],
            axis_dict["freq"][set_use[1]],
            axis_dict["time"][set_use[2]],
            axis_dict["jones"][set_use[3]],
        )
        calobj_full.gain_array[inds] = 0
        calobj_full.quality_array[inds] = 0
        calobj_full.flag_array[inds] = True
        calobj_full.input_flag_array[inds] = True

    # reset history to equality passes
    calobj3.history = calobj_full.history

    assert calobj3 == calobj_full


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__add__", "fast_concat"])
def test_add_errors(
    caltype, method, gain_data, delay_data, multi_spw_gain, wideband_gain
):
    """Test behavior that will raise errors"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data

    calobj2 = calobj.copy()

    if method == "fast_concat":
        kwargs = {"axis": "antenna", "inplace": True}
    else:
        kwargs = {}

    if method == "fast_concat":
        # test unknown axis values
        allowed_axes = ["antenna", "time", "jones"]
        if caltype == "gain":
            allowed_axes.append("freq")
        with pytest.raises(
            ValueError, match="Axis must be one of: " + ", ".join(allowed_axes)
        ):
            calobj.fast_concat(calobj2, axis="foo")

    else:
        # test addition of two identical objects
        with pytest.raises(
            ValueError,
            match="These objects have overlapping data and cannot be combined.",
        ):
            calobj + calobj2

    # test addition of UVCal and non-UVCal object
    with pytest.raises(ValueError, match="Only UVCal "):
        getattr(calobj, method)("foo", **kwargs)

    # test compatibility param mismatch
    calobj2.telescope_name = "PAPER"
    with pytest.raises(ValueError, match="Parameter telescope_name does not match"):
        getattr(calobj, method)(calobj2, **kwargs)

    # test array shape mismatch
    calobj2 = calobj.copy()
    calobj2.use_current_array_shapes()
    msg = (
        " objects must have the same `future_array_shapes` parameter. Use the "
        "`use_future_array_shapes` or `use_current_array_shapes` methods to convert "
        "them."
    )
    if method == "fast_concat":
        msg = "All" + msg
    else:
        msg = "Both" + msg

    with pytest.raises(ValueError, match=msg):
        getattr(calobj, method)(calobj2, **kwargs)

    # test flex_spw mismatch
    with pytest.raises(
        ValueError,
        match="To combine these data, flex_spw must be set to the same value",
    ):
        getattr(gain_data, method)(multi_spw_gain, **kwargs)

    # test wide_band mismatch
    with pytest.raises(
        ValueError,
        match="To combine these data, wide_band must be set to the same value",
    ):
        getattr(gain_data, method)(wideband_gain, **kwargs)


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.parametrize("axis", ["antenna", "freq", "time", "jones", "spw"])
@pytest.mark.parametrize("caltype", ["gain", "delay"])
def test_fast_concat_multiple_files(
    gain_data, delay_data, wideband_gain, multi_spw_delay, axis, caltype
):
    if caltype == "delay" and axis == "freq":
        pytest.skip("Cannot combine along the freq axis with old delay types")
    if axis == "spw":
        if caltype == "gain":
            calobj_full = wideband_gain
        else:
            calobj_full = multi_spw_delay
        n_objects = 3
    else:
        if caltype == "gain":
            calobj_full = gain_data
        else:
            calobj_full = delay_data
        n_objects = 4

    # add more jones terms to allow for better testing of selections
    if axis != "antenna":
        total_quality = True
    else:
        total_quality = False
    extend_jones_axis(calobj_full, total_quality=total_quality)

    axis_dict = {
        "antenna": {
            "arr_use": calobj_full.ant_array,
            "axis_len": calobj_full.Nants_data,
            "select_param": "antenna_nums",
        },
        "freq": {
            "arr_use": np.arange(calobj_full.Nfreqs),
            "axis_len": calobj_full.Nfreqs,
            "select_param": "freq_chans",
        },
        "spw": {
            "arr_use": calobj_full.spw_array,
            "axis_len": calobj_full.Nspws,
            "select_param": "spws",
        },
        "time": {
            "arr_use": calobj_full.time_array,
            "axis_len": calobj_full.Ntimes,
            "select_param": "times",
        },
        "jones": {
            "arr_use": calobj_full.jones_array,
            "axis_len": calobj_full.Njones,
            "select_param": "jones",
        },
    }

    select_params = {}
    ind_ranges = {
        0: np.arange(axis_dict[axis]["axis_len"] // n_objects),
        1: np.arange(
            axis_dict[axis]["axis_len"] // n_objects,
            axis_dict[axis]["axis_len"] * 2 // n_objects,
        ),
        2: np.arange(
            axis_dict[axis]["axis_len"] * 2 // n_objects,
            axis_dict[axis]["axis_len"] * 3 // n_objects,
        ),
        3: np.arange(
            axis_dict[axis]["axis_len"] * 3 // n_objects, axis_dict[axis]["axis_len"]
        ),
    }
    for obj_num in range(n_objects):
        select_params[obj_num] = {}
        select_params[obj_num][axis_dict[axis]["select_param"]] = axis_dict[axis][
            "arr_use"
        ][ind_ranges[obj_num]]
    calobj0 = calobj_full.select(**select_params[0], inplace=False)
    calobj1 = calobj_full.select(**select_params[1], inplace=False)
    calobj2 = calobj_full.select(**select_params[2], inplace=False)
    if n_objects == 4:
        calobj3 = calobj_full.select(**select_params[3], inplace=False)

    concat_list = [calobj1, calobj2]
    if n_objects == 4:
        concat_list.append(calobj3)
    calobj_final = calobj0.fast_concat(concat_list, axis=axis, inplace=False)
    calobj_final.history = calobj_full.history
    assert calobj_final == calobj_full


def test_jones_warning(gain_data):
    """Test having non-contiguous Jones elements"""
    calobj = gain_data
    calobj2 = calobj.copy()

    calobj2.jones_array[0] = -6
    calobj += calobj2
    calobj2.jones_array[0] = -8
    with uvtest.check_warnings(UserWarning, match="Combined Jones elements"):
        calobj.__iadd__(calobj2)
    assert sorted(calobj.jones_array) == [-8, -6, -5]


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("method", ["__iadd__", "fast_concat"])
def test_frequency_warnings(future_shapes, gain_data, method):
    """Test having uneven or non-contiguous frequencies"""
    # test having unevenly spaced frequency separations
    calobj = gain_data

    if not future_shapes:
        calobj.use_current_array_shapes()

    calobj2 = calobj.copy()

    go1 = calobj.copy()
    go2 = calobj2.copy()
    if future_shapes:
        freqs1 = calobj.freq_array[np.arange(0, 5)]
        freqs2 = calobj2.freq_array[np.arange(5, 10)]
    else:
        freqs1 = calobj.freq_array[0, np.arange(0, 5)]
        freqs2 = calobj2.freq_array[0, np.arange(5, 10)]

    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)

    if method == "fast_concat":
        kwargs = {"axis": "freq", "inplace": True}
    else:
        kwargs = {}

    # change the last frequency bin to be smaller than the others
    if future_shapes:
        df = calobj2.freq_array[-1] - calobj2.freq_array[-2]
        calobj2.freq_array[-1] = calobj2.freq_array[-2] + df / 2
    else:
        df = calobj2.freq_array[0, -1] - calobj2.freq_array[0, -2]
        calobj2.freq_array[0, -1] = calobj2.freq_array[0, -2] + df / 2
    with uvtest.check_warnings(
        UserWarning, match="Combined frequencies are not evenly spaced"
    ):
        getattr(calobj, method)(calobj2, **kwargs)

    assert calobj.freq_array.size == freqs1.size + freqs2.size

    # now check having "non-contiguous" frequencies
    calobj = go1.copy()
    calobj2 = go2.copy()
    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)

    # artificially space out frequencies
    calobj.freq_array *= 10
    calobj2.freq_array *= 10
    with uvtest.check_warnings(
        UserWarning,
        match="Combined frequencies are separated by more than their channel width",
    ):
        getattr(calobj, method)(calobj2, **kwargs)

    assert calobj.freq_array.size == freqs1.size + freqs2.size

    freqs1 *= 10
    freqs2 *= 10
    freqs = np.concatenate([freqs1, freqs2])
    if future_shapes:
        freq_arr = calobj.freq_array
    else:
        freq_arr = calobj.freq_array[0, :]
    assert np.allclose(
        freq_arr,
        freqs,
        rtol=calobj._freq_array.tols[0],
        atol=calobj._freq_array.tols[1],
    )


def test_parameter_warnings(gain_data):
    """Test changing a parameter that will raise a warning"""
    # change observer and select frequencies
    calobj = gain_data
    calobj2 = calobj.copy()

    calobj2.observer = "mystery_person"
    freqs1 = calobj.freq_array[np.arange(0, 5)]
    freqs2 = calobj2.freq_array[np.arange(5, 10)]
    calobj.select(frequencies=freqs1)
    calobj2.select(frequencies=freqs2)
    with uvtest.check_warnings(
        UserWarning, match="UVParameter observer does not match"
    ):
        calobj.__iadd__(calobj2)

    freqs = np.concatenate([freqs1, freqs2])
    assert np.allclose(
        calobj.freq_array,
        freqs,
        rtol=calobj._freq_array.tols[0],
        atol=calobj._freq_array.tols[1],
    )


@pytest.mark.filterwarnings("ignore:The input_flag_array is deprecated")
@pytest.mark.filterwarnings("ignore:When converting a delay-style cal to future array")
@pytest.mark.parametrize("caltype", ["gain", "delay"])
@pytest.mark.parametrize("method", ["__add__", "fast_concat"])
def test_multi_files(caltype, method, gain_data, delay_data_inputflag, tmp_path):
    """Test read function when multiple files are included"""
    if caltype == "gain":
        calobj = gain_data
    else:
        calobj = delay_data_inputflag

    calobj2 = calobj.copy()

    calobj_full = calobj.copy()
    n_times2 = calobj.Ntimes // 2
    # Break up delay object into two objects, divided in time
    times1 = calobj.time_array[:n_times2]
    times2 = calobj.time_array[n_times2:]
    calobj.select(times=times1)
    calobj2.select(times=times2)
    # Write those objects to files
    f1 = str(tmp_path / "read_multi1.calfits")
    f2 = str(tmp_path / "read_multi2.calfits")
    calobj.write_calfits(f1, clobber=True)
    calobj2.write_calfits(f2, clobber=True)
    # Read both files together
    if method == "fast_concat":
        calobj = UVCal.from_file([f1, f2], axis="time", use_future_array_shapes=True)
    else:
        warn_type = [DeprecationWarning]
        msg = [
            "Reading multiple files from file specific read methods is deprecated. "
            "Use the generic `UVCal.read` method instead."
        ]
        if caltype == "delay":
            warn_type += 2 * [UserWarning] + [DeprecationWarning] * 5
            msg += 2 * ["When converting a delay-style cal to future array"] + 5 * [
                "The input_flag_array is deprecated and will be removed in version 2.5"
            ]

        with uvtest.check_warnings(warn_type, match=msg):
            calobj.read_calfits([f1, f2], use_future_array_shapes=True)

    assert uvutils._check_histories(
        calobj_full.history + "  Downselected to specific times"
        " using pyuvdata. Combined data "
        "along time axis using pyuvdata.",
        calobj.history,
    )
    assert calobj.filename == ["read_multi1.calfits", "read_multi2.calfits"]
    calobj.history = calobj_full.history
    assert calobj == calobj_full

    # check metadata only read
    calobj = UVCal.from_file([f1, f2], read_data=False, use_future_array_shapes=True)
    calobj_full_metadata_only = calobj_full.copy(metadata_only=True)

    calobj.history = calobj_full_metadata_only.history
    assert calobj == calobj_full_metadata_only


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
def test_uvcal_get_methods(future_shapes, gain_data):
    # load data
    uvc = gain_data

    if not future_shapes:
        uvc.use_current_array_shapes()

    # test get methods: add in a known value and make sure it is returned
    key = (10, "Jee")
    uvc.gain_array[1] = 0.0
    gain_arr = uvc.get_gains(key)
    flag_arr = uvc.get_flags(key)
    quality_arr = uvc.get_quality(key)

    # test shapes
    assert np.all(np.isclose(gain_arr, 0.0))
    assert gain_arr.shape == (uvc.Nfreqs, uvc.Ntimes)
    assert flag_arr.shape == (uvc.Nfreqs, uvc.Ntimes)
    assert quality_arr.shape == (uvc.Nfreqs, uvc.Ntimes)

    # test against by-hand indexing
    if future_shapes:
        expected_array = uvc.gain_array[
            uvc.ant_array.tolist().index(10), :, :, uvc.jones_array.tolist().index(-5)
        ]
    else:
        expected_array = uvc.gain_array[
            uvc.ant_array.tolist().index(10),
            0,
            :,
            :,
            uvc.jones_array.tolist().index(-5),
        ]
    np.testing.assert_array_almost_equal(gain_arr, expected_array)

    # test variable key input
    gain_arr2 = uvc.get_gains(*key)
    np.testing.assert_array_almost_equal(gain_arr, gain_arr2)
    gain_arr2 = uvc.get_gains(key[0])
    np.testing.assert_array_almost_equal(gain_arr, gain_arr2)
    gain_arr2 = uvc.get_gains(key[:1])
    np.testing.assert_array_almost_equal(gain_arr, gain_arr2)
    gain_arr2 = uvc.get_gains(10, -5)
    np.testing.assert_array_almost_equal(gain_arr, gain_arr2)
    gain_arr2 = uvc.get_gains(10, "x")
    np.testing.assert_array_almost_equal(gain_arr, gain_arr2)

    # check has_key
    assert uvc._has_key(antnum=10)
    assert uvc._has_key(jpol="Jee")
    assert uvc._has_key(antnum=10, jpol="Jee")
    assert not uvc._has_key(antnum=10, jpol="Jnn")
    assert not uvc._has_key(antnum=101, jpol="Jee")

    # test exceptions
    pytest.raises(ValueError, uvc.get_gains, 1)
    pytest.raises(ValueError, uvc.get_gains, (10, "Jnn"))
    uvc.cal_type = "delay"
    pytest.raises(ValueError, uvc.get_gains, 10)


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
def test_write_read_optional_attrs(gain_data, tmp_path):
    # read a test file
    cal_in = gain_data

    # set some optional parameters
    cal_in.gain_scale = "Jy"
    cal_in.sky_field = "GLEAM"

    # write
    write_file_calfits = str(tmp_path / "test.calfits")
    cal_in.write_calfits(write_file_calfits, clobber=True)

    # read and compare
    # also check that passing a single file in a list works properly
    cal_in2 = UVCal.from_file([write_file_calfits], use_future_array_shapes=True)
    assert cal_in == cal_in2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("future_shapes", [True, False])
@pytest.mark.parametrize("caltype", ["gain", "delay", "unknown", None])
def test_copy(future_shapes, gain_data, delay_data_inputflag, caltype):
    """Test the copy method"""
    if caltype == "gain":
        uv_object = gain_data
    elif caltype == "delay":
        uv_object = delay_data_inputflag
    else:
        uv_object = gain_data
        with uvtest.check_warnings(
            DeprecationWarning,
            match="Setting the cal_type to 'unknown' is deprecated. This will become "
            "an error in version 2.5",
        ):
            uv_object._set_unknown_cal_type()
        uv_object.cal_type = caltype

    if not future_shapes:
        if caltype is not None:
            uv_object.use_current_array_shapes()
        else:
            with pytest.raises(
                ValueError,
                match="Cannot get required data params because cal_type is not set.",
            ):
                uv_object.use_current_array_shapes()

    if caltype is not None:
        uv_object_copy = uv_object.copy()
    else:
        with pytest.raises(
            ValueError,
            match="Cannot get required data params because cal_type is not set.",
        ):
            uv_object_copy = uv_object.copy()
        return
    assert uv_object_copy == uv_object

    uv_object_copy = uv_object.copy(metadata_only=True)
    assert uv_object_copy.metadata_only

    for name in uv_object._data_params:
        setattr(uv_object, name, None)
    assert uv_object_copy == uv_object

    uv_object_copy = uv_object.copy()
    assert uv_object_copy == uv_object

    return


@pytest.mark.parametrize("antnamefix", ["all", "partial"])
def test_match_antpos_antname(gain_data, antnamefix, tmp_path):
    # fix the antenna names in the uvcal object to match telescope object
    new_names = np.array(
        [name.replace("ant", "HH") for name in gain_data.antenna_names]
    )
    if antnamefix == "all":
        gain_data.antenna_names = new_names
    else:
        gain_data.antenna_names[0 : gain_data.Nants_telescope // 2] = new_names[
            0 : gain_data.Nants_telescope // 2
        ]

    # remove the antenna_positions to test matching them on read
    write_file = str(tmp_path / "test.calfits")
    write_file2 = str(tmp_path / "test2.calfits")
    gain_data.write_calfits(write_file)
    with fits.open(write_file, memmap=True) as hdu_list:
        hdunames = uvutils._fits_indexhdus(hdu_list)
        primary_hdu = hdu_list[0]
        ant_hdu = hdu_list[hdunames["ANTENNAS"]]

        table = Table(ant_hdu.data)
        del table["ANTXYZ"]
        ant_hdu = fits.BinTableHDU(table)
        ant_hdu.header["EXTNAME"] = "ANTENNAS"

        hdulist = fits.HDUList([primary_hdu, ant_hdu])

        hdulist.writeto(write_file2)
        hdulist.close()

    with uvtest.check_warnings(
        UserWarning, "antenna_positions is not set. Using known values for HERA."
    ):
        gain_data2 = UVCal.from_file(write_file2, use_future_array_shapes=True)

    assert gain_data2.antenna_positions is not None
    assert gain_data == gain_data2


@pytest.mark.parametrize("modtype", ["rename", "swap"])
def test_set_antpos_from_telescope_errors(gain_data, modtype, tmp_path):
    """Test that setting antenna positions doesn't happen if ants don't match."""
    # fix the antenna names in the uvcal object to match telescope object
    new_names = np.array(
        [name.replace("ant", "HH") for name in gain_data.antenna_names]
    )
    gain_data.antenna_names = new_names

    if modtype == "rename":
        # change the name & number of one of the antennas
        orig_num = gain_data.antenna_numbers[0]
        gain_data.antenna_names[0] = "HH400"
        gain_data.antenna_numbers[0] = 400
        gain_data.ant_array[np.where(gain_data.ant_array == orig_num)[0]] = 400
    else:
        # change the name of one antenna and swap the number with a different antenna
        orig_num = gain_data.antenna_numbers[0]
        gain_data.antenna_names[0] = "HH400"
        gain_data.antenna_numbers[0] = gain_data.antenna_numbers[1]
        gain_data.antenna_numbers[1] = orig_num

    # remove the antenna_positions to test matching them on read
    write_file = str(tmp_path / "test.calfits")
    write_file2 = str(tmp_path / "test2.calfits")
    gain_data.write_calfits(write_file)
    with fits.open(write_file, memmap=True) as hdu_list:
        hdunames = uvutils._fits_indexhdus(hdu_list)
        primary_hdu = hdu_list[0]
        ant_hdu = hdu_list[hdunames["ANTENNAS"]]

        table = Table(ant_hdu.data)
        del table["ANTXYZ"]
        ant_hdu = fits.BinTableHDU(table)
        ant_hdu.header["EXTNAME"] = "ANTENNAS"

        hdulist = fits.HDUList([primary_hdu, ant_hdu])

        hdulist.writeto(write_file2)
        hdulist.close()

    with pytest.raises(
        ValueError, match="Required UVParameter _antenna_positions has not been set."
    ):
        with uvtest.check_warnings(
            [UserWarning],
            match=[
                "Not all antennas have positions in the telescope object. "
                "Not setting antenna_positions."
            ],
        ):
            gain_data2 = UVCal.from_file(write_file2, use_future_array_shapes=True)

    with uvtest.check_warnings(
        UserWarning,
        match=[
            "Not all antennas have positions in the telescope object. "
            "Not setting antenna_positions."
        ],
    ):
        gain_data2 = UVCal.from_file(
            write_file2, use_future_array_shapes=True, run_check=False
        )

    assert gain_data2.antenna_positions is None


def test_read_errors():
    with pytest.raises(
        ValueError,
        match="File type could not be determined, use the file_type keyword to specify "
        "the type.",
    ):
        UVCal.from_file("foo.blah")

    gainfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.gain.calfits")
    with pytest.raises(
        ValueError,
        match="If filename is a list, tuple or array it cannot be nested or "
        "multi dimensional.",
    ):
        UVCal.from_file([[gainfile]])

    with pytest.raises(
        ValueError, match="The only supported file_types are 'calfits' and 'fhd'."
    ):
        UVCal.from_file(gainfile, file_type="foo")


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("uvcal_future_shapes", [True, False])
@pytest.mark.parametrize("flex_spw", [True, False])
def test_init_from_uvdata(
    uvdata_future_shapes, uvcal_future_shapes, flex_spw, uvcalibrate_data
):
    uvd, uvc = uvcalibrate_data

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()
        uvd.flex_spw_id_array = None

    if flex_spw:
        uvd._set_flex_spw()
        uvd.flex_spw_id_array = [1] * (uvd.Nfreqs // 2) + [2] * (uvd.Nfreqs // 2)
        uvd.spw_array = np.array([1, 2])
        uvd.Nspws = 2
        uvd.channel_width = np.full(uvd.Nfreqs, uvd.channel_width)
        uvd.check()

        uvc._set_flex_spw()
        uvc.flex_spw_id_array = [1] * (uvc.Nfreqs // 2) + [2] * (uvc.Nfreqs // 2)
        uvc.spw_array = np.array([1, 2])
        uvc.Nspws = 2
        uvc.channel_width = np.full(uvc.Nfreqs, uvc.channel_width)
        uvc.check()

    if not uvcal_future_shapes:
        uvc.use_current_array_shapes()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    uvc2 = uvc.copy(metadata_only=True)

    uvc_new = UVCal.initialize_from_uvdata(
        uvd, uvc.gain_convention, uvc.cal_style, future_array_shapes=uvcal_future_shapes
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.filterwarnings("ignore:Selected frequencies are not contiguous.")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("uvcal_future_shapes", [True, False])
@pytest.mark.parametrize("flex_spw", [True, False])
def test_init_from_uvdata_setfreqs(
    uvdata_future_shapes, uvcal_future_shapes, flex_spw, uvcalibrate_data
):
    uvd, uvc = uvcalibrate_data
    channel_width = uvd.channel_width[0]
    freqs_use = uvd.freq_array[0:5]

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()

    if not uvcal_future_shapes:
        uvc.use_current_array_shapes()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    uvc2 = uvc.copy(metadata_only=True)

    uvc2.select(frequencies=freqs_use)

    if flex_spw:
        flex_spw_id_array = np.array([1, 1, 2, 2, 2])
        uvc2._set_flex_spw()
        uvc2.flex_spw_id_array = flex_spw_id_array
        uvc2.spw_array = np.array([1, 2])
        uvc2.Nspws = 2
        uvc2.channel_width = np.full(uvc2.Nfreqs, uvc2.channel_width)
        # test passing a list instead of a single value
        channel_width = np.full(freqs_use.size, channel_width).tolist()
    else:
        if uvdata_future_shapes:
            flex_spw_id_array = np.zeros(5, dtype=int)
        else:
            flex_spw_id_array = None

    uvc_new = UVCal.initialize_from_uvdata(
        uvd,
        uvc.gain_convention,
        uvc.cal_style,
        future_array_shapes=uvcal_future_shapes,
        frequencies=freqs_use,
        channel_width=channel_width,
        flex_spw=flex_spw,
        flex_spw_id_array=flex_spw_id_array,
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    uvc_new.history = uvc2.history

    assert uvc_new == uvc2


def test_init_from_uvdata_setfreqs_errors(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data
    channel_width = uvd.channel_width[0]
    freqs_use = uvd.freq_array[np.newaxis, 0:5]

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    with pytest.raises(ValueError, match="Frequencies must be a 1 dimensional array"):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            frequencies=freqs_use,
            channel_width=channel_width,
        )

    with pytest.raises(
        ValueError, match="If frequencies is provided and flex_spw is True"
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            frequencies=freqs_use[0, :],
            channel_width=channel_width,
            flex_spw=True,
        )

    with pytest.raises(
        ValueError, match="channel_width must be provided if frequencies is provided"
    ):
        UVCal.initialize_from_uvdata(
            uvd, uvc.gain_convention, uvc.cal_style, frequencies=freqs_use[0, :]
        )

    with pytest.raises(
        ValueError, match="channel_width must be scalar if both future_array_shapes and"
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            future_array_shapes=False,
            frequencies=freqs_use[0, :],
            channel_width=np.full(freqs_use.size, channel_width),
        )


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("uvcal_future_shapes", [True, False])
@pytest.mark.parametrize("metadata_only", [True, False])
def test_init_from_uvdata_settimes(
    uvdata_future_shapes, uvcal_future_shapes, metadata_only, uvcalibrate_data
):
    uvd, uvc = uvcalibrate_data
    integration_time = np.mean(uvd.integration_time)
    times_use = uvc.time_array[0:3]

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()

    if not uvcal_future_shapes:
        uvc.use_current_array_shapes()
    elif metadata_only:
        # test passing in a list of integration times
        integration_time = np.full(
            times_use.size, np.mean(uvd.integration_time)
        ).tolist()

    uvc2 = uvc.copy(metadata_only=metadata_only)

    uvc2.select(times=times_use)

    uvc_new = UVCal.initialize_from_uvdata(
        uvd,
        uvc.gain_convention,
        uvc.cal_style,
        future_array_shapes=uvcal_future_shapes,
        metadata_only=metadata_only,
        times=times_use,
        integration_time=integration_time,
        time_range=uvc.time_range,
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    if not metadata_only:
        uvc2.gain_array *= 0.0
        uvc2.quality_array *= 0.0
        uvc2.flag_array[:] = False
        uvc2.total_quality_array = None

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


def test_init_from_uvdata_settimes_errors(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data
    times_use = uvc.time_array[0:3]
    integration_time = np.full(times_use.size, np.mean(uvd.integration_time)).tolist()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    uvc2 = uvc.copy(metadata_only=True)

    uvc2.select(times=times_use)

    with pytest.raises(
        ValueError, match="integation_time must be provided if times is provided"
    ):
        UVCal.initialize_from_uvdata(
            uvd, uvc.gain_convention, uvc.cal_style, times=times_use
        )

    with pytest.raises(
        ValueError,
        match="integration_time must be scalar if future_array_shapes is False.",
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            future_array_shapes=False,
            times=times_use,
            integration_time=integration_time,
        )


def test_init_from_uvdata_setjones(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    uvc2 = uvc.copy(metadata_only=True)

    uvc_new = UVCal.initialize_from_uvdata(
        uvd, uvc.gain_convention, uvc.cal_style, jones=[-5, -6]
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


@pytest.mark.parametrize("pol", ["nn", "ee", "rr", "ll"])
def test_init_single_pol(uvcalibrate_data, pol):
    uvd, uvc = uvcalibrate_data

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    if pol in ["ll", "rr"]:
        # convert to circular pol
        uvd.polarization_array = np.array([-1, -2, -3, -4])
        uvc.jones_array = np.array([-1, -2])

    # downselect to one pol
    uvd.select(polarizations=[pol])
    uvc.select(jones=[pol])

    uvc2 = uvc.copy(metadata_only=True)

    uvc_new = UVCal.initialize_from_uvdata(uvd, uvc.gain_convention, uvc.cal_style)

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


def test_init_from_uvdata_circular_pol(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    # convert to circular pol
    uvd.polarization_array = np.array([-1, -2, -3, -4])
    uvc.jones_array = np.array([-1, -2])

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    uvc2 = uvc.copy(metadata_only=True)

    uvc_new = UVCal.initialize_from_uvdata(uvd, uvc.gain_convention, uvc.cal_style)

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("uvcal_future_shapes", [True, False])
def test_init_from_uvdata_sky(
    uvdata_future_shapes, uvcal_future_shapes, uvcalibrate_data, fhd_cal_raw
):
    uvd, uvc = uvcalibrate_data
    uvc_sky = fhd_cal_raw

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()

    if not uvcal_future_shapes:
        uvc.use_current_array_shapes()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    # make cal object be a sky cal type
    uvc._set_sky()
    params_to_copy = [
        "sky_field",
        "sky_catalog",
        "ref_antenna_name",
        "diffuse_model",
        "baseline_range",
        "Nsources",
        "observer",
        "gain_scale",
        "git_hash_cal",
        "git_origin_cal",
        "extra_keywords",
    ]
    for param in params_to_copy:
        setattr(uvc, param, getattr(uvc_sky, param))

    # also set gain scale to test that it is set properly
    uvc.gain_scale = "Jy"

    uvc2 = uvc.copy(metadata_only=True)

    uvc_new = UVCal.initialize_from_uvdata(
        uvd,
        uvc.gain_convention,
        uvc.cal_style,
        future_array_shapes=uvcal_future_shapes,
        sky_field=uvc_sky.sky_field,
        sky_catalog=uvc_sky.sky_catalog,
        ref_antenna_name=uvc_sky.ref_antenna_name,
        diffuse_model=uvc_sky.diffuse_model,
        baseline_range=uvc_sky.baseline_range,
        Nsources=uvc_sky.Nsources,
        observer=uvc_sky.observer,
        gain_scale="Jy",
        git_hash_cal=uvc_sky.git_hash_cal,
        git_origin_cal=uvc_sky.git_origin_cal,
        extra_keywords=uvc_sky.extra_keywords,
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("uvcal_future_shapes", [True, False])
@pytest.mark.parametrize("flex_spw", [True, False])
@pytest.mark.parametrize("set_frange", [True, False])
def test_init_from_uvdata_delay(
    uvdata_future_shapes, uvcal_future_shapes, flex_spw, set_frange, uvcalibrate_data
):
    uvd, uvc = uvcalibrate_data

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    # make cal object be a delay cal type
    uvc2 = uvc.copy(metadata_only=True)
    uvc2._set_delay()
    uvc2.Nfreqs = 1
    uvc2.flex_spw_id_array = None
    uvc2.freq_array = None
    uvc2.channel_width = None
    uvc2.freq_range = np.array([[np.min(uvc.freq_array), np.max(uvc.freq_array)]])

    if flex_spw:
        spw_cut = uvd.Nfreqs // 2
        uvd._set_flex_spw()
        uvd.flex_spw_id_array = [1] * spw_cut + [2] * spw_cut
        uvd.spw_array = np.array([1, 2])
        uvd.Nspws = 2
        uvd.channel_width = np.full(uvd.Nfreqs, uvd.channel_width)
        uvd.check()

        uvc._set_flex_spw()
        uvc.flex_spw_id_array = np.asarray([1] * spw_cut + [2] * spw_cut)
        uvc.spw_array = np.array([1, 2])
        uvc.Nspws = 2
        uvc.channel_width = np.full(uvc.Nfreqs, uvc.channel_width)
        uvc.check()

    if not uvcal_future_shapes:
        uvc.use_current_array_shapes()
        uvc2.use_current_array_shapes()
    elif flex_spw:
        uvc2.spw_array = np.array([1, 2])
        uvc2.Nspws = 2
        uvc2.freq_range = np.asarray(
            [
                [np.min(uvc.freq_array[0:spw_cut]), np.max(uvc.freq_array[0:spw_cut])],
                [np.min(uvc.freq_array[spw_cut:]), np.max(uvc.freq_array[spw_cut:])],
            ]
        )
    uvc2.check()

    if set_frange:
        freq_range = uvc2.freq_range
        if flex_spw:
            spw_array = uvc2.spw_array
        else:
            # check that it works with 1d array for one spw
            freq_range = np.squeeze(uvc2.freq_range)
            spw_array = None
    else:
        freq_range = None
        spw_array = None

    uvc_new = UVCal.initialize_from_uvdata(
        uvd,
        uvc.gain_convention,
        uvc.cal_style,
        future_array_shapes=uvcal_future_shapes,
        cal_type="delay",
        freq_range=freq_range,
        spw_array=spw_array,
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


@pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0 when")
@pytest.mark.parametrize("uvdata_future_shapes", [True, False])
@pytest.mark.parametrize("flex_spw", [True, False])
@pytest.mark.parametrize("set_frange", [True, False])
def test_init_from_uvdata_wideband(
    uvdata_future_shapes, flex_spw, set_frange, uvcalibrate_data
):
    uvd, uvc = uvcalibrate_data

    # wide-band gain requires future array shapes

    if not uvdata_future_shapes:
        uvd.use_current_array_shapes()

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    # make cal object be a wide-band cal
    uvc2 = uvc.copy(metadata_only=True)
    uvc2._set_wide_band()
    uvc2.freq_range = np.asarray([[np.min(uvc.freq_array), np.max(uvc.freq_array)]])
    uvc2.Nfreqs = 1
    uvc2.flex_spw_id_array = None
    uvc2.freq_array = None
    uvc2.channel_width = None

    if flex_spw:
        spw_cut = uvd.Nfreqs // 2
        uvd._set_flex_spw()
        uvd.flex_spw_id_array = [1] * spw_cut + [2] * spw_cut
        uvd.spw_array = np.array([1, 2])
        uvd.Nspws = 2
        uvd.channel_width = np.full(uvd.Nfreqs, uvd.channel_width)
        uvd.check()

        uvc._set_flex_spw()
        uvc.flex_spw_id_array = np.asarray([1] * spw_cut + [2] * spw_cut)
        uvc.spw_array = np.array([1, 2])
        uvc.Nspws = 2
        uvc.channel_width = np.full(uvc.Nfreqs, uvc.channel_width)
        uvc.check()

        uvc2.spw_array = np.array([1, 2])
        uvc2.Nspws = 2
        uvc2.freq_range = np.asarray(
            [
                [np.min(uvc.freq_array[0:spw_cut]), np.max(uvc.freq_array[0:spw_cut])],
                [np.min(uvc.freq_array[spw_cut:]), np.max(uvc.freq_array[spw_cut:])],
            ]
        )

    uvc2.check()

    if set_frange:
        freq_range = uvc2.freq_range
        if flex_spw:
            spw_array = uvc2.spw_array
        else:
            spw_array = None
    else:
        freq_range = None
        spw_array = None

    uvc_new = UVCal.initialize_from_uvdata(
        uvd,
        uvc.gain_convention,
        uvc.cal_style,
        wide_band=True,
        freq_range=freq_range,
        spw_array=spw_array,
    )

    # antenna positions are different by ~6cm or less. The ones in the uvcal file
    # derive from info on our telescope object while the ones in the uvdata file
    # derive from the HERA correlator. I'm not sure why they're different, but it may be
    # because the data are a little old
    assert np.allclose(uvc2.antenna_positions, uvc_new.antenna_positions, atol=0.1)
    uvc_new.antenna_positions = uvc2.antenna_positions

    assert uvutils._check_histories(
        uvc_new.history,
        "Initialized from a UVData object with pyuvdata."
        " UVData history is: " + uvd.history,
    )

    uvc_new.history = uvc2.history

    # The times are different by 9.31322575e-10, which is below than our tolerance on
    # the time array (which is 1ms = 1.1574074074074074e-08) but it leads to differences
    # in the lsts of 5.86770454e-09 which are larger than our tolerance
    # (which is 1mas = 4.84813681109536e-09)
    # I'm not sure why the times are different at all, there must have been some loss
    # of precision in the processing pipeline.
    assert uvc_new._time_array == uvc2._time_array
    uvc_new.time_array = uvc2.time_array
    uvc_new.set_lsts_from_time_array()

    assert uvc_new == uvc2


def test_init_from_uvdata_basic_errors(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    with pytest.raises(
        ValueError, match="uvdata must be a UVData \\(or subclassed\\) object."
    ):
        UVCal.initialize_from_uvdata(uvc, uvc.gain_convention, uvc.cal_style)

    with pytest.raises(ValueError, match="cal_type must be either 'gain' or 'delay'."):
        UVCal.initialize_from_uvdata(
            uvd, uvc.gain_convention, uvc.cal_style, cal_type="unknown"
        )

    with pytest.raises(
        ValueError,
        match="If cal_style is 'sky', ref_antenna_name, sky_catalog and sky_field "
        "must all be provided.",
    ):
        UVCal.initialize_from_uvdata(uvd, uvc.gain_convention, "sky")

    uvd.polarization_array = np.array([1, 2, 3, 4])
    with pytest.raises(
        ValueError,
        match="jones parameter is None and uvdata object is in "
        "psuedo-stokes polarization. Please set jones.",
    ):
        UVCal.initialize_from_uvdata(uvd, uvc.gain_convention, uvc.cal_style)


def test_init_from_uvdata_freqrange_errors(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    with pytest.raises(
        ValueError,
        match="if future_array_shapes is True, freq_range must be an array shaped like",
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            cal_type="delay",
            freq_range=[1e8, 1.2e8, 1.3e8, 1.5e8],
        )

    with pytest.raises(
        ValueError,
        match="An spw_array must be provided for delay or wide-band cals if freq_range "
        "has multiple spectral windows",
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            cal_type="delay",
            freq_range=np.asarray([[1e8, 1.2e8], [1.3e8, 1.5e8]]),
        )

    with pytest.raises(
        ValueError,
        match="if future_array_shapes is False, freq_range must have 2 elements.",
    ):
        UVCal.initialize_from_uvdata(
            uvd,
            uvc.gain_convention,
            uvc.cal_style,
            cal_type="delay",
            future_array_shapes=False,
            freq_range=np.asarray([[1e8, 1.2e8], [1.3e8, 1.5e8]]),
        )


def test_init_from_uvdata_vary_chanwidth(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    # make uvdata have varying channel widths
    uvd.channel_width[-1] = uvd.channel_width[0] * 2.0

    with pytest.raises(
        ValueError, match="uvdata has varying channel widths but does not have"
    ):
        UVCal.initialize_from_uvdata(
            uvd, uvc.gain_convention, uvc.cal_style, future_array_shapes=False
        )


def test_init_from_uvdata_vary_inttimes(uvcalibrate_data):
    uvd, uvc = uvcalibrate_data

    # uvc has a time_range which it shouldn't really have because Ntimes > 1,
    # but that requirement is not enforced. Set it to None for this test
    uvc.time_range = None

    # make uvdata have varying integration times
    uvd.integration_time[-1] = uvd.integration_time[0] * 2.0

    with pytest.raises(
        ValueError,
        match="uvdata integration times vary. Please specify times and "
        "integration_time",
    ):
        UVCal.initialize_from_uvdata(uvd, uvc.gain_convention, uvc.cal_style)


# pyuvdata/__init__.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Init file for pyuvdata."""
import warnings
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from setuptools_scm import get_version

from .branch_scheme import branch_scheme

try:  # pragma: nocover
    # get accurate version for developer installs
    version_str = get_version(Path(__file__).parent.parent, local_scheme=branch_scheme)

    __version__ = version_str

except (LookupError, ImportError):
    try:
        # Set the version automatically from the package details.
        __version__ = version("pyuvdata")
    except PackageNotFoundError:  # pragma: nocover
        # package is not installed
        pass

# Filter annoying Cython warnings that serve no good purpose. see numpy#432
# needs to be done before the imports to work properly
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")

from .telescopes import Telescope, get_telescope, known_telescopes  # noqa
from .uvbeam import UVBeam  # noqa
from .uvcal import UVCal  # noqa
from .uvdata import FastUVH5Meta  # noqa
from .uvdata import UVData  # noqa
from .uvflag import UVFlag  # noqa

__all__ = [
    "UVData",
    "UVCal",
    "UVFlag",
    "UVBeam",
    "Telescope",
    "known_telescopes",
    "get_telescope",
]


# adapted from https://github.com/astropy/astropy/__init__.py
# please consult astropy/__init__.py for clarification on logic details

# Cleanup the top-level namespace.
# Delete everything that is not in __all__, a magic function,
# or is a submodule of this package
from types import ModuleType as __module_type__  # noqa

for varname in dir():
    if not (
        varname in __all__
        or (varname.startswith("__") and varname.endswith("__"))
        or (
            varname[0] != "_"
            and isinstance(locals()[varname], __module_type__)
            and locals()[varname].__name__.startswith(__name__ + ".")
        )
    ):
        del locals()[varname]

del varname, __module_type__


# pyuvdata/branch_scheme.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2020 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Define branch_scheme for versioning."""


def branch_scheme(version):  # pragma: nocover
    """
    Local version scheme that adds the branch name for absolute reproducibility.

    If and when this is added to setuptools_scm this function and file can be removed.
    """
    if version.exact or version.node is None:
        return version.format_choice("", "+d{time:{time_format}}", time_format="%Y%m%d")
    else:
        if version.branch == "main":
            return version.format_choice("+{node}", "+{node}.dirty")
        else:
            return version.format_choice("+{node}.{branch}", "+{node}.{branch}.dirty")


# pyuvdata/conftest.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Testing environment setup and teardown for pytest."""
import os

import numpy as np
import pytest
from astropy.coordinates import EarthLocation
from astropy.time import Time
from astropy.utils import iers

from pyuvdata import UVCal, UVData
from pyuvdata.data import DATA_PATH


@pytest.fixture(autouse=True, scope="session")
def setup_and_teardown_package():
    """Handle possible IERS issues."""
    # Do a calculation that requires a current IERS table. This will trigger
    # automatic downloading of the IERS table if needed, including trying the
    # mirror site in python 3 (but won't redownload if a current one exists).
    # If there's not a current IERS table and it can't be downloaded, turn off
    # auto downloading for the tests and turn it back on once all tests are
    # completed (done by extending auto_max_age).
    # Also, the check_warnings function will ignore IERS-related warnings.
    try:
        t1 = Time.now()
        t1.ut1
    except Exception:
        iers.conf.auto_max_age = None

    # Also ensure that we're downloading the site data from astropy
    EarthLocation._get_site_registry(force_download=True)

    yield

    iers.conf.auto_max_age = 30


@pytest.fixture(scope="session")
def uvcalibrate_init_data_main():
    """Make initial uvcalibrate inputs."""
    uvdata = UVData()
    uvdata.read(
        os.path.join(DATA_PATH, "zen.2458098.45361.HH.uvh5_downselected"),
        file_type="uvh5",
        use_future_array_shapes=True,
    )
    uvcal = UVCal()
    uvcal.read_calfits(
        os.path.join(DATA_PATH, "zen.2458098.45361.HH.omni.calfits_downselected"),
        use_future_array_shapes=True,
    )

    yield uvdata, uvcal


@pytest.fixture(scope="function")
def uvcalibrate_init_data(uvcalibrate_init_data_main):
    """Make function level initial uvcalibrate inputs."""
    uvdata_in, uvcal_in = uvcalibrate_init_data_main

    uvdata = uvdata_in.copy()
    uvcal = uvcal_in.copy()

    yield uvdata, uvcal


@pytest.fixture(scope="session")
def uvcalibrate_data_main(uvcalibrate_init_data_main):
    """Make uvcalibrate inputs."""
    uvdata_in, uvcal_in = uvcalibrate_init_data_main

    uvdata = uvdata_in.copy()
    uvcal = uvcal_in.copy()

    # fix the antenna names in the uvcal object to match the uvdata object
    uvcal.antenna_names = np.array(
        [name.replace("ant", "HH") for name in uvcal.antenna_names]
    )

    yield uvdata, uvcal


@pytest.fixture(scope="function")
def uvcalibrate_data(uvcalibrate_data_main):
    """Make function level uvcalibrate inputs."""
    uvdata_in, uvcal_in = uvcalibrate_data_main

    uvdata = uvdata_in.copy()
    uvcal = uvcal_in.copy()

    yield uvdata, uvcal


# pyuvdata/parameter.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""
Define UVParameters: data and metadata objects for interferometric data sets.

UVParameters are objects to hold specific data and metadata associated with
interferometric data sets. They are used as attributes for classes based on
UVBase. This module also includes specialized subclasses for particular types
of metadata.

"""
import builtins
import warnings

import astropy.units as units
import numpy as np
from astropy.coordinates import SkyCoord

from . import utils

__all__ = ["UVParameter", "AngleParameter", "LocationParameter"]


def _get_generic_type(expected_type, strict_type_check=False):
    """Return tuple of more generic types.

    Allows for more flexible type checking in the case when a Parameter's value
    changes precision or to/from a numpy dtype but still is the desired generic type.
    If a generic type cannot be found, the expected_type is returned

    Parameters
    ----------
    expected_type : Type or string
        The expected type of a Parameter object or a string of the name of a type.
    strict_type_check : bool
        If True the input expected_type is return exactly
    if strict_type_check:
        return expected_type exactly

    Returns
    -------
    Tuple of types based on input expected_type

    """
    if isinstance(expected_type, str):
        try:
            expected_type = getattr(builtins, expected_type)
        except AttributeError as err:
            raise ValueError(
                f"Input expected_type is a string with value: '{expected_type}'. "
                "When the expected_type is a string, it must be a Python builtin type."
            ) from err
    if strict_type_check:
        return expected_type

    for types in [
        (bool, np.bool_),
        (float, np.floating),
        (np.unsignedinteger),  # unexpected but just in case
        (int, np.integer),
        (complex, np.complexfloating),
    ]:
        if issubclass(expected_type, types):
            return types

    return expected_type


def _param_dict_equal(this_dict, other_dict):
    """
    Test if dicts are equal for parameter equality.

    Helper function pulled out to allow recursion for nested dicts
    """
    try:
        # Try a naive comparison first
        # this will fail if keys are the same
        # but cases differ.
        # so only look for exact equality
        # then default to the long test below.
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", "elementwise comparison failed")
            if this_dict == other_dict:
                return True, ""
    except (ValueError, TypeError):
        pass
        # this dict may contain arrays or Nones
        # we will need to check each item individually

    # check to see if they are equal other than
    # upper/lower case keys
    this_lower = {
        (k.lower() if isinstance(k, str) else k): v for k, v in this_dict.items()
    }
    other_lower = {
        (k.lower() if isinstance(k, str) else k): v for k, v in other_dict.items()
    }
    if set(this_lower.keys()) != set(other_lower.keys()):
        message_str = ", keys are not the same."
        return False, message_str
    else:
        # need to check if values are close,
        # not just equal
        for key in this_lower.keys():
            if isinstance(this_lower[key], dict):
                # nested dict, use recursion
                subdict_equal, subdict_message = _param_dict_equal(
                    this_lower[key], other_lower[key]
                )
                if subdict_equal:
                    continue
                else:
                    message_str = f", key {key} is a dict" + subdict_message
                    return False, message_str

            # this is not a dict, use other methods
            if this_lower[key] is None or other_lower[key] is None:
                if this_lower[key] is None and other_lower[key] is None:
                    continue
                else:
                    message_str = f", key {key} is not equal"
                    return False, message_str

            if isinstance(this_lower[key], (list, np.ndarray, tuple)) and isinstance(
                other_lower[key], (list, np.ndarray, tuple)
            ):
                this_array = np.asarray(this_lower[key])
                other_array = np.asarray(other_lower[key])
                if this_array.shape != other_array.shape:
                    message_str = f", key {key} is not equal"
                    return False, message_str
                if np.allclose(this_array, other_array):
                    continue
                else:
                    message_str = f", key {key} is not equal"
                    return False, message_str
            else:
                # this isn't a list, array or tuple
                try:
                    if np.isclose(this_lower[key], other_lower[key]):
                        continue
                    else:
                        message_str = f", key {key} is not equal"
                        return False, message_str
                except TypeError:
                    # this isn't a type that can be
                    # handled by np.isclose,
                    # test for equality
                    if this_lower[key] == other_lower[key]:
                        continue
                    else:
                        message_str = f", key {key} is not equal"
                        return False, message_str

    return True, ""


class UVParameter(object):
    """
    Data and metadata objects for interferometric data sets.

    Parameters
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : float or 2-tuple of float
        Tolerances for testing the equality of UVParameters. Either a single
        absolute value or a tuple of relative and absolute values to be used by
        np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precisions or to/from numpy
        dtypes to not break checks.

    Attributes
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : 2-tuple of float
        Relative and absolute tolerances for testing the equality of UVParameters, to be
        used by np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precisions or to/from numpy
        dtypes to not break checks.

    """

    def __init__(
        self,
        name,
        required=True,
        value=None,
        spoof_val=None,
        form=(),
        description="",
        expected_type=int,
        acceptable_vals=None,
        acceptable_range=None,
        tols=(1e-05, 1e-08),
        strict_type_check=False,
        ignore_eq_none: bool = False,
    ):
        """Init UVParameter object."""
        self.name = name
        self.required = required
        # cannot set a spoof_val for required parameters
        if not self.required:
            self.spoof_val = spoof_val
        self.value = value
        self.description = description
        self.form = form
        if self.form == "str":
            self.expected_type = str
            self.strict_type = True
        else:
            self.expected_type = _get_generic_type(
                expected_type, strict_type_check=strict_type_check
            )
            self.strict_type = strict_type_check
        self.acceptable_vals = acceptable_vals
        self.acceptable_range = acceptable_range
        if np.size(tols) == 1:
            # Only one tolerance given, assume absolute, set relative to zero
            self.tols = (0, tols)
        else:
            # relative and absolute tolerances to be used in np.isclose
            self.tols = tols

        self.ignore_eq_none = ignore_eq_none and not required

    def __eq__(self, other, silent=False):
        """
        Test if classes match and values are within tolerances.

        Parameters
        ----------
        other : UVParameter or subclass
            The other UVParameter to compare with this one.
        silent : bool
            When set to False (default), descriptive text is printed out when parameters
            do not match. If set to True, this text is not printed.
        """
        if not (
            isinstance(other, self.__class__) and isinstance(self, other.__class__)
        ):
            if not silent:
                print(f"{self.name} parameter classes are different")
            return False

        # if a parameter should be considered equal if one of them is None, exit here.
        if self.ignore_eq_none and (self.value is None or other.value is None):
            return True

        if self.value is None:
            if other.value is not None:
                if not silent:
                    print(f"{self.name} is None on left, but not right")
                return False
            else:
                return True
        if other.value is None:
            if self.value is not None:
                if not silent:
                    print(f"{self.name} is None on right, but not left")
                return False

        if isinstance(self.value, np.ndarray) and not isinstance(
            self.value.item(0), (str, np.str_)
        ):
            if not isinstance(other.value, np.ndarray):
                if not silent:
                    print(f"{self.name} parameter value is array, but other is not")
                return False
            if self.value.shape != other.value.shape:
                if not silent:
                    print(
                        f"{self.name} parameter value is array, shapes are " "different"
                    )
                return False

            if isinstance(self.value, units.Quantity):
                if not self.value.unit.is_equivalent(other.value.unit):
                    if not silent:
                        print(
                            f"{self.name} parameter value is an astropy Quantity, "
                            "units are not equivalent"
                        )
                    return False
                if not isinstance(self.tols[1], units.Quantity):
                    atol_use = self.tols[1] * self.value.unit
                else:
                    atol_use = self.tols[1]
                if not units.quantity.allclose(
                    self.value,
                    other.value,
                    rtol=self.tols[0],
                    atol=atol_use,
                    equal_nan=True,
                ):
                    if not silent:
                        print(
                            f"{self.name} parameter value is an astropy Quantity, "
                            "values are not close"
                        )
                    return False
            else:
                # check to see if strict types are used
                if self.strict_type:
                    # types must match
                    if other.strict_type:
                        # both strict, expected_type must match
                        if self.expected_type != other.expected_type:
                            if not silent:
                                print(
                                    f"{self.name} parameter has incompatible "
                                    f"types. Left is {self.expected_type}, right "
                                    f"is {other.expected_type}"
                                )
                            return False
                    elif not isinstance(self.value.item(0), other.expected_type):
                        if not silent:
                            print(
                                f"{self.name} parameter has incompatible dtypes. "
                                f"Left requires {self.expected_type}, right is "
                                f"{other.value.dtype}"
                            )
                        return False
                elif other.strict_type:
                    # types must match in the other direction
                    if not isinstance(other.value.item(0), self.expected_type):
                        if not silent:
                            print(
                                f"{self.name} parameter has incompatible dtypes. "
                                f"Left is {self.value.dtype}, right requires "
                                f"{other.expected_type}"
                            )
                        return False
                if not np.allclose(
                    self.value,
                    other.value,
                    rtol=self.tols[0],
                    atol=self.tols[1],
                    equal_nan=True,
                ):
                    if not silent:
                        print(
                            f"{self.name} parameter value is array, values are not "
                            "close"
                        )
                    return False
        else:
            # check to see if strict types are used
            if self.strict_type:
                # types must match
                if not isinstance(self.value, other.expected_type):
                    if not silent:
                        print(
                            f"{self.name} parameter has incompatible types. Left "
                            f"requires {type(self.value)}, right is "
                            f"{other.expected_type}"
                        )
                    return False
            if other.strict_type:
                # types must match in the other direction
                if not isinstance(other.value, self.expected_type):
                    if not silent:
                        print(
                            f"{self.name} parameter has incompatible types. Left "
                            f"is {self.expected_type}, right requires "
                            f"{type(other.value)}"
                        )
                    return False

            str_type = False
            if isinstance(self.value, str):
                str_type = True
            if isinstance(self.value, (list, np.ndarray, tuple)):
                if isinstance(self.value[0], str):
                    str_type = True

            if not str_type:
                if isinstance(other.value, np.ndarray):
                    if not silent:
                        print(
                            f"{self.name} parameter value is not an array, "
                            "but other is not"
                        )
                    return False
                try:
                    if not np.allclose(
                        np.array(self.value),
                        np.array(other.value),
                        rtol=self.tols[0],
                        atol=self.tols[1],
                        equal_nan=True,
                    ):
                        if not silent:
                            print(
                                f"{self.name} parameter value can be cast to an "
                                "array and tested with np.allclose. The values are "
                                "not close"
                            )
                        return False
                except TypeError:
                    if isinstance(self.value, dict):
                        message_str = f"{self.name} parameter is a dict"
                        dict_equal, dict_message_str = _param_dict_equal(
                            self.value, other.value
                        )

                        if dict_equal:
                            return True
                        else:
                            message_str += dict_message_str
                            if not silent:
                                print(message_str)
                            return False
                    else:
                        if self.value != other.value:
                            if not silent:
                                print(
                                    f"{self.name} parameter value is not a string "
                                    "or a dict and cannot be cast as a numpy "
                                    "array. The values are not equal."
                                )
                            return False

            else:
                if isinstance(self.value, (list, np.ndarray, tuple)):
                    if [s.strip() for s in self.value] != [
                        s.strip() for s in other.value
                    ]:
                        if not silent:
                            print(
                                f"{self.name} parameter value is a list of "
                                "strings, values are different"
                            )
                        return False
                else:
                    if self.value.strip() != other.value.strip():
                        if self.value.replace("\n", "").replace(
                            " ", ""
                        ) != other.value.replace("\n", "").replace(" ", ""):
                            if not silent:
                                print(
                                    f"{self.name} parameter value is a string, "
                                    "values are different"
                                )
                            return False

        return True

    def __ne__(self, other, silent=True):
        """
        Test if classes do not match or values are not within tolerances.

        Parameters
        ----------
        other : UVParameter or subclass
            The other UVParameter to compare with this one.
        silent : bool
            When set to False (default), descriptive text is printed out when parameters
            do not match. If set to True, this text is not printed.
        """
        return not self.__eq__(other, silent=silent)

    def apply_spoof(self):
        """Set value to spoof_val for non-required UVParameters."""
        self.value = self.spoof_val

    def expected_shape(self, uvbase):
        """
        Get the expected shape of the value based on the form.

        Parameters
        ----------
        uvbase : object
            Object with this UVParameter as an attribute. Needed
            because the form can refer to other UVParameters on this object.

        Returns
        -------
        tuple
            The expected shape of the value.
        """
        if self.form == "str":
            return self.form
        elif isinstance(self.form, (int, np.integer)):
            # Fixed shape, just return the form
            return (self.form,)
        else:
            # Given by other attributes, look up values
            eshape = ()
            for p in self.form:
                if isinstance(p, (int, np.integer)):
                    eshape = eshape + (p,)
                else:
                    val = getattr(uvbase, p)
                    if val is None:
                        raise ValueError(
                            f"Missing UVBase parameter {p} needed to "
                            f"calculate expected shape of parameter {self.name}"
                        )
                    eshape = eshape + (val,)
            return eshape

    def check_acceptability(self):
        """Check that values are acceptable."""
        if self.acceptable_vals is None and self.acceptable_range is None:
            return True, "No acceptability check"
        else:
            # either acceptable_vals or acceptable_range is set. Prefer acceptable_vals
            if self.acceptable_vals is not None:
                # acceptable_vals are a list of allowed values
                if self.expected_type is str:
                    # strings need to be converted to lower case
                    if isinstance(self.value, str):
                        value_set = {self.value.lower()}
                    else:
                        # this is a list or array of strings, make them all lower case
                        value_set = {x.lower() for x in self.value}
                    acceptable_vals = [x.lower() for x in self.acceptable_vals]
                else:
                    if isinstance(self.value, (list, np.ndarray)):
                        value_set = set(self.value)
                    else:
                        value_set = {self.value}
                    acceptable_vals = self.acceptable_vals
                for elem in value_set:
                    if elem not in acceptable_vals:
                        message = (
                            f"Value {elem}, is not in allowed values: {acceptable_vals}"
                        )
                        return False, message
                return True, "Value is acceptable"
            else:
                # acceptable_range is a tuple giving a range of allowed magnitudes
                testval = np.mean(np.abs(self.value))
                if (testval >= self.acceptable_range[0]) and (
                    testval <= self.acceptable_range[1]
                ):
                    return True, "Value is acceptable"
                else:
                    message = (
                        f"Mean of abs values, {testval}, is not in allowed range: "
                        f"{self.acceptable_range}"
                    )
                    return False, message


class AngleParameter(UVParameter):
    """
    Subclass of UVParameter for Angle type parameters.

    Adds extra methods for conversion to & from degrees (used by UVBase objects
    for _degrees properties associated with these parameters).

    Parameters
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : float or 2-tuple of float
        Tolerances for testing the equality of UVParameters. Either a single
        absolute value or a tuple of relative and absolute values to be used by
        np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    Attributes
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : 2-tuple of float
        Relative and absolute tolerances for testing the equality of UVParameters, to be
        used by np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    """

    def degrees(self):
        """Get value in degrees."""
        if self.value is None:
            return None
        else:
            return self.value * 180.0 / np.pi

    def set_degrees(self, degree_val):
        """
        Set value in degrees.

        Parameters
        ----------
        degree_val : float
            Value in degrees to use to set the value attribute.
        """
        if degree_val is None:
            self.value = None
        else:
            self.value = degree_val * np.pi / 180.0


class LocationParameter(UVParameter):
    """
    Subclass of UVParameter for location type parameters.

    Adds extra methods for conversion to & from lat/lon/alt in radians or
    degrees (used by UVBase objects for _lat_lon_alt and _lat_lon_alt_degrees
    properties associated with these parameters).

    Parameters
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    description : str
        Description of the data or metadata in the object.
    frame : str, optional
        Coordinate frame. Valid options are "itrs" (default) or "mcmf".
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : float or 2-tuple of float
        Tolerances for testing the equality of UVParameters. Either a single
        absolute value or a tuple of relative and absolute values to be used by
        np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    Attributes
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : int
       Always set to 3.
    description : str
        Description of the data or metadata in the object.
    frame : str, optional
        Coordinate frame. Valid options are "itrs" (default) or "mcmf".
    expected_type
        Always set to float.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : 2-tuple of float
        Relative and absolute tolerances for testing the equality of UVParameters, to be
        used by np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    """

    _range_dict = {"itrs": (6.35e6, 6.39e6), "mcmf": (1717100.0, 1757100.0)}

    def __init__(
        self,
        name,
        required=True,
        value=None,
        spoof_val=None,
        description="",
        frame="itrs",
        acceptable_range=-1,
        tols=1e-3,
    ):
        super(LocationParameter, self).__init__(
            name,
            required=required,
            value=value,
            spoof_val=spoof_val,
            form=3,
            description=description,
            expected_type=float,
            acceptable_range=acceptable_range,
            tols=tols,
        )
        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:
            return None
        else:
            # check defaults to False b/c exposed check kwarg exists in UVData
            return utils.LatLonAlt_from_XYZ(
                self.value, check_acceptability=False, frame=self.frame
            )

    def set_lat_lon_alt(self, lat_lon_alt):
        """
        Set value from (latitude, longitude, altitude) tuple in radians.

        Parameters
        ----------
        lat_lon_alt : 3-tuple of float
            Tuple with the latitude (radians), longitude (radians)
            and altitude (meters) to use to set the value attribute.
        """
        if lat_lon_alt is None:
            self.value = None
        else:
            self.value = utils.XYZ_from_LatLonAlt(
                lat_lon_alt[0], lat_lon_alt[1], lat_lon_alt[2], frame=self.frame
            )

    def lat_lon_alt_degrees(self):
        """Get value in (latitude, longitude, altitude) tuple in degrees."""
        if self.value is None:
            return None
        else:
            latitude, longitude, altitude = self.lat_lon_alt()
            return latitude * 180.0 / np.pi, longitude * 180.0 / np.pi, altitude

    def set_lat_lon_alt_degrees(self, lat_lon_alt_degree):
        """
        Set value from (latitude, longitude, altitude) tuple in degrees.

        Parameters
        ----------
        lat_lon_alt : 3-tuple of float
            Tuple with the latitude (degrees), longitude (degrees)
            and altitude (meters) to use to set the value attribute.

        """
        if lat_lon_alt_degree is None:
            self.value = None
        else:
            latitude, longitude, altitude = lat_lon_alt_degree
            self.value = utils.XYZ_from_LatLonAlt(
                latitude * np.pi / 180.0,
                longitude * np.pi / 180.0,
                altitude,
                frame=self.frame,
            )

    def check_acceptability(self):
        """Check that vector magnitudes are in range."""
        if self.acceptable_range is None:
            return True, "No acceptability check"
        else:
            # acceptable_range is a tuple giving a range of allowed vector magnitudes
            testval = np.sqrt(np.sum(np.abs(self.value) ** 2))
            if (testval >= self.acceptable_range[0]) and (
                testval <= self.acceptable_range[1]
            ):
                return True, "Value is acceptable"
            else:
                message = (
                    f"Value {testval}, is not in allowed range: {self.acceptable_range}"
                )
                return False, message


class SkyCoordParameter(UVParameter):
    """
    Subclass of UVParameter for SkyCoord parameters.

    Needed for handling tolerances properly. The `tols` attribute is interpreted as the
    tolerance of the sky separation in radians.

    Parameters
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    radian_tol : float
        Tolerance of the sky separation in radians.
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    Attributes
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str', int or tuple
        Either 'str' or an int (if a single value) or tuple giving information about the
        expected shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3
            - (): a single numeric value
            - 3: the value should be an array of shape (3, )

    description : str
        Description of the data or metadata in the object.
    expected_type
        Always set to SkyCoord.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : 2-tuple of float
        Set to (0, `radian_tol`).
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    """

    def __init__(
        self,
        name,
        required=True,
        value=None,
        spoof_val=None,
        form=(),
        description="",
        acceptable_range=None,
        # standard angle tolerance: 1 mas in radians.
        radian_tol=1 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0),
    ):
        super(SkyCoordParameter, self).__init__(
            name,
            required=required,
            value=value,
            spoof_val=spoof_val,
            form=form,
            description=description,
            expected_type=SkyCoord,
            acceptable_range=acceptable_range,
            tols=(0, radian_tol),
        )

    def __eq__(self, other, silent=False):
        if not issubclass(self.value.__class__, SkyCoord) or not issubclass(
            other.value.__class__, SkyCoord
        ):
            return super(SkyCoordParameter, self).__eq__(other, silent=silent)

        if self.value.shape != other.value.shape:
            if not silent:
                print(f"{self.name} parameter shapes are different")
            return False

        this_frame = self.value.frame.name
        other_frame = other.value.frame.name
        if this_frame != other_frame:
            if not silent:
                print(
                    f"{self.name} parameter has different frames, {this_frame} vs "
                    f"{other_frame}."
                )
            return False

        this_rep_type = self.value.representation_type
        other_rep_type = other.value.representation_type
        if this_rep_type != other_rep_type:
            if not silent:
                print(
                    f"{self.name} parameter has different representation_types, "
                    f"{this_rep_type} vs {other_rep_type}."
                )
            return False

        # finally calculate on sky separations
        sky_separation = self.value.separation(other.value).rad
        if np.any(sky_separation > self.tols[1]):
            if not silent:
                print(f"{self.name} parameter is not close. ")
            return False

        return True


# pyuvdata/telescopes.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Telescope information and known telescope list."""
import os

import numpy as np
from astropy.coordinates import Angle, EarthLocation

from pyuvdata.data import DATA_PATH

from . import parameter as uvp
from . import uvbase

__all__ = ["Telescope", "known_telescopes", "get_telescope"]

# We use astropy sites for telescope locations. The dict below is for
# telescopes not in astropy sites, or to include extra information for a telescope.

# The center_xyz is the location of the telescope in ITRF (earth-centered frame)

# Antenna positions can be specified via a csv file with the following columns:
# "name" -- antenna name, "number" -- antenna number, "x", "y", "z" -- ECEF coordinates
# relative to the telescope location.
KNOWN_TELESCOPES = {
    "PAPER": {
        "center_xyz": None,
        "latitude": Angle("-30d43m17.5s").radian,
        "longitude": Angle("21d25m41.9s").radian,
        "altitude": 1073.0,
        "citation": (
            "value taken from capo/cals/hsa7458_v000.py, "
            "comment reads KAT/SA  (GPS), altitude from elevationmap.net"
        ),
    },
    "HERA": {
        "center_xyz": None,
        "latitude": Angle("-30.72152612068925d").radian,
        "longitude": Angle("21.42830382686301d").radian,
        "altitude": 1051.69,
        "diameters": 14.0,
        "antenna_positions_file": "hera_ant_pos.csv",
        "citation": (
            "value taken from hera_mc geo.py script "
            "(using hera_cm_db_updates under the hood.)"
        ),
    },
    "SMA": {
        "center_xyz": None,
        "latitude": Angle("19d49m27.13895s").radian,
        "longitude": Angle("-155d28m39.08279s").radian,
        "altitude": 4083.948144,
        "citation": "Ho, P. T. P., Moran, J. M., & Lo, K. Y. 2004, ApJL, 616, L1",
    },
    "SZA": {
        "center_xyz": None,
        "latitude": Angle("37d16m49.3698s").radian,
        "longitude": Angle("-118d08m29.9126s").radian,
        "altitude": 2400.0,
        "citation": "Unknown",
    },
    "OVRO-LWA": {
        "center_xyz": None,
        "latitude": Angle("37.239777271d").radian,
        "longitude": Angle("-118.281666695d").radian,
        "altitude": 1183.48,
        "citation": "OVRO Sharepoint Documentation",
    },
}


class Telescope(uvbase.UVBase):
    """
    A class for defining a telescope for use with UVData objects.

    Attributes
    ----------
    citation : str
        text giving source of telescope information
    telescope_name : UVParameter of str
        name of the telescope
    telescope_location : UVParameter of array_like
        telescope location xyz coordinates in ITRF (earth-centered frame).
    antenna_diameters : UVParameter of float
        Optional, antenna diameters in meters. Used by CASA to construct a
        default beam if no beam is supplied.
    """

    def __init__(self):
        """Create a new Telescope object."""
        # add the UVParameters to the class
        # use the same names as in UVData so they can be automatically set
        self.citation = None

        self._telescope_name = uvp.UVParameter(
            "telescope_name", description="name of telescope " "(string)", form="str"
        )
        desc = (
            "telescope location: xyz in ITRF (earth-centered frame). "
            "Can also be set using telescope_location_lat_lon_alt or "
            "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,
        )
        desc = (
            "Antenna diameters in meters. Used by CASA to "
            "construct a default beam if no beam is supplied."
        )
        self._antenna_diameters = uvp.UVParameter(
            "antenna_diameters",
            required=False,
            description=desc,
            expected_type=float,
            tols=1e-3,  # 1 mm
        )

        desc = "Number of antennas in the array."
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", required=False, description=desc, expected_type=int
        )

        desc = (
            "List of antenna names, shape (Nants_telescope), "
            "with numbers given by antenna_numbers."
        )
        self._antenna_names = uvp.UVParameter(
            "antenna_names",
            required=False,
            description=desc,
            form=("Nants_telescope",),
            expected_type=str,
        )

        desc = (
            "List of integer antenna numbers corresponding to antenna_names, "
            "shape (Nants_telescope)."
        )
        self._antenna_numbers = uvp.UVParameter(
            "antenna_numbers",
            required=False,
            description=desc,
            form=("Nants_telescope",),
            expected_type=int,
        )

        desc = (
            "Array giving coordinates of antennas relative to "
            "telescope_location (ITRF frame), shape (Nants_telescope, 3), "
            "units meters. See the tutorial page in the documentation "
            "for an example of how to convert this to topocentric frame."
        )
        self._antenna_positions = uvp.UVParameter(
            "antenna_positions",
            required=False,
            description=desc,
            form=("Nants_telescope", 3),
            expected_type=float,
            tols=1e-3,  # 1 mm
        )

        super(Telescope, self).__init__()


def known_telescopes():
    """
    Get list of known telescopes.

    Returns
    -------
    list of str
        List of known telescope names.
    """
    astropy_sites = [site for site in EarthLocation.get_site_names() if site != ""]
    known_telescopes = list(set(astropy_sites + list(KNOWN_TELESCOPES.keys())))
    return known_telescopes


def _parse_antpos_file(antenna_positions_file):
    """
    Interpret the antenna positions file.

    Parameters
    ----------
    antenna_positions_file : str
        Name of the antenna_positions_file, which is assumed to be in DATA_PATH.
        Should contain antenna names, numbers and ECEF positions relative to the
        telescope location.

    Returns
    -------
    antenna_names : array of str
        Antenna names.
    antenna_names : array of int
        Antenna numbers.
    antenna_positions : array of float
        Antenna positions in ECEF relative to the telescope location.

    """
    columns = ["name", "number", "x", "y", "z"]
    formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble]

    dt = np.format_parser(formats, columns, [])
    ant_array = np.genfromtxt(
        antenna_positions_file,
        delimiter=",",
        autostrip=True,
        skip_header=1,
        dtype=dt.dtype,
    )
    antenna_names = ant_array["name"]
    antenna_numbers = ant_array["number"]
    antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T

    return antenna_names, antenna_numbers, antenna_positions.astype("float")


def get_telescope(telescope_name, telescope_dict_in=None):
    """
    Get Telescope object for a telescope in telescope_dict.

    Parameters
    ----------
    telescope_name : str
        Name of a telescope
    telescope_dict_in: dict
        telescope info dict. Default is None, meaning use KNOWN_TELESCOPES
        (other values are only used for testing)

    Returns
    -------
    Telescope object
        The Telescope object associated with telescope_name.
    """
    if telescope_dict_in is None:
        telescope_dict_in = KNOWN_TELESCOPES

    astropy_sites = EarthLocation.get_site_names()
    telescope_keys = list(telescope_dict_in.keys())
    telescope_list = [tel.lower() for tel in telescope_keys]
    if telescope_name in astropy_sites:
        telescope_loc = EarthLocation.of_site(telescope_name)

        obj = Telescope()
        obj.telescope_name = telescope_name
        obj.citation = "astropy sites"
        obj.telescope_location = np.array(
            [telescope_loc.x.value, telescope_loc.y.value, telescope_loc.z.value]
        )

    elif telescope_name.lower() in telescope_list:
        telescope_index = telescope_list.index(telescope_name.lower())
        telescope_dict = telescope_dict_in[telescope_keys[telescope_index]]
        obj = Telescope()
        obj.citation = telescope_dict["citation"]
        obj.telescope_name = telescope_keys[telescope_index]
        if telescope_dict["center_xyz"] is not None:
            obj.telescope_location = telescope_dict["center_xyz"]
        else:
            if (
                telescope_dict["latitude"] is None
                or telescope_dict["longitude"] is None
                or telescope_dict["altitude"] is None
            ):
                raise ValueError(
                    "either the center_xyz or the "
                    "latitude, longitude and altitude of the "
                    "telescope must be specified"
                )
            obj.telescope_location_lat_lon_alt = (
                telescope_dict["latitude"],
                telescope_dict["longitude"],
                telescope_dict["altitude"],
            )
    else:
        # no telescope matching this name
        return False

    # check for extra info
    if telescope_name.lower() in telescope_list:
        telescope_index = telescope_list.index(telescope_name.lower())
        telescope_dict = telescope_dict_in[telescope_keys[telescope_index]]
        if "diameters" in telescope_dict.keys():
            obj.antenna_diameters = telescope_dict["diameters"]

        if "antenna_positions_file" in telescope_dict.keys():
            antpos_file = os.path.join(
                DATA_PATH, telescope_dict["antenna_positions_file"]
            )
            antenna_names, antenna_numbers, antenna_positions = _parse_antpos_file(
                antpos_file
            )
            obj.Nants_telescope = antenna_names.size
            obj.antenna_names = antenna_names
            obj.antenna_numbers = antenna_numbers
            obj.antenna_positions = antenna_positions

    obj.check(run_check_acceptability=True)

    return obj


# pyuvdata/utils.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Commonly used utility functions."""
from __future__ import annotations

import copy
import re
import warnings
from collections.abc import Iterable
from copy import deepcopy
from functools import lru_cache, wraps
from typing import Iterable as IterableType

import erfa
import numpy as np
from astropy import units
from astropy.coordinates import Angle, Distance, EarthLocation, SkyCoord
from astropy.coordinates.matrix_utilities import rotation_matrix
from astropy.time import Time
from astropy.utils import iers
from scipy.spatial.distance import cdist

from . import _utils

try:
    from lunarsky import MoonLocation
    from lunarsky import SkyCoord as LunarSkyCoord
    from lunarsky import Time as LTime

    LUNAR_RADIUS = MoonLocation._lunar_radius
    hasmoon = True
except ImportError:
    hasmoon = False


__all__ = [
    "POL_STR2NUM_DICT",
    "POL_NUM2STR_DICT",
    "CONJ_POL_DICT",
    "JONES_STR2NUM_DICT",
    "JONES_NUM2STR_DICT",
    "LatLonAlt_from_XYZ",
    "XYZ_from_LatLonAlt",
    "rotECEF_from_ECEF",
    "ECEF_from_rotECEF",
    "ENU_from_ECEF",
    "ECEF_from_ENU",
    "phase_uvw",
    "unphase_uvw",
    "uvcalibrate",
    "apply_uvflag",
    "get_lst_for_time",
    "polstr2num",
    "polnum2str",
    "jstr2num",
    "jnum2str",
    "parse_polstr",
    "parse_jpolstr",
    "conj_pol",
    "reorder_conj_pols",
    "baseline_to_antnums",
    "antnums_to_baseline",
    "baseline_index_flip",
    "get_baseline_redundancies",
    "get_antenna_redundancies",
    "collapse",
    "mean_collapse",
    "absmean_collapse",
    "quadmean_collapse",
    "or_collapse",
    "and_collapse",
]

# fmt: off
# polarization constants
# maps polarization strings to polarization integers
POL_STR2NUM_DICT = {"pI": 1, "pQ": 2, "pU": 3, "pV": 4,
                    "I": 1, "Q": 2, "U": 3, "V": 4,  # support straight stokes names
                    "rr": -1, "ll": -2, "rl": -3, "lr": -4,
                    "xx": -5, "yy": -6, "xy": -7, "yx": -8,
                    "hh": -5, "vv": -6, "hv": -7, "vh": -8}

# maps polarization integers to polarization strings
POL_NUM2STR_DICT = {1: "pI", 2: "pQ", 3: "pU", 4: "pV",
                    -1: "rr", -2: "ll", -3: "rl", -4: "lr",
                    -5: "xx", -6: "yy", -7: "xy", -8: "yx"}

# maps how polarizations change when antennas are swapped
CONJ_POL_DICT = {"xx": "xx", "yy": "yy", "xy": "yx", "yx": "xy",
                 "ee": "ee", "nn": "nn", "en": "ne", "ne": "en",
                 "rr": "rr", "ll": "ll", "rl": "lr", "lr": "rl",
                 "I": "I", "Q": "Q", "U": "U", "V": "V",
                 "pI": "pI", "pQ": "pQ", "pU": "pU", "pV": "pV"}

# maps jones matrix element strings to jones integers
# Add entries that don't start with "J" to allow shorthand versions
JONES_STR2NUM_DICT = {"Jxx": -5, "Jyy": -6, "Jxy": -7, "Jyx": -8,
                      "xx": -5, "x": -5, "yy": -6, "y": -6, "xy": -7, "yx": -8,
                      "Jrr": -1, "Jll": -2, "Jrl": -3, "Jlr": -4,
                      "rr": -1, "r": -1, "ll": -2, "l": -2, "rl": -3, "lr": -4}
# maps jones integers to jones matrix element strings
JONES_NUM2STR_DICT = {-1: "Jrr", -2: "Jll", -3: "Jrl", -4: "Jlr",
                      -5: "Jxx", -6: "Jyy", -7: "Jxy", -8: "Jyx"}

# maps uvdata pols to input feed polarizations
POL_TO_FEED_DICT = {"xx": ["x", "x"], "yy": ["y", "y"],
                    "xy": ["x", "y"], "yx": ["y", "x"],
                    "ee": ["e", "e"], "nn": ["n", "n"],
                    "en": ["e", "n"], "ne": ["n", "e"],
                    "rr": ["r", "r"], "ll": ["l", "l"],
                    "rl": ["r", "l"], "lr": ["l", "r"]}

# fmt: on


def _get_iterable(x):
    """Return iterable version of input."""
    if isinstance(x, Iterable):
        return x
    else:
        return (x,)


def _fits_gethduaxis(hdu, axis):
    """
    Make axis arrays for fits files.

    Parameters
    ----------
    hdu : astropy.io.fits HDU object
        The HDU to make an axis array for.
    axis : int
        The axis number of interest (1-based).

    Returns
    -------
    ndarray of float
        Array of values for the specified axis.

    """
    ax = str(axis)
    axis_num = hdu.header["NAXIS" + ax]
    val = hdu.header["CRVAL" + ax]
    delta = hdu.header["CDELT" + ax]
    index = hdu.header["CRPIX" + ax] - 1

    return delta * (np.arange(axis_num) - index) + val


def _fits_indexhdus(hdulist):
    """
    Get a dict of table names and HDU numbers from a FITS HDU list.

    Parameters
    ----------
    hdulist : list of astropy.io.fits HDU objects
        List of HDUs to get names for

    Returns
    -------
    dict
        dictionary with table names as keys and HDU number as values.

    """
    tablenames = {}
    for i in range(len(hdulist)):
        try:
            tablenames[hdulist[i].header["EXTNAME"]] = i
        except KeyError:
            continue
    return tablenames


def _get_fits_extra_keywords(header, keywords_to_skip=None):
    """
    Get any extra keywords and return as dict.

    Parameters
    ----------
    header : FITS header object
        header object to get extra_keywords from.
    keywords_to_skip : list of str
        list of keywords to not include in extra keywords in addition to standard
        FITS keywords.

    Returns
    -------
    dict
        dict of extra keywords.
    """
    # List standard FITS header items that are still should not be included in
    # extra_keywords
    # These are the beginnings of FITS keywords to ignore, the actual keywords
    # often include integers following these names (e.g. NAXIS1, CTYPE3)
    std_fits_substrings = [
        "HISTORY",
        "SIMPLE",
        "BITPIX",
        "EXTEND",
        "BLOCKED",
        "GROUPS",
        "PCOUNT",
        "GCOUNT",
        "BSCALE",
        "BZERO",
        "NAXIS",
        "PTYPE",
        "PSCAL",
        "PZERO",
        "CTYPE",
        "CRVAL",
        "CRPIX",
        "CDELT",
        "CROTA",
        "CUNIT",
    ]

    if keywords_to_skip is not None:
        std_fits_substrings.extend(keywords_to_skip)

    extra_keywords = {}
    # find all the other header items and keep them as extra_keywords
    for key in header:
        # check if key contains any of the standard FITS substrings
        if np.any([sub in key for sub in std_fits_substrings]):
            continue
        if key == "COMMENT":
            extra_keywords[key] = str(header.get(key))
        elif key != "":
            extra_keywords[key] = header.get(key)

    return extra_keywords


def _check_history_version(history, version_string):
    """Check if version_string is present in history string."""
    if version_string.replace(" ", "") in history.replace("\n", "").replace(" ", ""):
        return True
    else:
        return False


def _check_histories(history1, history2):
    """Check if two histories are the same."""
    if history1.replace("\n", "").replace(" ", "") == history2.replace(
        "\n", ""
    ).replace(" ", ""):
        return True
    else:
        return False


def _combine_history_addition(history1, history2):
    """
    Find extra history to add to have minimal repeats.

    Parameters
    ----------
    history1 : str
        First history.
    history2 : str
        Second history

    Returns
    -------
    str
        Extra history to add to first history.

    """
    # first check if they're the same to avoid more complicated processing.
    if _check_histories(history1, history2):
        return None

    hist2_words = history2.split(" ")
    add_hist = ""
    test_hist1 = " " + history1 + " "
    for i, word in enumerate(hist2_words):
        if " " + word + " " not in test_hist1:
            add_hist += " " + word
            keep_going = i + 1 < len(hist2_words)
            while keep_going:
                if (hist2_words[i + 1] == " ") or (
                    " " + hist2_words[i + 1] + " " not in test_hist1
                ):
                    add_hist += " " + hist2_words[i + 1]
                    del hist2_words[i + 1]
                    keep_going = i + 1 < len(hist2_words)
                else:
                    keep_going = False

    if add_hist == "":
        add_hist = None
    return add_hist


def _test_array_constant(array, tols=None):
    """
    Check if an array contains constant values to some tolerance.

    Uses np.isclose on the min & max of the arrays with the given tolerances.

    Parameters
    ----------
    array : np.ndarray or UVParameter
        UVParameter or array to check for constant values.
    tols : tuple of float, optional
        length 2 tuple giving (rtol, atol) to pass to np.isclose, defaults to (0, 0) if
        passing an array, otherwise defaults to using the tolerance on the UVParameter.

    Returns
    -------
    bool
        True if the array is constant to the given tolerances, False otherwise.
    """
    # Import UVParameter here rather than at the top to avoid circular imports
    from pyuvdata.parameter import UVParameter

    if isinstance(array, UVParameter):
        array_to_test = array.value
        if tols is None:
            tols = array.tols
    else:
        array_to_test = array
        if tols is None:
            tols = (0, 0)
    assert isinstance(tols, tuple), "tols must be a length-2 tuple"
    assert len(tols) == 2, "tols must be a length-2 tuple"

    if array_to_test.size == 1:
        # arrays with 1 element are constant by definition
        return True

    # if min and max are equal don't bother with tolerance checking
    if np.min(array_to_test) == np.max(array_to_test):
        return True

    return np.isclose(
        np.min(array_to_test), np.max(array_to_test), rtol=tols[0], atol=tols[1]
    )


def _test_array_constant_spacing(array, tols=None):
    """
    Check if an array is constantly spaced to some tolerance.

    Calls _test_array_constant on the np.diff of the array.

    Parameters
    ----------
    array : np.ndarray or UVParameter
        UVParameter or array to check for constant spacing.
    tols : tuple of float, optional
        length 2 tuple giving (rtol, atol) to pass to np.isclose, defaults to (0, 0) if
        passing an array, otherwise defaults to using the tolerance on the UVParameter.

    Returns
    -------
    bool
        True if the array spacing is constant to the given tolerances, False otherwise.
    """
    # Import UVParameter here rather than at the top to avoid circular imports
    from pyuvdata.parameter import UVParameter

    if isinstance(array, UVParameter):
        array_to_test = array.value
        if tols is None:
            tols = array.tols
    else:
        array_to_test = array
        if tols is None:
            tols = (0, 0)
    assert isinstance(tols, tuple), "tols must be a length-2 tuple"
    assert len(tols) == 2, "tols must be a length-2 tuple"

    if array_to_test.size <= 2:
        # arrays with 1 or 2 elements are constantly spaced by definition
        return True

    array_diff = np.diff(array_to_test)
    return _test_array_constant(array_diff, tols=tols)


def _check_flex_spw_contiguous(spw_array, flex_spw_id_array):
    """
    Check if the spectral windows are contiguous for flex_spw datasets.

    This checks the flex_spw_id_array to make sure that all channels for each
    spectral window are together in one block, versus being interspersed (e.g.,
    channel #1 and #3 is in spw #1, channels #2 and #4 are in spw #2). In theory,
    UVH5 and UVData objects can handle this, but MIRIAD, MIR, UVFITS, and MS file
    formats cannot, so we just consider it forbidden.

    Parameters
    ----------
    spw_array : array of integers
        Array of spectral window numbers, shape (Nspws,).
    flex_spw_id_array : array of integers
        Array of spectral window numbers per frequency channel, shape (Nfreqs,).

    """
    exp_spw_ids = np.unique(spw_array)
    # This is an internal consistency check to make sure that the indexes match
    # up as expected -- this shouldn't error unless someone is mucking with
    # settings they shouldn't be.
    assert np.all(np.unique(flex_spw_id_array) == exp_spw_ids), (
        "There are some entries in flex_spw_id_array that are not in spw_array. "
        "This is a bug, please report it in an issue."
    )

    n_breaks = np.sum(flex_spw_id_array[1:] != flex_spw_id_array[:-1])
    if (n_breaks + 1) != spw_array.size:
        raise ValueError(
            "Channels from different spectral windows are interspersed with "
            "one another, rather than being grouped together along the "
            "frequency axis. Most file formats do not support such "
            "non-grouping of data."
        )


def _check_freq_spacing(
    freq_array,
    freq_tols,
    channel_width,
    channel_width_tols,
    flex_spw,
    future_array_shapes,
    spw_array,
    flex_spw_id_array,
    raise_errors=True,
):
    """
    Check if frequencies are evenly spaced and separated by their channel width.

    This is a requirement for writing uvfits & miriad files.

    Parameters
    ----------
    freq_array : array of float
        Array of frequencies, shape (1, Nfreqs) or (Nfreqs,) if future_array_shapes=True
    freq_tols : tuple of float
        freq_array tolerances (from uvobj._freq_array.tols).
    channel_width : float or array of float
        Channel widths, either a scalar or an array of shape (Nfreqs,) if flex_spw=True
        and/or future_array_shapes=True.
    channel_width_tols : tuple of float
        channel_width tolerances (from uvobj._channel_width.tols).
    future_array_shapes : bool
        Indicates that parameters have future shapes.
    flex_spw :  bool
        Indicates there are flexible spectral windows.
    spw_array : array of integers or None
        Array of spectral window numbers, shape (Nspws,). Required if flex_spw is True.
    flex_spw_id_array : array of integers or None
        Array of spectral window numbers per frequency channel, shape (Nfreqs,).
        Required if flex_spw is True.
    raise_errors : bool
        Option to raise errors if the various checks do not pass.

    Returns
    -------
    spacing_error : bool
        Flag that channel spacings or channel widths are not equal.
    chanwidth_error : bool
        Flag that channel spacing does not match channel width.

    """
    spacing_error = False
    chanwidth_error = False
    Nfreqs = freq_array.size
    if future_array_shapes:
        freq_spacing = np.diff(freq_array)
        freq_array_use = freq_array
    else:
        freq_spacing = np.diff(freq_array[0])
        freq_array_use = freq_array[0]

    if Nfreqs == 1:
        # Skip all of this if there is only 1 channel
        pass
    elif flex_spw:
        # Check to make sure that the flexible spectral window has indicies set up
        # correctly (grouped together) for this check
        _check_flex_spw_contiguous(spw_array, flex_spw_id_array)
        diff_chanwidth = np.diff(channel_width)
        freq_dir = []
        # We want to grab unique spw IDs, in the order that they appear in the data
        select_mask = np.append((np.diff(flex_spw_id_array) != 0), True)
        for idx in flex_spw_id_array[select_mask]:
            chan_mask = flex_spw_id_array == idx
            diffs = np.diff(freq_array_use[chan_mask])
            if diffs.size > 0:
                freq_dir += [np.sign(np.mean(diffs))] * np.sum(chan_mask)
            else:
                freq_dir += [1.0]

        # Pop off the first entry, since the above arrays are diff'd
        # (and thus one element shorter)
        freq_dir = np.array(freq_dir[1:])
        # Ignore cases where looking at the boundaries of spectral windows
        bypass_check = flex_spw_id_array[1:] != flex_spw_id_array[:-1]
        if not np.all(
            np.logical_or(
                bypass_check,
                np.isclose(diff_chanwidth, 0.0, rtol=freq_tols[0], atol=freq_tols[1]),
            )
        ):
            spacing_error = True
        if not np.all(
            np.logical_or(
                bypass_check,
                np.isclose(
                    freq_spacing,
                    channel_width[1:] * freq_dir,
                    rtol=freq_tols[0],
                    atol=freq_tols[1],
                ),
            )
        ):
            chanwidth_error = True
    else:
        freq_dir = np.sign(np.mean(freq_spacing))
        if not _test_array_constant(freq_spacing, freq_tols):
            spacing_error = True
        if future_array_shapes:
            if not _test_array_constant(channel_width, freq_tols):
                spacing_error = True
            else:
                if not np.isclose(
                    np.mean(freq_spacing),
                    np.mean(channel_width) * freq_dir,
                    rtol=channel_width_tols[0],
                    atol=channel_width_tols[1],
                ):
                    chanwidth_error = True
        else:
            if not np.isclose(
                np.mean(freq_spacing),
                channel_width * freq_dir,
                rtol=channel_width_tols[0],
                atol=channel_width_tols[1],
            ):
                chanwidth_error = True
    if raise_errors and spacing_error:
        raise ValueError(
            "The frequencies are not evenly spaced (probably "
            "because of a select operation) or has differing "
            "values of channel widths. Some file formats "
            "(e.g. uvfits, miriad) and methods (frequency_average) "
            "do not support unevenly spaced frequencies."
        )
    if raise_errors and chanwidth_error:
        raise ValueError(
            "The frequencies are separated by more than their "
            "channel width (probably because of a select operation). "
            "Some file formats (e.g. uvfits, miriad) and "
            "methods (frequency_average) do not support "
            "frequencies that are spaced by more than their "
            "channel width."
        )

    return spacing_error, chanwidth_error


def _sort_freq_helper(
    Nfreqs,
    freq_array,
    Nspws,
    spw_array,
    flex_spw,
    flex_spw_id_array,
    future_array_shapes,
    spw_order,
    channel_order,
    select_spw,
):
    """
    Figure out the frequency sorting order for object based frequency sorting.

    Parameters
    ----------
    Nfreqs :  int
        Number of frequencies, taken directly from the object parameter.
    freq_array :  array_like of float
        Frequency array, taken directly from the object parameter.
    Nfreqs :  int
        Number of spectral windows, taken directly from the object parameter.
    spw_array :  array_like of int
        Spectral window array, taken directly from the object parameter.
    flex_spw :  bool
        Flag indicating whether the object has flexible spectral windows, taken
        directly from the object parameter.
    flex_spw_id_array : array_like of int
        Array of SPW IDs for each channel, taken directly from the object parameter.
    future_array_shapes : bool
        Flag indicating whether the object uses the future array shapes, taken
        directly from the object parameter.
    spw_order : str or array_like of int
        A string describing the desired order of spectral windows along the
        frequecy axis. Allowed strings include `number` (sort on spectral window
        number) and `freq` (sort on median frequency). A '-' can be prepended
        to signify descending order instead of the default ascending order,
        e.g., if you have SPW #1 and 2, and wanted them ordered as [2, 1],
        you would specify `-number`. Alternatively, one can supply an index array
        of length Nspws that specifies how to shuffle the spws (this is not the desired
        final spw order).  Default is to apply no sorting of spectral windows.
    channel_order : str or array_like of int
        A string describing the desired order of frequency channels within a
        spectral window. Allowed strings include `freq`, which will sort channels
        within a spectral window by frequency. A '-' can be optionally prepended
        to signify descending order instead of the default ascending order.
        Alternatively, one can supply an index array of length Nfreqs that
        specifies the new order. Default is to apply no sorting of channels
        within a single spectral window. Note that proving an array_like of ints
        will cause the values given to `spw_order` and `select_spw` to be ignored.
    select_spw : int or array_like of int
        An int or array_like of ints which specifies which spectral windows to
        apply sorting. Note that setting this argument will cause the value
        given to `spw_order` to be ignored.

    Returns
    -------
    index_array : ndarray of int
        Array giving the desired order of the channels to be used for sorting along the
        frequency axis

    Raises
    ------
    UserWarning
        Raised if providing arguments to select_spw and channel_order (the latter
        overrides the former).
    ValueError
        Raised if select_spw contains values not in spw_array, or if channel_order
        is not the same length as freq_array.

    """
    if (spw_order is None) and (channel_order is None):
        warnings.warn(
            "Not specifying either spw_order or channel_order causes "
            "no sorting actions to be applied. Returning object unchanged."
        )
        return

    # Check to see if there are arguments we should be ignoring
    if isinstance(channel_order, (np.ndarray, list, tuple)):
        if select_spw is not None:
            warnings.warn(
                "The select_spw argument is ignored when providing an "
                "array_like of int for channel_order"
            )
        if spw_order is not None:
            warnings.warn(
                "The spw_order argument is ignored when providing an "
                "array_like of int for channel_order"
            )
        channel_order = np.asarray(channel_order)
        if not channel_order.size == Nfreqs or not np.all(
            np.sort(channel_order) == np.arange(Nfreqs)
        ):
            raise ValueError(
                "Index array for channel_order must contain all indicies for "
                "the frequency axis, without duplicates."
            )
        index_array = channel_order
    else:
        index_array = np.arange(Nfreqs)
        # Multipy by 1.0 here to make a cheap copy of the array to manipulate
        temp_freqs = 1.0 * (freq_array if future_array_shapes else freq_array[0, :])
        # Same trick for ints -- add 0 to make a cheap copy
        temp_spws = 0 + (
            flex_spw_id_array if flex_spw else (np.zeros(Nfreqs) + spw_array)
        )

        # Check whether or not we need to sort the channels in individual windows
        sort_spw = {idx: channel_order is not None for idx in spw_array}
        if select_spw is not None:
            if spw_order is not None:
                warnings.warn(
                    "The spw_order argument is ignored when providing an "
                    "argument for select_spw"
                )
            if channel_order is None:
                warnings.warn(
                    "Specifying select_spw without providing channel_order causes "
                    "no sorting actions to be applied. Returning object unchanged."
                )
                return
            if isinstance(select_spw, (np.ndarray, list, tuple)):
                sort_spw = {idx: idx in select_spw for idx in spw_array}
            else:
                sort_spw = {idx: idx == select_spw for idx in spw_array}
        elif spw_order is not None:
            if isinstance(spw_order, (np.ndarray, list, tuple)):
                spw_order = np.asarray(spw_order)
                if not spw_order.size == Nspws or not np.all(
                    np.sort(spw_order) == np.arange(Nspws)
                ):
                    raise ValueError(
                        "Index array for spw_order must contain all indicies for "
                        "the spw_array, without duplicates."
                    )
            elif spw_order not in ["number", "freq", "-number", "-freq", None]:
                raise ValueError(
                    "spw_order can only be one of 'number', '-number', "
                    "'freq', '-freq', None or an index array of length Nspws"
                )
            elif Nspws > 1:
                # Only need to do this step if we actually have multiple spws.

                # If the string starts with a '-', then we will flip the order at
                # the end of the operation
                flip_spws = spw_order[0] == "-"

                if "number" in spw_order:
                    spw_order = np.argsort(spw_array)
                elif "freq" in spw_order:
                    spw_order = np.argsort(
                        [np.median(temp_freqs[temp_spws == idx]) for idx in spw_array]
                    )
                if flip_spws:
                    spw_order = np.flip(spw_order)
            else:
                spw_order = np.arange(Nspws)
            # Now that we know the spw order, we can apply the first sort
            index_array = np.concatenate(
                [index_array[temp_spws == spw] for spw in spw_array[spw_order]]
            )
            temp_freqs = temp_freqs[index_array]
            temp_spws = temp_spws[index_array]
        # Spectral windows are assumed sorted at this point
        if channel_order is not None:
            if channel_order not in ["freq", "-freq"]:
                raise ValueError(
                    "channel_order can only be one of 'freq' or '-freq' or an index "
                    "array of length Nfreqs"
                )
            for idx in spw_array:
                if sort_spw[idx]:
                    select_mask = temp_spws == idx
                    subsort_order = index_array[select_mask]
                    subsort_order = subsort_order[np.argsort(temp_freqs[select_mask])]
                    index_array[select_mask] = (
                        np.flip(subsort_order)
                        if channel_order[0] == "-"
                        else subsort_order
                    )
    if np.all(index_array[1:] > index_array[:-1]):
        # Nothing to do - the data are already sorted!
        return

    return index_array


def baseline_to_antnums(baseline, Nants_telescope):
    """
    Get the antenna numbers corresponding to a given baseline number.

    Parameters
    ----------
    baseline : int or array_like of ints
        baseline number
    Nants_telescope : int
        number of antennas

    Returns
    -------
    int or array_like of int
        first antenna number(s)
    int or array_like of int
        second antenna number(s)

    """
    if Nants_telescope > 2147483648:
        raise ValueError(
            "error Nants={Nants}>2147483648 not supported".format(Nants=Nants_telescope)
        )
    if np.any(np.asarray(baseline) < 0):
        raise ValueError("negative baseline numbers are not supported")
    if np.any(np.asarray(baseline) > 4611686018498691072):
        raise ValueError("baseline numbers > 4611686018498691072 are not supported")

    return_array = isinstance(baseline, (np.ndarray, list, tuple))
    ant1, ant2 = _utils.baseline_to_antnums(
        np.ascontiguousarray(baseline, dtype=np.int64)
    )
    if return_array:
        return ant1, ant2
    else:
        return ant1.item(0), ant2.item(0)


def antnums_to_baseline(ant1, ant2, Nants_telescope, attempt256=False):
    """
    Get the baseline number corresponding to two given antenna numbers.

    Parameters
    ----------
    ant1 : int or array_like of int
        first antenna number
    ant2 : int or array_like of int
        second antenna number
    Nants_telescope : int
        number of antennas
    attempt256 : bool
        Option to try to use the older 256 standard used in
        many uvfits files. If there are antenna numbers >= 256, the 2048
        standard will be used unless there are antenna numbers >= 2048
        or Nants_telescope > 2048. In that case, the 2147483648 standard
        will be used. Default is False.

    Returns
    -------
    int or array of int
        baseline number corresponding to the two antenna numbers.

    """
    if Nants_telescope is not None and Nants_telescope > 2147483648:
        raise ValueError(
            "cannot convert ant1, ant2 to a baseline index "
            "with Nants={Nants}>2147483648.".format(Nants=Nants_telescope)
        )
    if np.any(np.concatenate((np.unique(ant1), np.unique(ant2))) >= 2147483648):
        raise ValueError(
            "cannot convert ant1, ant2 to a baseline index "
            "with antenna numbers greater than 2147483647."
        )
    if np.any(np.concatenate((np.unique(ant1), np.unique(ant2))) < 0):
        raise ValueError(
            "cannot convert ant1, ant2 to a baseline index "
            "with antenna numbers less than zero."
        )

    nants_less2048 = True
    if Nants_telescope is not None and Nants_telescope > 2048:
        nants_less2048 = False

    return_array = isinstance(ant1, (np.ndarray, list, tuple))
    baseline = _utils.antnums_to_baseline(
        np.ascontiguousarray(ant1, dtype=np.int64),
        np.ascontiguousarray(ant2, dtype=np.int64),
        attempt256=attempt256,
        nants_less2048=nants_less2048,
    )
    if return_array:
        return baseline
    else:
        return baseline.item(0)


def baseline_index_flip(baseline, Nants_telescope):
    """Change baseline number to reverse antenna order."""
    ant1, ant2 = baseline_to_antnums(baseline, Nants_telescope)
    return antnums_to_baseline(ant2, ant1, Nants_telescope)


def _x_orientation_rep_dict(x_orientation):
    """Create replacement dict based on x_orientation."""
    if x_orientation.lower() == "east" or x_orientation.lower() == "e":
        return {"x": "e", "y": "n"}
    elif x_orientation.lower() == "north" or x_orientation.lower() == "n":
        return {"x": "n", "y": "e"}
    else:
        raise ValueError("x_orientation not recognized.")


def np_cache(function):
    function = lru_cache(function)

    @wraps(function)
    def wrapper(pol, x_orientation=None):
        try:
            return function(pol, x_orientation=x_orientation)
        except TypeError:
            if isinstance(pol, Iterable):
                # Assume the reason that we got a type error is that pol was an array.
                pol = tuple(pol)
            return function(pol, x_orientation=x_orientation)

    # copy lru_cache attributes over too
    wrapper.cache_info = function.cache_info
    wrapper.cache_clear = function.cache_clear

    return wrapper


@np_cache
def polstr2num(pol: str | IterableType[str], x_orientation: str | None = None):
    """
    Convert polarization str to number according to AIPS Memo 117.

    Prefer 'pI', 'pQ', 'pU' and 'pV' to make it clear that these are pseudo-Stokes,
    not true Stokes, but also supports 'I', 'Q', 'U', 'V'.

    Parameters
    ----------
    pol : str
        polarization string
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    int
        Number corresponding to string

    Raises
    ------
    ValueError
        If the pol string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(POL_STR2NUM_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in POL_STR2NUM_DICT.items():
                new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[new_key] = value
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    poldict = {k.lower(): v for k, v in dict_use.items()}
    if isinstance(pol, str):
        out = poldict[pol.lower()]
    elif isinstance(pol, Iterable):
        out = [poldict[key.lower()] for key in pol]
    else:
        raise ValueError(
            "Polarization {p} cannot be converted to a polarization number.".format(
                p=pol
            )
        )
    return out


@np_cache
def polnum2str(num, x_orientation=None):
    """
    Convert polarization number to str according to AIPS Memo 117.

    Uses 'pI', 'pQ', 'pU' and 'pV' to make it clear that these are pseudo-Stokes,
    not true Stokes

    Parameters
    ----------
    num : int
        polarization number
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to convert to
        E/N strings. See corresonding parameter on UVData for more details.

    Returns
    -------
    str
        String corresponding to polarization number

    Raises
    ------
    ValueError
        If the polarization number cannot be converted to a polarization string.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(POL_NUM2STR_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in POL_NUM2STR_DICT.items():
                new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[key] = new_val
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    if isinstance(num, (int, np.int32, np.int64)):
        out = dict_use[num]
    elif isinstance(num, Iterable):
        out = [dict_use[i] for i in num]
    else:
        raise ValueError(
            "Polarization {p} cannot be converted to string.".format(p=num)
        )
    return out


@np_cache
def jstr2num(jstr, x_orientation=None):
    """
    Convert jones polarization str to number according to calfits memo.

    Parameters
    ----------
    jstr : str or array_like of str
        antenna (jones) polarization string(s) to convert.
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    int or list of int
        antenna (jones) polarization number(s) corresponding to the input string(s)

    Raises
    ------
    ValueError
        If the jones string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(JONES_STR2NUM_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in JONES_STR2NUM_DICT.items():
                new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[new_key] = value
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    jdict = {k.lower(): v for k, v in dict_use.items()}
    if isinstance(jstr, str):
        out = jdict[jstr.lower()]
    elif isinstance(jstr, Iterable):
        out = [jdict[key.lower()] for key in jstr]
    else:
        raise ValueError(
            "Jones polarization {j} cannot be converted to index.".format(j=jstr)
        )
    return out


@np_cache
def jnum2str(jnum, x_orientation=None):
    """
    Convert jones polarization number to str according to calfits memo.

    Parameters
    ----------
    num : int or array_like of int
        antenna (jones) polarization number(s) to convert to strings
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to convert to
        E/N strings. See corresonding parameter on UVData for more details.

    Returns
    -------
    str or list of str
        antenna (jones) polarization string(s) corresponding to number

    Raises
    ------
    ValueError
        If the jones polarization number cannot be converted to a jones
        polarization string.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(JONES_NUM2STR_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in JONES_NUM2STR_DICT.items():
                new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[key] = new_val
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    if isinstance(jnum, (int, np.int32, np.int64)):
        out = dict_use[jnum]
    elif isinstance(jnum, Iterable):
        out = [dict_use[i] for i in jnum]
    else:
        raise ValueError(
            "Jones polarization {j} cannot be converted to string.".format(j=jnum)
        )
    return out


@np_cache
def parse_polstr(polstr, x_orientation=None):
    """
    Parse a polarization string and return pyuvdata standard polarization string.

    See utils.POL_STR2NUM_DICT for options.

    Parameters
    ----------
    polstr : str
        polarization string
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    str
        AIPS Memo 117 standard string

    Raises
    ------
    ValueError
        If the pol string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    return polnum2str(
        polstr2num(polstr, x_orientation=x_orientation), x_orientation=x_orientation
    )


@np_cache
def parse_jpolstr(jpolstr, x_orientation=None):
    """
    Parse a Jones polarization string and return pyuvdata standard jones string.

    See utils.JONES_STR2NUM_DICT for options.

    Parameters
    ----------
    jpolstr : str
        Jones polarization string

    Returns
    -------
    str
        calfits memo standard string

    Raises
    ------
    ValueError
        If the jones string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    return jnum2str(
        jstr2num(jpolstr, x_orientation=x_orientation), x_orientation=x_orientation
    )


def conj_pol(pol):
    """
    Return the polarization for the conjugate baseline.

    For example, (1, 2, 'xy') = conj(2, 1, 'yx').
    The returned polarization is determined by assuming the antenna pair is
    reversed in the data, and finding the correct polarization correlation
    which will yield the requested baseline when conjugated. Note this means
    changing the polarization for linear cross-pols, but keeping auto-pol
    (e.g. xx) and Stokes the same.

    Parameters
    ----------
    pol : str or int
        Polarization string or integer.

    Returns
    -------
    cpol : str or int
        Polarization as if antennas are swapped (type matches input)

    """
    cpol_dict = {k.lower(): v for k, v in CONJ_POL_DICT.items()}

    if isinstance(pol, str):
        cpol = cpol_dict[pol.lower()]
    elif isinstance(pol, Iterable):
        cpol = [conj_pol(p) for p in pol]
    elif isinstance(pol, (int, np.int32, np.int64)):
        cpol = polstr2num(cpol_dict[polnum2str(pol).lower()])
    else:
        raise ValueError("Polarization not recognized, cannot be conjugated.")
    return cpol


def reorder_conj_pols(pols):
    """
    Reorder multiple pols, swapping pols that are conjugates of one another.

    For example ('xx', 'xy', 'yx', 'yy') -> ('xx', 'yx', 'xy', 'yy')
    This is useful for the _key2inds function in the case where an antenna
    pair is specified but the conjugate pair exists in the data. The conjugated
    data should be returned in the order of the polarization axis, so after
    conjugating the data, the pols need to be reordered.
    For example, if a file contains antpair (0, 1) and pols 'xy' and 'yx', but
    the user requests antpair (1, 0), they should get:
    [(1x, 0y), (1y, 0x)] = [conj(0y, 1x), conj(0x, 1y)]

    Parameters
    ----------
    pols : array_like of str or int
        Polarization array (strings or ints).

    Returns
    -------
    conj_order : ndarray of int
        Indices to reorder polarization array.
    """
    if not isinstance(pols, Iterable):
        raise ValueError("reorder_conj_pols must be given an array of polarizations.")
    cpols = np.array([conj_pol(p) for p in pols])  # Array needed for np.where
    conj_order = [np.where(cpols == p)[0][0] if p in cpols else -1 for p in pols]
    if -1 in conj_order:
        raise ValueError(
            "Not all conjugate pols exist in the polarization array provided."
        )
    return conj_order


def LatLonAlt_from_XYZ(xyz, frame="ITRS", check_acceptability=True):
    """
    Calculate lat/lon/alt from ECEF x,y,z.

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    frame : str
        Coordinate frame of xyz.
        Valid options are ITRS (default) or MCMF.
    check_acceptability : bool
        Flag to check XYZ coordinates are reasonable.

    Returns
    -------
    latitude :  ndarray or float
        latitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    longitude :  ndarray or float
        longitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    altitude :  ndarray or float
        altitude, numpy array (if Npts > 1) or value (if Npts = 1) in meters

    """
    frame = frame.upper()
    if not hasmoon and frame == "MCMF":
        raise ValueError("Need to install `lunarsky` package to work with MCMF frame.")

    if frame == "ITRS":
        accept_bounds = (6.35e6, 6.39e6)
    elif frame == "MCMF":
        accept_bounds = (1.71e6, 1.75e6)

    # convert to a numpy array
    xyz = np.asarray(xyz)
    if xyz.ndim > 1 and xyz.shape[1] != 3:
        raise ValueError("The expected shape of ECEF xyz array is (Npts, 3).")

    squeeze = xyz.ndim == 1

    if squeeze:
        xyz = xyz[np.newaxis, :]

    xyz = np.ascontiguousarray(xyz.T, dtype=np.float64)

    # checking for acceptable values
    if check_acceptability:
        if frame not in ["ITRS", "MCMF"]:
            raise ValueError(f'Cannot check acceptability for unknown frame "{frame}".')
        norms = np.linalg.norm(xyz, axis=0)
        if not all(
            np.logical_and(norms >= accept_bounds[0], norms <= accept_bounds[1])
        ):
            raise ValueError(
                f"xyz values should be {frame} x, y, z coordinates in meters"
            )
    # this helper function returns one 2D array because it is less overhead for cython
    if frame == "ITRS":
        lla = _utils._lla_from_xyz(xyz)
    elif frame == "MCMF":
        lla = erfa.gc2gde(LUNAR_RADIUS, 0.0, xyz.T)
        lla = np.asarray((lla[1], lla[0], lla[2]))  # Swap lon/lat
    else:
        raise ValueError(
            f'No spherical to cartesian transform defined for frame "{frame}".'
        )

    if squeeze:
        return lla[0, 0], lla[1, 0], lla[2, 0]
    return lla[0], lla[1], lla[2]


def XYZ_from_LatLonAlt(latitude, longitude, altitude, frame="ITRS"):
    """
    Calculate ECEF x,y,z from lat/lon/alt values.

    Parameters
    ----------
    latitude :  ndarray or float
        latitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    longitude :  ndarray or float
        longitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    altitude :  ndarray or float
        altitude, numpy array (if Npts > 1) or value (if Npts = 1) in meters
    frame : str
        Coordinate frame of xyz.
        Valid options are ITRS (default) or MCMF.

    Returns
    -------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.

    """
    latitude = np.ascontiguousarray(latitude, dtype=np.float64)
    longitude = np.ascontiguousarray(longitude, dtype=np.float64)
    altitude = np.ascontiguousarray(altitude, dtype=np.float64)

    n_pts = latitude.size

    frame = frame.upper()
    if not hasmoon and frame == "MCMF":
        raise ValueError("Need to install `lunarsky` package to work with MCMF frame.")

    if longitude.size != n_pts:
        raise ValueError(
            "latitude, longitude and altitude must all have the same length"
        )
    if altitude.size != n_pts:
        raise ValueError(
            "latitude, longitude and altitude must all have the same length"
        )
    if frame == "ITRS":
        xyz = _utils._xyz_from_latlonalt(latitude, longitude, altitude)
        xyz = xyz.T
    elif frame == "MCMF":
        xyz = erfa.gd2gce(LUNAR_RADIUS, 0.0, longitude, latitude, altitude)
    else:
        raise ValueError(
            f'No cartesian to spherical transform defined for frame "{frame}".'
        )

    if n_pts == 1:
        return xyz[0]

    return xyz


def rotECEF_from_ECEF(xyz, longitude):
    """
    Get rotated ECEF positions such that the x-axis goes through the longitude.

    Miriad and uvfits expect antenna positions in this frame
    (with longitude of the array center/telescope location)

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    longitude : float
        longitude in radians to rotate coordinates to
        (usually the array center/telescope location).

    Returns
    -------
    ndarray of float
        Rotated ECEF coordinates, shape (Npts, 3).

    """
    angle = -1 * longitude
    rot_matrix = np.array(
        [
            [np.cos(angle), -1 * np.sin(angle), 0],
            [np.sin(angle), np.cos(angle), 0],
            [0, 0, 1],
        ]
    )
    return rot_matrix.dot(xyz.T).T


def ECEF_from_rotECEF(xyz, longitude):
    """
    Calculate ECEF from a rotated ECEF (Inverse of rotECEF_from_ECEF).

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with rotated ECEF x,y,z coordinates.
    longitude : float
        longitude in radians giving the x direction of the rotated coordinates
        (usually the array center/telescope location).

    Returns
    -------
    ndarray of float
        ECEF coordinates, shape (Npts, 3).

    """
    angle = longitude
    rot_matrix = np.array(
        [
            [np.cos(angle), -1 * np.sin(angle), 0],
            [np.sin(angle), np.cos(angle), 0],
            [0, 0, 1],
        ]
    )
    return rot_matrix.dot(xyz.T).T


def ENU_from_ECEF(xyz, latitude, longitude, altitude, frame="ITRS"):
    """
    Calculate local ENU (east, north, up) coordinates from ECEF coordinates.

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    latitude : float
        Latitude of center of ENU coordinates in radians.
    longitude : float
        Longitude of center of ENU coordinates in radians.
    altitude : float
        Altitude of center of ENU coordinates in radians.
    frame : str
        Coordinate frame of xyz.
        Valid options are ITRS (default) or MCMF.

    Returns
    -------
    ndarray of float
        numpy array, shape (Npts, 3), with local ENU coordinates

    """
    frame = frame.upper()
    if not hasmoon and frame == "MCMF":
        raise ValueError("Need to install `lunarsky` package to work with MCMF frame.")

    if frame == "ITRS":
        sensible_radius_range = (6.35e6, 6.39e6)
        world = "earth"
    elif frame == "MCMF":
        world = "moon"
        sensible_radius_range = (1.71e6, 1.75e6)
    else:
        raise ValueError(f'No ENU_from_ECEF transform defined for frame "{frame}".')

    xyz = np.asarray(xyz)
    if xyz.ndim > 1 and xyz.shape[1] != 3:
        raise ValueError("The expected shape of ECEF xyz array is (Npts, 3).")

    squeeze = False
    if xyz.ndim == 1:
        squeeze = True
        xyz = xyz[np.newaxis, :]
    xyz = np.ascontiguousarray(xyz.T, dtype=np.float64)

    # check that these are sensible ECEF values -- their magnitudes need to be
    # on the order of Earth's radius
    ecef_magnitudes = np.linalg.norm(xyz, axis=0)
    if np.any(ecef_magnitudes <= sensible_radius_range[0]) or np.any(
        ecef_magnitudes >= sensible_radius_range[1]
    ):
        raise ValueError(
            f"{frame} vector magnitudes must be on the order"
            f" of the radius of the {world}"
        )

    if frame == "ITRS":
        # the cython utility expects (3, Npts) for faster manipulation
        # transpose after we get the array back to match the expected shape
        enu = _utils._ENU_from_ECEF(
            xyz,
            np.ascontiguousarray(latitude, dtype=np.float64),
            np.ascontiguousarray(longitude, dtype=np.float64),
            np.ascontiguousarray(altitude, dtype=np.float64),
        )
        enu = enu.T

    elif frame == "MCMF":
        xyz_cent = erfa.gd2gce(LUNAR_RADIUS, 0.0, longitude, latitude, altitude)
        ecef_to_enu = np.matmul(
            rotation_matrix(-longitude, "z", unit="rad"),
            rotation_matrix(latitude, "y", unit="rad"),
        ).T

        ecef_to_enu = ecef_to_enu[[2, 1, 0]]
        enu = np.dot(ecef_to_enu, (xyz.T - xyz_cent).T).T

    if squeeze:
        enu = np.squeeze(enu)

    return enu


def ECEF_from_ENU(enu, latitude, longitude, altitude, frame="ITRS"):
    """
    Calculate ECEF coordinates from local ENU (east, north, up) coordinates.

    Parameters
    ----------
    enu : ndarray of float
        numpy array, shape (Npts, 3), with local ENU coordinates.
    latitude : float
        Latitude of center of ENU coordinates in radians.
    longitude : float
        Longitude of center of ENU coordinates in radians.
    altitude : float
        Altitude of center of ENU coordinates in radians.
    frame : str
        Coordinate frame of xyz.
        Valid options are ITRS (default) or MCMF.


    Returns
    -------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.

    """
    frame = frame.upper()

    if frame not in ["ITRS", "MCMF"]:
        raise ValueError(f'No ECEF_from_ENU transform defined for frame "{frame}".')

    if not hasmoon and frame == "MCMF":
        raise ValueError("Need to install `lunarsky` package to work with MCMF frame.")

    enu = np.asarray(enu)
    if enu.ndim > 1 and enu.shape[1] != 3:
        raise ValueError("The expected shape of the ENU array is (Npts, 3).")
    squeeze = False

    if enu.ndim == 1:
        squeeze = True
        enu = enu[np.newaxis, :]
    enu = np.ascontiguousarray(enu.T, dtype=np.float64)

    if frame == "ITRS":
        # the cython utility expects (3, Npts) for faster manipulation
        # transpose after we get the array back to match the expected shape
        xyz = _utils._ECEF_from_ENU(
            enu,
            np.ascontiguousarray(latitude, dtype=np.float64),
            np.ascontiguousarray(longitude, dtype=np.float64),
            np.ascontiguousarray(altitude, dtype=np.float64),
        )
        xyz = xyz.T
    elif frame == "MCMF":
        xyz_cent = erfa.gd2gce(LUNAR_RADIUS, 0.0, longitude, latitude, altitude)
        ecef_to_enu = np.matmul(
            rotation_matrix(-longitude, "z", unit="rad"),
            rotation_matrix(latitude, "y", unit="rad"),
        ).T
        enu_to_ecef = np.linalg.inv(ecef_to_enu[[2, 1, 0]])
        xyz_rel = np.dot(enu_to_ecef, enu)
        xyz = xyz_cent + xyz_rel.T

    if squeeze:
        xyz = np.squeeze(xyz)

    return xyz


def phase_uvw(ra, dec, initial_uvw):
    """
    Calculate phased uvws/positions from unphased ones in an icrs or gcrs frame.

    This method is deprecated because it is associated with the old style of phasing,
    which is less correct than the way we do it now.

    This code expects input uvws or positions relative to the telescope
    location in the same frame that ra/dec are in (e.g. icrs or gcrs) and
    returns phased ones in the same frame.

    Note that this code is nearly identical to ENU_from_ECEF, except that it
    uses an arbitrary phasing center rather than a coordinate center.

    Parameters
    ----------
    ra : float
        Right ascension of phase center.
    dec : float
        Declination of phase center.
    initial_uvw : ndarray of float
        Unphased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    Returns
    -------
    uvw : ndarray of float
        uvw array in the same frame as initial_uvws, ra and dec.

    """
    warnings.warn(
        "This function supports the old phasing method and will be removed along with "
        "the old phasing code in version 2.4",
        DeprecationWarning,
    )
    if initial_uvw.ndim == 1:
        initial_uvw = initial_uvw[np.newaxis, :]

    return _utils._phase_uvw(
        np.float64(ra),
        np.float64(dec),
        np.ascontiguousarray(initial_uvw.T, dtype=np.float64),
    ).T


def unphase_uvw(ra, dec, uvw):
    """
    Calculate unphased uvws/positions from phased ones in an icrs or gcrs frame.

    This method is deprecated because it is associated with the old style of phasing,
    which is less correct than the way we do it now.

    This code expects phased uvws or positions in the same frame that ra/dec
    are in (e.g. icrs or gcrs) and returns unphased ones in the same frame.

    Parameters
    ----------
    ra : float
        Right ascension of phase center.
    dec : float
        Declination of phase center.
    uvw : ndarray of float
        Phased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    Returns
    -------
    unphased_uvws : ndarray of float
        Unphased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    """
    warnings.warn(
        "This function supports the old phasing method and will be removed along with "
        "the old phasing code in version 2.4",
        DeprecationWarning,
    )
    if uvw.ndim == 1:
        uvw = uvw[np.newaxis, :]

    return _utils._unphase_uvw(
        np.float64(ra), np.float64(dec), np.ascontiguousarray(uvw.T, dtype=np.float64)
    ).T


def polar2_to_cart3(lon_array, lat_array):
    """
    Convert 2D polar coordinates into 3D cartesian coordinates.

    This is a simple routine for converting a set of spherical angular coordinates
    into a 3D cartesian vectors, where the x-direction is set by the position (0, 0).

    Parameters
    ----------
    lon_array : float or ndarray
        Longitude coordinates, which increases in the counter-clockwise direction.
        Units of radians. Can either be a float or ndarray -- if the latter, must have
        the same shape as lat_array.
    lat_array : float or ndarray
        Latitude coordinates, where 0 falls on the equator of the sphere.  Units of
        radians. Can either be a float or ndarray -- if the latter, must have the same
        shape as lat_array.

    Returns
    -------
    xyz_array : ndarray of float
        Cartesian coordinates of the given longitude and latitude on a unit sphere.
        Shape is (3, coord_shape), where coord_shape is the shape of lon_array and
        lat_array if they were provided as type ndarray, otherwise (3,).
    """
    # Check to make sure that we are not playing with mixed types
    if type(lon_array) is not type(lat_array):
        raise ValueError(
            "lon_array and lat_array must either both be floats or ndarrays."
        )
    if isinstance(lon_array, np.ndarray):
        if lon_array.shape != lat_array.shape:
            raise ValueError("lon_array and lat_array must have the same shape.")

    # Once we know that lon_array and lat_array are of the same shape,
    # time to create our 3D set of vectors!
    xyz_array = np.array(
        [
            np.cos(lon_array) * np.cos(lat_array),
            np.sin(lon_array) * np.cos(lat_array),
            np.sin(lat_array),
        ],
        dtype=float,
    )

    return xyz_array


def cart3_to_polar2(xyz_array):
    """
    Convert 3D cartesian coordinates into 2D polar coordinates.

    This is a simple routine for converting a set of 3D cartesian vectors into
    spherical coordinates, where the position (0, 0) lies along the x-direction.

    Parameters
    ----------
    xyz_array : ndarray of float
        Cartesian coordinates, need not be of unit vector length. Shape is
        (3, coord_shape).

    Returns
    -------
    lon_array : ndarray of float
        Longitude coordinates, which increases in the counter-clockwise direction.
        Units of radians, shape is (coord_shape,).
    lat_array : ndarray of float
        Latitude coordinates, where 0 falls on the equator of the sphere.  Units of
        radians, shape is (coord_shape,).
    """
    if not isinstance(xyz_array, np.ndarray):
        raise ValueError("xyz_array must be an ndarray.")
    if xyz_array.ndim == 0:
        raise ValueError("xyz_array must have ndim > 0")
    if xyz_array.shape[0] != 3:
        raise ValueError("xyz_array must be length 3 across the zeroth axis.")

    # The longitude coord is relatively easy to calculate, just take the X and Y
    # components and find the arctac of the pair.
    lon_array = np.mod(np.arctan2(xyz_array[1], xyz_array[0]), 2.0 * np.pi, dtype=float)

    # If we _knew_ that xyz_array was always of length 1, then this call could be a much
    # simpler one to arcsin. But to make this generic, we'll use the length of the XY
    # component along with arctan2.
    lat_array = np.arctan2(
        xyz_array[2], np.sqrt((xyz_array[0:2] ** 2.0).sum(axis=0)), dtype=float
    )

    # Return the two arrays
    return lon_array, lat_array


def _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot):
    """
    Apply a rotation matrix to a series of vectors.

    This is a simple convenience function which wraps numpy's matmul function for use
    with various vector rotation functions in this module. This code could, in
    principle, be replaced by a cythonized piece of code, although the matmul function
    is _pretty_ well optimized already. This function is not meant to be called by
    users, but is instead used by multiple higher-level utility functions (namely those
    that perform rotations).

    Parameters
    ----------
    xyz_array : ndarray of floats
        Array of vectors to be rotated. When nrot > 1, shape may be (n_rot, 3, n_vec)
        or (1, 3, n_vec), the latter is useful for when performing multiple rotations
        on a fixed set of vectors. If nrot = 1, shape may be (1, 3, n_vec), (3, n_vec),
        or (3,).
    rot_matrix : ndarray of floats
        Series of rotation matricies to be applied to the stack of vectors. Must be
        of shape (n_rot, 3, 3)
    n_rot : int
        Number of individual rotation matricies to be applied.

    Returns
    -------
    rotated_xyz : ndarray of floats
        Array of vectors that have been rotated, of shape (n_rot, 3, n_vectors,).
    """
    # Do a quick check to make sure that things look sensible
    if rot_matrix.shape != (n_rot, 3, 3):
        raise ValueError(
            "rot_matrix must be of shape (n_rot, 3, 3), where n_rot=%i." % n_rot
        )
    if (xyz_array.ndim == 3) and (
        (xyz_array.shape[0] not in [1, n_rot]) or (xyz_array.shape[-2] != 3)
    ):
        raise ValueError("Misshaped xyz_array - expected shape (n_rot, 3, n_vectors).")
    if (xyz_array.ndim < 3) and (xyz_array.shape[0] != 3):
        raise ValueError("Misshaped xyz_array - expected shape (3, n_vectors) or (3,).")
    rotated_xyz = np.matmul(rot_matrix, xyz_array)

    return rotated_xyz


def _rotate_one_axis(xyz_array, rot_amount, rot_axis):
    """
    Rotate an array of 3D positions around the a single axis (x, y, or z).

    This function performs a basic rotation of 3D vectors about one of the priciple
    axes -- the x-axis, the y-axis, or the z-axis.

    Note that the rotations here obey the right-hand rule -- that is to say, from the
    perspective of the positive side of the axis of rotation, a positive rotation will
    cause points on the plane intersecting this axis to move in a counter-clockwise
    fashion.

    Parameters
    ----------
    xyz_array : ndarray of float
        Set of 3-dimensional vectors be rotated, in typical right-handed cartesian
        order, e.g. (x, y, z). Shape is (Nrot, 3, Nvectors).
    rot_amount : float or ndarray of float
        Amount (in radians) to rotate the given set of coordinates. Can either be a
        single float (or ndarray of shape (1,)) if rotating all vectors by the same
        amount, otherwise expected to be shape (Nrot,).
    rot_axis : int
        Axis around which the rotation is applied. 0 is the x-axis, 1 is the y-axis,
        and 2 is the z-axis.

    Returns
    -------
    rotated_xyz : ndarray of float
        Set of rotated 3-dimensional vectors, shape (Nrot, 3, Nvector).
    """
    # If rot_amount is None or all zeros, then this is just one big old no-op.
    if (rot_amount is None) or np.all(rot_amount == 0.0):
        if np.ndim(xyz_array) == 1:
            return deepcopy(xyz_array[np.newaxis, :, np.newaxis])
        elif np.ndim(xyz_array) == 2:
            return deepcopy(xyz_array[np.newaxis, :, :])
        else:
            return deepcopy(xyz_array)

    # Check and see how big of a rotation matrix we need
    n_rot = 1 if (not isinstance(rot_amount, np.ndarray)) else (rot_amount.shape[0])
    n_vec = xyz_array.shape[-1]

    # The promotion of values to float64 is to suppress numerical precision issues,
    # since the matrix math can - in limited circumstances - introduce precision errors
    # of order 10x the limiting numerical precision of the float. For a float32/single,
    # thats a part in 1e6 (~arcsec-level errors), but for a float64 it translates to
    # a part in 1e15.
    rot_matrix = np.zeros((3, 3, n_rot), dtype=np.float64)

    # Figure out which pieces of the matrix we need to update
    temp_jdx = (rot_axis + 1) % 3
    temp_idx = (rot_axis + 2) % 3

    # Fill in the rotation matricies accordingly
    rot_matrix[rot_axis, rot_axis] = 1
    rot_matrix[temp_idx, temp_idx] = np.cos(rot_amount, dtype=np.float64)
    rot_matrix[temp_jdx, temp_jdx] = rot_matrix[temp_idx, temp_idx]
    rot_matrix[temp_idx, temp_jdx] = np.sin(rot_amount, dtype=np.float64)
    rot_matrix[temp_jdx, temp_idx] = -rot_matrix[temp_idx, temp_jdx]

    # The rot matrix was shape (3, 3, n_rot) to help speed up filling in the elements
    # of each matrix, but now we want to flip it into its proper shape of (n_rot, 3, 3)
    rot_matrix = np.transpose(rot_matrix, axes=[2, 0, 1])

    if (n_rot == 1) and (n_vec == 1) and (xyz_array.ndim == 3):
        # This is a special case where we allow the rotation axis to "expand" along
        # the 0th axis of the rot_amount arrays. For xyz_array, if n_vectors = 1
        # but n_rot !=1, then it's a lot faster (by about 10x) to "switch it up" and
        # swap the n_vector and  n_rot axes, and then swap them back once everything
        # else is done.
        return np.transpose(
            _rotate_matmul_wrapper(
                np.transpose(xyz_array, axes=[2, 1, 0]), rot_matrix, n_rot
            ),
            axes=[2, 1, 0],
        )
    else:
        return _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot)


def _rotate_two_axis(xyz_array, rot_amount1, rot_amount2, rot_axis1, rot_axis2):
    """
    Rotate an array of 3D positions sequentially around a pair of axes (x, y, or z).

    This function performs a sequential pair of basic rotations of 3D vectors about
    the priciple axes -- the x-axis, the y-axis, or the z-axis.

    Note that the rotations here obey the right-hand rule -- that is to say, from the
    perspective of the positive side of the axis of rotation, a positive rotation will
    cause points on the plane intersecting this axis to move in a counter-clockwise
    fashion.

    Parameters
    ----------
    xyz_array : ndarray of float
        Set of 3-dimensional vectors be rotated, in typical right-handed cartesian
        order, e.g. (x, y, z). Shape is (Nrot, 3, Nvectors).
    rot_amount1 : float or ndarray of float
        Amount (in radians) of rotatation to apply during the first rotation of the
        sequence, to the given set of coordinates. Can either be a single float (or
        ndarray of shape (1,)) if rotating all vectors by the same amount, otherwise
        expected to be shape (Nrot,).
    rot_amount2 : float or ndarray of float
        Amount (in radians) of rotatation to apply during the second rotation of the
        sequence, to the given set of coordinates. Can either be a single float (or
        ndarray of shape (1,)) if rotating all vectors by the same amount, otherwise
        expected to be shape (Nrot,).
    rot_axis1 : int
        Axis around which the first rotation is applied. 0 is the x-axis, 1 is the
        y-axis, and 2 is the z-axis.
    rot_axis2 : int
        Axis around which the second rotation is applied. 0 is the x-axis, 1 is the
        y-axis, and 2 is the z-axis.

    Returns
    -------
    rotated_xyz : ndarray of float
        Set of rotated 3-dimensional vectors, shape (Nrot, 3, Nvector).

    """
    # Capture some special cases upfront, where we can save ourselves a bit of work
    no_rot1 = (rot_amount1 is None) or np.all(rot_amount1 == 0.0)
    no_rot2 = (rot_amount2 is None) or np.all(rot_amount2 == 0.0)
    if no_rot1 and no_rot2:
        # If rot_amount is None, then this is just one big old no-op.
        return deepcopy(xyz_array)
    elif no_rot1:
        # If rot_amount1 is None, then ignore it and just work w/ the 2nd rotation
        return _rotate_one_axis(xyz_array, rot_amount2, rot_axis2)
    elif no_rot2:
        # If rot_amount2 is None, then ignore it and just work w/ the 1st rotation
        return _rotate_one_axis(xyz_array, rot_amount1, rot_axis1)
    elif rot_axis1 == rot_axis2:
        # Capture the case where someone wants to do a sequence of rotations on the same
        # axis. Also known as just rotating a single axis.
        return _rotate_one_axis(xyz_array, rot_amount1 + rot_amount2, rot_axis1)

    # Figure out how many individual rotation matricies we need, accounting for the
    # fact that these can either be floats or ndarrays.
    n_rot = max(
        rot_amount1.shape[0] if isinstance(rot_amount1, np.ndarray) else 1,
        rot_amount2.shape[0] if isinstance(rot_amount2, np.ndarray) else 1,
    )
    n_vec = xyz_array.shape[-1]

    # The promotion of values to float64 is to suppress numerical precision issues,
    # since the matrix math can - in limited circumstances - introduce precision errors
    # of order 10x the limiting numerical precision of the float. For a float32/single,
    # thats a part in 1e6 (~arcsec-level errors), but for a float64 it translates to
    # a part in 1e15.
    rot_matrix = np.empty((3, 3, n_rot), dtype=np.float64)

    # There are two permulations per pair of axes -- when the pair is right-hand
    # oriented vs left-hand oriented. Check here which one it is. For example,
    # rotating first on the x-axis, second on the y-axis is considered a
    # "right-handed" pair, whereas z-axis first, then y-axis would be considered
    # a "left-handed" pair.
    lhd_order = np.mod(rot_axis2 - rot_axis1, 3) != 1

    temp_idx = [
        np.mod(rot_axis1 - lhd_order, 3),
        np.mod(rot_axis1 + 1 - lhd_order, 3),
        np.mod(rot_axis1 + 2 - lhd_order, 3),
    ]

    # We're using lots of sin and cos calculations -- doing them once upfront saves
    # quite a bit of time by eliminating redundant calculations
    sin_lo = np.sin(rot_amount2 if lhd_order else rot_amount1, dtype=np.float64)
    cos_lo = np.cos(rot_amount2 if lhd_order else rot_amount1, dtype=np.float64)
    sin_hi = np.sin(rot_amount1 if lhd_order else rot_amount2, dtype=np.float64)
    cos_hi = np.cos(rot_amount1 if lhd_order else rot_amount2, dtype=np.float64)

    # Take care of the diagonal terms first, since they aren't actually affected by the
    # order of rotational opertations
    rot_matrix[temp_idx[0], temp_idx[0]] = cos_hi
    rot_matrix[temp_idx[1], temp_idx[1]] = cos_lo
    rot_matrix[temp_idx[2], temp_idx[2]] = cos_lo * cos_hi

    # Now time for the off-diagonal terms, as a set of 3 pairs. The rotation matrix
    # for a left-hand oriented pair of rotation axes (e.g., x-rot, then y-rot) is just
    # a transpose of the right-hand orientation of the same pair (e.g., y-rot, then
    # x-rot).
    rot_matrix[temp_idx[0 + lhd_order], temp_idx[1 - lhd_order]] = sin_lo * sin_hi
    rot_matrix[temp_idx[0 - lhd_order], temp_idx[lhd_order - 1]] = (
        cos_lo * sin_hi * ((-1.0) ** lhd_order)
    )

    rot_matrix[temp_idx[1 - lhd_order], temp_idx[0 + lhd_order]] = 0.0
    rot_matrix[temp_idx[1 + lhd_order], temp_idx[2 - lhd_order]] = sin_lo * (
        (-1.0) ** (1 + lhd_order)
    )

    rot_matrix[temp_idx[lhd_order - 1], temp_idx[0 - lhd_order]] = sin_hi * (
        (-1.0) ** (1 + lhd_order)
    )
    rot_matrix[temp_idx[2 - lhd_order], temp_idx[1 + lhd_order]] = (
        sin_lo * cos_hi * ((-1.0) ** (lhd_order))
    )

    # The rot matrix was shape (3, 3, n_rot) to help speed up filling in the elements
    # of each matrix, but now we want to flip it into its proper shape of (n_rot, 3, 3)
    rot_matrix = np.transpose(rot_matrix, axes=[2, 0, 1])

    if (n_rot == 1) and (n_vec == 1) and (xyz_array.ndim == 3):
        # This is a special case where we allow the rotation axis to "expand" along
        # the 0th axis of the rot_amount arrays. For xyz_array, if n_vectors = 1
        # but n_rot !=1, then it's a lot faster (by about 10x) to "switch it up" and
        # swap the n_vector and  n_rot axes, and then swap them back once everything
        # else is done.
        return np.transpose(
            _rotate_matmul_wrapper(
                np.transpose(xyz_array, axes=[2, 1, 0]), rot_matrix, n_rot
            ),
            axes=[2, 1, 0],
        )
    else:
        return _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot)


def calc_uvw(
    app_ra=None,
    app_dec=None,
    frame_pa=None,
    lst_array=None,
    use_ant_pos=True,
    uvw_array=None,
    antenna_positions=None,
    antenna_numbers=None,
    ant_1_array=None,
    ant_2_array=None,
    old_app_ra=None,
    old_app_dec=None,
    old_frame_pa=None,
    telescope_lat=None,
    telescope_lon=None,
    from_enu=False,
    to_enu=False,
):
    """
    Calculate an array of baseline coordinates, in either uvw or ENU.

    This routine is meant as a convenience function for producing baseline coordinates
    based under a few different circumstances:

    1) Calculating ENU coordinates using antenna positions
    2) Calculating uvw coordinates at a given sky position using antenna positions
    3) Converting from ENU coordinates to uvw coordinates
    4) Converting from uvw coordinate to ENU coordinates
    5) Converting from uvw coordinates at one sky position to another sky position

    Different conversion pathways have different parameters that are required.

    Parameters
    ----------
    app_ra : ndarray of float
        Apparent RA of the target phase center, required if calculating baseline
        coordinates in uvw-space (vs ENU-space). Shape is (Nblts,), units are
        radians.
    app_dec : ndarray of float
        Apparent declination of the target phase center, required if calculating
        baseline coordinates in uvw-space (vs ENU-space). Shape is (Nblts,),
        units are radians.
    frame_pa : ndarray of float
        Position angle between the great circle of declination in the apparent frame
        versus that of the reference frame, used for making sure that "North" on
        the derived maps points towards a particular celestial pole (not just the
        topocentric one). Required if not deriving baseline coordinates from antenna
        positions, from_enu=False, and a value for old_frame_pa is given. Shape is
        (Nblts,), units are radians.
    old_app_ra : ndarray of float
        Apparent RA of the previous phase center, required if not deriving baseline
        coordinates from antenna positions and from_enu=False. Shape is (Nblts,),
        units are radians.
    old_app_dec : ndarray of float
        Apparent declination of the previous phase center, required if not deriving
        baseline coordinates from antenna positions and from_enu=False. Shape is
        (Nblts,), units are radians.
    old_frame_pa : ndarray of float
        Frame position angle of the previous phase center, required if not deriving
        baseline coordinates from antenna positions, from_enu=False, and a value
        for frame_pa is supplied. Shape is (Nblts,), units are radians.
    lst_array : ndarray of float
        Local apparent sidereal time, required if deriving baseline coordinates from
        antenna positions, or converting to/from ENU coordinates. Shape is (Nblts,).
    use_ant_pos : bool
        Switch to determine whether to derive uvw values from the antenna positions
        (if set to True), or to use the previously calculated uvw coordinates to derive
        new the new baseline vectors (if set to False). Default is True.
    uvw_array : ndarray of float
        Array of previous baseline coordinates (in either uvw or ENU), required if
        not deriving new coordinates from antenna positions.  Shape is (Nblts, 3).
    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).
    antenna_numbers: ndarray of int
        List of antenna numbers, ordered in the same way as `antenna_positions` (e.g.,
        `antenna_numbers[0]` should given the number of antenna that resides at ECEF
        position given by `antenna_positions[0]`). Shape is (Nants,), requred if not
        providing `uvw_array`. Contains all unique entires of the joint set of
        `ant_1_array` and `ant_2_array`.
    ant_1_array : ndarray of int
        Antenna number of the first antenna in the baseline pair, for all baselines
        Required if not providing `uvw_array`, shape is (Nblts,).
    ant_2_array : ndarray of int
        Antenna number of the second antenna in the baseline pair, for all baselines
        Required if not providing `uvw_array`, shape is (Nblts,).
    telescope_lat : float
        Latitude of the phase center, units radians, required if deriving baseline
        coordinates from antenna positions, or converting to/from ENU coordinates.
    telescope_lon : float
        Longitude of the phase center, units radians, required if deriving baseline
        coordinates from antenna positions, or converting to/from ENU coordinates.
    from_enu : boolean
        Set to True if uvw_array is expressed in ENU coordinates. Default is False.
    to_enu : boolean
        Set to True if you would like the output expressed in ENU coordinates. Default
        is False.

    Returns
    -------
    new_coords : ndarray of float64
        Set of baseline coordinates, shape (Nblts, 3).
    """
    if to_enu:
        if lst_array is None and not use_ant_pos:
            raise ValueError(
                "Must include lst_array to calculate baselines in ENU coordinates!"
            )
        if telescope_lat is None:
            raise ValueError(
                "Must include telescope_lat to calculate baselines "
                "in ENU coordinates!"
            )
    else:
        if ((app_ra is None) or (app_dec is None)) and frame_pa is None:
            raise ValueError(
                "Must include both app_ra and app_dec, or frame_pa to calculate "
                "baselines in uvw coordinates!"
            )

    if use_ant_pos:
        # Assume at this point we are dealing w/ antenna positions
        if antenna_positions is None:
            raise ValueError("Must include antenna_positions if use_ant_pos=True.")
        if (ant_1_array is None) or (ant_2_array is None) or (antenna_numbers is None):
            raise ValueError(
                "Must include ant_1_array, ant_2_array, and antenna_numbers "
                "setting use_ant_pos=True."
            )
        if lst_array is None and not to_enu:
            raise ValueError(
                "Must include lst_array if use_ant_pos=True and not calculating "
                "baselines in ENU coordinates."
            )
        if telescope_lon is None:
            raise ValueError("Must include telescope_lon if use_ant_pos=True.")

        ant_dict = {ant_num: idx for idx, ant_num in enumerate(antenna_numbers)}
        ant_1_index = np.array(
            [ant_dict[ant_num] for ant_num in ant_1_array], dtype=int
        )
        ant_2_index = np.array(
            [ant_dict[ant_num] for ant_num in ant_2_array], dtype=int
        )

        N_ants = antenna_positions.shape[0]
        # Use the app_ra, app_dec, and lst_array arrays to figure out how many unique
        # rotations are actually needed. If the ratio of Nblts to number of unique
        # entries is favorable, we can just rotate the antenna positions and save
        # outselves a bit of work.
        if to_enu:
            # If to_enu, skip all this -- there's only one unique ha + dec combo
            unique_mask = np.zeros(len(ant_1_index), dtype=np.bool_)
            unique_mask[0] = True
        else:
            unique_mask = np.append(
                True,
                (
                    ((lst_array[:-1] - app_ra[:-1]) != (lst_array[1:] - app_ra[1:]))
                    | (app_dec[:-1] != app_dec[1:])
                ),
            )

        # GHA -> Hour Angle as measured at Greenwich (because antenna coords are
        # centered such that x-plane intersects the meridian at longitude 0).
        if to_enu:
            # Unprojected coordinates are given in the ENU convention -- that's
            # equivalent to calculating uvw's based on zenith. We can use that to our
            # advantage and spoof the gha and dec based on telescope lon and lat
            unique_gha = np.zeros(1) - telescope_lon
            unique_dec = np.zeros(1) + telescope_lat
            unique_pa = None
        else:
            unique_gha = (lst_array[unique_mask] - app_ra[unique_mask]) - telescope_lon
            unique_dec = app_dec[unique_mask]
            unique_pa = 0.0 if frame_pa is None else frame_pa[unique_mask]

        # Tranpose the ant vectors so that they are in the proper shape
        ant_vectors = np.transpose(antenna_positions)[np.newaxis, :, :]
        # Apply rotations, and then reorganize the ndarray so that you can access
        # individual antenna vectors quickly.
        ant_rot_vectors = np.reshape(
            np.transpose(
                _rotate_one_axis(
                    _rotate_two_axis(ant_vectors, unique_gha, unique_dec, 2, 1),
                    unique_pa,
                    0,
                ),
                axes=[0, 2, 1],
            ),
            (-1, 3),
        )

        unique_mask[0] = False
        unique_map = np.cumsum(unique_mask) * N_ants
        new_coords = (
            ant_rot_vectors[unique_map + ant_2_index]
            - ant_rot_vectors[unique_map + ant_1_index]
        )
    else:
        if uvw_array is None:
            raise ValueError("Must include uvw_array if use_ant_pos=False.")
        if from_enu:
            if to_enu:
                # Well this was pointless... returning your uvws unharmed
                return uvw_array
            # Unprojected coordinates appear to be stored in ENU coordinates -- that's
            # equivalent to calculating uvw's based on zenith. We can use that to our
            # advantage and spoof old_app_ra and old_app_dec based on lst_array and
            # telescope_lat
            if telescope_lat is None:
                raise ValueError(
                    "Must include telescope_lat if moving between "
                    "ENU (i.e., 'unprojected') and uvw coordinates!"
                )
            if lst_array is None:
                raise ValueError(
                    "Must include lst_array if moving between ENU "
                    "(i.e., 'unprojected') and uvw coordinates!"
                )
        else:
            if (old_frame_pa is None) and not (frame_pa is None or to_enu):
                raise ValueError(
                    "Must include old_frame_pa values if data are phased and "
                    "applying new position angle values (frame_pa)."
                )
            if ((old_app_ra is None) and not (app_ra is None or to_enu)) or (
                (old_app_dec is None) and not (app_dec is None or to_enu)
            ):
                raise ValueError(
                    "Must include old_app_ra and old_app_dec values when data are "
                    "already phased and phasing to a new position."
                )
        # For this operation, all we need is the delta-ha coverage, which _should_ be
        # entirely encapsulated by the change in RA.
        if (app_ra is None) and (old_app_ra is None):
            gha_delta_array = 0.0
        else:
            gha_delta_array = (lst_array if from_enu else old_app_ra) - (
                lst_array if to_enu else app_ra
            )

        # Notice below there's an axis re-orientation here, to go from uvw -> XYZ,
        # where X is pointing in the direction of the source. This is mostly here
        # for convenience and code legibility -- a slightly different pair of
        # rotations would give you the same result w/o needing to cycle the axes.

        # Up front, we want to trap the corner-case where the sky position you are
        # phasing up to hasn't changed, just the position angle (i.e., which way is
        # up on the map). This is a much easier transform to handle.
        if np.all(gha_delta_array == 0.0) and np.all(old_app_dec == app_dec):
            new_coords = _rotate_one_axis(
                uvw_array[:, [2, 0, 1], np.newaxis],
                frame_pa - (0.0 if old_frame_pa is None else old_frame_pa),
                0,
            )[:, :, 0]
        else:
            new_coords = _rotate_two_axis(
                _rotate_two_axis(  # Yo dawg, I heard you like rotation matricies...
                    uvw_array[:, [2, 0, 1], np.newaxis],
                    0.0 if (from_enu or old_frame_pa is None) else (-old_frame_pa),
                    (-telescope_lat) if from_enu else (-old_app_dec),
                    0,
                    1,
                ),
                gha_delta_array,
                telescope_lat if to_enu else app_dec,
                2,
                1,
            )

            # One final rotation applied here, to compensate for the fact that we want
            # the Dec-axis of our image (Fourier dual to the v-axis) to be aligned with
            # the chosen frame, if we not in ENU coordinates
            if not to_enu:
                new_coords = _rotate_one_axis(new_coords, frame_pa, 0)

            # Finally drop the now-vestigal last axis of the array
            new_coords = new_coords[:, :, 0]

    # There's one last task to do, which is to re-align the axes from projected
    # XYZ -> uvw, where X (which points towards the source) falls on the w axis,
    # and Y and Z fall on the u and v axes, respectively.
    return new_coords[:, [1, 2, 0]]


def transform_sidereal_coords(
    lon,
    lat,
    in_coord_frame,
    out_coord_frame,
    in_coord_epoch=None,
    out_coord_epoch=None,
    time_array=None,
):
    """
    Transform a given set of coordinates from one sidereal coordinate frame to another.

    Uses astropy to convert from a coordinates from sidereal frame into another.
    This function will support transforms from several frames, including GCRS,
    FK5 (i.e., J2000), FK4 (i.e., B1950), Galactic, Supergalactic, CIRS, HCRS, and
    a few others (basically anything that doesn't require knowing the observers
    location on Earth/other celestial body).

    Parameters
    ----------
    lon_coord : float or ndarray of floats
        Logitudinal coordinate to be transformed, typically expressed as the right
        ascension, in units of radians. Can either be a float, or an ndarray of
        floats with shape (Ncoords,). Must agree with lat_coord.
    lat_coord : float or ndarray of floats
        Latitudinal coordinate to be transformed, typically expressed as the
        declination, in units of radians. Can either be a float, or an ndarray of
        floats with shape (Ncoords,). Must agree with lon_coord.
    in_coord_frame : string
        Reference frame for the provided coordinates.  Expected to match a list of
        those supported within the astropy SkyCoord object. An incomplete list includes
        'gcrs', 'fk4', 'fk5', 'galactic', 'supergalactic', 'cirs', and 'hcrs'.
    out_coord_frame : string
        Reference frame to output coordinates in. Expected to match a list of
        those supported within the astropy SkyCoord object. An incomplete list includes
        'gcrs', 'fk4', 'fk5', 'galactic', 'supergalactic', 'cirs', and 'hcrs'.
    in_coord_epoch : float
        Epoch for the input coordinate frame. Optional parameter, only required
        when using either the FK4 (B1950) or FK5 (J2000) coordinate systems. Units are
        in fractional years.
    out_coord_epoch : float
        Epoch for the output coordinate frame. Optional parameter, only required
        when using either the FK4 (B1950) or FK5 (J2000) coordinate systems. Units are
        in fractional years.
    time_array : float or ndarray of floats
        Julian date(s) to which the coordinates correspond to, only used in frames
        with annular motion terms (e.g., abberation in GCRS). Can either be a float,
        or an ndarray of floats with shape (Ntimes,), assuming that either lat_coord
        and lon_coord are floats, or that Ntimes == Ncoords.

    Returns
    -------
    new_lat : float or ndarray of floats
        Longitudinal coordinates, in units of radians. Output will be an ndarray
        if any inputs were, with shape (Ncoords,) or (Ntimes,), depending on inputs.
    new_lon : float or ndarray of floats
        Latidudinal coordinates, in units of radians. Output will be an ndarray
        if any inputs were, with shape (Ncoords,) or (Ntimes,), depending on inputs.
    """
    lon_coord = lon * units.rad
    lat_coord = lat * units.rad

    # Check here to make sure that lat_coord and lon_coord are the same length,
    # either 1 or len(time_array)
    if lat_coord.shape != lon_coord.shape:
        raise ValueError("lon and lat must be the same shape.")

    if lon_coord.ndim == 0:
        lon_coord.shape += (1,)
        lat_coord.shape += (1,)

    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    in_epoch = None
    if isinstance(in_coord_epoch, str) or isinstance(in_coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        in_epoch = Time(in_coord_epoch)
    elif in_coord_epoch is not None:
        if in_coord_frame.lower() in ["fk4", "fk4noeterms"]:
            in_epoch = Time(in_coord_epoch, format="byear")
        else:
            in_epoch = Time(in_coord_epoch, format="jyear")

    # Now do the same for the outbound frame
    out_epoch = None
    if isinstance(out_coord_epoch, str) or isinstance(out_coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        out_epoch = Time(out_coord_epoch)
    elif out_coord_epoch is not None:
        if out_coord_frame.lower() in ["fk4", "fk4noeterms"]:
            out_epoch = Time(out_coord_epoch, format="byear")
        else:
            out_epoch = Time(out_coord_epoch, format="jyear")

    # Make sure that time array matched up with what we expect. Thanks to astropy
    # weirdness, time_array has to be the same length as lat/lon coords
    rep_time = False
    rep_crds = False
    if time_array is None:
        time_obj_array = None
    else:
        if isinstance(time_array, Time):
            time_obj_array = time_array
        else:
            time_obj_array = Time(time_array, format="jd", scale="utc")
        if (time_obj_array.size != 1) and (lon_coord.size != 1):
            if time_obj_array.shape != lon_coord.shape:
                raise ValueError(
                    "Shape of time_array must be either that of "
                    " lat_coord/lon_coord if len(time_array) > 1."
                )
        else:
            rep_crds = (time_obj_array.size != 1) and (lon_coord.size == 1)
            rep_time = (time_obj_array.size == 1) and (lon_coord.size != 1)
    if rep_crds:
        lon_coord = np.repeat(lon_coord, len(time_array))
        lat_coord = np.repeat(lat_coord, len(time_array))
    if rep_time:
        time_obj_array = Time(
            np.repeat(time_obj_array.jd, len(lon_coord)), format="jd", scale="utc"
        )
    coord_object = SkyCoord(
        lon_coord,
        lat_coord,
        frame=in_coord_frame,
        equinox=in_epoch,
        obstime=time_obj_array,
    )

    # Easiest, most general way to transform to the new frame is to create a dummy
    # SkyCoord with all the attributes needed -- note that we particularly need this
    # in order to use a non-standard equinox/epoch
    new_coord = coord_object.transform_to(
        SkyCoord(0, 0, unit="rad", frame=out_coord_frame, equinox=out_epoch)
    )

    return new_coord.spherical.lon.rad, new_coord.spherical.lat.rad


def transform_icrs_to_app(
    time_array,
    ra,
    dec,
    telescope_loc,
    epoch=2000.0,
    pm_ra=None,
    pm_dec=None,
    vrad=None,
    dist=None,
    astrometry_library=None,
):
    """
    Transform a set of coordinates in ICRS to topocentric/apparent coordinates.

    This utility uses one of three libraries (astropy, NOVAS, or ERFA) to calculate
    the apparent (i.e., topocentric) coordinates of a source at a given time and
    location, given a set of coordinates expressed in the ICRS frame. These coordinates
    are most typically used for defining the phase center of the array (i.e, calculating
    baseline vectors).

    As of astropy v4.2, the agreement between the three libraries is consistent down to
    the level of better than 1 mas, with the values produced by astropy and pyERFA
    consistent to bettter than 10 µas (this is not surprising, given that astropy uses
    pyERFA under the hood for astrometry). ERFA is the default as it outputs
    coordinates natively in the apparent frame (whereas NOVAS and astropy do not), as
    well as the fact that of the three libraries, it produces results the fastest.

    Parameters
    ----------
    time_array : float or array-like of float
        Julian dates to calculate coordinate positions for. Can either be a single
        float, or an array-like of shape (Ntimes,).
    ra : float or array-like of float
        ICRS RA of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (with the exception of telescope location parameters).
    dec : float or array-like of float
        ICRS Dec of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (with the exception of telescope location parameters).
    telescope_loc : array-like of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containing (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    epoch : int or float or str or Time object
        Epoch of the coordinate data supplied, only used when supplying proper motion
        values. If supplying a number, it will assumed to be in Julian years. Default
        is J2000.0.
    pm_ra : float or array-like of float
        Proper motion in RA of the source, expressed in units of milliarcsec / year.
        Proper motion values are applied relative to the J2000 (i.e., RA/Dec ICRS
        values should be set to their expected values when the epoch is 2000.0).
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Note that
        units are in dRA/dt, not cos(Dec)*dRA/dt. Not required.
    pm_dec : float or array-like of float
        Proper motion in Dec of the source, expressed in units of milliarcsec / year.
        Proper motion values are applied relative to the J2000 (i.e., RA/Dec ICRS
        values should be set to their expected values when the epoch is 2000.0).
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required.
    vrad : float or array-like of float
        Radial velocity of the source, expressed in units of km / sec. Can either be
        a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (namely ra_coord and dec_coord). Not required.
    dist : float or array-like of float
        Distance of the source, expressed in milliarcseconds. Can either be a single
        float or array of shape (Ntimes,), although this must be consistent with other
        parameters (namely ra_coord and dec_coord). Not required.
    astrometry_library : str
        Library used for running the coordinate conversions. Allowed options are
        'erfa' (which uses the pyERFA), 'novas' (which uses the python-novas library),
        and 'astropy' (which uses the astropy utilities). Default is erfa unless
        the telescope_location is a MoonLocation object, in which case the default is
        astropy.

    Returns
    -------
    app_ra : ndarray of floats
        Apparent right ascension coordinates, in units of radians, of shape (Ntimes,).
    app_dec : ndarray of floats
        Apparent declination coordinates, in units of radians, of shape (Ntimes,).
    """
    # Make sure that the library requested is actually permitted
    if astrometry_library is None:
        if hasmoon and isinstance(telescope_loc, MoonLocation):
            astrometry_library = "astropy"
        else:
            astrometry_library = "erfa"

    if astrometry_library not in ["erfa", "novas", "astropy"]:
        raise ValueError(
            "Requested coordinate transformation library is not supported, please "
            "select either 'erfa', 'novas', or 'astropy' for astrometry_library."
        )

    ra_coord = ra * units.rad
    dec_coord = dec * units.rad

    # Check here to make sure that ra_coord and dec_coord are the same length,
    # either 1 or len(time_array)
    multi_coord = ra_coord.size != 1
    if ra_coord.shape != dec_coord.shape:
        raise ValueError("ra and dec must be the same shape.")

    pm_ra_coord = None if pm_ra is None else pm_ra * (units.mas / units.yr)
    pm_dec_coord = None if pm_dec is None else pm_dec * (units.mas / units.yr)
    d_coord = (
        None if (dist is None or np.all(dist == 0.0)) else Distance(dist * units.pc)
    )
    v_coord = None if vrad is None else vrad * (units.km / units.s)

    opt_list = [pm_ra_coord, pm_dec_coord, d_coord, v_coord]
    opt_names = ["pm_ra", "pm_dec", "dist", "vrad"]
    # Check the optional inputs, make sure that they're sensible
    for item, name in zip(opt_list, opt_names):
        if item is not None:
            if ra_coord.shape != item.shape:
                raise ValueError("%s must be the same shape as ra and dec." % name)

    if isinstance(telescope_loc, EarthLocation) or (
        hasmoon and isinstance(telescope_loc, MoonLocation)
    ):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    if (
        hasmoon
        and isinstance(site_loc, MoonLocation)
        and astrometry_library != "astropy"
    ):
        raise NotImplementedError(
            "MoonLocation telescopes are only supported with the 'astropy' astrometry "
            "library"
        )

    # Useful for both astropy and novas methods, the latter of which gives easy
    # access to the IERS data that we want.
    if isinstance(time_array, Time):
        time_obj_array = time_array
    else:
        time_obj_array = Time(time_array, format="jd", scale="utc")

    if time_obj_array.size != 1:
        if (time_obj_array.shape != ra_coord.shape) and multi_coord:
            raise ValueError(
                "time_array must be of either of length 1 (single "
                "float) or same length as ra and dec."
            )
    elif time_obj_array.ndim == 0:
        # Make the array at least 1-dimensional so we don't run into indexing
        # issues later.
        time_obj_array = Time([time_obj_array])

    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    coord_epoch = None
    if isinstance(epoch, str) or isinstance(epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        coord_epoch = Time(epoch)
    elif epoch is not None:
        coord_epoch = Time(epoch, format="jyear")

    # Note if time_array is a single element
    multi_time = time_obj_array.size != 1

    # Get IERS data, which is needed for NOVAS and ERFA
    polar_motion_data = iers.earth_orientation_table.get()

    pm_x_array, pm_y_array = polar_motion_data.pm_xy(time_obj_array)
    delta_x_array, delta_y_array = polar_motion_data.dcip_xy(time_obj_array)

    pm_x_array = pm_x_array.to_value("arcsec")
    pm_y_array = pm_y_array.to_value("arcsec")
    delta_x_array = delta_x_array.to_value("marcsec")
    delta_y_array = delta_y_array.to_value("marcsec")
    # Catch the case where we don't have CIP delta values yet (they don't typically have
    # predictive values like the polar motion does)
    delta_x_array[np.isnan(delta_x_array)] = 0.0
    delta_y_array[np.isnan(delta_y_array)] = 0.0

    # If the source was instantiated w/ floats, it'll be a 0-dim object, which will
    # throw errors if we try to treat it as an array. Reshape to a 1D array of len 1
    # so that all the calls can be uniform
    if ra_coord.ndim == 0:
        ra_coord.shape += (1,)
        dec_coord.shape += (1,)
        if pm_ra_coord is not None:
            pm_ra
        if d_coord is not None:
            d_coord.shape += (1,)
        if v_coord is not None:
            v_coord.shape += (1,)

    # If there is an epoch and a proper motion, apply that motion now

    if astrometry_library == "astropy":
        # Astropy doesn't have (oddly enough) a way of getting at the apparent RA/Dec
        # directly, but we can cheat this by going to AltAz, and then coverting back
        # to apparent RA/Dec using the telescope lat and LAST.
        if (epoch is not None) and (pm_ra is not None) and (pm_dec is not None):
            # astropy is a bit weird in how it handles proper motion, so rather than
            # fight with it to do it all in one step, we separate it into two: first
            # apply proper motion to ICRS, then transform to topocentric.
            sky_coord = SkyCoord(
                ra=ra_coord,
                dec=dec_coord,
                pm_ra_cosdec=pm_ra_coord * np.cos(dec_coord),
                pm_dec=pm_dec_coord,
                frame="icrs",
            )

            sky_coord = sky_coord.apply_space_motion(dt=(time_obj_array - coord_epoch))
            ra_coord = sky_coord.ra
            dec_coord = sky_coord.dec
            if d_coord is not None:
                d_coord = d_coord.repeat(ra_coord.size)
            if v_coord is not None:
                v_coord = v_coord.repeat(ra_coord.size)

        if isinstance(site_loc, EarthLocation):
            sky_coord = SkyCoord(
                ra=ra_coord,
                dec=dec_coord,
                distance=d_coord,
                radial_velocity=v_coord,
                frame="icrs",
            )

            azel_data = sky_coord.transform_to(
                SkyCoord(
                    np.zeros_like(time_obj_array) * units.rad,
                    np.zeros_like(time_obj_array) * units.rad,
                    location=site_loc,
                    obstime=time_obj_array,
                    frame="altaz",
                )
            )
        else:
            sky_coord = LunarSkyCoord(
                ra=ra_coord,
                dec=dec_coord,
                distance=d_coord,
                radial_velocity=v_coord,
                frame="icrs",
            )

            azel_data = sky_coord.transform_to(
                LunarSkyCoord(
                    np.zeros_like(time_obj_array) * units.rad,
                    np.zeros_like(time_obj_array) * units.rad,
                    location=site_loc,
                    obstime=time_obj_array,
                    frame="lunartopo",
                )
            )

        app_ha, app_dec = erfa.ae2hd(
            azel_data.az.rad, azel_data.alt.rad, site_loc.lat.rad
        )
        app_ra = np.mod(
            time_obj_array.sidereal_time("apparent", longitude=site_loc.lon).rad
            - app_ha,
            2 * np.pi,
        )

    elif astrometry_library == "novas":
        # Import the NOVAS library only if it's needed/available.
        try:
            import novas_de405  # noqa
            from novas import compat as novas
            from novas.compat import eph_manager
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "novas and/or novas_de405 are not installed but is required for "
                "NOVAS functionality"
            ) from e

        # Call is needed to load high-precision ephem data in NOVAS
        jd_start, jd_end, number = eph_manager.ephem_open()

        # Define the obs location, which is needed to calculate diurnal abb term
        # and polar wobble corrections
        site_loc = novas.make_on_surface(
            site_loc.lat.deg,  # latitude in deg
            site_loc.lon.deg,  # Longitude in deg
            site_loc.height.to_value("m"),  # Height in meters
            0.0,  # Temperature, set to 0 for now (no atm refrac)
            0.0,  # Pressure, set to 0 for now (no atm refrac)
        )

        # NOVAS wants things in terrestial time and UT1
        tt_time_array = time_obj_array.tt.jd
        ut1_time_array = time_obj_array.ut1.jd
        gast_array = time_obj_array.sidereal_time("apparent", "greenwich").rad

        if np.any(tt_time_array < jd_start) or np.any(tt_time_array > jd_end):
            raise ValueError(
                "No current support for JPL ephems outside of 1700 - 2300 AD. "
                "Check back later (or possibly earlier)..."
            )

        app_ra = np.zeros(tt_time_array.shape) + np.zeros(ra_coord.shape)
        app_dec = np.zeros(tt_time_array.shape) + np.zeros(ra_coord.shape)

        for idx in range(len(app_ra)):
            if multi_coord or (idx == 0):
                # Create a catalog entry for the source in question
                cat_entry = novas.make_cat_entry(
                    "dummy_name",  # Dummy source name
                    "GKK",  # Catalog ID, fixed for now
                    156,  # Star ID number, fixed for now
                    ra_coord[idx].to_value("hourangle"),
                    dec_coord[idx].to_value("deg"),
                    0.0
                    if pm_ra is None
                    else (
                        pm_ra_coord.to_value("mas/yr")
                        * np.cos(dec_coord[idx].to_value("rad"))
                    ),
                    0.0 if pm_dec is None else pm_dec_coord.to_value("mas/yr"),
                    0.0
                    if (dist is None or np.any(dist == 0.0))
                    else (d_coord.kiloparsec**-1.0),
                    0.0 if (vrad is None) else v_coord.to_value("km/s"),
                )

            # Update polar wobble parameters for a given timestamp
            if multi_time or (idx == 0):
                gast = gast_array[idx]
                pm_x = pm_x_array[idx] * np.cos(gast) + pm_y_array[idx] * np.sin(gast)
                pm_y = pm_y_array[idx] * np.cos(gast) - pm_x_array[idx] * np.sin(gast)
                tt_time = tt_time_array[idx]
                ut1_time = ut1_time_array[idx]
                novas.cel_pole(tt_time, 2, delta_x_array[idx], delta_y_array[idx])

            # Calculate topocentric RA/Dec values
            [temp_ra, temp_dec] = novas.topo_star(
                tt_time, (tt_time - ut1_time) * 86400.0, cat_entry, site_loc, accuracy=0
            )
            xyz_array = polar2_to_cart3(
                temp_ra * (np.pi / 12.0), temp_dec * (np.pi / 180.0)
            )
            xyz_array = novas.wobble(tt_time, pm_x, pm_y, xyz_array, 1)

            app_ra[idx], app_dec[idx] = cart3_to_polar2(np.array(xyz_array))
    elif astrometry_library == "erfa":
        # liberfa wants things in radians
        pm_x_array *= np.pi / (3600.0 * 180.0)
        pm_y_array *= np.pi / (3600.0 * 180.0)
        [_, _, _, app_dec, app_ra, eqn_org] = erfa.atco13(
            ra_coord.to_value("rad"),
            dec_coord.to_value("rad"),
            0.0 if (pm_ra is None) else pm_ra_coord.to_value("rad/yr"),
            0.0 if (pm_dec is None) else pm_dec_coord.to_value("rad/yr"),
            0.0 if (dist is None or np.any(dist == 0.0)) else (d_coord.pc**-1.0),
            0.0 if (vrad is None) else v_coord.to_value("km/s"),
            time_obj_array.utc.jd,
            0.0,
            time_obj_array.delta_ut1_utc,
            site_loc.lon.rad,
            site_loc.lat.rad,
            site_loc.height.to_value("m"),
            pm_x_array,
            pm_y_array,
            0,  # ait pressure, used for refraction (ignored)
            0,  # amb temperature, used for refraction (ignored)
            0,  # rel humidity, used for refraction (ignored)
            0,  # wavelength, used for refraction (ignored)
        )

        app_ra = np.mod(app_ra - eqn_org, 2 * np.pi)

    return app_ra, app_dec


def transform_app_to_icrs(
    time_array, app_ra, app_dec, telescope_loc, astrometry_library=None
):
    """
    Transform a set of coordinates in topocentric/apparent to ICRS coordinates.

    This utility uses either astropy or erfa to calculate the ICRS  coordinates of
    a given set of apparent source coordinates. These coordinates are most typically
    used for defining the celestial/catalog position of a source. Note that at present,
    this is only implemented in astropy and pyERFA, although it could hypothetically
    be extended to NOVAS at some point.

    Parameters
    ----------
    time_array : float or ndarray of float
        Julian dates to calculate coordinate positions for. Can either be a single
        float, or an ndarray of shape (Ntimes,).
    app_ra : float or ndarray of float
        ICRS RA of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ncoord,). Note that if time_array is
        not a singleton value, then Ncoord must be equal to Ntimes.
    app_dec : float or ndarray of float
        ICRS Dec of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ncoord,). Note that if time_array is
        not a singleton value, then Ncoord must be equal to Ntimes.
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containing (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    astrometry_library : str
        Library used for running the coordinate conversions. Allowed options are
        'erfa' (which uses the pyERFA), and 'astropy' (which uses the astropy
        utilities). Default is erfa unless the telescope_location is a MoonLocation
        object, in which case the default is astropy.

    Returns
    -------
    icrs_ra : ndarray of floats
        ICRS right ascension coordinates, in units of radians, of either shape
        (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    icrs_dec : ndarray of floats
        ICRS declination coordinates, in units of radians, of either shape
        (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    """
    # Make sure that the library requested is actually permitted
    if astrometry_library is None:
        if hasmoon and isinstance(telescope_loc, MoonLocation):
            astrometry_library = "astropy"
        else:
            astrometry_library = "erfa"

    if astrometry_library not in ["erfa", "astropy"]:
        raise ValueError(
            "Requested coordinate transformation library is not supported, please "
            "select either 'erfa' or 'astropy' for astrometry_library."
        )

    ra_coord = app_ra * units.rad
    dec_coord = app_dec * units.rad

    # Check here to make sure that ra_coord and dec_coord are the same length,
    # either 1 or len(time_array)
    multi_coord = ra_coord.size != 1
    if ra_coord.shape != dec_coord.shape:
        raise ValueError("app_ra and app_dec must be the same shape.")

    if isinstance(telescope_loc, EarthLocation) or (
        hasmoon and isinstance(telescope_loc, MoonLocation)
    ):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    if (
        hasmoon
        and isinstance(site_loc, MoonLocation)
        and astrometry_library != "astropy"
    ):
        raise NotImplementedError(
            "MoonLocation telescopes are only supported with the 'astropy' astrometry "
            "library"
        )

    assert time_array.size > 0
    if isinstance(time_array, Time):
        time_obj_array = time_array
    else:
        time_obj_array = Time(time_array, format="jd", scale="utc")

    if time_obj_array.size != 1:
        if (time_obj_array.shape != ra_coord.shape) and multi_coord:
            raise ValueError(
                "time_array must be of either of length 1 (single "
                "float) or same length as ra and dec."
            )
    elif time_obj_array.ndim == 0:
        # Make the array at least 1-dimensional so we don't run into indexing
        # issues later.
        time_obj_array = Time([time_obj_array])

    if astrometry_library == "astropy":
        az_coord, el_coord = erfa.hd2ae(
            np.mod(
                time_obj_array.sidereal_time("apparent", longitude=site_loc.lon).rad
                - ra_coord.to_value("rad"),
                2 * np.pi,
            ),
            dec_coord.to_value("rad"),
            site_loc.lat.rad,
        )

        if isinstance(site_loc, EarthLocation):
            sky_coord = SkyCoord(
                az_coord * units.rad,
                el_coord * units.rad,
                frame="altaz",
                location=site_loc,
                obstime=time_obj_array,
            )
        else:
            sky_coord = LunarSkyCoord(
                az_coord * units.rad,
                el_coord * units.rad,
                frame="lunartopo",
                location=site_loc,
                obstime=time_obj_array,
            )

        coord_data = sky_coord.transform_to("icrs")
        icrs_ra = coord_data.ra.rad
        icrs_dec = coord_data.dec.rad
    elif astrometry_library == "erfa":
        # Get IERS data, which is needed for highest precision
        polar_motion_data = iers.earth_orientation_table.get()

        pm_x_array, pm_y_array = polar_motion_data.pm_xy(time_obj_array)
        pm_x_array = pm_x_array.to_value("rad")
        pm_y_array = pm_y_array.to_value("rad")

        bpn_matrix = erfa.pnm06a(time_obj_array.tt.jd, 0.0)
        cip_x, cip_y = erfa.bpn2xy(bpn_matrix)
        cio_s = erfa.s06(time_obj_array.tt.jd, 0.0, cip_x, cip_y)
        eqn_org = erfa.eors(bpn_matrix, cio_s)

        # Observed to ICRS via ERFA
        icrs_ra, icrs_dec = erfa.atoc13(
            "r",
            ra_coord.to_value("rad") + eqn_org,
            dec_coord.to_value("rad"),
            time_obj_array.utc.jd,
            0.0,  # Second half of the UT date, not needed
            time_obj_array.delta_ut1_utc,
            site_loc.lon.rad,
            site_loc.lat.rad,
            site_loc.height.value,
            pm_x_array,
            pm_y_array,
            0,  # ait pressure, used for refraction (ignored)
            0,  # amb temperature, used for refraction (ignored)
            0,  # rel humidity, used for refraction (ignored)
            0,  # wavelength, used for refraction (ignored)
        )

    # Return back the two RA/Dec arrays
    return icrs_ra, icrs_dec


def calc_parallactic_angle(app_ra, app_dec, lst_array, telescope_lat):
    """
    Calculate the parallactic angle between RA/Dec and the AltAz frame.

    Parameters
    ----------
    app_ra : ndarray of floats
        Array of apparent RA values in units of radians, shape (Ntimes,).
    app_dec : ndarray of floats
        Array of apparent dec values in units of radians, shape (Ntimes,).
    telescope_lat : float
        Latitude of the observatory, in units of radians.
    lst_array : float or ndarray of float
        Array of local apparent sidereal timesto calculate position angle values
        for, in units of radians. Can either be a single float or an array of shape
        (Ntimes,).
    """
    # This is just a simple wrapped around the pas function in ERFA
    return erfa.pas(app_ra, app_dec, lst_array, telescope_lat)


def calc_frame_pos_angle(
    time_array,
    app_ra,
    app_dec,
    telescope_loc,
    ref_frame,
    ref_epoch=None,
    offset_pos=(np.pi / 360.0),
):
    """
    Calculate an position angle given apparent position and reference frame.

    This function is used to determine the position angle between the great
    circle of declination in apparent coordinates, versus that in a given
    reference frame. Note that this is slightly different than parallactic
    angle, which is the difference between apparent declination and elevation.

    Paramters
    ---------
    time_array : float or ndarray of floats
        Array of julian dates to calculate position angle values for, of shape
        (Ntimes,).
    app_ra : ndarray of floats
        Array of apparent RA values in units of radians, shape (Ntimes,).
    app_dec : ndarray of floats
        Array of apparent dec values in units of radians, shape (Ntimes,).
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the observer.
        Can either be provided as an astropy EarthLocation, or an array-like of shape
        (3,) containing the latitude, longitude, and altitude, in that order, with units
        of radians, radians, and meters, respectively.
    offset_pos : float
        Distance of the offset position used to calculate the frame PA. Default
        is 0.5 degrees, which should be sufficent for most applications.
    ref_frame : str
        Coordinate frame to calculate position angles for. Can be any of the
        several supported frames in astropy (a limited list: fk4, fk5, icrs,
        gcrs, cirs, galactic).
    ref_epoch : str or flt
        Epoch of the coordinates, only used when ref_frame = fk4 or fk5. Given
        in unites of fractional years, either as a float or as a string with
        the epoch abbreviation (e.g, Julian epoch 2000.0 would be J2000.0).


    Returns
    -------
    frame_pa : ndarray of floats
        Array of position angles, in units of radians.
    """
    # Check to see if the position angles should default to zero
    if (ref_frame is None) or (ref_frame == "topo"):
        # No-op detected, ENGAGE MAXIMUM SNARK!
        return np.zeros_like(time_array)

    # This creates an array of unique entries of ra + dec + time, since the processing
    # time for each element can be non-negligible, and entries along the Nblt axis can
    # be highly redundant.
    unique_mask = np.union1d(
        np.union1d(
            np.unique(app_ra, return_index=True)[1],
            np.unique(app_dec, return_index=True)[1],
        ),
        np.unique(time_array, return_index=True)[1],
    )

    # Pluck out the unique entries for each
    unique_ra = app_ra[unique_mask]
    unique_dec = app_dec[unique_mask]
    unique_time = time_array[unique_mask]

    # Figure out how many elements we need to transform
    n_coord = len(unique_mask)

    # Offset north/south positions by 0.5 deg, such that the PA is determined over a
    # 1 deg arc.
    up_dec = unique_dec + (np.pi / 360.0)
    dn_dec = unique_dec - (np.pi / 360.0)
    up_ra = dn_ra = unique_ra

    # Wrap the positions if they happen to go over the poles
    up_ra[up_dec > (np.pi / 2.0)] = np.mod(
        up_ra[up_dec > (np.pi / 2.0)] + np.pi, 2.0 * np.pi
    )
    up_dec[up_dec > (np.pi / 2.0)] = np.pi - up_dec[up_dec > (np.pi / 2.0)]

    dn_ra[-dn_dec > (np.pi / 2.0)] = np.mod(
        dn_ra[dn_dec > (np.pi / 2.0)] + np.pi, 2.0 * np.pi
    )
    dn_dec[-dn_dec > (np.pi / 2.0)] = np.pi - dn_dec[-dn_dec > (np.pi / 2.0)]

    # Run the set of offset coordinates through the "reverse" transform. The two offset
    # positions are concat'd together to help reduce overheads
    ref_ra, ref_dec = calc_sidereal_coords(
        np.tile(unique_time, 2),
        np.concatenate((dn_ra, up_ra)),
        np.concatenate((dn_dec, up_dec)),
        telescope_loc,
        ref_frame,
        coord_epoch=ref_epoch,
    )

    # Use the pas function from ERFA to calculate the position angle. The negative sign
    # is here because we're measuring PA of app -> frame, but we want frame -> app.
    unique_pa = -erfa.pas(
        ref_ra[:n_coord], ref_dec[:n_coord], ref_ra[n_coord:], ref_dec[n_coord:]
    )

    # Finally, we have to go back through and "fill in" the redundant entries
    frame_pa = np.zeros_like(app_ra)
    for idx in range(n_coord):
        select_mask = np.logical_and(
            np.logical_and(unique_ra[idx] == app_ra, unique_dec[idx] == app_dec),
            unique_time[idx] == time_array,
        )
        frame_pa[select_mask] = unique_pa[idx]

    return frame_pa


def lookup_jplhorizons(
    target_name,
    time_array,
    telescope_loc=None,
    high_cadence=False,
    force_indv_lookup=None,
):
    """
    Lookup solar system body coordinates via the JPL-Horizons service.

    This utility is useful for generating ephemerides, which can then be interpolated in
    order to provide positional data for a target which is moving, such as planetary
    bodies and other solar system objects. Use of this function requires the
    installation of the `astroquery` module.


    Parameters
    ----------
    target_name : str
        Name of the target to gather an ephemeris for. Must match the name
        in the JPL-Horizons database.
    time_array : array-like of float
        Times in UTC Julian days to gather an ephemeris for.
    telescope_loc : array-like of float
        ITRF latitude, longitude, and altitude (rel to sea-level) of the observer. Must
        be an array-like of shape (3,) containing the latitude, longitude, and
        altitude, in that order, with units of radians, radians, and meters,
        respectively.
    high_cadence : bool
        If set to True, will calculate ephemeris points every 3 minutes in time, as
        opposed to the default of every 3 hours.
    force_indv_lookup : bool
        If set to True, will calculate coordinate values for each value found within
        `time_array`. If False, a regularized time grid is sampled that encloses the
        values contained within `time_array`. Default is False, unless `time_array` is
        of length 1, in which the default is set to True.


    Returns
    -------
    ephem_times : ndarray of float
        Times for which the ephemeris values were calculated, in UTC Julian days.
    ephem_ra : ndarray of float
        ICRS Right ascension of the target at the values within `ephem_times`, in
        units of radians.
    ephem_dec : ndarray of float
        ICRS Declination of the target at the values within `ephem_times`, in units
        of radians.
    ephem_dist : ndarray of float
        Distance of the target relative to the observer, at the values within
        `ephem_times`, in units of parsecs.
    ephem_vel : ndarray of float
        Velocity of the targets relative to the observer, at the values within
        `ephem_times`, in units of km/sec.
    """
    try:
        from astroquery.jplhorizons import Horizons
    except ImportError as err:  # pragma: no cover
        raise ImportError(
            "astroquery is not installed but is required for "
            "planet ephemeris functionality"
        ) from err
    from json import load as json_load
    from os.path import join as path_join

    from pyuvdata.data import DATA_PATH

    # Get the telescope location into a format that JPL-Horizons can understand,
    # which is nominally a dict w/ entries for lon (units of deg), lat (units of
    # deg), and elevation (units of km).
    if isinstance(telescope_loc, EarthLocation):
        site_loc = {
            "lon": telescope_loc.lon.deg,
            "lat": telescope_loc.lat.deg,
            "elevation": telescope_loc.height.to_value(unit=units.km),
        }
    elif hasmoon and isinstance(telescope_loc, MoonLocation):
        raise NotImplementedError(
            "Cannot lookup JPL positions for telescopes with a MoonLocation"
        )
    elif telescope_loc is None:
        # Setting to None will report the geocentric position
        site_loc = None
    else:
        site_loc = {
            "lon": telescope_loc[1] * (180.0 / np.pi),
            "lat": telescope_loc[0] * (180.0 / np.pi),
            "elevation": telescope_loc[2] * (0.001),  # m -> km
        }

    # If force_indv_lookup is True, or unset but only providing a single value, then
    # just calculate the RA/Dec for the times requested rather than creating a table
    # to interpolate from.
    if force_indv_lookup or (
        (np.array(time_array).size == 1) and (force_indv_lookup is None)
    ):
        epoch_list = np.unique(time_array)
        if len(epoch_list) > 50:
            raise ValueError(
                "Requesting too many individual ephem points from JPL-Horizons. This "
                "can be remedied by setting force_indv_lookup=False or limiting the "
                "number of values in time_array."
            )
    else:
        # When querying for multiple times, its faster (and kinder to the
        # good folks at JPL) to create a range to query, and then interpolate
        # between values. The extra buffer of 0.001 or 0.25 days for high and
        # low cadence is to give enough data points to allow for spline
        # interpolation of the data.
        if high_cadence:
            start_time = np.min(time_array) - 0.001
            stop_time = np.max(time_array) + 0.001
            step_time = "3m"
            n_entries = (stop_time - start_time) * (1440.0 / 3.0)
        else:
            # The start/stop time here are setup to maximize reusability of the
            # data, since astroquery appears to cache the results from previous
            # queries.
            start_time = (0.25 * np.floor(4.0 * np.min(time_array))) - 0.25
            stop_time = (0.25 * np.ceil(4.0 * np.max(time_array))) + 0.25
            step_time = "3h"
            n_entries = (stop_time - start_time) * (24.0 / 3.0)
        # We don't want to overtax the JPL service, so limit ourselves to 1000
        # individual queries at a time. Note that this is likely a conservative
        # cap for JPL-Horizons, but there should be exceptionally few applications
        # that actually require more than this.
        if n_entries > 1000:
            if (len(np.unique(time_array)) <= 50) and (force_indv_lookup is None):
                # If we have a _very_ sparse set of epochs, pass that along instead
                epoch_list = np.unique(time_array)
            else:
                # Otherwise, time to raise an error
                raise ValueError(
                    "Too many ephem points requested from JPL-Horizons. This "
                    "can be remedied by setting high_cadance=False or limiting "
                    "the number of values in time_array."
                )
        else:
            epoch_list = {
                "start": Time(start_time, format="jd").isot,
                "stop": Time(stop_time, format="jd").isot,
                "step": step_time,
            }
    # Check to make sure dates are within the 1700-2200 time range,
    # since not all targets are supported outside of this range
    if (np.min(time_array) < 2341973.0) or (np.max(time_array) > 2524593.0):
        raise ValueError(
            "No current support for JPL ephems outside of 1700 - 2300 AD. "
            "Check back later (or possibly earlier)..."
        )

    # JPL-Horizons has a separate catalog with what it calls 'major bodies',
    # and will throw an error if you use the wrong catalog when calling for
    # astrometry. We'll use the dict below to capture this behavior.
    with open(path_join(DATA_PATH, "jpl_major_bodies.json"), "r") as fhandle:
        major_body_dict = json_load(fhandle)

    target_id = target_name
    id_type = "smallbody"
    # If we find the target in the major body database, then we can extract the
    # target ID to make the query a bit more robust (otherwise JPL-Horizons will fail
    # on account that id will find multiple partial matches: e.g., "Mars" will be
    # matched with "Mars", "Mars Explorer", "Mars Barycenter"..., and JPL-Horizons will
    # not know which to choose).
    if target_name in major_body_dict.keys():
        target_id = major_body_dict[target_name]
        id_type = None

    query_obj = Horizons(
        id=target_id, location=site_loc, epochs=epoch_list, id_type=id_type
    )
    # If not in the major bodies catalog, try the minor bodies list, and if
    # still not found, throw an error.
    try:
        ephem_data = query_obj.ephemerides(extra_precision=True)
    except KeyError:
        # This is a fix for an astroquery + JPL-Horizons bug, that's related to
        # API change on JPL's side. In this case, the source is identified, but
        # astroquery can't correctly parse the return message from JPL-Horizons.
        # See astroquery issue #2169.
        ephem_data = query_obj.ephemerides(extra_precision=False)  # pragma: no cover
    except ValueError as err:
        query_obj._session.close()
        if "Unknown target" in str(err):
            raise ValueError(
                "Target ID is not recognized in either the small or major bodies "
                "catalogs, please consult the JPL-Horizons database for supported "
                "targets (https://ssd.jpl.nasa.gov/?horizons)."
            ) from err
        else:
            raise  # pragma: no cover
    # This is explicitly closed here to trap a bug that occassionally throws an
    # unexpected warning, see astroquery issue #1807
    query_obj._session.close()

    # Now that we have the ephem data, extract out the relevant data
    ephem_times = np.array(ephem_data["datetime_jd"])
    ephem_ra = np.array(ephem_data["RA"]) * (np.pi / 180.0)
    ephem_dec = np.array(ephem_data["DEC"]) * (np.pi / 180.0)
    ephem_dist = np.array(ephem_data["delta"])  # AU
    ephem_vel = np.array(ephem_data["delta_rate"])  # km/s

    return ephem_times, ephem_ra, ephem_dec, ephem_dist, ephem_vel


def interpolate_ephem(
    time_array, ephem_times, ephem_ra, ephem_dec, ephem_dist=None, ephem_vel=None
):
    """
    Interpolates ephemerides to give positions for requested times.

    This is a simple tool for calculated interpolated RA and Dec positions, as well
    as distances and velocities, for a given ephemeris. Under the hood, the method
    uses as cubic spline interpolation to calculate values at the requested times,
    provided that there are enough values to interpolate over to do so (requires
    >= 4 points), otherwise a linear interpolation is used.

    Parameters
    ----------
    time_array : array-like of floats
        Times to interpolate positions for, in UTC Julian days.
    ephem_times : array-like of floats
        Times in UTC Julian days which describe that match to the recorded postions
        of the target. Must be array-like, of shape (Npts,), where Npts is the number
        of ephemeris points.
    ephem_ra : array-like of floats
        Right ascencion of the target, at the times given in `ephem_times`. Units are
        in radians, must have the same shape as `ephem_times`.
    ephem_dec : array-like of floats
        Declination of the target, at the times given in `ephem_times`. Units are
        in radians, must have the same shape as `ephem_times`.
    ephem_dist : array-like of floats
        Distance of the target from the observer, at the times given in `ephem_times`.
        Optional argument, in units of parsecs. Must have the same shape as
        `ephem_times`.
    ephem_vel : array-like of floats
        Velocities of the target, at the times given in `ephem_times`. Optional
        argument, in units of km/sec. Must have the same shape as `ephem_times`.

    Returns
    -------
    ra_vals : ndarray of float
        Interpolated RA values, returned as an ndarray of floats with
        units of radians, and the same shape as `time_array`.
    dec_vals : ndarray of float
        Interpolated declination values, returned as an ndarray of floats with
        units of radians, and the same shape as `time_array`.
    dist_vals : None or ndarray of float
        If `ephem_dist` was provided, an ndarray of floats (with same shape as
        `time_array`) with the interpolated target distances, in units of parsecs.
        If `ephem_dist` was not provided, this returns as None.
    vel_vals : None or ndarray of float
        If `ephem_vals` was provided, an ndarray of floats (with same shape as
        `time_array`) with the interpolated target velocities, in units of km/sec.
        If `ephem_vals` was not provided, this returns as None.

    """
    # We're importing this here since it's only used for this one function
    from scipy.interpolate import interp1d

    ephem_shape = np.array(ephem_times).shape

    # Make sure that things look reasonable
    if np.array(ephem_ra).shape != ephem_shape:
        raise ValueError("ephem_ra must have the same shape as ephem_times.")

    if np.array(ephem_dec).shape != ephem_shape:
        raise ValueError("ephem_dec must have the same shape as ephem_times.")

    if (np.array(ephem_dist).shape != ephem_shape) and (ephem_dist is not None):
        raise ValueError("ephem_dist must have the same shape as ephem_times.")

    if (np.array(ephem_vel).shape != ephem_shape) and (ephem_vel is not None):
        raise ValueError("ephem_vel must have the same shape as ephem_times.")

    ra_vals = np.zeros_like(time_array, dtype=float)
    dec_vals = np.zeros_like(time_array, dtype=float)
    dist_vals = None if ephem_dist is None else np.zeros_like(time_array, dtype=float)
    vel_vals = None if ephem_vel is None else np.zeros_like(time_array, dtype=float)

    if len(ephem_times) == 1:
        ra_vals += ephem_ra
        dec_vals += ephem_dec
        if ephem_dist is not None:
            dist_vals += ephem_dist
        if ephem_vel is not None:
            vel_vals += ephem_vel
    else:
        if len(ephem_times) > 3:
            interp_kind = "cubic"
        else:
            interp_kind = "linear"

        # If we have values that line up perfectly, just use those directly
        select_mask = np.isin(time_array, ephem_times)
        if np.any(select_mask):
            time_select = time_array[select_mask]
            ra_vals[select_mask] = interp1d(ephem_times, ephem_ra, kind="nearest")(
                time_select
            )
            dec_vals[select_mask] = interp1d(ephem_times, ephem_dec, kind="nearest")(
                time_select
            )
            if ephem_dist is not None:
                dist_vals[select_mask] = interp1d(
                    ephem_times, ephem_dist, kind="nearest"
                )(time_select)
            if ephem_vel is not None:
                vel_vals[select_mask] = interp1d(
                    ephem_times, ephem_vel, kind="nearest"
                )(time_select)

        # If we have values lining up between grid points, use spline interpolation
        # to calculate their values
        select_mask = ~select_mask
        if np.any(select_mask):
            time_select = time_array[select_mask]
            ra_vals[select_mask] = interp1d(ephem_times, ephem_ra, kind=interp_kind)(
                time_select
            )
            dec_vals[select_mask] = interp1d(ephem_times, ephem_dec, kind=interp_kind)(
                time_select
            )
            if ephem_dist is not None:
                dist_vals[select_mask] = interp1d(
                    ephem_times, ephem_dist, kind=interp_kind
                )(time_select)
            if ephem_vel is not None:
                vel_vals[select_mask] = interp1d(
                    ephem_times, ephem_vel, kind=interp_kind
                )(time_select)

    return (ra_vals, dec_vals, dist_vals, vel_vals)


def calc_app_coords(
    lon_coord,
    lat_coord,
    coord_frame="icrs",
    coord_epoch=None,
    coord_times=None,
    coord_type="sidereal",
    time_array=None,
    lst_array=None,
    telescope_loc=None,
    pm_ra=None,
    pm_dec=None,
    vrad=None,
    dist=None,
):
    """
    Calculate apparent coordinates for several different coordinate types.

    This function calculates apparent positions at the current epoch.

    Parameters
    ----------
    lon_coord : float or ndarray of float
        Longitudinal (e.g., RA) coordinates, units of radians. Must match the same
        shape as lat_coord.
    lat_coord : float or ndarray of float
        Latitudinal (e.g., Dec) coordinates, units of radians. Must match the same
        shape as lon_coord.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        coord_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.
    coord_times : float or ndarray of float
        Only used when `coord_type="ephem"`, the JD UTC time for each value of
        `lon_coord` and `lat_coord`. These values are used to interpolate `lon_coord`
        and `lat_coord` values to those times listed in `time_array`.
    coord_type : str
        coord_type : str
            Type of source to calculate coordinates for. Must be one of:
                "sidereal" (fixed RA/Dec),
                "ephem" (RA/Dec that moves with time),
                "driftscan" (fixed az/el position),
                "unprojected" (alias for "driftscan" with (Az, Alt) = (0 deg, 90 deg)).
    time_array : float or ndarray of float or Time object
        Times for which the apparent coordinates were calculated, in UTC JD. If more
        than a single element, must be the same shape as lon_coord and lat_coord if
        both of those are arrays (instead of single floats).
    telescope_loc : array-like of floats or EarthLocation or MoonLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, a lunarsky
        Moonlocation, or a tuple of shape (3,) containing (in order) the latitude,
        longitude, and altitude for a position on Earth in units of radians, radians,
        and meters, respectively.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy. Default is ICRS.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        ref_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.
    pm_ra : float or ndarray of float
        Proper motion in RA of the source, expressed in units of milliarcsec / year.
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required, motion is calculated relative to the value of `coord_epoch`.
    pm_dec : float or ndarray of float
        Proper motion in Dec of the source, expressed in units of milliarcsec / year.
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required, motion is calculated relative to the value of `coord_epoch`.
    vrad : float or ndarray of float
        Radial velocity of the source, expressed in units of km / sec. Can either be
        a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (namely ra_coord and dec_coord). Not required.
    dist : float or ndarray of float
        Distance of the source, expressed in milliarcseconds. Can either be a single
        float or array of shape (Ntimes,), although this must be consistent with other
        parameters (namely ra_coord and dec_coord). Not required.

    Returns
    -------
    app_ra : ndarray of floats
        Apparent right ascension coordinates, in units of radians.
    app_dec : ndarray of floats
        Apparent declination coordinates, in units of radians.
    """
    if isinstance(telescope_loc, EarthLocation) or (
        hasmoon and isinstance(telescope_loc, MoonLocation)
    ):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    if isinstance(site_loc, EarthLocation):
        frame = "itrs"
    else:
        frame = "mcmf"

    # Time objects and unique don't seem to play well together, so we break apart
    # their handling here
    if isinstance(time_array, Time):
        unique_time_array, unique_mask = np.unique(time_array.utc.jd, return_index=True)
    else:
        unique_time_array, unique_mask = np.unique(time_array, return_index=True)

    if coord_type == "unphased":
        warnings.warn(
            "The 'unphased' catalog type has been renamed to 'unprojected'. Using "
            "unprojected for now, this warning will become an error in version 2.4",
            DeprecationWarning,
        )
        coord_type = "unprojected"

    if coord_type in ["driftscan", "unprojected"]:
        if lst_array is None:
            unique_lst = get_lst_for_time(
                unique_time_array,
                site_loc.lat.deg,
                site_loc.lon.deg,
                site_loc.height.to_value("m"),
                frame=frame,
            )
        else:
            unique_lst = lst_array[unique_mask]

    if coord_type == "sidereal":
        # If the coordinates are not in the ICRS frame, go ahead and transform them now
        if coord_frame != "icrs":
            icrs_ra, icrs_dec = transform_sidereal_coords(
                lon_coord,
                lat_coord,
                coord_frame,
                "icrs",
                in_coord_epoch=coord_epoch,
                time_array=unique_time_array,
            )
        else:
            icrs_ra = lon_coord
            icrs_dec = lat_coord
        unique_app_ra, unique_app_dec = transform_icrs_to_app(
            unique_time_array,
            icrs_ra,
            icrs_dec,
            site_loc,
            pm_ra=pm_ra,
            pm_dec=pm_dec,
            vrad=vrad,
            dist=dist,
        )

    elif coord_type == "driftscan":
        # Use the ERFA function ae2hd, which will do all the heavy
        # lifting for us
        unique_app_ha, unique_app_dec = erfa.ae2hd(
            lon_coord, lat_coord, site_loc.lat.rad
        )
        # The above returns HA/Dec, so we just need to rotate by
        # the LST to get back app RA and Dec
        unique_app_ra = np.mod(unique_app_ha + unique_lst, 2 * np.pi)
        unique_app_dec = unique_app_dec + np.zeros_like(unique_app_ra)
    elif coord_type == "ephem":
        interp_ra, interp_dec, _, _ = interpolate_ephem(
            unique_time_array, coord_times, lon_coord, lat_coord
        )
        if coord_frame != "icrs":
            icrs_ra, icrs_dec = transform_sidereal_coords(
                interp_ra,
                interp_dec,
                coord_frame,
                "icrs",
                in_coord_epoch=coord_epoch,
                time_array=unique_time_array,
            )
        else:
            icrs_ra = interp_ra
            icrs_dec = interp_dec
        # TODO: Vel and distance handling to be integrated here, once they are are
        # needed for velocity frame tracking
        unique_app_ra, unique_app_dec = transform_icrs_to_app(
            unique_time_array, icrs_ra, icrs_dec, site_loc, pm_ra=pm_ra, pm_dec=pm_dec
        )
    elif coord_type == "unprojected":
        # This is the easiest one - this is just supposed to be ENU, so set the
        # apparent coords to the current lst and telescope_lat.
        unique_app_ra = unique_lst.copy()
        unique_app_dec = np.zeros_like(unique_app_ra) + site_loc.lat.rad
    else:
        raise ValueError("Object type %s is not recognized." % coord_type)

    # Now that we've calculated all the unique values, time to backfill through the
    # "redundant" entries in the Nblt axis.
    app_ra = np.zeros(np.array(time_array).shape)
    app_dec = np.zeros(np.array(time_array).shape)

    # Need this promotion in order to match entries
    if isinstance(time_array, Time):
        unique_time_array = Time(unique_time_array, format="jd", scale="utc")
    for idx, unique_time in enumerate(unique_time_array):
        select_mask = time_array == unique_time
        app_ra[select_mask] = unique_app_ra[idx]
        app_dec[select_mask] = unique_app_dec[idx]

    return app_ra, app_dec


def calc_sidereal_coords(
    time_array, app_ra, app_dec, telescope_loc, coord_frame, coord_epoch=None
):
    """
    Calculate sidereal coordinates given apparent coordinates.

    This function calculates coordinates in the requested frame (at a given epoch)
    from a set of apparent coordinates.

    Parameters
    ----------
    time_array : float or ndarray of float or Time object
        Times for which the apparent coordinates were calculated, in UTC JD. Must
        match the shape of app_ra and app_dec.
    app_ra : float or ndarray of float
        Array of apparent right ascension coordinates, units of radians. Must match
        the shape of time_array and app_dec.
    app_ra : float or ndarray of float
        Array of apparent right declination coordinates, units of radians. Must match
        the shape of time_array and app_dec.
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containing (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy. Default is ICRS.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        ref_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.

    Returns
    -------
    ref_ra : ndarray of floats
        Right ascension coordinates in the requested frame, in units of radians.
        Either shape (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    ref_dec : ndarray of floats
        Declination coordinates in the requested frame, in units of radians.
        Either shape (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    """
    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    epoch = None
    if isinstance(coord_epoch, str) or isinstance(coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        epoch = Time(coord_epoch)
    elif coord_epoch is not None:
        if coord_frame.lower() in ["fk4", "fk4noeterms"]:
            epoch = Time(coord_epoch, format="byear")
        else:
            epoch = Time(coord_epoch, format="jyear")

    icrs_ra, icrs_dec = transform_app_to_icrs(
        time_array, app_ra, app_dec, telescope_loc
    )

    if coord_frame == "icrs":
        ref_ra, ref_dec = (icrs_ra, icrs_dec)
    else:
        ref_ra, ref_dec = transform_sidereal_coords(
            icrs_ra,
            icrs_dec,
            "icrs",
            coord_frame,
            out_coord_epoch=epoch,
            time_array=time_array,
        )

    return ref_ra, ref_dec


def get_lst_for_time(
    jd_array, latitude, longitude, altitude, astrometry_library=None, frame="itrs"
):
    """
    Get the local apparent sidereal time for a set of jd times at an earth location.

    This function calculates the local apparent sidereal time (LAST), given a UTC time
    and a position on the Earth, using either the astropy or NOVAS libraries. It
    is important to note that there is an apporoximate 20 microsecond difference
    between the two methods, presumably due to small differences in the apparent
    reference frame. These differences will cancel out when calculating coordinates
    in the TOPO frame, so long as apparent coordinates are calculated using the
    same library (i.e., astropy or NOVAS). Failing to do so can introduce errors
    up to ~1 mas in the horizontal coordinate system (i.e., AltAz).

    Parameters
    ----------
    jd_array : ndarray of float
        JD times to get lsts for.
    latitude : float
        Latitude of location to get lst for in degrees.
    longitude : float
        Longitude of location to get lst for in degrees.
    altitude : float
        Altitude of location to get lst for in meters.
    astrometry_library : str
        Library used for running the LST calculations. Allowed options are 'erfa'
        (which uses the pyERFA), 'novas' (which uses the python-novas library),
        and 'astropy' (which uses the astropy utilities). Default is erfa unless
        the telescope_location is a MoonLocation object, in which case the default is
        astropy.
    frame : str
        Reference frame for latitude/longitude/altitude.
        Options are itrs (default) or mcmf.

    Returns
    -------
    ndarray of float
        LASTs in radians corresponding to the jd_array.

    """
    if astrometry_library is None:
        if frame == "itrs":
            astrometry_library = "erfa"
        else:
            astrometry_library = "astropy"

    if astrometry_library not in ["erfa", "astropy", "novas"]:
        raise ValueError(
            "Requested coordinate transformation library is not supported, please "
            "select either 'erfa' or 'astropy' for astrometry_library."
        )

    if isinstance(jd_array, np.ndarray):
        lst_array = np.zeros_like(jd_array)
    else:
        lst_array = np.zeros(1)

    jd, reverse_inds = np.unique(jd_array, return_inverse=True)

    if frame.upper() == "MCMF":
        if not hasmoon:
            raise ValueError(
                "Need to install `lunarsky` package to work with MCMF frame."
            )
        TimeClass = LTime
        loc = MoonLocation.from_selenodetic(
            Angle(longitude, unit="deg"), Angle(latitude, unit="deg"), altitude
        )
        if not astrometry_library == "astropy":
            raise NotImplementedError(
                "The MCMF frame is only supported with the 'astropy' astrometry "
                "library"
            )
    else:
        TimeClass = Time  # astropy.time.Time
        loc = (Angle(longitude, unit="deg"), Angle(latitude, unit="deg"), altitude)

    times = TimeClass(jd, format="jd", scale="utc", location=loc)

    if iers.conf.auto_max_age is None:  # pragma: no cover
        delta, status = times.get_delta_ut1_utc(return_status=True)
        if np.any(
            np.isin(status, (iers.TIME_BEFORE_IERS_RANGE, iers.TIME_BEYOND_IERS_RANGE))
        ):
            warnings.warn(
                "time is out of IERS range, setting delta ut1 utc to "
                "extrapolated value"
            )
            times.delta_ut1_utc = delta
    if astrometry_library == "erfa":
        # This appears to be what astropy is using under the hood,
        # so it _should_ be totally consistent.
        gast_array = erfa.gst06a(times.ut1.jd, 0.0, times.tt.jd, 0.0)
        lst_array = np.mod(gast_array + (longitude * (np.pi / 180.0)), 2.0 * np.pi)[
            reverse_inds
        ]
    elif astrometry_library == "astropy":
        lst_array = times.sidereal_time("apparent").radian[reverse_inds]
    elif astrometry_library == "novas":
        # Import the NOVAS library only if it's needed/available.
        try:
            import novas_de405  # noqa
            from novas import compat as novas
            from novas.compat import eph_manager
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "novas and/or novas_de405 are not installed but is required for "
                "NOVAS functionality"
            ) from e

        jd_start, jd_end, number = eph_manager.ephem_open()

        tt_time_array = times.tt.value
        ut1_time_array = times.ut1.value
        polar_motion_data = iers.earth_orientation_table.get()

        delta_x_array = np.interp(
            times.mjd,
            polar_motion_data["MJD"].value,
            polar_motion_data["dX_2000A_B"].value,
            left=0.0,
            right=0.0,
        )

        delta_y_array = np.interp(
            times.mjd,
            polar_motion_data["MJD"].value,
            polar_motion_data["dY_2000A_B"].value,
            left=0.0,
            right=0.0,
        )

        # Catch the case where we don't have CIP delta values yet (they don't typically
        # have predictive values like the polar motion does)
        delta_x_array[np.isnan(delta_x_array)] = 0.0
        delta_y_array[np.isnan(delta_y_array)] = 0.0

        for idx in range(len(times)):
            novas.cel_pole(
                tt_time_array[idx], 2, delta_x_array[idx], delta_y_array[idx]
            )
            # The NOVAS routine will return Greenwich Apparent Sidereal Time (GAST),
            # in units of hours
            lst_array[reverse_inds == idx] = novas.sidereal_time(
                ut1_time_array[idx],
                0.0,
                (tt_time_array[idx] - ut1_time_array[idx]) * 86400.0,
            )

        # Add the telescope lon to convert from GAST to LAST (local)
        lst_array = np.mod(lst_array + (longitude / 15.0), 24.0)

        # Convert from hours back to rad
        lst_array *= np.pi / 12.0

    return lst_array


def _adj_list(vecs, tol, n_blocks=None):
    """Identify neighbors of each vec in vecs, to distance tol."""
    n_items = len(vecs)
    max_items = 2**10  # Max array size used is max_items**2. Avoid using > 1 GiB

    if n_blocks is None:
        n_blocks = max(n_items // max_items, 1)

    # We may sort blocks so that some pairs of blocks may be skipped.
    # Reorder vectors by x.

    order = np.argsort(vecs[:, 0])
    blocks = np.array_split(order, n_blocks)
    adj = [{k} for k in range(n_items)]  # Adjacency lists
    for b1 in blocks:
        for b2 in blocks:
            v1, v2 = vecs[b1], vecs[b2]
            # Check for no overlap, with tolerance.
            xmin1 = v1[0, 0] - tol
            xmax1 = v1[-1, 0] + tol
            xmin2 = v2[0, 0] - tol
            xmax2 = v2[-1, 0] + tol
            if max(xmin1, xmin2) > min(xmax1, xmax2):
                continue

            adj_mat = cdist(vecs[b1], vecs[b2]) < tol
            for bi, col in enumerate(adj_mat):
                adj[b1[bi]] = adj[b1[bi]].union(b2[col])
    return [frozenset(g) for g in adj]


def _find_cliques(adj, strict=False):
    n_items = len(adj)

    loc_gps = []
    visited = np.zeros(n_items, dtype=bool)
    for k in range(n_items):
        if visited[k]:
            continue
        a0 = adj[k]
        visited[k] = True
        if all(adj[it].__hash__() == a0.__hash__() for it in a0):
            group = list(a0)
            group.sort()
            visited[list(a0)] = True
            loc_gps.append(group)

    # Require all adjacency lists to be isolated maximal cliques:
    if strict:
        if not all(sorted(st) in loc_gps for st in adj):
            raise ValueError("Non-isolated cliques found in graph.")

    return loc_gps


def find_clusters(location_ids, location_vectors, tol, strict=False):
    """
    Find clusters of vectors (e.g. redundant baselines, times).

    Parameters
    ----------
    location_ids : array_like of int
        ID labels for locations.
    location_vectors : array_like of float
        location vectors, can be multidimensional
    tol : float
        tolerance for clusters
    strict : bool
        Require that all adjacency lists be isolated maximal cliques.
        This ensures that vectors do not fall into multiple clusters.
        Default: False

    Returns
    -------
    list of list of location_ids

    """
    location_vectors = np.asarray(location_vectors)
    location_ids = np.asarray(location_ids)
    if location_vectors.ndim == 1:
        location_vectors = location_vectors[:, np.newaxis]

    adj = _adj_list(location_vectors, tol)  # adj = list of sets

    loc_gps = _find_cliques(adj, strict=strict)
    loc_gps = [np.sort(location_ids[gp]).tolist() for gp in loc_gps]
    return loc_gps


def get_baseline_redundancies(
    baselines, baseline_vecs, tol=1.0, include_conjugates=False, with_conjugates=False
):
    """
    Find redundant baseline groups.

    Parameters
    ----------
    baselines : array_like of int
        Baseline numbers, shape (Nbls,)
    baseline_vecs : array_like of float
        Baseline vectors in meters, shape (Nbls, 3)
    tol : float
        Absolute tolerance of redundancy, in meters.
    include_conjugates : bool
        Option to include baselines that are redundant when flipped.
    with_conjugates : bool
        Deprecated, use `include_conjugates` instead. Option to include baselines that
        are redundant when flipped.

    Returns
    -------
    baseline_groups : list of lists of int
        list of lists of redundant baseline numbers
    vec_bin_centers : list of array_like of float
        List of vectors describing redundant group centers
    lengths : list of float
        List of redundant group baseline lengths in meters
    baseline_ind_conj : list of int
        List of baselines that are redundant when reversed. Only returned if
        include_conjugates is True

    """
    Nbls = baselines.shape[0]

    if not baseline_vecs.shape == (Nbls, 3):
        raise ValueError("Baseline vectors must be shape (Nbls, 3)")

    baseline_vecs = copy.copy(baseline_vecs)  # Protect the vectors passed in.

    if with_conjugates:
        warnings.warn(
            "The with_conjugates keyword is deprecated and will be removed in "
            "version 2.4. Use include_conjugates instead.",
            DeprecationWarning,
        )
        include_conjugates = True

    if include_conjugates:
        conjugates = []
        for bv in baseline_vecs:
            uneg = bv[0] < -tol
            uzer = np.isclose(bv[0], 0.0, atol=tol)
            vneg = bv[1] < -tol
            vzer = np.isclose(bv[1], 0.0, atol=tol)
            wneg = bv[2] < -tol
            conjugates.append(uneg or (uzer and vneg) or (uzer and vzer and wneg))

        conjugates = np.array(conjugates, dtype=bool)
        baseline_vecs[conjugates] *= -1
        baseline_ind_conj = baselines[conjugates]
        bl_gps, vec_bin_centers, lens = get_baseline_redundancies(
            baselines, baseline_vecs, tol=tol, include_conjugates=False
        )
        return bl_gps, vec_bin_centers, lens, baseline_ind_conj

    try:
        bl_gps = find_clusters(baselines, baseline_vecs, tol, strict=True)
    except ValueError as exc:
        raise ValueError(
            "Some baselines are falling into multiple"
            " redundant groups. Lower the tolerance to resolve ambiguity."
        ) from exc

    n_unique = len(bl_gps)
    vec_bin_centers = np.zeros((n_unique, 3))
    for gi, gp in enumerate(bl_gps):
        inds = [np.where(i == baselines)[0] for i in gp]
        vec_bin_centers[gi] = np.mean(baseline_vecs[inds, :], axis=0)

    lens = np.sqrt(np.sum(vec_bin_centers**2, axis=1))
    return bl_gps, vec_bin_centers, lens


def get_antenna_redundancies(
    antenna_numbers, antenna_positions, tol=1.0, include_autos=False
):
    """
    Find redundant baseline groups based on antenna positions.

    Parameters
    ----------
    antenna_numbers : array_like of int
        Antenna numbers, shape (Nants,).
    antenna_positions : array_like of float
        Antenna position vectors in the ENU (topocentric) frame in meters,
        shape (Nants, 3).
    tol : float
        Redundancy tolerance in meters.
    include_autos : bool
        Option to include autocorrelations.

    Returns
    -------
    baseline_groups : list of lists of int
        list of lists of redundant baseline numbers
    vec_bin_centers : list of array_like of float
        List of vectors describing redundant group centers
    lengths : list of float
        List of redundant group baseline lengths in meters

    Notes
    -----
    The baseline numbers refer to antenna pairs (a1, a2) such that
    the baseline vector formed from ENU antenna positions,
    blvec = enu[a1] - enu[a2]
    is close to the other baselines in the group.

    This is achieved by putting baselines in a form of the u>0
    convention, but with a tolerance in defining the signs of
    vector components.

    To guarantee that the same baseline numbers are present in a UVData
    object, ``UVData.conjugate_bls('u>0', uvw_tol=tol)``, where `tol` is
    the tolerance used here.

    """
    Nants = antenna_numbers.size

    bls = []
    bl_vecs = []

    for aj in range(Nants):
        mini = aj + 1
        if include_autos:
            mini = aj
        for ai in range(mini, Nants):
            anti, antj = antenna_numbers[ai], antenna_numbers[aj]
            bidx = antnums_to_baseline(antj, anti, Nants)
            bv = antenna_positions[ai] - antenna_positions[aj]
            bl_vecs.append(bv)
            bls.append(bidx)
    bls = np.array(bls)
    bl_vecs = np.array(bl_vecs)
    gps, vecs, lens, conjs = get_baseline_redundancies(
        bls, bl_vecs, tol=tol, include_conjugates=True
    )
    # Flip the baselines in the groups.
    for gi, gp in enumerate(gps):
        for bi, bl in enumerate(gp):
            if bl in conjs:
                gps[gi][bi] = baseline_index_flip(bl, Nants)

    return gps, vecs, lens


def mean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging data.

    This is similar to np.average, except it handles infs (by giving them
    zero weight) and zero weight axes (by forcing result to be inf with zero
    output weight).

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        Whether to return the sum of the square of the weights. Default is False.

    """
    arr = copy.deepcopy(arr)  # avoid changing outside
    if weights is None:
        weights = np.ones_like(arr)
    else:
        weights = copy.deepcopy(weights)
    weights = weights * np.logical_not(np.isinf(arr))
    arr[np.isinf(arr)] = 0
    weight_out = np.sum(weights, axis=axis)
    if return_weights_square:
        weights_square = weights**2
        weights_square_out = np.sum(weights_square, axis=axis)
    out = np.sum(weights * arr, axis=axis)
    where = weight_out > 1e-10
    out = np.true_divide(out, weight_out, where=where)
    out = np.where(where, out, np.inf)
    if return_weights and return_weights_square:
        return out, weight_out, weights_square_out
    elif return_weights:
        return out, weight_out
    elif return_weights_square:
        return out, weights_square_out
    else:
        return out


def absmean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging absolute value of data.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        whether to return the sum of the squares of the weights. Default is False.

    """
    return mean_collapse(
        np.abs(arr),
        weights=weights,
        axis=axis,
        return_weights=return_weights,
        return_weights_square=return_weights_square,
    )


def quadmean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging in quadrature.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        whether to return the sum of the squares of the weights. Default is False.

    """
    out = mean_collapse(
        np.abs(arr) ** 2,
        weights=weights,
        axis=axis,
        return_weights=return_weights,
        return_weights_square=return_weights_square,
    )
    if return_weights and return_weights_square:
        return np.sqrt(out[0]), out[1], out[2]
    elif return_weights or return_weights_square:
        return np.sqrt(out[0]), out[1]
    else:
        return np.sqrt(out)


def or_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse using OR operation.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        NOT USED, but kept for symmetry with other collapsing functions.
    axis : int or tuple, optional
        Axis or axes to collapse (take OR over). Default is all.
    return_weights : bool
        Whether to return dummy weights array.
        NOTE: the dummy weights will simply be an array of ones
    return_weights_square: bool
        NOT USED, but kept for symmetry with other collapsing functions.

    """
    if arr.dtype != np.bool_:
        raise ValueError("Input to or_collapse function must be boolean array")
    out = np.any(arr, axis=axis)
    if (weights is not None) and not np.all(weights == weights.reshape(-1)[0]):
        warnings.warn("Currently weights are not handled when OR-ing boolean arrays.")
    if return_weights:
        return out, np.ones_like(out, dtype=np.float64)
    else:
        return out


def and_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse using AND operation.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        NOT USED, but kept for symmetry with other collapsing functions.
    axis : int or tuple, optional
        Axis or axes to collapse (take AND over). Default is all.
    return_weights : bool
        Whether to return dummy weights array.
        NOTE: the dummy weights will simply be an array of ones
    return_weights_square: bool
        NOT USED, but kept for symmetry with other collapsing functions.

    """
    if arr.dtype != np.bool_:
        raise ValueError("Input to and_collapse function must be boolean array")
    out = np.all(arr, axis=axis)
    if (weights is not None) and not np.all(weights == weights.reshape(-1)[0]):
        warnings.warn("Currently weights are not handled when AND-ing boolean arrays.")
    if return_weights:
        return out, np.ones_like(out, dtype=np.float64)
    else:
        return out


def collapse(
    arr, alg, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Parent function to collapse an array with a given algorithm.

    Parameters
    ----------
    arr : array
        Input array to process.
    alg : str
        Algorithm to use. Must be defined in this function with
        corresponding subfunction above.
    weights: ndarray, optional
        weights for collapse operation (e.g. weighted mean).
        NOTE: Some subfunctions do not use the weights. See corresponding
        doc strings.
    axis : int or tuple, optional
        Axis or axes to collapse. Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        Whether to return the sum of the squares of the weights. Default is False.

    """
    collapse_dict = {
        "mean": mean_collapse,
        "absmean": absmean_collapse,
        "quadmean": quadmean_collapse,
        "or": or_collapse,
        "and": and_collapse,
    }
    try:
        out = collapse_dict[alg](
            arr,
            weights=weights,
            axis=axis,
            return_weights=return_weights,
            return_weights_square=return_weights_square,
        )
    except KeyError as err:
        raise ValueError(
            "Collapse algorithm must be one of: "
            + ", ".join(collapse_dict.keys())
            + "."
        ) from err
    return out


def uvcalibrate(
    uvdata,
    uvcal,
    inplace=True,
    prop_flags=True,
    Dterm_cal=False,
    flip_gain_conj=False,
    delay_convention="minus",
    undo=False,
    time_check=True,
    ant_check=True,
):
    """
    Calibrate a UVData object with a UVCal object.

    Parameters
    ----------
    uvdata : UVData object
        UVData object to calibrate.
    uvcal : UVCal object
        UVCal object containing the calibration.
    inplace : bool, optional
        if True edit uvdata in place, else return a calibrated copy
    prop_flags : bool, optional
        if True, propagate calibration flags to data flags
        and doesn't use flagged gains. Otherwise, uses flagged gains and
        does not propagate calibration flags to data flags.
    Dterm_cal : bool, optional
        Calibrate the off-diagonal terms in the Jones matrix if present
        in uvcal. Default is False. Currently not implemented.
    flip_gain_conj : bool, optional
        This function uses the UVData ant_1_array and ant_2_array to specify the
        antennas in the UVCal object. By default, the conjugation convention, which
        follows the UVData convention (i.e. ant2 - ant1), is that the applied
        gain = ant1_gain * conjugate(ant2_gain). If the other convention is required,
        set flip_gain_conj=True.
    delay_convention : str, optional
        Exponent sign to use in conversion of 'delay' to 'gain' cal_type
        if the input uvcal is not inherently 'gain' cal_type. Default to 'minus'.
    undo : bool, optional
        If True, undo the provided calibration. i.e. apply the calibration with
        flipped gain_convention. Flag propagation rules apply the same.
    time_check : bool
        Option to check that times match between the UVCal and UVData
        objects if UVCal has a single time or time range. Times are always
        checked if UVCal has multiple times.
    ant_check : bool
        Option to check that all antennas with data on the UVData
        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.

    Returns
    -------
    UVData, optional
        Returns if not inplace

    """
    if uvcal.cal_type == "gain" and uvcal.wide_band:
        raise ValueError(
            "uvcalibrate currently does not support wide-band calibrations"
        )
    if uvcal.cal_type == "delay" and uvcal.Nspws > 1:
        # To fix this, need to make UVCal.convert_to_gain support multiple spws
        raise ValueError(
            "uvcalibrate currently does not support multi spectral window delay "
            "calibrations"
        )

    if not inplace:
        uvdata = uvdata.copy()

    # check both objects
    uvdata.check()
    uvcal.check()

    # Check whether the UVData antennas *that have data associated with them*
    # have associated data in the UVCal object
    uvdata_unique_nums = np.unique(np.append(uvdata.ant_1_array, uvdata.ant_2_array))
    uvdata.antenna_names = np.asarray(uvdata.antenna_names)
    uvdata_used_antnames = np.array(
        [
            uvdata.antenna_names[np.where(uvdata.antenna_numbers == antnum)][0]
            for antnum in uvdata_unique_nums
        ]
    )
    uvcal_unique_nums = np.unique(uvcal.ant_array)
    uvcal.antenna_names = np.asarray(uvcal.antenna_names)
    uvcal_used_antnames = np.array(
        [
            uvcal.antenna_names[np.where(uvcal.antenna_numbers == antnum)][0]
            for antnum in uvcal_unique_nums
        ]
    )

    ant_arr_match = uvcal_used_antnames.tolist() == uvdata_used_antnames.tolist()

    if not ant_arr_match:
        # check more carefully
        name_missing = []
        for this_ant_name in uvdata_used_antnames:
            wh_ant_match = np.nonzero(uvcal_used_antnames == this_ant_name)
            if wh_ant_match[0].size == 0:
                name_missing.append(this_ant_name)

        if len(name_missing) > 0:
            if len(name_missing) == uvdata_used_antnames.size:
                # all antenna_names with data on UVData are missing on UVCal.
                if not ant_check:
                    warnings.warn(
                        "All antenna names with data on UVData are missing "
                        "on UVCal. Since ant_check is False, calibration will "
                        "proceed but all data will be flagged."
                    )
                else:
                    raise ValueError(
                        "All antenna names with data on UVData are missing "
                        "on UVCal. To continue with calibration "
                        "(and flag all the data), set ant_check=False."
                    )
            else:
                # Only some antenna_names with data on UVData are missing on UVCal
                if not ant_check:
                    warnings.warn(
                        f"Antennas {name_missing} have data on UVData but are missing "
                        "on UVCal. Since ant_check is False, calibration will "
                        "proceed and the data for these antennas will be flagged."
                    )
                else:
                    raise ValueError(
                        f"Antennas {name_missing} have data on UVData but "
                        "are missing on UVCal. To continue calibration and "
                        "flag the data from missing antennas, set ant_check=False."
                    )

    uvdata_times = np.unique(uvdata.time_array)
    downselect_cal_times = False
    if uvcal.Ntimes > 1:
        if uvcal.Ntimes < uvdata.Ntimes:
            raise ValueError(
                "The uvcal object has more than one time but fewer than the "
                "number of unique times on the uvdata object."
            )
        uvcal_times = np.unique(uvcal.time_array)
        try:
            time_arr_match = np.allclose(
                uvcal_times,
                uvdata_times,
                atol=uvdata._time_array.tols[1],
                rtol=uvdata._time_array.tols[0],
            )
        except ValueError:
            time_arr_match = False

        if not time_arr_match:
            # check more carefully
            uvcal_times_to_keep = []
            for this_time in uvdata_times:
                wh_time_match = np.nonzero(
                    np.isclose(
                        uvcal.time_array - this_time,
                        0,
                        atol=uvdata._time_array.tols[1],
                        rtol=uvdata._time_array.tols[0],
                    )
                )
                if wh_time_match[0].size > 0:
                    uvcal_times_to_keep.append(uvcal.time_array[wh_time_match][0])
                else:
                    raise ValueError(
                        f"Time {this_time} exists on UVData but not on UVCal."
                    )
            if len(uvcal_times_to_keep) < uvcal.Ntimes:
                downselect_cal_times = True

    elif uvcal.time_range is None:
        # only one UVCal time, no time_range.
        # This cannot match if UVData.Ntimes > 1.
        # If they are both NTimes = 1, then check if they're close.
        if uvdata.Ntimes > 1 or not np.isclose(
            uvdata_times,
            uvcal.time_array,
            atol=uvdata._time_array.tols[1],
            rtol=uvdata._time_array.tols[0],
        ):
            if not time_check:
                warnings.warn(
                    "Times do not match between UVData and UVCal "
                    "but time_check is False, so calibration "
                    "will be applied anyway."
                )
            else:
                raise ValueError(
                    "Times do not match between UVData and UVCal. "
                    "Set time_check=False to apply calibration anyway."
                )
    else:
        # time_array is length 1 and time_range exists: check uvdata_times in time_range
        if (
            np.min(uvdata_times) < uvcal.time_range[0]
            or np.max(uvdata_times) > uvcal.time_range[1]
        ):
            if not time_check:
                warnings.warn(
                    "Times do not match between UVData and UVCal "
                    "but time_check is False, so calibration "
                    "will be applied anyway."
                )
            else:
                raise ValueError(
                    "Times do not match between UVData and UVCal. "
                    "Set time_check=False to apply calibration anyway. "
                )

    downselect_cal_freq = False
    if uvcal.freq_array is not None:
        if uvdata.future_array_shapes:
            uvdata_freq_arr_use = uvdata.freq_array
        else:
            uvdata_freq_arr_use = uvdata.freq_array[0, :]
        if uvcal.future_array_shapes:
            uvcal_freq_arr_use = uvcal.freq_array
        else:
            uvcal_freq_arr_use = uvcal.freq_array[0, :]
        try:
            freq_arr_match = np.allclose(
                np.sort(uvcal_freq_arr_use),
                np.sort(uvdata_freq_arr_use),
                atol=uvdata._freq_array.tols[1],
                rtol=uvdata._freq_array.tols[0],
            )
        except ValueError:
            freq_arr_match = False

        if freq_arr_match is False:
            # check more carefully
            uvcal_freqs_to_keep = []
            for this_freq in uvdata_freq_arr_use:
                wh_freq_match = np.nonzero(
                    np.isclose(
                        uvcal.freq_array - this_freq,
                        0,
                        atol=uvdata._freq_array.tols[1],
                        rtol=uvdata._freq_array.tols[0],
                    )
                )
                if wh_freq_match[0].size > 0:
                    uvcal_freqs_to_keep.append(uvcal.freq_array[wh_freq_match][0])
                else:
                    raise ValueError(
                        f"Frequency {this_freq} exists on UVData but not on UVCal."
                    )
            if len(uvcal_freqs_to_keep) < uvcal.Nfreqs:
                downselect_cal_freq = True

    # check if uvdata.x_orientation isn't set (it's required for uvcal)
    uvd_x = uvdata.x_orientation
    if uvd_x is None:
        # use the uvcal x_orientation throughout
        uvd_x = uvcal.x_orientation
        warnings.warn(
            "UVData object does not have `x_orientation` specified but UVCal does. "
            "Matching based on `x` and `y` only "
        )

    uvdata_pol_strs = polnum2str(uvdata.polarization_array, x_orientation=uvd_x)
    uvcal_pol_strs = jnum2str(uvcal.jones_array, x_orientation=uvcal.x_orientation)
    uvdata_feed_pols = {
        feed for pol in uvdata_pol_strs for feed in POL_TO_FEED_DICT[pol]
    }
    for feed in uvdata_feed_pols:
        # get diagonal jones str
        jones_str = parse_jpolstr(feed, x_orientation=uvcal.x_orientation)
        if jones_str not in uvcal_pol_strs:
            raise ValueError(
                f"Feed polarization {feed} exists on UVData but not on UVCal. "
            )

    # downselect UVCal times, frequencies
    if downselect_cal_freq or downselect_cal_times:
        if not downselect_cal_times:
            uvcal_times_to_keep = None
        elif not downselect_cal_freq:
            uvcal_freqs_to_keep = None

        uvcal_use = uvcal.select(
            times=uvcal_times_to_keep, frequencies=uvcal_freqs_to_keep, inplace=False
        )

        new_uvcal = True
    else:
        uvcal_use = uvcal
        new_uvcal = False

    # input checks
    if uvcal_use.cal_type == "delay":
        if not new_uvcal:
            # make a copy to convert to gain
            uvcal_use = uvcal_use.copy()
            new_uvcal = True
        if uvdata.future_array_shapes:
            freq_array_use = uvdata.freq_array
        else:
            freq_array_use = uvdata.freq_array[0, :]
        if uvcal.future_array_shapes == uvdata.future_array_shapes:
            channel_width = uvdata.channel_width
        elif uvcal.future_array_shapes:
            channel_width = np.zeros(uvdata.Nfreqs, dtype=float) + uvdata.channel_width
        else:
            channel_width = uvdata.channel_width[0]
        uvcal_use.convert_to_gain(
            delay_convention=delay_convention,
            freq_array=freq_array_use,
            channel_width=channel_width,
        )

    # D-term calibration
    if Dterm_cal:
        # check for D-terms
        if -7 not in uvcal_use.jones_array and -8 not in uvcal_use.jones_array:
            raise ValueError(
                "Cannot apply D-term calibration without -7 or -8"
                "Jones polarization in uvcal object."
            )
        raise NotImplementedError("D-term calibration is not yet implemented.")

    # No D-term calibration
    else:
        # key is number, value is name
        uvdata_ant_dict = dict(zip(uvdata.antenna_numbers, uvdata.antenna_names))
        # opposite: key is name, value is number
        uvcal_ant_dict = dict(zip(uvcal.antenna_names, uvcal.antenna_numbers))

        # iterate over keys
        for key in uvdata.get_antpairpols():
            # get indices for this key
            blt_inds = uvdata.antpair2ind(key)
            pol_ind = np.argmin(
                np.abs(uvdata.polarization_array - polstr2num(key[2], uvd_x))
            )

            # try to get gains for each antenna
            ant1_num = key[0]
            ant2_num = key[1]

            feed1, feed2 = POL_TO_FEED_DICT[key[2]]
            try:
                uvcal_ant1_num = uvcal_ant_dict[uvdata_ant_dict[ant1_num]]
            except KeyError:
                uvcal_ant1_num = None
            try:
                uvcal_ant2_num = uvcal_ant_dict[uvdata_ant_dict[ant2_num]]
            except KeyError:
                uvcal_ant2_num = None

            uvcal_key1 = (uvcal_ant1_num, feed1)
            uvcal_key2 = (uvcal_ant2_num, feed2)
            if (uvcal_ant1_num is None or uvcal_ant2_num is None) or not (
                uvcal_use._has_key(*uvcal_key1) and uvcal_use._has_key(*uvcal_key2)
            ):
                if uvdata.future_array_shapes:
                    uvdata.flag_array[blt_inds, :, pol_ind] = True
                else:
                    uvdata.flag_array[blt_inds, 0, :, pol_ind] = True
                continue
            if flip_gain_conj:
                gain = (
                    np.conj(uvcal_use.get_gains(uvcal_key1))
                    * uvcal_use.get_gains(uvcal_key2)
                ).T  # tranpose to match uvdata shape
            else:
                gain = (
                    uvcal_use.get_gains(uvcal_key1)
                    * np.conj(uvcal_use.get_gains(uvcal_key2))
                ).T  # tranpose to match uvdata shape
            flag = (uvcal_use.get_flags(uvcal_key1) | uvcal_use.get_flags(uvcal_key2)).T

            # propagate flags
            if prop_flags:
                mask = np.isclose(gain, 0.0) | flag
                gain[mask] = 1.0
                if uvdata.future_array_shapes:
                    uvdata.flag_array[blt_inds, :, pol_ind] += mask
                else:
                    uvdata.flag_array[blt_inds, 0, :, pol_ind] += mask

            # apply to data
            mult_gains = uvcal_use.gain_convention == "multiply"
            if undo:
                mult_gains = not mult_gains
            if uvdata.future_array_shapes:
                if mult_gains:
                    uvdata.data_array[blt_inds, :, pol_ind] *= gain
                else:
                    uvdata.data_array[blt_inds, :, pol_ind] /= gain
            else:
                if mult_gains:
                    uvdata.data_array[blt_inds, 0, :, pol_ind] *= gain
                else:
                    uvdata.data_array[blt_inds, 0, :, pol_ind] /= gain

    # update attributes
    uvdata.history += "\nCalibrated with pyuvdata.utils.uvcalibrate."
    if undo:
        uvdata.vis_units = "uncalib"
    else:
        if uvcal_use.gain_scale is not None:
            uvdata.vis_units = uvcal_use.gain_scale

    if not inplace:
        return uvdata


def apply_uvflag(
    uvd, uvf, inplace=True, unflag_first=False, flag_missing=True, force_pol=True
):
    """
    Apply flags from a UVFlag to a UVData instantiation.

    Note that if uvf.Nfreqs or uvf.Ntimes is 1, it will broadcast flags across
    that axis.

    Parameters
    ----------
    uvd : UVData object
        UVData object to add flags to.
    uvf : UVFlag object
        A UVFlag object in flag mode.
    inplace : bool
        If True overwrite flags in uvd, otherwise return new object
    unflag_first : bool
        If True, completely unflag the UVData before applying flags.
        Else, OR the inherent uvd flags with uvf flags.
    flag_missing : bool
        If input uvf is a baseline type and antpairs in uvd do not exist in uvf,
        flag them in uvd. Otherwise leave them untouched.
    force_pol : bool
        If True, broadcast flags to all polarizations if they do not match.
        Only works if uvf.Npols == 1.

    Returns
    -------
    UVData
        If not inplace, returns new UVData object with flags applied

    """
    # assertions
    if uvf.mode != "flag":
        raise ValueError("UVFlag must be flag mode")

    if not inplace:
        uvd = uvd.copy()

    # make a deepcopy by default b/c it is generally edited inplace downstream
    uvf = uvf.copy()

    # convert to baseline type
    if uvf.type != "baseline":
        # edits inplace
        uvf.to_baseline(uvd, force_pol=force_pol)

    else:
        # make sure polarizations match or force_pol
        uvd_pols, uvf_pols = (
            uvd.polarization_array.tolist(),
            uvf.polarization_array.tolist(),
        )
        if set(uvd_pols) != set(uvf_pols):
            if uvf.Npols == 1 and force_pol:
                # if uvf is 1pol we can make them match: also edits inplace
                uvf.polarization_array = uvd.polarization_array
                uvf.Npols = len(uvf.polarization_array)
                uvf_pols = uvf.polarization_array.tolist()

            else:
                raise ValueError("Input uvf and uvd polarizations do not match")

        # make sure polarization ordering is correct: also edits inplace
        uvf.polarization_array = uvf.polarization_array[
            [uvd_pols.index(pol) for pol in uvf_pols]
        ]

    # check time and freq shapes match: if Ntimes or Nfreqs is 1, allow
    # implicit broadcasting
    if uvf.Ntimes == 1:
        mismatch_times = False
    elif uvf.Ntimes == uvd.Ntimes:
        tdiff = np.unique(uvf.time_array) - np.unique(uvd.time_array)
        mismatch_times = np.any(tdiff > np.max(np.abs(uvf._time_array.tols)))
    else:
        mismatch_times = True
    if mismatch_times:
        raise ValueError("UVFlag and UVData have mismatched time arrays.")

    if uvf.Nfreqs == 1:
        mismatch_freqs = False
    elif uvf.Nfreqs == uvd.Nfreqs:
        fdiff = np.unique(uvf.freq_array) - np.unique(uvd.freq_array)
        mismatch_freqs = np.any(fdiff > np.max(np.abs(uvf._freq_array.tols)))
    else:
        mismatch_freqs = True
    if mismatch_freqs:
        raise ValueError("UVFlag and UVData have mismatched frequency arrays.")

    # unflag if desired
    if unflag_first:
        uvd.flag_array[:] = False

    # iterate over antpairs and apply flags: TODO need to be able to handle
    # conjugated antpairs
    uvf_antpairs = uvf.get_antpairs()
    for ap in uvd.get_antpairs():
        uvd_ap_inds = uvd.antpair2ind(ap)
        if ap not in uvf_antpairs:
            if flag_missing:
                uvd.flag_array[uvd_ap_inds] = True
            continue
        uvf_ap_inds = uvf.antpair2ind(*ap)
        # addition of boolean is OR
        if uvd.future_array_shapes == uvf.future_array_shapes:
            uvd.flag_array[uvd_ap_inds] += uvf.flag_array[uvf_ap_inds]
        elif uvd.future_array_shapes:
            uvd.flag_array[uvd_ap_inds] += uvf.flag_array[uvf_ap_inds, 0, :, :]
        else:
            uvd.flag_array[uvd_ap_inds, 0, :, :] += uvf.flag_array[uvf_ap_inds]

    uvd.history += "\nFlagged with pyuvdata.utils.apply_uvflags."

    if not inplace:
        return uvd


def parse_ants(uv, ant_str, print_toggle=False, x_orientation=None):
    """
    Get antpair and polarization from parsing an aipy-style ant string.

    Used to support the select function. Generates two lists of antenna pair
    tuples and polarization indices based on parsing of the string ant_str.
    If no valid polarizations (pseudo-Stokes params, or combinations of [lr]
    or [xy]) or antenna numbers are found in ant_str, ant_pairs_nums and
    polarizations are returned as None.

    Parameters
    ----------
    uv : UVBase Object
        A UVBased object that supports the following functions and parameters:
        - get_ants
        - get_antpairs
        - get_pols
        These are used to construct the baseline ant_pair_nums
        and polarizations returned.
    ant_str : str
        String containing antenna information to parse. Can be 'all',
        'auto', 'cross', or combinations of antenna numbers and polarization
        indicators 'l' and 'r' or 'x' and 'y'.  Minus signs can also be used
        in front of an antenna number or baseline to exclude it from being
        output in ant_pairs_nums. If ant_str has a minus sign as the first
        character, 'all,' will be appended to the beginning of the string.
        See the tutorial for examples of valid strings and their behavior.
    print_toggle : bool
        Boolean for printing parsed baselines for a visual user check.
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. If input uv object has an `x_orientation`
        parameter and the input to this function is `None`, the value from the
        object will be used. Any input given to this function will override the
        value on the uv object. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    ant_pairs_nums : list of tuples of int or None
        List of tuples containing the parsed pairs of antenna numbers, or
        None if ant_str is 'all' or a pseudo-Stokes polarizations.
    polarizations : list of int or None
        List of desired polarizations or None if ant_str does not contain a
        polarization specification.

    """
    required_attrs = ["get_ants", "get_antpairs", "get_pols"]
    if not all(hasattr(uv, attr) for attr in required_attrs):
        raise ValueError(
            "UVBased objects must have all the following attributes in order "
            f"to call 'parse_ants': {required_attrs}."
        )

    if x_orientation is None and (
        hasattr(uv, "x_orientation") and uv.x_orientation is not None
    ):
        x_orientation = uv.x_orientation

    ant_re = r"(\(((-?\d+[lrxy]?,?)+)\)|-?\d+[lrxy]?)"
    bl_re = "(^(%s_%s|%s),?)" % (ant_re, ant_re, ant_re)
    str_pos = 0
    ant_pairs_nums = []
    polarizations = []
    ants_data = uv.get_ants()
    ant_pairs_data = uv.get_antpairs()
    pols_data = uv.get_pols()
    warned_ants = []
    warned_pols = []

    if ant_str.startswith("-"):
        ant_str = "all," + ant_str

    while str_pos < len(ant_str):
        m = re.search(bl_re, ant_str[str_pos:])
        if m is None:
            if ant_str[str_pos:].upper().startswith("ALL"):
                if len(ant_str[str_pos:].split(",")) > 1:
                    ant_pairs_nums = uv.get_antpairs()
            elif ant_str[str_pos:].upper().startswith("AUTO"):
                for pair in ant_pairs_data:
                    if pair[0] == pair[1] and pair not in ant_pairs_nums:
                        ant_pairs_nums.append(pair)
            elif ant_str[str_pos:].upper().startswith("CROSS"):
                for pair in ant_pairs_data:
                    if not (pair[0] == pair[1] or pair in ant_pairs_nums):
                        ant_pairs_nums.append(pair)
            elif ant_str[str_pos:].upper().startswith("PI"):
                polarizations.append(polstr2num("pI"))
            elif ant_str[str_pos:].upper().startswith("PQ"):
                polarizations.append(polstr2num("pQ"))
            elif ant_str[str_pos:].upper().startswith("PU"):
                polarizations.append(polstr2num("pU"))
            elif ant_str[str_pos:].upper().startswith("PV"):
                polarizations.append(polstr2num("pV"))
            else:
                raise ValueError("Unparsible argument {s}".format(s=ant_str))

            comma_cnt = ant_str[str_pos:].find(",")
            if comma_cnt >= 0:
                str_pos += comma_cnt + 1
            else:
                str_pos = len(ant_str)
        else:
            m = m.groups()
            str_pos += len(m[0])
            if m[2] is None:
                ant_i_list = [m[8]]
                ant_j_list = list(uv.get_ants())
            else:
                if m[3] is None:
                    ant_i_list = [m[2]]
                else:
                    ant_i_list = m[3].split(",")

                if m[6] is None:
                    ant_j_list = [m[5]]
                else:
                    ant_j_list = m[6].split(",")

            for ant_i in ant_i_list:
                include_i = True
                if type(ant_i) == str and ant_i.startswith("-"):
                    ant_i = ant_i[1:]  # nibble the - off the string
                    include_i = False

                for ant_j in ant_j_list:
                    include_j = True
                    if type(ant_j) == str and ant_j.startswith("-"):
                        ant_j = ant_j[1:]
                        include_j = False

                    pols = None
                    ant_i, ant_j = str(ant_i), str(ant_j)
                    if not ant_i.isdigit():
                        ai = re.search(r"(\d+)([x,y,l,r])", ant_i).groups()

                    if not ant_j.isdigit():
                        aj = re.search(r"(\d+)([x,y,l,r])", ant_j).groups()

                    if ant_i.isdigit() and ant_j.isdigit():
                        ai = [ant_i, ""]
                        aj = [ant_j, ""]
                    elif ant_i.isdigit() and not ant_j.isdigit():
                        if "x" in ant_j or "y" in ant_j:
                            pols = ["x" + aj[1], "y" + aj[1]]
                        else:
                            pols = ["l" + aj[1], "r" + aj[1]]
                        ai = [ant_i, ""]
                    elif not ant_i.isdigit() and ant_j.isdigit():
                        if "x" in ant_i or "y" in ant_i:
                            pols = [ai[1] + "x", ai[1] + "y"]
                        else:
                            pols = [ai[1] + "l", ai[1] + "r"]
                        aj = [ant_j, ""]
                    elif not ant_i.isdigit() and not ant_j.isdigit():
                        pols = [ai[1] + aj[1]]

                    ant_tuple = (abs(int(ai[0])), abs(int(aj[0])))

                    # Order tuple according to order in object
                    if ant_tuple in ant_pairs_data:
                        pass
                    elif ant_tuple[::-1] in ant_pairs_data:
                        ant_tuple = ant_tuple[::-1]
                    else:
                        if not (
                            ant_tuple[0] in ants_data or ant_tuple[0] in warned_ants
                        ):
                            warned_ants.append(ant_tuple[0])
                        if not (
                            ant_tuple[1] in ants_data or ant_tuple[1] in warned_ants
                        ):
                            warned_ants.append(ant_tuple[1])
                        if pols is not None:
                            for pol in pols:
                                if not (pol.lower() in pols_data or pol in warned_pols):
                                    warned_pols.append(pol)
                        continue

                    if include_i and include_j:
                        if ant_tuple not in ant_pairs_nums:
                            ant_pairs_nums.append(ant_tuple)
                        if pols is not None:
                            for pol in pols:
                                if (
                                    pol.lower() in pols_data
                                    and polstr2num(pol, x_orientation=x_orientation)
                                    not in polarizations
                                ):
                                    polarizations.append(
                                        polstr2num(pol, x_orientation=x_orientation)
                                    )
                                elif not (
                                    pol.lower() in pols_data or pol in warned_pols
                                ):
                                    warned_pols.append(pol)
                    else:
                        if pols is not None:
                            for pol in pols:
                                if pol.lower() in pols_data:
                                    if uv.Npols == 1 and [pol.lower()] == pols_data:
                                        ant_pairs_nums.remove(ant_tuple)
                                    if (
                                        polstr2num(pol, x_orientation=x_orientation)
                                        in polarizations
                                    ):
                                        polarizations.remove(
                                            polstr2num(pol, x_orientation=x_orientation)
                                        )
                                elif not (
                                    pol.lower() in pols_data or pol in warned_pols
                                ):
                                    warned_pols.append(pol)
                        elif ant_tuple in ant_pairs_nums:
                            ant_pairs_nums.remove(ant_tuple)

    if ant_str.upper() == "ALL":
        ant_pairs_nums = None
    elif len(ant_pairs_nums) == 0:
        if not ant_str.upper() in ["AUTO", "CROSS"]:
            ant_pairs_nums = None

    if len(polarizations) == 0:
        polarizations = None
    else:
        polarizations.sort(reverse=True)

    if print_toggle:
        print("\nParsed antenna pairs:")
        if ant_pairs_nums is not None:
            for pair in ant_pairs_nums:
                print(pair)

        print("\nParsed polarizations:")
        if polarizations is not None:
            for pol in polarizations:
                print(polnum2str(pol, x_orientation=x_orientation))

    if len(warned_ants) > 0:
        warnings.warn(
            "Warning: Antenna number {a} passed, but not present "
            "in the ant_1_array or ant_2_array".format(
                a=(",").join(map(str, warned_ants))
            )
        )

    if len(warned_pols) > 0:
        warnings.warn(
            "Warning: Polarization {p} is not present in "
            "the polarization_array".format(p=(",").join(warned_pols).upper())
        )

    return ant_pairs_nums, polarizations


def _combine_filenames(filename1, filename2):
    """Combine the filename attribute from multiple UVBase objects.

    The 4 cases are:
    1. `filename1` has been set, `filename2` has not
    2. `filename1` has not been set, `filename2` has
    3. `filename1` and `filename2` both have been set
    4. `filename1` and `filename2` both have not been set
    In case (1), we do not want to update the attribute, because it is
    already set correctly. In case (2), we want to replace `filename1`
    with the value from `filename2. In case (3), we want to take the union of
    the sets of the filenames. In case (4), we want the filename attribute
    to still be `None`.

    Parameters
    ----------
    filename1 : list of str or None
        The list of filenames for the first UVBase object. If it is not set, it
        should be `None`.
    filename2 : list of str or None
        The list of filenames for the second UVData object. If it is not set, it
        should be `None`.

    Returns
    -------
    combined_filenames : list of str or None
        The combined list, with potentially duplicate entries removed.
    """
    combined_filenames = filename1
    if filename1 is not None:
        if filename2 is not None:
            combined_filenames = sorted(set(filename1).union(set(filename2)))
    elif filename2 is not None:
        combined_filenames = filename2

    return combined_filenames


def _get_dset_shape(dset, indices):
    """
    Given a 3-tuple of indices, determine the indexed array shape.

    Parameters
    ----------
    dset : numpy array or h5py dataset
        A numpy array or a reference to an HDF5 dataset on disk. Requires the
        `dset.shape` attribute exists and returns a tuple.
    indices : tuple
        A 3-tuple with the indices to extract along each dimension of dset.
        Each element should contain a list of indices, a slice element,
        or a list of slice elements that will be concatenated after slicing.
        For data arrays with 4 dimensions, the second dimension (the old spw axis)
        should not be included because it can only be length one.

    Returns
    -------
    tuple
        a 3- or 4-tuple with the shape of the indexed array
    tuple
        a 3- or 4-tuple with indices used (will be different than input if dset has
        4 dimensions)
    """
    dset_shape = list(dset.shape)
    if len(dset_shape) == 4 and len(indices) == 3:
        indices = (indices[0], np.s_[:], indices[1], indices[2])

    for i, inds in enumerate(indices):
        # check for integer
        if isinstance(inds, (int, np.integer)):
            dset_shape[i] = 1
        # check for slice object
        if isinstance(inds, slice):
            dset_shape[i] = _get_slice_len(inds, dset_shape[i])
        # check for list
        if isinstance(inds, list):
            # check for list of integers
            if isinstance(inds[0], (int, np.integer)):
                dset_shape[i] = len(inds)
            elif isinstance(inds[0], slice):
                dset_shape[i] = sum((_get_slice_len(s, dset_shape[i]) for s in inds))

    return dset_shape, indices


def _convert_to_slices(indices, max_nslice_frac=0.1):
    """
    Convert list of indices to a list of slices.

    Parameters
    ----------
    indices : list
        A 1D list of integers for array indexing.
    max_nslice_frac : float
        A float from 0 -- 1. If the number of slices
        needed to represent input 'indices' divided by len(indices)
        exceeds this fraction, then we determine that we cannot
        easily represent 'indices' with a list of slices.

    Returns
    -------
    list
        list of slice objects used to represent indices
    bool
        If True, indices is easily represented by slices
        (max_nslice_frac condition met), otherwise False

    Notes
    -----
    Example:
        if: indices = [1, 2, 3, 4, 10, 11, 12, 13, 14]
        then: slices = [slice(1, 5, 1), slice(11, 15, 1)]
    """
    # check for integer index
    if isinstance(indices, (int, np.integer)):
        indices = [indices]
    # check for already a slice
    if isinstance(indices, slice):
        return [indices], True
    # assert indices is longer than 2, or return trivial solutions
    if len(indices) == 0:
        return [slice(0, 0, 0)], False
    elif len(indices) == 1:
        return [slice(indices[0], indices[0] + 1, 1)], True
    elif len(indices) == 2:
        return [slice(indices[0], indices[1] + 1, indices[1] - indices[0])], True

    # setup empty slices list
    Ninds = len(indices)
    slices = []

    # iterate over indices
    for i, ind in enumerate(indices):
        if i == 0:
            # start the first slice object
            start = ind
            last_step = indices[i + 1] - ind
            continue

        # calculate step from previous index
        step = ind - indices[i - 1]

        # if step != last_step, this ends the slice
        if step != last_step:
            # append to list
            slices.append(slice(start, indices[i - 1] + 1, last_step))

            # check if this is the last element
            if i == Ninds - 1:
                # append last element
                slices.append(slice(ind, ind + 1, 1))
                continue

            # setup next step
            start = ind
            last_step = indices[i + 1] - ind

        # check if this is the last element
        elif i == Ninds - 1:
            # end slice and append
            slices.append(slice(start, ind + 1, step))

    # determine whether slices are a reasonable representation
    Nslices = len(slices)
    passed = (float(Nslices) / len(indices)) < max_nslice_frac

    return slices, passed


def _get_slice_len(s, axlen):
    """
    Get length of a slice s into array of len axlen.

    Parameters
    ----------
    s : slice object
        Slice object to index with
    axlen : int
        Length of axis s slices into

    Returns
    -------
    int
        Length of slice object
    """
    if s.start is None:
        start = 0
    else:
        start = s.start
    if s.stop is None:
        stop = axlen
    else:
        stop = np.min([s.stop, axlen])
    if s.step is None:
        step = 1
    else:
        step = s.step

    return ((stop - 1 - start) // step) + 1


def _index_dset(dset, indices, input_array=None):
    """
    Index a UVH5 data, flags or nsamples h5py dataset.

    Parameters
    ----------
    dset : h5py dataset
        A reference to an HDF5 dataset on disk.
    indices : tuple
        A 3-tuple with the indices to extract along each dimension of dset.
        Each element should contain a list of indices, a slice element,
        or a list of slice elements that will be concatenated after slicing.
        Indices must be provided such that all dimensions can be indexed
        simultaneously. For data arrays with 4 dimensions, the second dimension
        (the old spw axis) should not be included because it can only be length one.

    Returns
    -------
    ndarray
        The indexed dset

    Notes
    -----
    This makes and fills an empty array with dset indices.
    For trivial indexing, (e.g. a trivial slice), constructing
    a new array and filling it is suboptimal over direct
    indexing, e.g. dset[indices].
    This function specializes in repeated slices over the same axis,
    e.g. if indices is [[slice(0, 5), slice(10, 15), ...], ..., ]
    """
    # get dset and arr shape
    dset_shape = dset.shape
    arr_shape, indices = _get_dset_shape(dset, indices)

    if input_array is None:
        # create empty array of dset dtype
        arr = np.empty(arr_shape, dtype=dset.dtype)
    else:
        arr = input_array

    # get arr and dset indices for each dimension in indices
    dset_indices = []
    arr_indices = []
    for i, dset_inds in enumerate(indices):
        if isinstance(dset_inds, (int, np.integer)):
            # this dimension is len 1, so slice is fine
            arr_indices.append([slice(None)])
            dset_indices.append([[dset_inds]])

        elif isinstance(dset_inds, slice):
            # this dimension is just a slice, so slice is fine
            arr_indices.append([slice(None)])
            dset_indices.append([dset_inds])

        elif isinstance(dset_inds, (list, np.ndarray)):
            if isinstance(dset_inds[0], (int, np.integer)):
                # this is a list of integers, append slice
                arr_indices.append([slice(None)])
                dset_indices.append([dset_inds])
            elif isinstance(dset_inds[0], slice):
                # this is a list of slices, need list of slice lens
                slens = [_get_slice_len(s, dset_shape[i]) for s in dset_inds]
                ssums = [sum(slens[:j]) for j in range(len(slens))]
                arr_inds = [slice(s, s + l) for s, l in zip(ssums, slens)]
                arr_indices.append(arr_inds)
                dset_indices.append(dset_inds)

    if len(dset_shape) == 3:
        freq_dim = 1
        pol_dim = 2
    else:
        freq_dim = 2
        pol_dim = 3

    # iterate over each of the 3 axes and fill the array
    for blt_arr, blt_dset in zip(arr_indices[0], dset_indices[0]):
        for freq_arr, freq_dset in zip(arr_indices[freq_dim], dset_indices[freq_dim]):
            for pol_arr, pol_dset in zip(arr_indices[pol_dim], dset_indices[pol_dim]):
                if input_array is None:
                    # index dset and assign to arr
                    if len(dset_shape) == 3:
                        arr[blt_arr, freq_arr, pol_arr] = dset[
                            blt_dset, freq_dset, pol_dset
                        ]
                    else:
                        arr[blt_arr, :, freq_arr, pol_arr] = dset[
                            blt_dset, :, freq_dset, pol_dset
                        ]
                else:
                    # index arr and assign to dset
                    if len(dset_shape) == 3:
                        dset[blt_dset, freq_dset, pol_dset] = arr[
                            blt_arr, freq_arr, pol_arr
                        ]
                    else:
                        dset[blt_dset, :, freq_dset, pol_dset] = arr[
                            blt_arr, :, freq_arr, pol_arr
                        ]

    if input_array is None:
        return arr
    else:
        return


def determine_blt_order(
    time_array, ant_1_array, ant_2_array, baseline_array, Nbls, Ntimes
) -> tuple[str] | None:
    """Get the blt order from analysing metadata."""
    times = time_array
    ant1 = ant_1_array
    ant2 = ant_2_array
    bls = baseline_array

    time_bl = True
    time_a = True
    time_b = True
    bl_time = True
    a_time = True
    b_time = True
    bl_order = True
    a_order = True
    b_order = True
    time_order = True

    if Nbls == 1 and Ntimes == 1:
        return ("baseline", "time")  # w.l.o.g.

    for i, (t, a, b, bl) in enumerate(
        zip(times[1:], ant1[1:], ant2[1:], bls[1:]), start=1
    ):
        on_bl_boundary = i % Nbls == 0
        on_time_boundary = i % Ntimes == 0

        if t < times[i - 1]:
            time_bl = False
            time_a = False
            time_b = False
            time_order = False

            if not on_time_boundary:
                bl_time = False
                a_time = False
                b_time = False

            if bl == bls[i - 1]:
                bl_time = False
            if a == ant1[i - 1]:
                a_time = False
            if b == ant2[i - 1]:
                b_time = False

        elif t == times[i - 1]:
            if bl < bls[i - 1]:
                time_bl = False
            if a < ant1[i - 1]:
                time_a = False
            if b < ant2[i - 1]:
                time_b = False

        if bl < bls[i - 1]:
            bl_time = False
            bl_order = False
            if not on_bl_boundary:
                time_bl = False
        if a < ant1[i - 1]:
            a_time = False
            a_order = False
            if not on_bl_boundary:
                time_a = False
        if b < ant2[i - 1]:
            b_time = False
            b_order = False
            if not on_bl_boundary:
                time_b = False

        if not any(
            (
                time_bl,
                time_a,
                time_b,
                time_bl,
                bl_time,
                a_time,
                b_time,
                bl_order,
                a_order,
                b_order,
                time_order,
            )
        ):
            break

    if Nbls > 1 and Ntimes > 1:
        assert not (
            (time_bl and bl_time)
            or (time_a and a_time)
            or (time_b and b_time)
            or (time_order and a_order)
            or (time_order and b_order)
            or (a_order and b_order)
            or (time_order and bl_order)
        ), (
            "Something went wrong when trying to determine the order of the blts axis. "
            "Please raise an issue on github, as this is not meant to happen."
            "None of the following should ever be True: \n"
            f"\ttime_bl and bl_time: {time_bl and bl_time}\n"
            f"\ttime_a and a_time: {time_a and a_time}\n"
            f"\ttime_b and b_time: {time_b and b_time}\n"
            f"\ttime_order and a_order: {time_order and a_order}\n"
            f"\ttime_order and b_order: {time_order and b_order}\n"
            f"\ta_order and b_order: {a_order and b_order}\n"
            f"\ttime_order and bl_order: {time_order and bl_order}\n\n"
            "Please include the following information in your issue:\n"
            f"Nbls: {Nbls}\n"
            f"Ntimes: {Ntimes}\n"
            f"TIMES: {times}\n"
            f"ANT1: {ant1}\n"
            f"ANT2: {ant2}\n"
            f"BASELINES: {bls}\n"
        )

    if time_bl:
        return ("time", "baseline")
    if bl_time:
        return ("baseline", "time")
    if time_a:
        return ("time", "ant1")
    if a_time:
        return ("ant1", "time")
    if time_b:
        return ("time", "ant2")
    if b_time:
        return ("ant2", "time")
    if bl_order:
        return ("baseline",)
    if a_order:
        return ("ant1",)
    if b_order:
        return ("ant2",)
    if time_order:
        return ("time",)

    return None


def determine_rectangularity(
    time_array: np.ndarray,
    baseline_array: np.ndarray,
    nbls: int,
    ntimes: int,
    blt_order: str | tuple[str] | None = None,
):
    """Determine if the data is rectangular or not.

    Parameters
    ----------
    time_array : array_like
        Array of times in JD.
    baseline_array : array_like
        Array of baseline integers.
    nbls : int
        Number of baselines.
    ntimes : int
        Number of times.
    blt_order : str or tuple of str, optional
        If known, pass the blt_order, which can short-circuit the determination
        of rectangularity.

    Returns
    -------
    is_rect : bool
        True if the data is rectangular, False otherwise.
    time_axis_faster_than_bls : bool
        True if the data is rectangular and the time axis is the last axis (i.e. times
        change first, then bls). False either if baselines change first, OR if it is
        not rectangular.

    Notes
    -----
    Rectangular data is defined as data for which using regular slicing of size Ntimes
    or Nbls will give you either all the same time and all different baselines, or
    vice versa. This does NOT require that the baselines and times are sorted within
    that structure.
    """
    # check if the data is rectangular
    time_first = True
    bl_first = True

    if time_array.size != nbls * ntimes:
        return False, False
    elif nbls * ntimes == 1:
        return True, True
    elif nbls == 1:
        return True, True
    elif ntimes == 1:
        return True, False
    elif blt_order == ("baseline", "time"):
        return True, True
    elif blt_order == ("time", "baseline"):
        return True, False

    # That's all the easiest checks.
    if time_array[1] == time_array[0]:
        time_first = False
    if baseline_array[1] == baseline_array[0]:
        bl_first = False
    if not time_first and not bl_first:
        return False, False

    if time_first:
        time_array = time_array.reshape((nbls, ntimes))
        baseline_array = baseline_array.reshape((nbls, ntimes))
        if np.sum(np.abs(np.diff(time_array, axis=0))) != 0:
            return False, False
        if np.sum(np.abs(np.diff(baseline_array, axis=1))) != 0:
            return False, False
        return True, True
    elif bl_first:
        time_array = time_array.reshape((ntimes, nbls))
        baseline_array = baseline_array.reshape((ntimes, nbls))
        if np.sum(np.abs(np.diff(time_array, axis=1))) != 0:
            return False, False
        if np.sum(np.abs(np.diff(baseline_array, axis=0))) != 0:
            return False, False
        return True, False


# pyuvdata/uvbase.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""
Base class for objects with UVParameter attributes.

Subclassed by UVData and Telescope.
"""
import copy
import warnings

import numpy as np
from astropy.coordinates import SkyCoord
from astropy.units import Quantity

from . import __version__
from . import parameter as uvp
from .utils import _get_iterable

__all__ = ["UVBase"]


def _warning(msg, *a, **kwargs):
    """
    Improve the printing of user warnings.

    Parameters
    ----------
    msg : str
        Input warning message.
    a
        postional parameters not used by this formatting method.
    kwargs
        named parameters not used by this formatting method.

    Returns
    -------
    str
        Input warning message with new line character appended to improve warning
        formatting.

    """
    return str(msg) + "\n"


class UVBase(object):
    """
    Base class for objects with UVParameter attributes.

    This class is intended to be subclassed and its init method should be
    called in the subclass init after all associated UVParameter attributes are
    defined. The init method of this base class creates properties
    (named using UVParameter.name) from all the UVParameter attributes on the subclass.
    AngleParameter and LocationParameter attributes also have extra convenience
    properties defined:

    AngleParameter:

        UVParameter.name+'_degrees'

    LocationParameter:

        UVParameter.name+'_lat_lon_alt'
        UVParameter.name+'_lat_lon_alt_degrees'
    """

    def _setup_parameters(self):
        """Set up parameter objects to be able to be referenced by their names."""
        # set any UVParameter attributes to be properties
        for p in self:
            this_param = getattr(self, p)
            attr_name = this_param.name
            setattr(
                self.__class__,
                attr_name,
                property(self.prop_fget(p), self.prop_fset(p)),
            )
            if isinstance(this_param, uvp.AngleParameter):
                setattr(
                    self.__class__,
                    attr_name + "_degrees",
                    property(self.degree_prop_fget(p), self.degree_prop_fset(p)),
                )
            elif isinstance(this_param, uvp.LocationParameter):
                setattr(
                    self.__class__,
                    attr_name + "_lat_lon_alt",
                    property(
                        self.lat_lon_alt_prop_fget(p), self.lat_lon_alt_prop_fset(p)
                    ),
                )
                setattr(
                    self.__class__,
                    attr_name + "_lat_lon_alt_degrees",
                    property(
                        self.lat_lon_alt_degrees_prop_fget(p),
                        self.lat_lon_alt_degrees_prop_fset(p),
                    ),
                )
        return

    def __init__(self):
        """Create properties from UVParameter attributes."""
        warnings.formatwarning = _warning

        self._setup_parameters()

        # String to add to history of any files written with this version of pyuvdata
        self.pyuvdata_version_str = (
            f"  Read/written with pyuvdata version: {__version__ }."
        )

    def __setstate__(self, state):
        """
        Set the state of the object from given input state.

        This is useful for pickling.

        Parameters
        ----------
        state
            input state to assign to the __dict__.

        """
        self.__dict__ = state
        self._setup_parameters()

    def prop_fget(self, param_name):
        """
        Getter method for UVParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to get, corresponds the the UVParameter.name.

        Returns
        -------
        fget
            getter method to use for the property definition.

        """

        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.value

        return fget

    def prop_fset(self, param_name):
        """
        Setter method for UVParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to set, corresponds the the UVParameter.name.

        Returns
        -------
        fset
            setter method to use for the property definition.

        """

        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.value = value
            setattr(self, param_name, this_param)

        return fset

    def degree_prop_fget(self, param_name):
        """
        Degree getter method for AngleParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to get, corresponds the the UVParameter.name with "_degrees"
            appended.

        Returns
        -------
        fget
            getter method to use for the property definition.

        """

        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.degrees()

        return fget

    def degree_prop_fset(self, param_name):
        """
        Degree setter method for AngleParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to set, corresponds the the UVParameter.name with "_degrees"
            appended.

        Returns
        -------
        fset
            setter method to use for the property definition.

        """

        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_degrees(value)
            setattr(self, param_name, this_param)

        return fset

    def lat_lon_alt_prop_fget(self, param_name):
        """
        Lat/lon/alt getter method for LocationParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to get, corresponds the the UVParameter.name with
            "_lat_lon_alt" appended.

        Returns
        -------
        fget
            getter method to use for the property definition.

        """

        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.lat_lon_alt()

        return fget

    def lat_lon_alt_prop_fset(self, param_name):
        """
        Lat/lon/alt setter method for LocationParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to set, corresponds the the UVParameter.name with
            "_lat_lon_alt" appended.

        Returns
        -------
        fset
            setter method to use for the property definition.

        """

        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_lat_lon_alt(value)
            setattr(self, param_name, this_param)

        return fset

    def lat_lon_alt_degrees_prop_fget(self, param_name):
        """
        Lat/lon/alt degree getter method for LocationParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to get, corresponds the the UVParameter.name with
            "_lat_lon_alt_degrees" appended.

        Returns
        -------
        fget
            getter method to use for the property definition.

        """

        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.lat_lon_alt_degrees()

        return fget

    def lat_lon_alt_degrees_prop_fset(self, param_name):
        """
        Lat/lon/alt degree setter method for LocationParameter properties.

        Parameters
        ----------
        param_name : str
            Property name to set, corresponds the the UVParameter.name with
            "_lat_lon_alt_degrees" appended.

        Returns
        -------
        fset
            setter method to use for the property definition.

        """

        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_lat_lon_alt_degrees(value)
            setattr(self, param_name, this_param)

        return fset

    def __iter__(self, uvparams_only=True):
        """
        Iterate over all (UVParameter) attributes.

        Parameters
        ----------
        uvparams_only : bool
            Option to only iterate over UVParameter attributes.

        Yields
        ------
        attribute : UVParameter or any type
            Object attributes, exclusively UVParameter objects if uvparams_only is True.

        """
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        param_list = []
        for a in attribute_list:
            if uvparams_only:
                attr = getattr(self, a)
                if isinstance(attr, uvp.UVParameter):
                    param_list.append(a)
            else:
                param_list.append(a)
        for a in param_list:
            yield a

    def required(self):
        """
        Iterate over all required UVParameter attributes.

        Yields
        ------
        UVParameter
            required UVParameters on this object.

        """
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        required_list = []
        for a in attribute_list:
            attr = getattr(self, a)
            if isinstance(attr, uvp.UVParameter):
                if attr.required:
                    required_list.append(a)
        for a in required_list:
            yield a

    def extra(self):
        """
        Iterate over all non-required UVParameter attributes.

        Yields
        ------
        UVParameter
            optional (non-required) UVParameters on this object.

        """
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        extra_list = []
        for a in attribute_list:
            attr = getattr(self, a)
            if isinstance(attr, uvp.UVParameter):
                if not attr.required:
                    extra_list.append(a)
        for a in extra_list:
            yield a

    def __eq__(
        self, other, check_extra=True, allowed_failures=("filename",), silent=False
    ):
        """
        Test if classes match and parameters are equal.

        Parameters
        ----------
        other : class
            Other class instance to check
        check_extra : bool
            Option to specify whether to include all parameters, or just the
            required ones. Default is True.
        allowed_failures : iterable of str, optional
            List or tuple of parameter names that are allowed to fail while
            still passing an overall equality check. These should only include
            optional parameters. By default, the `filename` parameter will be
            ignored. 'blt_order' is also ignored, because currently it is ascertained
            directly, if not provided, by UVH5 files, but not by other file types.
            In any case, if it was to fail, other parameters would fail as well.
        silent : bool
            Option to turn off printing explanations of why equality fails. Useful to
            prevent __ne__ from printing lots of messages.

        Returns
        -------
        bool
            True if the two instances are equivalent.

        """
        if isinstance(other, self.__class__):
            # only check that required parameters are identical
            self_required = []
            other_required = []
            for param in self.required():
                self_required.append(param)
            for param in other.required():
                other_required.append(param)
            if set(self_required) != set(other_required):
                if not silent:
                    print(
                        "Sets of required parameters do not match. "
                        f"Left is {self_required},"
                        f" right is {other_required}."
                    )
                return False

            if check_extra:
                self_extra = []
                other_extra = []
                for param in self.extra():
                    self_extra.append(param)
                for param in other.extra():
                    other_extra.append(param)
                if set(self_extra) != set(other_extra):
                    if not silent:
                        print(
                            "Sets of extra parameters do not match. "
                            f"Left is {self_extra},"
                            f" right is {other_extra}."
                        )
                    return False
                p_check = self_required + self_extra
            else:
                p_check = self_required

            if allowed_failures is not None:
                if isinstance(allowed_failures, str):
                    # convert a single string into a length-1 list
                    allowed_failures = [allowed_failures]
                if isinstance(allowed_failures, tuple):
                    # convert a tuple into a list
                    allowed_failures = list(allowed_failures)

                for i, param in enumerate(allowed_failures):
                    if not param.startswith("_"):
                        param = "_" + param
                        allowed_failures[i] = param
                    if param not in self_required:
                        if param in p_check:
                            p_check.remove(param)

            p_equal = True
            for param in p_check:
                self_param = getattr(self, param)
                other_param = getattr(other, param)
                if self_param.__ne__(other_param, silent=silent):
                    if not silent:
                        print(
                            f"parameter {param} does not match. Left is "
                            f"{self_param.value}, right is {other_param.value}."
                        )
                    p_equal = False

            if allowed_failures is not None:
                for param in allowed_failures:
                    if hasattr(self, param):
                        self_param = getattr(self, param)
                        other_param = getattr(other, param)
                        if self_param.__ne__(other_param, silent=silent):
                            if not silent:
                                print(
                                    f"parameter {param} does not match, but is not "
                                    "required to for equality. Left is "
                                    f"{self_param.value}, right is {other_param.value}."
                                )

            return p_equal
        else:
            if not silent:
                print("Classes do not match")
            return False

    def __ne__(
        self, other, check_extra=True, allowed_failures=("filename",), silent=True
    ):
        """
        Test if classes match and parameters are not equal.

        Parameters
        ----------
        other : class
            Other class instance to check
        check_extra : bool
            Option to specify whether to include all parameters, or just the
            required ones. Default is True.
        allowed_failures : iterable of str, optional
            List or tuple of parameter names that are allowed to fail while
            still passing an overall equality check. These should only include
            optional parameters. By default, the `filename` parameter will be
            ignored.
        silent : bool
            Option to turn off printing explanations of why equality fails. Useful to
            prevent __ne__ from printing lots of messages.

        Returns
        -------
        bool
            True if the two instances are equivalent.

        """
        return not self.__eq__(
            other,
            check_extra=check_extra,
            allowed_failures=allowed_failures,
            silent=silent,
        )

    def check(
        self, check_extra=True, run_check_acceptability=True, ignore_requirements=False
    ):
        """
        Check that required parameters exist and have the correct shapes.

        Optionally, check that the values are acceptable.

        Parameters
        ----------
        check_extra : bool
            If true, check shapes and values on all parameters,
            otherwise only check required parameters.
        run_check_acceptability : bool
            Option to check if values in parameters are acceptable.
        ignore_requirements : bool
            Do not error if a required parameter isn't set.
            This allows the user to run the shape/acceptability checks
            on parameters in a partially-defined UVData object.

        Returns
        -------
        bool
            True if the checks pass.

        Raises
        ------
        ValueError
            If required UVParameter values have not been set or if set UVParameters
            values do not have the expected names, shapes, types or values.

        """
        if check_extra:
            p_check = list(self.required()) + list(self.extra())
        else:
            p_check = list(self.required())

        for p in p_check:
            param = getattr(self, p)
            if p != ("_" + param.name):
                raise ValueError(
                    f"UVParameter {p} does not follow the required naming convention"
                    f"(expected be {'_' + param.name})."
                )

            # Check required parameter exists
            if param.value is None:
                if ignore_requirements:
                    continue
                if param.required is True:
                    raise ValueError(f"Required UVParameter {p} has not been set.")
            else:
                # Check parameter shape
                eshape = param.expected_shape(self)
                # default value of eshape is ()
                if eshape == "str" or (eshape == () and param.expected_type == "str"):
                    # Check that it's a string
                    if not isinstance(param.value, str):
                        raise ValueError(
                            f"UVParameter {p} expected to be string, but is not."
                        )
                else:
                    # Check the shape of the parameter value. Note that np.shape
                    # returns an empty tuple for single numbers.
                    # eshape should do the same.
                    if not np.shape(param.value) == eshape:
                        raise ValueError(
                            "UVParameter {param} is not expected shape. "
                            "Parameter shape is {pshape}, expected shape is "
                            "{eshape}.".format(
                                param=p, pshape=np.shape(param.value), eshape=eshape
                            )
                        )
                    # Handle SkyCoord objects separately
                    if isinstance(param, uvp.SkyCoordParameter):
                        if not issubclass(param.value.__class__, SkyCoord):
                            raise ValueError(
                                f"UVParameter {p} should be a subclass of a "
                                f"SkyCoord object but it is {type(param.value)}."
                            )
                        else:
                            # matches expected type. Don't need to iterate through it.
                            continue  # pragma: no cover

                    # Quantity objects complicate things slightly
                    # Do a separate check with warnings until a quantity based
                    # parameter value is created
                    if isinstance(param.value, Quantity):
                        # check if user put expected type as a type of quantity
                        # not a more generic type of number.
                        if any(
                            issubclass(param_type, Quantity)
                            for param_type in _get_iterable(param.expected_type)
                        ):
                            # Verify the param is an instance
                            # of the specific Quantity type
                            if not isinstance(param.value, param.expected_type):
                                raise ValueError(
                                    f"UVParameter {p} is a Quantity object "
                                    "but not the appropriate type. "
                                    f"Is {type(param.value)} but "
                                    f"expected {param.expected_type}."
                                )
                            else:
                                # matches expected type
                                continue  # pragma: no cover
                        else:
                            # Expected type is not a Quantity subclass
                            # Assuming it is a data type like float, int, etc
                            # continuing with check below
                            warnings.warn(
                                f"Parameter {p} is a Quantity object, "
                                "but the expected type is a precision identifier: "
                                f"{param.expected_type}. "
                                "Testing the precision of the value, but this "
                                "check will fail in a future version."
                            )
                            check_vals = [param.value.item(0).value]
                    elif eshape == ():
                        # Single element
                        check_vals = [param.value]
                    else:
                        if isinstance(param.value, (list, tuple)):
                            # List & tuples needs to be handled differently than array
                            # list values may be different types, so they all
                            # need to be checked
                            check_vals = list(param.value)
                        else:
                            # numpy array
                            # the code below ensures that the check value is the type
                            # given by the dtype
                            check_vals = [param.value.dtype.type(param.value.item(0))]

                    for val in check_vals:
                        if not isinstance(val, param.expected_type):
                            raise ValueError(
                                f"UVParameter {p} is not the appropriate"
                                f" type. Is:  {type(val)}. "
                                f"Should be: {param.expected_type}."
                            )

                if run_check_acceptability:
                    accept, message = param.check_acceptability()
                    if not accept:
                        raise ValueError(
                            f"UVParameter {p} has unacceptable values. {message}"
                        )

        return True

    def copy(self):
        """
        Make and return a copy of the object.

        Returns
        -------
        UVBase
            A deep copy of this object.

        """
        return copy.deepcopy(self)


# pyuvdata/uvcal/__init__.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

"""Init file for UVCal."""
from .uvcal import *  # noqa


# pyuvdata/uvcal/calfits.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Class for reading and writing calibration FITS files."""
import os
import warnings

import numpy as np
from astropy.io import fits

from .. import utils as uvutils
from .uvcal import UVCal, _future_array_shapes_warning

__all__ = ["CALFITS"]


class CALFITS(UVCal):
    """
    Defines a calfits-specific class for reading and writing calfits files.

    This class should not be interacted with directly, instead use the read_calfits
    and write_calfits methods on the UVCal class.

    """

    def write_calfits(
        self,
        filename,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        clobber=False,
    ):
        """
        Write the data to a calfits file.

        Parameters
        ----------
        filename : str
            The calfits file to write to.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters before writing the file.
        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 before writing the file.
        clobber : bool
            Option to overwrite the filename if the file already exists.

        """
        if self.Nspws > 1:
            raise ValueError(
                "The calfits format does not support multiple spectral windows"
            )

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

        # calfits allows for frequency spacing to not equal channel widths as long as
        # the frequencies are evenly spaced, so only raise spacing error
        spacing_error, chanwidth_error = self._check_freq_spacing(raise_errors=False)
        if spacing_error:
            raise ValueError(
                "Frequencies are not evenly spaced or have differing "
                "values of channel widths. The calfits format does not support "
                "unevenly spaced frequencies or varying channel widths."
            )

        # we've already run the check_freq_spacing, so spacings and channel widths are
        # the same to our tolerances
        if self.freq_array is not None:
            if self.future_array_shapes:
                ref_freq = self.freq_array[0]
            else:
                ref_freq = self.freq_array[0, 0]
        else:
            # can only get here if future_shapes is True
            ref_freq = self.freq_range[0, 0]

        if self.Nfreqs > 1:
            if chanwidth_error:
                # this means that the frequencies are evenly spaced but do not
                # match our channel widths. Use some rounding to get a good delta.
                if self.future_array_shapes:
                    freq_arr_use = self.freq_array
                else:
                    freq_arr_use = self.freq_array[0, :]
                rounded_spacing = np.around(
                    np.diff(freq_arr_use),
                    int(np.ceil(np.log10(self._freq_array.tols[1]) * -1)),
                )
                delta_freq_array = rounded_spacing[0]
            else:
                if self.future_array_shapes or self.flex_spw:
                    delta_freq_array = np.median(self.channel_width)
                else:
                    delta_freq_array = self.channel_width
        else:
            if self.channel_width is not None:
                if self.future_array_shapes or self.flex_spw:
                    delta_freq_array = self.channel_width[0]
                else:
                    delta_freq_array = self.channel_width
            else:
                # default to 1 Hz for wide-band cals with Nfreqs=1 and no channel
                # width info
                delta_freq_array = 1.0

        if self.Ntimes > 1:
            if not uvutils._test_array_constant_spacing(self._time_array):
                raise ValueError(
                    "The times are not evenly spaced (probably "
                    "because of a select operation). The calfits format "
                    "does not support unevenly spaced times."
                )
            time_spacing = np.diff(self.time_array)
            if self.future_array_shapes:
                if not uvutils._test_array_constant(self._integration_time):
                    raise ValueError(
                        "The integration times are variable. The calfits format "
                        "does not support variable integration times."
                    )
                median_int_time = np.median(self.integration_time)
                if np.isclose(time_spacing[0], median_int_time / (24.0 * 60.0**2)):
                    time_spacing = median_int_time / (24.0 * 60.0**2)
                else:
                    rounded_spacing = np.around(
                        time_spacing,
                        int(
                            np.ceil(
                                np.log10(self._time_array.tols[1] / self.Ntimes) * -1
                            )
                            + 1
                        ),
                    )
                    time_spacing = rounded_spacing[0]
            else:
                if np.isclose(
                    time_spacing[0], self.integration_time / (24.0 * 60.0**2)
                ):
                    time_spacing = self.integration_time / (24.0 * 60.0**2)
                else:
                    rounded_spacing = np.around(
                        time_spacing,
                        int(
                            np.ceil(
                                np.log10(self._time_array.tols[1] / self.Ntimes) * -1
                            )
                            + 1
                        ),
                    )
                    time_spacing = rounded_spacing[0]
        else:
            if self.future_array_shapes:
                time_spacing = self.integration_time[0] / (24.0 * 60.0**2)
            else:
                time_spacing = self.integration_time / (24.0 * 60.0**2)

        if self.Njones > 1:
            if not uvutils._test_array_constant_spacing(self._jones_array):
                raise ValueError(
                    "The jones values are not evenly spaced."
                    "The calibration fits file format does not"
                    " support unevenly spaced polarizations."
                )
            jones_spacing = self.jones_array[1] - self.jones_array[0]
        else:
            jones_spacing = -1

        prihdr = fits.Header()
        if self.total_quality_array is not None:
            totqualhdr = fits.Header()
            totqualhdr["EXTNAME"] = "TOTQLTY"
        if self.cal_type != "gain":
            sechdr = fits.Header()
            sechdr["EXTNAME"] = "FLAGS"
        # Conforming to fits format
        prihdr["SIMPLE"] = True
        prihdr["TELESCOP"] = self.telescope_name
        prihdr["ARRAYX"] = self.telescope_location[0]
        prihdr["ARRAYY"] = self.telescope_location[1]
        prihdr["ARRAYZ"] = self.telescope_location[2]
        prihdr["LAT"] = self.telescope_location_lat_lon_alt_degrees[0]
        prihdr["LON"] = self.telescope_location_lat_lon_alt_degrees[1]
        prihdr["ALT"] = self.telescope_location_lat_lon_alt[2]
        prihdr["GNCONVEN"] = self.gain_convention
        prihdr["CALTYPE"] = self.cal_type
        prihdr["CALSTYLE"] = self.cal_style
        if self.sky_field is not None:
            prihdr["FIELD"] = self.sky_field
        if self.sky_catalog is not None:
            prihdr["CATALOG"] = self.sky_catalog
        if self.ref_antenna_name is not None:
            prihdr["REFANT"] = self.ref_antenna_name
        if self.Nsources is not None:
            prihdr["NSOURCES"] = self.Nsources
        if self.baseline_range is not None:
            prihdr["BL_RANGE"] = (
                "[" + ", ".join([str(b) for b in self.baseline_range]) + "]"
            )
        if self.diffuse_model is not None:
            prihdr["DIFFUSE"] = self.diffuse_model
        if self.gain_scale is not None:
            prihdr["GNSCALE"] = self.gain_scale
        if self.future_array_shapes:
            if self.Ntimes > 1:
                prihdr["INTTIME"] = median_int_time
            else:
                prihdr["INTTIME"] = self.integration_time[0]
        else:
            prihdr["INTTIME"] = self.integration_time

        if self.future_array_shapes or self.flex_spw:
            if self.Nfreqs > 1:
                prihdr["CHWIDTH"] = np.median(self.channel_width)
            else:
                prihdr["CHWIDTH"] = delta_freq_array
        else:
            prihdr["CHWIDTH"] = self.channel_width

        prihdr["XORIENT"] = self.x_orientation
        if self.future_array_shapes and self.freq_range is not None:
            freq_range_use = self.freq_range[0, :]
        else:
            freq_range_use = self.freq_range
        if self.cal_type == "delay":
            prihdr["FRQRANGE"] = ",".join(map(str, freq_range_use))
        elif self.freq_range is not None:
            prihdr["FRQRANGE"] = ",".join(map(str, freq_range_use))

        if self.time_range is not None:
            prihdr["TMERANGE"] = ",".join(map(str, self.time_range))

        if self.observer:
            prihdr["OBSERVER"] = self.observer
        if self.git_origin_cal:
            prihdr["ORIGCAL"] = self.git_origin_cal
        if self.git_hash_cal:
            prihdr["HASHCAL"] = self.git_hash_cal

        if self.cal_type == "unknown":
            raise ValueError(
                "unknown calibration type. Do not know how to " "store parameters"
            )

        # Define primary header values
        # Arrays have (column-major) dimensions of
        # [Nimages, Njones, Ntimes, Nfreqs, 1, Nantennas]
        # For a "delay"-type calibration, Nfreqs is a shallow axis

        # set the axis for number of arrays
        prihdr["CTYPE1"] = ("Narrays", "Number of image arrays.")
        prihdr["CUNIT1"] = "Integer"
        prihdr["CDELT1"] = 1
        prihdr["CRPIX1"] = 1
        prihdr["CRVAL1"] = 1

        # Jones axis
        prihdr["CTYPE2"] = ("JONES", "Jones matrix array")
        prihdr["CUNIT2"] = ("Integer", "representative integer for polarization.")
        prihdr["CRPIX2"] = 1
        prihdr["CRVAL2"] = self.jones_array[0]  # always start with first jones.
        prihdr["CDELT2"] = jones_spacing

        # time axis
        prihdr["CTYPE3"] = ("TIME", "Time axis.")
        prihdr["CUNIT3"] = ("JD", "Time in julian date format")
        prihdr["CRPIX3"] = 1
        prihdr["CRVAL3"] = self.time_array[0]
        prihdr["CDELT3"] = time_spacing

        # freq axis
        prihdr["CTYPE4"] = ("FREQS", "Frequency.")
        prihdr["CUNIT4"] = "Hz"
        prihdr["CRPIX4"] = 1
        prihdr["CRVAL4"] = ref_freq
        prihdr["CDELT4"] = delta_freq_array

        # spw axis: number of spectral windows
        prihdr["CTYPE5"] = ("IF", "Spectral window number.")
        prihdr["CUNIT5"] = "Integer"
        prihdr["CRPIX5"] = 1
        prihdr["CRVAL5"] = 1
        prihdr["CDELT5"] = 1

        # antenna axis
        prihdr["CTYPE6"] = ("ANTAXIS", "See ANTARR in ANTENNA extension for values.")
        prihdr["CUNIT6"] = "Integer"
        prihdr["CRPIX6"] = 1
        prihdr["CRVAL6"] = 1
        prihdr["CDELT6"] = -1

        # end standard keywords; begin user-defined keywords
        for key, value in self.extra_keywords.items():
            # header keywords have to be 8 characters or less
            if len(str(key)) > 8:
                warnings.warn(
                    "key {key} in extra_keywords is longer than 8 "
                    "characters. It will be truncated to 8 as required "
                    "by the calfits file format.".format(key=key)
                )
            keyword = key[:8].upper()
            if isinstance(value, (dict, list, np.ndarray)):
                raise TypeError(
                    "Extra keyword {keyword} is of {keytype}. "
                    "Only strings and numbers are "
                    "supported in calfits.".format(keyword=key, keytype=type(value))
                )

            if keyword == "COMMENT":
                for line in value.splitlines():
                    prihdr.add_comment(line)
            else:
                prihdr[keyword] = value

        for line in self.history.splitlines():
            prihdr.add_history(line)

        # define data section based on calibration type
        if self.cal_type == "gain":
            calfits_data_shape = (
                self.Nants_data,
                1,
                self.Nfreqs,
                self.Ntimes,
                self.Njones,
                1,
            )
            if self.input_flag_array is not None:
                pridata = np.concatenate(
                    [
                        np.reshape(self.gain_array.real, calfits_data_shape),
                        np.reshape(self.gain_array.imag, calfits_data_shape),
                        np.reshape(self.flag_array, calfits_data_shape),
                        np.reshape(self.input_flag_array, calfits_data_shape),
                        np.reshape(self.quality_array, calfits_data_shape),
                    ],
                    axis=-1,
                )
            else:
                pridata = np.concatenate(
                    [
                        np.reshape(self.gain_array.real, calfits_data_shape),
                        np.reshape(self.gain_array.imag, calfits_data_shape),
                        np.reshape(self.flag_array, calfits_data_shape),
                        np.reshape(self.quality_array, calfits_data_shape),
                    ],
                    axis=-1,
                )

        elif self.cal_type == "delay":
            calfits_data_shape = (self.Nants_data, 1, 1, self.Ntimes, self.Njones, 1)
            pridata = np.concatenate(
                [
                    np.reshape(self.delay_array, calfits_data_shape),
                    np.reshape(self.quality_array, calfits_data_shape),
                ],
                axis=-1,
            )

            # Set headers for the second hdu containing the flags. Only in
            # cal_type=delay
            # Can't put in primary header because frequency axis is shallow there,
            # but not here
            # Header values are the same as the primary header
            sechdr["CTYPE1"] = ("Narrays", "Number of image arrays.")
            sechdr["CUNIT1"] = "Integer"
            sechdr["CRPIX1"] = 1
            sechdr["CRVAL1"] = 1
            sechdr["CDELT1"] = 1

            sechdr["CTYPE2"] = ("JONES", "Jones matrix array")
            sechdr["CUNIT2"] = ("Integer", "representative integer for polarization.")
            sechdr["CRPIX2"] = 1
            sechdr["CRVAL2"] = self.jones_array[0]  # always start with first jones.
            sechdr["CDELT2"] = jones_spacing

            sechdr["CTYPE3"] = ("TIME", "Time axis.")
            sechdr["CUNIT3"] = ("JD", "Time in julian date format")
            sechdr["CRPIX3"] = 1
            sechdr["CRVAL3"] = self.time_array[0]
            sechdr["CDELT3"] = time_spacing

            sechdr["CTYPE4"] = ("FREQS", "Valid frequencies to apply delay.")
            sechdr["CUNIT4"] = "Hz"
            sechdr["CRPIX4"] = 1
            sechdr["CRVAL4"] = ref_freq
            sechdr["CDELT4"] = delta_freq_array

            sechdr["CTYPE5"] = ("IF", "Spectral window number.")
            sechdr["CUNIT5"] = "Integer"
            sechdr["CRPIX5"] = 1
            sechdr["CRVAL5"] = 1
            sechdr["CDELT5"] = 1

            sechdr["CTYPE6"] = (
                "ANTAXIS",
                "See ANTARR in ANTENNA extension for values.",
            )

            # convert from bool to int64; undone on read
            calfits_data_shape = (
                self.Nants_data,
                1,
                self.Nfreqs,
                self.Ntimes,
                self.Njones,
                1,
            )
            if self.future_array_shapes:
                # need to broadcast the flags back to the expected shape
                flag_array_use = np.repeat(self.flag_array, self.Nfreqs, axis=2)
                if self.input_flag_array is not None:
                    input_flag_array_use = np.repeat(
                        self.input_flag_array, self.Nfreqs, axis=2
                    )
            else:
                flag_array_use = self.flag_array
                input_flag_array_use = self.input_flag_array

            if self.input_flag_array is not None:
                secdata = np.concatenate(
                    [
                        np.reshape(flag_array_use.astype(np.int64), calfits_data_shape),
                        np.reshape(
                            input_flag_array_use.astype(np.int64), calfits_data_shape
                        ),
                    ],
                    axis=-1,
                )
            else:
                secdata = np.reshape(
                    flag_array_use.astype(np.int64), calfits_data_shape
                )

        if self.total_quality_array is not None:
            # Set headers for the hdu containing the total_quality_array
            # No antenna axis, so we have [Njones, Ntime, Nfreq, 1]
            totqualhdr["CTYPE1"] = ("JONES", "Jones matrix array")
            totqualhdr["CUNIT1"] = (
                "Integer",
                "representative integer for polarization.",
            )
            totqualhdr["CRPIX1"] = 1
            totqualhdr["CRVAL1"] = self.jones_array[0]  # always start with first jones.
            totqualhdr["CDELT1"] = jones_spacing

            totqualhdr["CTYPE2"] = ("TIME", "Time axis.")
            totqualhdr["CUNIT2"] = ("JD", "Time in julian date format")
            totqualhdr["CRPIX2"] = 1
            totqualhdr["CRVAL2"] = self.time_array[0]
            totqualhdr["CDELT2"] = time_spacing

            totqualhdr["CTYPE3"] = ("FREQS", "Valid frequencies to apply delay.")
            totqualhdr["CUNIT3"] = "Hz"
            totqualhdr["CRPIX3"] = 1
            totqualhdr["CRVAL3"] = ref_freq
            totqualhdr["CDELT3"] = delta_freq_array

            # spws axis: number of spectral windows
            totqualhdr["CTYPE4"] = ("IF", "Spectral window number.")
            totqualhdr["CUNIT4"] = "Integer"
            totqualhdr["CRPIX4"] = 1
            totqualhdr["CRVAL4"] = 1
            totqualhdr["CDELT4"] = 1

            if self.cal_type == "gain":
                calfits_tqa_shape = (1, self.Nfreqs, self.Ntimes, self.Njones)
            else:
                calfits_tqa_shape = (1, 1, self.Ntimes, self.Njones)
            totqualdata = np.reshape(self.total_quality_array, calfits_tqa_shape)

        # make HDUs
        prihdu = fits.PrimaryHDU(data=pridata, header=prihdr)

        # ant HDU
        col1 = fits.Column(name="ANTNAME", format="8A", array=self.antenna_names)
        col2 = fits.Column(name="ANTINDEX", format="D", array=self.antenna_numbers)
        if self.Nants_data == self.Nants_telescope:
            col3 = fits.Column(name="ANTARR", format="D", array=self.ant_array)
        else:
            # ant_array is shorter than the other columns.
            # Pad the extra rows with -1s. Need to undo on read.
            nants_add = self.Nants_telescope - self.Nants_data
            ant_array_use = np.append(
                self.ant_array, np.zeros(nants_add, dtype=np.int64) - 1
            )
            col3 = fits.Column(name="ANTARR", format="D", array=ant_array_use)
        col4 = fits.Column(name="ANTXYZ", format="3D", array=self.antenna_positions)
        cols = fits.ColDefs([col1, col2, col3, col4])
        ant_hdu = fits.BinTableHDU.from_columns(cols)
        ant_hdu.header["EXTNAME"] = "ANTENNAS"

        hdulist = fits.HDUList([prihdu, ant_hdu])

        if self.cal_type != "gain":
            sechdu = fits.ImageHDU(data=secdata, header=sechdr)
            hdulist.append(sechdu)

        if self.total_quality_array is not None:
            totqualhdu = fits.ImageHDU(data=totqualdata, header=totqualhdr)
            hdulist.append(totqualhdu)

        hdulist.writeto(filename, overwrite=clobber)
        hdulist.close()

    def read_calfits(
        self,
        filename,
        read_data=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        use_future_array_shapes=False,
    ):
        """
        Read data from a calfits file.

        Parameters
        ----------
        filename : str
            The calfits file to read from.
        read_data : bool
            Read in the gains or delays, quality arrays and flag arrays.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metadata only object.
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        """
        # update filename attribute
        basename = os.path.basename(filename)
        self.filename = [basename]
        self._filename.form = (1,)

        with fits.open(filename) as fname:
            hdr = fname[0].header.copy()
            hdunames = uvutils._fits_indexhdus(fname)

            anthdu = fname[hdunames["ANTENNAS"]]
            self.Nants_telescope = anthdu.header["NAXIS2"]
            antdata = anthdu.data
            self.antenna_names = np.array(list(map(str, antdata["ANTNAME"])))
            self.antenna_numbers = np.array(list(map(int, antdata["ANTINDEX"])))
            self.ant_array = np.array(list(map(int, antdata["ANTARR"])))
            if np.min(self.ant_array) < 0:
                # ant_array was shorter than the other columns, so it was
                # padded with -1s.
                # Remove the padded entries.
                self.ant_array = self.ant_array[np.where(self.ant_array >= 0)[0]]

            if anthdu.header["TFIELDS"] > 3:
                self.antenna_positions = antdata["ANTXYZ"]

            self.channel_width = hdr.pop("CHWIDTH")
            self.integration_time = hdr.pop("INTTIME")
            self.telescope_name = hdr.pop("TELESCOP")

            x_telescope = hdr.pop("ARRAYX", None)
            y_telescope = hdr.pop("ARRAYY", None)
            z_telescope = hdr.pop("ARRAYZ", None)
            lat = hdr.pop("LAT", None)
            lon = hdr.pop("LON", None)
            alt = hdr.pop("ALT", None)
            if (
                x_telescope is not None
                and y_telescope is not None
                and z_telescope is not None
            ):
                self.telescope_location = np.array(
                    [x_telescope, y_telescope, z_telescope]
                )
            elif lat is not None and lon is not None and alt is not None:
                self.telescope_location_lat_lon_alt_degrees = (lat, lon, alt)
            if self.telescope_location is None or self.antenna_positions is None:
                try:
                    self.set_telescope_params()
                except ValueError as ve:
                    warnings.warn(str(ve))

            self.history = str(hdr.get("HISTORY", ""))

            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                if not self.history.endswith("\n"):
                    self.history += "\n"

                self.history += self.pyuvdata_version_str

            time_range = hdr.pop("TMERANGE", None)
            if time_range is not None:
                self.time_range = np.asarray(
                    list(map(np.float64, time_range.split(",")))
                )
            self.gain_convention = hdr.pop("GNCONVEN")
            self.gain_scale = hdr.pop("GNSCALE", None)
            self.x_orientation = hdr.pop("XORIENT")
            self.cal_type = hdr.pop("CALTYPE")
            if self.cal_type == "delay":
                self.freq_range = list(map(float, hdr.pop("FRQRANGE").split(",")))
            else:
                if "FRQRANGE" in hdr:
                    self.freq_range = list(map(float, hdr.pop("FRQRANGE").split(",")))

            self.cal_style = hdr.pop("CALSTYLE")
            if self.cal_style == "sky":
                self._set_sky()
            elif self.cal_style == "redundant":
                self._set_redundant()

            self.sky_field = hdr.pop("FIELD", None)
            self.sky_catalog = hdr.pop("CATALOG", None)
            self.ref_antenna_name = hdr.pop("REFANT", None)
            self.Nsources = hdr.pop("NSOURCES", None)
            bl_range_string = hdr.pop("BL_RANGE", None)
            if bl_range_string is not None:
                self.baseline_range = [
                    float(b) for b in bl_range_string.strip("[").strip("]").split(",")
                ]
            self.diffuse_model = hdr.pop("DIFFUSE", None)

            self.observer = hdr.pop("OBSERVER", None)
            self.git_origin_cal = hdr.pop("ORIGCAL", None)
            self.git_hash_cal = hdr.pop("HASHCAL", None)

            # generate polarization and time array for either cal_type.
            self.Njones = hdr.pop("NAXIS2")
            self.jones_array = uvutils._fits_gethduaxis(fname[0], 2)
            self.Ntimes = hdr.pop("NAXIS3")
            self.time_array = uvutils._fits_gethduaxis(fname[0], 3)

            if self.telescope_location is not None:
                proc = self.set_lsts_from_time_array(background=background_lsts)
            else:
                proc = None

            self.Nspws = hdr.pop("NAXIS5")
            assert self.Nspws == 1, (
                "This file appears to have multiple spectral windows, which is not "
                "supported by the calfits format."
            )
            # subtract 1 to be zero-indexed
            self.spw_array = uvutils._fits_gethduaxis(fname[0], 5) - 1

            self.Nants_data = hdr.pop("NAXIS6")
            if self.cal_type == "gain":
                self._set_gain()
                self.Nfreqs = hdr.pop("NAXIS4")
                self.freq_array = uvutils._fits_gethduaxis(fname[0], 4)
                self.freq_array.shape = (1,) + self.freq_array.shape

                self.flex_spw_id_array = np.full(
                    self.Nfreqs, self.spw_array[0], dtype=int
                )
            if self.cal_type == "delay":
                self._set_delay()

                sechdu = fname[hdunames["FLAGS"]]
                # generate frequency array from flag data unit
                # (no freq axis in primary).
                self.Nfreqs = sechdu.header["NAXIS4"]
                assert self.Nspws == 1, (
                    "This file appears to have multiple spectral windows, which is not "
                    "supported by the calfits format."
                )
                self.freq_array = uvutils._fits_gethduaxis(sechdu, 4)
                self.freq_array.shape = (1,) + self.freq_array.shape

                spw_array = uvutils._fits_gethduaxis(sechdu, 5) - 1

                if not np.allclose(spw_array, self.spw_array):
                    raise ValueError(
                        "Spectral window values are different in FLAGS HDU than"
                        " in primary HDU"
                    )

                time_array = uvutils._fits_gethduaxis(sechdu, 3)
                if not np.allclose(
                    time_array,
                    self.time_array,
                    rtol=self._time_array.tols[0],
                    atol=self._time_array.tols[0],
                ):
                    raise ValueError(
                        "Time values are different in FLAGS HDU than in primary HDU"
                    )

                jones_array = uvutils._fits_gethduaxis(sechdu, 2)
                if not np.allclose(
                    jones_array,
                    self.jones_array,
                    rtol=self._jones_array.tols[0],
                    atol=self._jones_array.tols[0],
                ):
                    raise ValueError(
                        "Jones values are different in FLAGS HDU than in primary HDU"
                    )

            # get data.
            if read_data:
                data = fname[0].data
                if self.cal_type == "gain":
                    self.gain_array = (
                        data[:, :, :, :, :, 0] + 1j * data[:, :, :, :, :, 1]
                    )
                    self.flag_array = data[:, :, :, :, :, 2].astype("bool")
                    if hdr.pop("NAXIS1") == 5:
                        self.input_flag_array = data[:, :, :, :, :, 3].astype("bool")
                        self.quality_array = data[:, :, :, :, :, 4]
                    else:
                        self.quality_array = data[:, :, :, :, :, 3]

                if self.cal_type == "delay":
                    self.delay_array = data[:, :, :, :, :, 0]
                    self.quality_array = data[:, :, :, :, :, 1]

                    flag_data = sechdu.data
                    if sechdu.header["NAXIS1"] == 2:
                        self.flag_array = flag_data[:, :, :, :, :, 0].astype("bool")
                        self.input_flag_array = flag_data[:, :, :, :, :, 1].astype(
                            "bool"
                        )
                    else:
                        self.flag_array = flag_data[:, :, :, :, :, 0].astype("bool")

                # get total quality array if present
                if "TOTQLTY" in hdunames:
                    totqualhdu = fname[hdunames["TOTQLTY"]]
                    self.total_quality_array = totqualhdu.data
                    spw_array = uvutils._fits_gethduaxis(totqualhdu, 4) - 1
                    if not np.allclose(spw_array, self.spw_array):
                        raise ValueError(
                            "Spectral window values are different in "
                            "TOTQLTY HDU than in primary HDU. primary HDU "
                            "has {pspw}, TOTQLTY has {tspw}".format(
                                pspw=self.spw_array, tspw=spw_array
                            )
                        )

                    if self.cal_type != "delay":
                        # delay-type files won't have a freq_array
                        freq_array = uvutils._fits_gethduaxis(totqualhdu, 3)
                        freq_array.shape = (1,) + freq_array.shape
                        if not np.allclose(
                            freq_array,
                            self.freq_array,
                            rtol=self._freq_array.tols[0],
                            atol=self._freq_array.tols[0],
                        ):
                            raise ValueError(
                                "Frequency values are different in TOTQLTY HDU than"
                                " in primary HDU"
                            )

                    time_array = uvutils._fits_gethduaxis(totqualhdu, 2)
                    if not np.allclose(
                        time_array,
                        self.time_array,
                        rtol=self._time_array.tols[0],
                        atol=self._time_array.tols[0],
                    ):
                        raise ValueError(
                            "Time values are different in TOTQLTY HDU than in "
                            "primary HDU"
                        )

                    jones_array = uvutils._fits_gethduaxis(totqualhdu, 1)
                    if not np.allclose(
                        jones_array,
                        self.jones_array,
                        rtol=self._jones_array.tols[0],
                        atol=self._jones_array.tols[0],
                    ):
                        raise ValueError(
                            "Jones values are different in TOTQLTY HDU than in "
                            "primary HDU"
                        )

                else:
                    self.total_quality_array = None

            self.extra_keywords = uvutils._get_fits_extra_keywords(hdr)

        # wait for LSTs if set in background
        if proc is not None:
            proc.join()

        if use_future_array_shapes:
            self.use_future_array_shapes()

        else:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )


# pyuvdata/uvcal/fhd_cal.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Class for reading FHD calibration save files."""

import os
import warnings

import numpy as np
from scipy.io import readsav

from .. import utils as uvutils
from ..uvdata.fhd import get_fhd_history, get_fhd_layout_info
from .uvcal import UVCal, _future_array_shapes_warning

__all__ = ["FHDCal"]


class FHDCal(UVCal):
    """
    Defines a FHD-specific subclass of UVCal for reading FHD calibration save files.

    This class should not be interacted with directly, instead use the read_fhd_cal
    method on the UVCal class.

    """

    def read_fhd_cal(
        self,
        cal_file,
        obs_file,
        layout_file=None,
        settings_file=None,
        raw=True,
        read_data=True,
        background_lsts=True,
        extra_history=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        use_future_array_shapes=False,
    ):
        """
        Read data from an FHD cal.sav file.

        Parameters
        ----------
        cal_file : str
            The cal.sav file to read from.
        obs_file : str
            The obs.sav file to read from.
        layout_file : str
            The FHD layout file. Required for antenna_positions to be set.
        settings_file : str, optional
            The settings_file to read from. Optional, but very useful for provenance.
        raw : bool
            Option to use the raw (per antenna, per frequency) solution or
            to use the fitted (polynomial over phase/amplitude) solution.
            Default is True (meaning use the raw solutions).
        read_data : bool
            Read in the gains, quality array and flag data. If set to False, only
            the metadata will be read in. Setting read_data to False results in
            a metadata only object. If read_data is False, a settings file must be
            provided.
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        extra_history : str or list of str, optional
            String(s) to add to the object's history parameter.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        """
        if not read_data and settings_file is None:
            raise ValueError("A settings_file must be provided if read_data is False.")

        filelist = [cal_file, obs_file]
        if layout_file is not None:
            filelist.append(layout_file)
        if settings_file is not None:
            filelist.append(settings_file)
        for filename in filelist:
            # update filelist
            basename = os.path.basename(filename)
            self.filename = uvutils._combine_filenames(self.filename, [basename])
            self._filename.form = (len(self.filename),)

        this_dict = readsav(obs_file, python_dict=True)
        obs_data = this_dict["obs"]
        bl_info = obs_data["BASELINE_INFO"][0]

        self.Nspws = 1
        self.spw_array = np.array([0])

        self.Nfreqs = int(obs_data["N_FREQ"][0])
        self.freq_array = np.zeros((1, len(bl_info["FREQ"][0])), dtype=np.float64)
        self.freq_array[0, :] = bl_info["FREQ"][0]
        self.channel_width = float(obs_data["FREQ_RES"][0])

        self.flex_spw_id_array = np.zeros(self.Nfreqs, dtype=int)

        # FHD only calculates one calibration over all the times.
        # obs_data.n_time /cal_data.n_times gives the number of times that goes into
        # that one calibration, UVCal.Ntimes gives the number of separate calibrations
        # along the time axis.
        self.Ntimes = 1
        time_array = bl_info["jdate"][0]

        self.time_array = np.array([np.mean(time_array)])

        # this is generated in FHD by subtracting the JD of neighboring
        # integrations. This can have limited accuracy, so it can be slightly
        # off the actual value.
        # (e.g. 1.999426... rather than 2)
        time_res = obs_data["TIME_RES"][0]
        # time_res is constrained to be a scalar currently
        self.integration_time = np.float64(time_res)

        # array of used frequencies (1: used, 0: flagged)
        freq_use = bl_info["freq_use"][0]
        # array of used antennas (1: used, 0: flagged)
        ant_use = bl_info["tile_use"][0]
        # array of used times (1: used, 0: flagged)
        time_use = bl_info["time_use"][0]

        time_array_use = time_array[np.where(time_use > 0)]
        self.time_range = np.asarray([np.min(time_array_use), np.max(time_array_use)])

        self.telescope_name = obs_data["instrument"][0].decode("utf8")
        latitude = np.deg2rad(float(obs_data["LAT"][0]))
        longitude = np.deg2rad(float(obs_data["LON"][0]))
        altitude = float(obs_data["ALT"][0])

        # get the stuff FHD read from the antenna table (in layout file)
        if layout_file is not None:
            obs_tile_names = [
                ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist()
            ]
            if self.telescope_name.lower() == "mwa":
                obs_tile_names = [
                    "Tile" + "0" * (3 - len(ant.strip())) + ant.strip()
                    for ant in obs_tile_names
                ]

            layout_param_dict = get_fhd_layout_info(
                layout_file,
                self.telescope_name,
                latitude,
                longitude,
                altitude,
                self._lst_array.tols,
                self._telescope_location.tols,
                obs_tile_names,
                run_check_acceptability=True,
            )

            layout_params_to_ignore = [
                "gst0",
                "rdate",
                "earth_omega",
                "dut1",
                "timesys",
                "diameters",
            ]
            for key, value in layout_param_dict.items():
                if key not in layout_params_to_ignore:
                    setattr(self, key, value)

        else:
            warnings.warn(
                "No layout file, antenna_postions will not be defined "
                "and antenna_names might be incorrect."
            )

            self.telescope_location_lat_lon_alt = (latitude, longitude, altitude)
            # FHD stores antenna numbers, not names, in the "TILE_NAMES" field
            self.antenna_names = [
                ant.decode("utf8") for ant in bl_info["TILE_NAMES"][0].tolist()
            ]
            self.antenna_numbers = np.array([int(ant) for ant in self.antenna_names])
            if self.telescope_name.lower() == "mwa":
                self.antenna_names = [
                    "Tile" + "0" * (3 - len(ant.strip())) + ant.strip()
                    for ant in self.antenna_names
                ]
            self.Nants_telescope = len(self.antenna_names)

        self.antenna_names = np.asarray(self.antenna_names)

        try:
            self.set_telescope_params()
        except ValueError as ve:
            warnings.warn(str(ve))

        # need to make sure telescope location is defined properly before this call
        if self.telescope_location is not None:
            proc = self.set_lsts_from_time_array(background=background_lsts)

        self._set_sky()
        self.sky_field = "phase center (RA, Dec): ({ra}, {dec})".format(
            ra=obs_data["orig_phasera"][0], dec=obs_data["orig_phasedec"][0]
        )
        self.gain_convention = "divide"
        self.x_orientation = "east"

        self._set_gain()

        # currently don't have branch info. may change in future.
        self.git_origin_cal = "https://github.com/EoRImaging/FHD"
        self.git_hash_cal = obs_data["code_version"][0].decode("utf8")

        if "DELAYS" in obs_data.dtype.names:
            if obs_data["delays"][0] is not None:
                self.extra_keywords["delays"] = (
                    "[" + ", ".join(str(int(d)) for d in obs_data["delays"][0]) + "]"
                )
        if settings_file is not None:
            self.history, self.observer = get_fhd_history(
                settings_file, return_user=True
            )
        else:
            warnings.warn("No settings file, history will be incomplete")
            self.history = ""

        if extra_history is not None:
            if isinstance(extra_history, (list, tuple)):
                self.history += "\n" + "\n".join(extra_history)
            else:
                self.history += "\n" + extra_history

        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            if self.history.endswith("\n"):
                self.history += self.pyuvdata_version_str
            else:
                self.history += "\n" + self.pyuvdata_version_str

        if not read_data:
            n_pols = int(obs_data["N_POL"])
            # FHD only has the diagonal elements (jxx, jyy), so limit to 2
            self.Njones = int(np.min([n_pols, 2]))

            # for calibration FHD includes all antennas in the antenna table,
            # regardless of whether or not they have data
            self.Nants_data = len(self.antenna_names)

            # get details from settings file
            keywords = [
                "ref_antenna_name",
                "catalog_name",
                "n_sources",
                "min_cal_baseline",
                "max_cal_baseline",
                "galaxy_model",
                "diffuse_model",
                "auto_scale",
                "n_vis_cal",
                "time_avg",
                "conv_thresh",
            ]
            if not raw:
                keywords += [
                    "polyfit",
                    "bandpass",
                    "mode_fit",
                    "amp_degree",
                    "phase_degree",
                ]

            settings_lines = {}
            with open(settings_file, "r") as read_obj:
                cal_start = False
                for line in read_obj:
                    if not cal_start:
                        if line.startswith("##CAL"):
                            cal_start = True
                    else:
                        if line.startswith("##"):
                            break
                        # in cal structure section
                        for kw in keywords:
                            if line.strip().startswith(kw.upper()):
                                settings_lines[kw] = line.split()[1:]
            self.ref_antenna_name = settings_lines["ref_antenna_name"][0]
            self.Nsources = int(settings_lines["n_sources"][0])
            self.sky_catalog = settings_lines["catalog_name"][0]
            self.baseline_range = [
                float(settings_lines["min_cal_baseline"][0]),
                float(settings_lines["max_cal_baseline"][0]),
            ]
            galaxy_model = int(settings_lines["galaxy_model"][0])
            if len(settings_lines["diffuse_model"]) > 0:
                diffuse_model = settings_lines["diffuse_model"][0]
            else:
                diffuse_model = ""
            auto_scale = settings_lines["auto_scale"]
            n_vis_cal = np.int64(settings_lines["n_vis_cal"][0])
            time_avg = int(settings_lines["time_avg"][0])
            conv_thresh = float(settings_lines["conv_thresh"][0])

            if not raw:
                polyfit = int(settings_lines["polyfit"][0])
                bandpass = int(settings_lines["bandpass"][0])
                mode_fit = settings_lines["mode_fit"]
                # for some reason, it's a float if it's one value,
                # and integers otherwise
                if len(mode_fit) == 1:
                    mode_fit = float(mode_fit[0])
                else:
                    mode_fit = np.array(mode_fit, dtype=np.int64)

                amp_degree = int(settings_lines["amp_degree"][0])
                phase_degree = int(settings_lines["phase_degree"][0])

        else:
            this_dict = readsav(cal_file, python_dict=True)
            cal_data = this_dict["cal"]
            self.Njones = int(cal_data["n_pol"][0])
            self.Nants_data = int(cal_data["n_tile"][0])

            self.sky_catalog = cal_data["skymodel"][0]["catalog_name"][0].decode("utf8")
            self.ref_antenna_name = (
                cal_data["ref_antenna_name"][0].decode("utf8").strip()
            )
            self.Nsources = int(cal_data["skymodel"][0]["n_sources"][0])
            self.baseline_range = [
                float(cal_data["min_cal_baseline"][0]),
                float(cal_data["max_cal_baseline"][0]),
            ]

            galaxy_model = cal_data["skymodel"][0]["galaxy_model"][0]
            diffuse_model = cal_data["skymodel"][0]["diffuse_model"][0]

            auto_scale = cal_data["auto_scale"][0]
            n_vis_cal = cal_data["n_vis_cal"][0]
            time_avg = cal_data["time_avg"][0]
            conv_thresh = cal_data["conv_thresh"][0]

            if not raw:
                polyfit = cal_data["polyfit"][0]
                bandpass = cal_data["bandpass"][0]
                mode_fit = cal_data["mode_fit"][0]
                amp_degree = cal_data["amp_degree"][0]
                phase_degree = cal_data["phase_degree"][0]

            # Now read data like arrays
            fit_gain_array_in = cal_data["gain"][0]
            fit_gain_array = np.zeros(
                self._gain_array.expected_shape(self), dtype=np.complex128
            )
            for jones_i, arr in enumerate(fit_gain_array_in):
                fit_gain_array[:, 0, :, 0, jones_i] = arr
            if raw:
                res_gain_array_in = cal_data["gain_residual"][0]
                res_gain_array = np.zeros(
                    self._gain_array.expected_shape(self), dtype=np.complex128
                )
                for jones_i, arr in enumerate(res_gain_array_in):
                    res_gain_array[:, 0, :, 0, jones_i] = arr
                self.gain_array = fit_gain_array + res_gain_array
            else:
                self.gain_array = fit_gain_array

            # FHD doesn't really have a chi^2 measure. What is has is a convergence
            # measure. The solution converged well if this is less than the convergence
            # threshold ('conv_thresh' in extra_keywords).
            self.quality_array = np.zeros_like(self.gain_array, dtype=np.float64)
            convergence = cal_data["convergence"][0]
            for jones_i, arr in enumerate(convergence):
                self.quality_array[:, 0, :, 0, jones_i] = arr

            # Currently this can't include the times because the flag array
            # dimensions has to match the gain array dimensions.
            # This is somewhat artificial...
            self.flag_array = np.zeros_like(self.gain_array, dtype=np.bool_)
            flagged_ants = np.where(ant_use == 0)[0]
            for ant in flagged_ants:
                self.flag_array[ant, :] = 1
            flagged_freqs = np.where(freq_use == 0)[0]
            for freq in flagged_freqs:
                self.flag_array[:, :, freq] = 1

        if self.telescope_name.lower() == "mwa":
            self.ref_antenna_name = (
                "Tile" + "0" * (3 - len(self.ref_antenna_name)) + self.ref_antenna_name
            )
        # In Python 3, we sometimes get Unicode, sometimes bytes
        # doesn't reliably show up in tests though, so excluding it from coverage
        if isinstance(galaxy_model, bytes):  # pragma: nocover
            galaxy_model = galaxy_model.decode("utf8")
        if galaxy_model == 0:
            galaxy_model = None
        else:
            galaxy_model = "gsm"

        if isinstance(diffuse_model, bytes):
            diffuse_model = diffuse_model.decode("utf8")
        if diffuse_model == "":
            diffuse_model = None
        else:
            diffuse_model = os.path.basename(diffuse_model)

        if galaxy_model is not None:
            if diffuse_model is not None:
                self.diffuse_model = galaxy_model + " + " + diffuse_model
            else:
                self.diffuse_model = galaxy_model
        elif diffuse_model is not None:
            self.diffuse_model = diffuse_model

        # FHD only has the diagonal elements (jxx, jyy) and if there's only one
        # present it must be jxx
        if self.Njones == 1:
            self.jones_array = np.array([-5])
        else:
            self.jones_array = np.array([-5, -6])

        # for calibration FHD creates gain array of shape (Nfreqs, Nants_telescope)
        # rather than (Nfreqs, Nants_data). This means the antenna array will
        # contain all antennas in the antenna table instead of only those
        # which had data in the original uvfits file
        self.ant_array = self.antenna_numbers

        self.extra_keywords["autoscal".upper()] = (
            "[" + ", ".join(str(d) for d in auto_scale) + "]"
        )
        self.extra_keywords["nvis_cal".upper()] = n_vis_cal
        self.extra_keywords["time_avg".upper()] = time_avg
        self.extra_keywords["cvgthres".upper()] = conv_thresh

        if not raw:
            self.extra_keywords["polyfit".upper()] = polyfit
            self.extra_keywords["bandpass".upper()] = bandpass
            if isinstance(mode_fit, (list, tuple, np.ndarray)):
                self.extra_keywords["mode_fit".upper()] = (
                    "[" + ", ".join(str(m) for m in mode_fit) + "]"
                )
            else:
                self.extra_keywords["mode_fit".upper()] = mode_fit
            self.extra_keywords["amp_deg".upper()] = amp_degree
            self.extra_keywords["phse_deg".upper()] = phase_degree

        # wait for LSTs if set in background
        if proc is not None:
            proc.join()

        if use_future_array_shapes:
            self.use_future_array_shapes()
        else:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )


# pyuvdata/uvcal/uvcal.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Primary container for radio interferometer calibration solutions."""

import copy
import os
import threading
import warnings

import numpy as np

from .. import parameter as uvp
from .. import telescopes as uvtel
from .. import utils as uvutils
from ..uvbase import UVBase
from ..uvdata import UVData

__all__ = ["UVCal"]

_future_array_shapes_warning = (
    "The shapes of several attributes will be changing in the future to remove the "
    "deprecated spectral window axis. You can call the `use_future_array_shapes` "
    "method to convert to the future array shapes now or set the parameter of the same "
    "name on this method to both convert to the future array shapes and silence this "
    "warning. See the UVCal tutorial on ReadTheDocs for more details about these "
    "shape changes."
)


class UVCal(UVBase):
    """
    A class defining calibration solutions for interferometric data.

    Attributes
    ----------
    UVParameter objects :
        For full list see the documentation on ReadTheDocs:
        http://pyuvdata.readthedocs.io/en/latest/.
        Some are always required, some are required for certain cal_types and cal_styles
        and others are always optional.

    """

    def __init__(self):
        self._Nfreqs = uvp.UVParameter(
            "Nfreqs", description="Number of frequency channels", expected_type=int
        )
        self._Njones = uvp.UVParameter(
            "Njones",
            description="Number of Jones calibration "
            "parameters (Number of Jones matrix elements "
            "calculated in calibration).",
            expected_type=int,
        )
        desc = (
            "Number of times with different calibrations calculated "
            "(if a calibration is calculated over a range of integrations, "
            "this gives the number of separate calibrations along the time axis)."
        )
        self._Ntimes = uvp.UVParameter("Ntimes", description=desc, expected_type=int)
        self._history = uvp.UVParameter(
            "history",
            description="String of history, units English",
            form="str",
            expected_type=str,
        )
        self._Nspws = uvp.UVParameter(
            "Nspws",
            description="Number of spectral windows "
            "(ie non-contiguous spectral chunks). ",
            expected_type=int,
        )

        desc = "Name of telescope. e.g. HERA. String."
        self._telescope_name = uvp.UVParameter(
            "telescope_name", description=desc, form="str", expected_type=str
        )

        desc = (
            "Number of antennas that have data associated with them "
            "(i.e. length of ant_array), which may be smaller than the number"
            "of antennas in the telescope (i.e. length of antenna_numbers)."
        )
        self._Nants_data = uvp.UVParameter(
            "Nants_data", description=desc, expected_type=int
        )

        desc = (
            "Number of antennas in the antenna_numbers array. May be larger "
            "than the number of antennas with gains associated with them."
        )
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", description=desc, expected_type=int
        )

        desc = (
            "Telescope location: xyz in ITRF (earth-centered frame). "
            "Can also be accessed using telescope_location_lat_lon_alt or "
            "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,
        )

        desc = (
            "Array of integer antenna numbers that appear in self.gain_array,"
            " with shape (Nants_data,). "
            "This array is ordered to match the inherent ordering of the zeroth"
            " axis of self.gain_array."
        )
        self._ant_array = uvp.UVParameter(
            "ant_array", description=desc, expected_type=int, form=("Nants_data",)
        )

        desc = (
            "Array of antenna names with shape (Nants_telescope,). "
            "Ordering of elements matches ordering of antenna_numbers."
        )
        self._antenna_names = uvp.UVParameter(
            "antenna_names",
            description=desc,
            form=("Nants_telescope",),
            expected_type=str,
        )

        desc = (
            "Array of all integer-valued antenna numbers in the telescope with "
            "shape (Nants_telescope,). Ordering of elements matches that of "
            "antenna_names. This array is not necessarily identical to "
            "ant_array, in that this array holds all antenna numbers "
            "associated with the telescope, not just antennas with data, and "
            "has an in principle non-specific ordering."
        )
        self._antenna_numbers = uvp.UVParameter(
            "antenna_numbers",
            description=desc,
            form=("Nants_telescope",),
            expected_type=int,
        )

        desc = (
            "Array giving coordinates of antennas relative to "
            "telescope_location (ITRF frame), shape (Nants_telescope, 3), "
            "units meters. See the tutorial page in the documentation "
            "for an example of how to convert this to topocentric frame."
        )
        self._antenna_positions = uvp.UVParameter(
            "antenna_positions",
            description=desc,
            form=("Nants_telescope", 3),
            expected_type=float,
            tols=1e-3,  # 1 mm
            required=True,
        )

        desc = (
            "Option to support 'wide-band' calibration solutions with gains or delays "
            "that apply over a range of frequencies rather than having distinct values "
            "at each frequency. Delay type cal solutions are always 'wide-band' if "
            "future_array_shapes is True. If it is True several other parameters are "
            "affected: future_array_shapes is also True; the data-like arrays have a "
            "spw axis that is Nspws long rather than a frequency axis that is Nfreqs "
            "long; the `freq_range` parameter is required and the `freq_array` and "
            "`channel_width` parameters are not required."
        )
        self._wide_band = uvp.UVParameter(
            "wide_band", description=desc, expected_type=bool, value=False
        )

        self._spw_array = uvp.UVParameter(
            "spw_array",
            description="Array of spectral window numbers, shape (Nspws).",
            form=("Nspws",),
            expected_type=int,
        )

        # this dimensionality of freq_array does not allow for different spws
        # to have different numbers of channels
        desc = (
            "Array of frequencies, center of the channel, "
            "shape (1, Nfreqs) or (Nfreqs,) if future_array_shapes=True, units Hz."
            "Not required if future_array_shapes=True and wide_band=True."
        )
        # TODO: Spw axis to be collapsed in future release
        self._freq_array = uvp.UVParameter(
            "freq_array",
            description=desc,
            form=(1, "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )  # mHz

        desc = (
            "Width of frequency channels (Hz). If flex_spw = False and "
            "future_array_shapes=False, then it is a "
            "single value of type = float, otherwise it is an array of shape "
            "(Nfreqs,), type = float."
            "Not required if future_array_shapes=True and wide_band=True."
        )
        self._channel_width = uvp.UVParameter(
            "channel_width", description=desc, expected_type=float, tols=1e-3
        )  # 1 mHz

        desc = (
            "Required if cal_type='delay' or wide_band=True. Frequency range that "
            "solutions are valid for. If future_array_shapes is False it is a "
            "length 2 array with [start_frequency, end_frequency], otherwise it is an "
            "array of shape (Nspws, 2). Units are Hz."
        )
        self._freq_range = uvp.UVParameter(
            "freq_range",
            required=False,
            description=desc,
            form=2,
            expected_type=float,
            tols=1e-3,
        )

        desc = (
            "Array of antenna polarization integers, shape (Njones). "
            "linear pols -5:-8 (jxx, jyy, jxy, jyx)."
            "circular pols -1:-4 (jrr, jll. jrl, jlr)."
        )

        self._jones_array = uvp.UVParameter(
            "jones_array",
            description=desc,
            expected_type=int,
            acceptable_vals=list(np.arange(-8, 0)),
            form=("Njones",),
        )

        desc = (
            "Time range (in JD) that cal solutions are valid for."
            "list: [start_time, end_time] in JD. Should only be set if Ntimes is 1."
        )
        self._time_range = uvp.UVParameter(
            "time_range", description=desc, form=2, expected_type=float, required=False
        )

        desc = (
            "Array of calibration solution times, center of integration, "
            "shape (Ntimes), units Julian Date"
        )
        self._time_array = uvp.UVParameter(
            "time_array",
            description=desc,
            form=("Ntimes",),
            expected_type=float,
            tols=1e-3 / (60.0 * 60.0 * 24.0),
        )

        # standard angle tolerance: 1 mas in radians.
        radian_tol = 1 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0)
        desc = "Array of lsts, center of integration, shape (Ntimes), units radians"
        self._lst_array = uvp.UVParameter(
            "lst_array",
            description=desc,
            form=("Ntimes",),
            expected_type=float,
            tols=radian_tol,
            required=True,
        )

        desc = (
            "Integration time of a time bin, units seconds. "
            "If future_array_shapes=False, then it is a single value of type = float, "
            "otherwise it is an array of shape (Ntimes), type = float."
        )
        self._integration_time = uvp.UVParameter(
            "integration_time", description=desc, expected_type=float, tols=1e-3
        )  # 1ms

        desc = (
            "The convention for applying the calibration solutions to data."
            'Values are "divide" or "multiply", indicating that to calibrate '
            "one should divide or multiply uncalibrated data by gains. "
            "Mathematically this indicates the alpha exponent in the equation: "
            "calibrated data = gain^alpha * uncalibrated data. A value of "
            '"divide" represents alpha=-1 and "multiply" represents alpha=1.'
        )
        self._gain_convention = uvp.UVParameter(
            "gain_convention",
            form="str",
            expected_type=str,
            description=desc,
            acceptable_vals=["divide", "multiply"],
        )

        desc = (
            "Array of flags to be applied to calibrated data (logical OR "
            "of input and flag generated by calibration). True is flagged. "
            "Shape: (Nants_data, 1, Nfreqs, Ntimes, Njones) or "
            "(Nants_data, Nfreqs, Ntimes, Njones) if future_array_shapes=True and "
            "wide_band=False or (Nants_data, Nspws, Ntimes, Njones) if wide_band=True, "
            "type = bool."
        )
        self._flag_array = uvp.UVParameter(
            "flag_array",
            description=desc,
            form=("Nants_data", 1, "Nfreqs", "Ntimes", "Njones"),
            expected_type=bool,
        )

        desc = (
            "Array of qualities of calibration solutions. "
            "The shape depends on cal_type, if the cal_type is 'gain', the shape is: "
            "(Nants_data, 1, Nfreqs, Ntimes, Njones) or "
            "(Nants_data, Nfreqs, Ntimes, Njones) if future_array_shapes=True and "
            "wide_band=False or (Nants_data, Nspws, Ntimes, Njones) if wide_band=True, "
            "if the cal_type is 'delay', the shape is "
            "(Nants_data, 1, 1, Ntimes, Njones) or (Nants_data, Nspws, Ntimes, Njones) "
            "if future_array_shapes=True. The type is float."
        )
        self._quality_array = uvp.UVParameter(
            "quality_array",
            description=desc,
            form=("Nants_data", 1, "Nfreqs", "Ntimes", "Njones"),
            expected_type=float,
        )

        desc = (
            "Orientation of the physical dipole corresponding to what is "
            'labelled as the x polarization. Options are "east" '
            '(indicating east/west orientation) and "north" (indicating '
            "north/south orientation)"
        )
        self._x_orientation = uvp.UVParameter(
            "x_orientation",
            description=desc,
            expected_type=str,
            acceptable_vals=["east", "north"],
        )

        # --- cal_type parameters ---
        desc = "cal type parameter. Values are delay or gain."
        self._cal_type = uvp.UVParameter(
            "cal_type",
            form="str",
            expected_type=str,
            value="gain",
            description=desc,
            acceptable_vals=["delay", "gain", "unknown"],
        )

        desc = (
            'Required if cal_type = "gain". Array of gains, '
            "shape: (Nants_data, 1, Nfreqs, Ntimes, Njones) or "
            "(Nants_data, Nfreqs, Ntimes, Njones) if future_array_shapes=True, or "
            "(Nants_data, Nspws, Ntimes, Njones) if wide_band=True, "
            "type = complex float."
        )
        self._gain_array = uvp.UVParameter(
            "gain_array",
            description=desc,
            required=False,
            form=("Nants_data", 1, "Nfreqs", "Ntimes", "Njones"),
            expected_type=complex,
        )

        desc = (
            'Required if cal_type = "delay". Array of delays with units of seconds. '
            "Shape: (Nants_data, 1, 1, Ntimes, Njones) or "
            "(Nants_data, Nspws, Ntimes, Njones) if future_array_shapes=True, "
            "type=float."
        )
        self._delay_array = uvp.UVParameter(
            "delay_array",
            description=desc,
            required=False,
            form=("Nants_data", "Nspws", 1, "Ntimes", "Njones"),
            expected_type=float,
        )

        # --- flexible spectral window information ---

        desc = (
            "Option to construct a 'flexible spectral window', which stores"
            "all spectral channels across the frequency axis of data_array. "
            "Allows for spectral windows of variable sizes, and channels of "
            "varying widths."
        )
        self._flex_spw = uvp.UVParameter(
            "flex_spw", description=desc, expected_type=bool, value=False
        )

        desc = (
            "Required if flex_spw = True and will always be required for non-wide-band "
            "objects starting in version 3.0. Maps individual channels along the "
            "frequency axis to individual spectral windows, as listed in the "
            "spw_array. Shape (Nfreqs), type = int."
        )
        self._flex_spw_id_array = uvp.UVParameter(
            "flex_spw_id_array",
            description=desc,
            form=("Nfreqs",),
            expected_type=int,
            required=False,
        )

        desc = "Flag indicating that this object is using the future array shapes."
        self._future_array_shapes = uvp.UVParameter(
            "future_array_shapes", description=desc, expected_type=bool, value=False
        )

        # --- cal_style parameters ---
        desc = "Style of calibration. Values are sky or redundant."
        self._cal_style = uvp.UVParameter(
            "cal_style",
            form="str",
            expected_type=str,
            description=desc,
            acceptable_vals=["sky", "redundant"],
        )

        desc = (
            "Required if cal_style = 'sky'. Short string describing field "
            "center or dominant source."
        )
        self._sky_field = uvp.UVParameter(
            "sky_field", form="str", required=False, expected_type=str, description=desc
        )

        desc = 'Required if cal_style = "sky". Name of calibration catalog.'
        self._sky_catalog = uvp.UVParameter(
            "sky_catalog",
            form="str",
            required=False,
            expected_type=str,
            description=desc,
        )

        desc = 'Required if cal_style = "sky". Phase reference antenna.'
        self._ref_antenna_name = uvp.UVParameter(
            "ref_antenna_name",
            form="str",
            required=False,
            expected_type=str,
            description=desc,
        )

        desc = "Number of sources used."
        self._Nsources = uvp.UVParameter(
            "Nsources", required=False, expected_type=int, description=desc
        )

        desc = "Range of baselines used for calibration."
        self._baseline_range = uvp.UVParameter(
            "baseline_range",
            form=2,
            required=False,
            expected_type=float,
            description=desc,
        )

        desc = "Name of diffuse model."
        self._diffuse_model = uvp.UVParameter(
            "diffuse_model",
            form="str",
            required=False,
            expected_type=str,
            description=desc,
        )

        # --- truly optional parameters ---
        desc = (
            "The gain scale of the calibration, which indicates the units of the "
            "calibrated visibilities. For example, Jy or K str."
        )
        self._gain_scale = uvp.UVParameter(
            "gain_scale",
            form="str",
            expected_type=str,
            description=desc,
            required=False,
        )

        desc = (
            "Deprecated, support will be removed in version 2.5. Array of input flags, "
            "True is flagged. shape: (Nants_data, 1, Nfreqs, Ntimes, Njones) or "
            "(Nants_data, Nfreqs, Ntimes, Njones) if future_array_shapes=True, "
            "type = bool."
        )
        self._input_flag_array = uvp.UVParameter(
            "input_flag_array",
            description=desc,
            required=False,
            form=("Nants_data", 1, "Nfreqs", "Ntimes", "Njones"),
            expected_type=bool,
        )

        desc = "Origin (on github for e.g) of calibration software. Url and branch."
        self._git_origin_cal = uvp.UVParameter(
            "git_origin_cal",
            form="str",
            expected_type=str,
            description=desc,
            required=False,
        )

        desc = (
            "Commit hash of calibration software (from git_origin_cal) used "
            "to generate solutions."
        )
        self._git_hash_cal = uvp.UVParameter(
            "git_hash_cal",
            form="str",
            expected_type=str,
            description=desc,
            required=False,
        )

        desc = "Name of observer who calculated solutions in this file."
        self._observer = uvp.UVParameter(
            "observer", form="str", description=desc, expected_type=str, required=False
        )

        desc = (
            "Array of qualities of the calibration for entire arrays. "
            "The shape depends on cal_type, if the cal_type is 'gain', "
            "the shape is: (1, Nfreqs, Ntimes, Njones) or "
            "(Nfreqs, Ntimes, Njones) if future_array_shapes=True and wide_band=False, "
            "or (Nspws, Ntimes, Njones) if wide_band=True. "
            "If the cal_type is 'delay', the shape is (1, 1, Ntimes, Njones) or "
            "(Nspws, Ntimes, Njones) if future_array_shapes=True, type = float."
        )
        self._total_quality_array = uvp.UVParameter(
            "total_quality_array",
            description=desc,
            form=(1, "Nfreqs", "Ntimes", "Njones"),
            expected_type=float,
            required=False,
        )

        desc = (
            "Any user supplied extra keywords, type=dict. Keys should be "
            "8 character or less strings if writing to calfits files. "
            "Use the special key 'comment' for long multi-line string comments."
        )
        self._extra_keywords = uvp.UVParameter(
            "extra_keywords",
            required=False,
            description=desc,
            value={},
            spoof_val={},
            expected_type=dict,
        )

        desc = (
            "List of strings containing the unique basenames (not the full path) of "
            "input files."
        )
        self._filename = uvp.UVParameter(
            "filename", required=False, description=desc, expected_type=str
        )

        super(UVCal, self).__init__()

    def _set_flex_spw(self):
        """
        Set flex_spw to True, and adjust required parameters.

        This method should not be called directly by users; instead it is called
        by the file-reading methods to indicate that an object has multiple spectral
        windows concatenated together across the frequency axis.
        """
        assert (
            not self.wide_band
        ), "Cannot set objects wide_band objects to have flexible spectral windows."
        # Mark once-optional arrays as now required
        self.flex_spw = True
        self._flex_spw_id_array.required = True
        # Now make sure that chan_width is set to be an array
        self._channel_width.form = ("Nfreqs",)

    def _set_wide_band(self, wide_band=True):
        """
        Set the wide_band parameter and adjust required parameters.

        The wide_band can only be set to True if future_array_shapes is True.

        This method should not be called directly by users; instead it is called
        by the file-reading methods to indicate that an object is a wide-band
        calibration solution which supports gain or delay values per spectral window.

        """
        if wide_band:
            assert (
                self.future_array_shapes
            ), "future_array_shapes must be True to set wide_band to True."
            # TODO think about what to test for in flex_spw_id_array
            assert (
                not self.flex_spw
            ), "Cannot set objects with flexible spectral windows to wide_band."
        elif self.future_array_shapes:
            assert self.cal_type != "delay", (
                "delay objects cannot have wide_band=False if future_array_shapes is "
                "True"
            )
        self.wide_band = wide_band

        if wide_band:
            self._freq_array.required = False
            self._channel_width.required = False
            # in version 3.0, also set _flex_spw_id_array to not be required
            self._freq_range.required = True

            data_shape_params = [
                "gain_array",
                "delay_array",
                "flag_array",
                "input_flag_array",
                "quality_array",
            ]
            data_form = ("Nants_data", "Nspws", "Ntimes", "Njones")
            tot_qual_form = ("Nspws", "Ntimes", "Njones")

            for param_name in self._data_params:
                if param_name in data_shape_params:
                    getattr(self, "_" + param_name).form = data_form
                elif param_name == "total_quality_array":
                    getattr(self, "_" + param_name).form = tot_qual_form

        else:
            self._freq_array.required = True
            self._channel_width.required = True
            # in version 3.0, also set _flex_spw_id_array to be required
            self._freq_range.required = False

            if self.future_array_shapes:
                # can only get here if not a delay solution
                data_shape_params = [
                    "gain_array",
                    "flag_array",
                    "input_flag_array",
                    "quality_array",
                ]

                data_form = ("Nants_data", "Nfreqs", "Ntimes", "Njones")
                tot_qual_form = ("Nfreqs", "Ntimes", "Njones")

                for param_name in self._data_params:
                    if param_name in data_shape_params:
                        getattr(self, "_" + param_name).form = data_form
                    elif param_name == "total_quality_array":
                        getattr(self, "_" + param_name).form = tot_qual_form

    def _set_gain(self):
        """Set cal_type to 'gain' and adjust required parameters."""
        self.cal_type = "gain"
        self._gain_array.required = True
        self._delay_array.required = False
        self._freq_range.required = False
        self._freq_array.required = True
        self._channel_width.required = True
        self._quality_array.form = self._gain_array.form
        self._total_quality_array.form = self._gain_array.form[1:]

    def _set_delay(self):
        """Set cal_type to 'delay' and adjust required parameters."""
        self.cal_type = "delay"
        self._gain_array.required = False
        self._delay_array.required = True
        self._freq_range.required = True
        self._freq_array.required = False
        self._channel_width.required = False
        self._quality_array.form = self._delay_array.form
        self._total_quality_array.form = self._delay_array.form[1:]

        if self.future_array_shapes:
            self._set_wide_band()

    def _set_unknown_cal_type(self):
        """Set cal_type to 'unknown' and adjust required parameters.

        Deprecated.
        """
        warnings.warn(
            "Setting the cal_type to 'unknown' is deprecated. This will become an "
            "error in version 2.5",
            DeprecationWarning,
        )
        self.cal_type = "unknown"
        self._gain_array.required = False
        self._delay_array.required = False
        self._freq_range.required = False
        self._freq_array.required = True
        self._quality_array.form = self._gain_array.form
        self._total_quality_array.form = self._gain_array.form[1:]

    def _set_sky(self):
        """Set cal_style to 'sky' and adjust required parameters."""
        self.cal_style = "sky"
        self._sky_field.required = True
        self._sky_catalog.required = True
        self._ref_antenna_name.required = True

    def _set_redundant(self):
        """Set cal_style to 'redundant' and adjust required parameters."""
        self.cal_style = "redundant"
        self._sky_field.required = False
        self._sky_catalog.required = False
        self._ref_antenna_name.required = False

    @property
    def _data_params(self):
        """List of strings giving the data-like parameters."""
        return [
            "gain_array",
            "delay_array",
            "flag_array",
            "quality_array",
            "total_quality_array",
            "input_flag_array",
        ]

    @property
    def _required_data_params(self):
        """List of strings giving the required data-like parameters."""
        cal_type = self._cal_type.value
        if cal_type is None:
            raise ValueError(
                "Cannot get required data params because cal_type is not set."
            )

        if cal_type == "gain":
            return ["gain_array", "flag_array", "quality_array"]
        elif cal_type == "delay":
            return ["delay_array", "flag_array", "quality_array"]
        else:
            return ["flag_array", "quality_array"]

    @property
    def data_like_parameters(self):
        """Iterate defined parameters which are data-like (not metadata-like)."""
        for key in self._data_params:
            if hasattr(self, key):
                yield getattr(self, key)

    @property
    def metadata_only(self):
        """
        Property that determines whether this is a metadata only object.

        An object is metadata only if data_array, nsample_array and flag_array
        are all None.
        """
        metadata_only = all(d is None for d in self.data_like_parameters)

        for param_name in self._required_data_params:
            getattr(self, "_" + param_name).required = not metadata_only

        return metadata_only

    def _set_future_array_shapes(self):
        """
        Set future_array_shapes to True and adjust required parameters.

        This method should not be called directly by users; instead it is called
        by file-reading methods and `use_future_array_shapes` to indicate the
        `future_array_shapes` is True and define expected parameter shapes.

        """
        self.future_array_shapes = True
        self._freq_array.form = ("Nfreqs",)
        self._channel_width.form = ("Nfreqs",)
        self._integration_time.form = ("Ntimes",)
        self._freq_range.form = ("Nspws", 2)

        data_shape_params = [
            "gain_array",
            "flag_array",
            "input_flag_array",
            "quality_array",
        ]
        if self.cal_type == "delay":
            self._set_wide_band()
            data_shape_params.append("delay_array")

        if self.wide_band:
            data_form = ("Nants_data", "Nspws", "Ntimes", "Njones")
            tot_qual_form = ("Nspws", "Ntimes", "Njones")
        else:
            data_form = ("Nants_data", "Nfreqs", "Ntimes", "Njones")
            tot_qual_form = ("Nfreqs", "Ntimes", "Njones")

        for param_name in self._data_params:
            if param_name in data_shape_params:
                getattr(self, "_" + param_name).form = data_form
            if param_name == "delay_array":
                # only get here if cal_type is not "delay"
                self._delay_array.form = ("Nants_data", "Nspws", "Ntimes", "Njones")
            elif param_name == "total_quality_array":
                getattr(self, "_" + param_name).form = tot_qual_form

    def use_future_array_shapes(self):
        """
        Change the array shapes of this object to match the planned future shapes.

        This method sets allows users to convert to the planned array shapes changes
        before the changes go into effect. This method sets the `future_array_shapes`
        parameter on this object to True.

        """
        if self.future_array_shapes:
            return

        self._set_future_array_shapes()
        if not self.metadata_only:
            # remove the length-1 spw axis for all data-like parameters
            # except the delay array, which should have the length-1 freq axis removed
            for param_name in self._data_params:
                param_value = getattr(self, param_name)
                if param_value is None:
                    continue
                if param_name == "delay_array":
                    setattr(self, param_name, (param_value)[:, :, 0, :, :])
                elif param_name == "total_quality_array":
                    setattr(self, param_name, (param_value)[0, :, :, :])
                else:
                    setattr(self, param_name, (param_value)[:, 0, :, :, :])

            if self.cal_type == "delay":
                warnings.warn(
                    "When converting a delay-style cal to future array shapes the "
                    "flag_array (and input_flag_array if it exists) must drop the "
                    "frequency axis so that it will be the same shape as the "
                    "delay_array. This will be done using the "
                    "`pyuvdata.utils.and_collapse` function which will only flag an "
                    "antpol-time if all of the frequecies are flagged for that "
                    "antpol-time. To preserve the full flag information, create a "
                    "UVFlag object from this cal object before this operation. "
                    "In the future, these flag arrays will be removed from UVCal "
                    "objects in favor of using UVFlag objects."
                )
                self.flag_array = uvutils.and_collapse(self.flag_array, axis=1)[
                    :, np.newaxis, :, :
                ]
                if self.input_flag_array is not None:
                    self.input_flag_array = uvutils.and_collapse(
                        self.input_flag_array, axis=1
                    )[:, np.newaxis, :, :]

        if self.cal_type == "delay":
            self.freq_array = None
            self.channel_width = None
            self.Nfreqs = 1
        else:
            self.freq_range = None

        # remove the length-1 spw axis for the freq_array
        if self.freq_array is not None:
            self.freq_array = self.freq_array[0, :]

        if self.freq_range is not None:
            # force freq_range to have an spw axis
            self.freq_range = np.repeat(
                (np.asarray(self.freq_range))[np.newaxis, :], self.Nspws, axis=0
            )

        # force integration_time to be an array of length Ntimes
        self.integration_time = (
            np.zeros(self.Ntimes, dtype=np.float64) + self.integration_time
        )

        if not self.flex_spw and self.channel_width is not None:
            # make channel_width be an array of length Nfreqs rather than a single value
            # (not needed with flexible spws because this is already done in that case)
            self.channel_width = (
                np.zeros(self.Nfreqs, dtype=np.float64) + self.channel_width
            )

    def use_current_array_shapes(self):
        """
        Change the array shapes of this object to match the current shapes.

        This method sets allows users to convert back to the current array shapes.
        This method sets the `future_array_shapes` parameter on this object to False.
        """
        warnings.warn(
            "This method will be removed in version 3.0 when the current array shapes "
            "are no longer supported.",
            DeprecationWarning,
        )
        if not self.future_array_shapes:
            return

        if self.Nspws > 1 and self.wide_band:
            raise ValueError(
                "Cannot use current array shapes if Nspws > 1 and wide_band is True"
            )

        if self.cal_type != "delay" and self.wide_band:
            raise ValueError(
                "Cannot use current array shapes if cal_style is not 'delay' and "
                "wide_band is True."
            )

        if not self.flex_spw:
            if self.channel_width is not None:
                unique_channel_widths = np.unique(self.channel_width)
                if unique_channel_widths.size > 1:
                    raise ValueError(
                        "channel_width parameter contains multiple unique values, but "
                        "only one spectral window is present. Cannot collapse "
                        "channel_width to a single value."
                    )
                self._channel_width.form = ()
                self.channel_width = unique_channel_widths[0]

        unique_integration_times = np.unique(self.integration_time)
        if unique_integration_times.size > 1:
            raise ValueError(
                "integration_time parameter contains multiple unique values. "
                "Cannot collapse integration_time to a single value."
            )
        self._integration_time.form = ()
        self.integration_time = unique_integration_times[0]

        self.future_array_shapes = False
        self.wide_band = False

        gain_shape_params = ["gain_array", "flag_array", "input_flag_array"]
        delay_shape_params = ["delay_array"]
        if self.cal_type == "delay":
            delay_shape_params.append("quality_array")
        else:
            gain_shape_params.append("quality_array")

        for param_name in self._data_params:
            if param_name in gain_shape_params:
                getattr(self, "_" + param_name).form = (
                    "Nants_data",
                    1,
                    "Nfreqs",
                    "Ntimes",
                    "Njones",
                )
            elif param_name in delay_shape_params:
                getattr(self, "_" + param_name).form = (
                    "Nants_data",
                    "Nspws",
                    1,
                    "Ntimes",
                    "Njones",
                )
            elif param_name == "total_quality_array":
                if self.cal_type == "delay":
                    getattr(self, "_" + param_name).form = (1, 1, "Ntimes", "Njones")
                else:
                    getattr(self, "_" + param_name).form = (
                        1,
                        "Nfreqs",
                        "Ntimes",
                        "Njones",
                    )

        if not self.metadata_only:
            for param_name in self._data_params:
                param_value = getattr(self, param_name)
                if param_value is None:
                    continue
                if param_name == "delay_array":
                    setattr(
                        self,
                        param_name,
                        (getattr(self, param_name))[:, :, np.newaxis, :, :],
                    )
                elif param_name == "total_quality_array":
                    setattr(
                        self,
                        param_name,
                        (getattr(self, param_name))[np.newaxis, :, :, :],
                    )
                else:
                    setattr(
                        self,
                        param_name,
                        (getattr(self, param_name))[:, np.newaxis, :, :, :],
                    )
            if self.cal_type == "delay":
                # make the flag array have a frequency axis again
                self.flag_array = np.repeat(self.flag_array, self.Nfreqs, axis=2)
                if self.input_flag_array is not None:
                    self.input_flag_array = np.repeat(
                        self.input_flag_array, self.Nfreqs, axis=2
                    )

        self._freq_array.form = (1, "Nfreqs")
        if self.freq_array is not None:
            self.freq_array = self.freq_array[np.newaxis, :]
        elif self.Nfreqs == 1:
            self.freq_array = np.full((1, 1), self.freq_range[0, 0])

        self._freq_range.form = (2,)
        if self.freq_range is not None:
            self.freq_range = self.freq_range[0, :].tolist()
        else:
            self.freq_range = [np.min(self.freq_array), np.max(self.freq_array)]

    def set_telescope_params(self, overwrite=False):
        """
        Set telescope related parameters.

        If the telescope_name is in the known_telescopes, set the telescope
        location to the value for the known telescope. Also set the antenna positions
        if they are not set on the object and are available for the telescope.

        Parameters
        ----------
        overwrite : bool
            Option to overwrite existing telescope-associated parameters with
            the values from the known telescope.

        Raises
        ------
        ValueError
            if the telescope_name is not in known telescopes
        """
        telescope_obj = uvtel.get_telescope(self.telescope_name)
        if telescope_obj is not False:
            if self.telescope_location is None or overwrite is True:
                warnings.warn(
                    "telescope_location is not set. Using known values "
                    f"for {telescope_obj.telescope_name}."
                )
                self.telescope_location = telescope_obj.telescope_location

            if telescope_obj.antenna_positions is not None and (
                self.antenna_positions is None or overwrite is True
            ):
                ant_inds = []
                telescope_ant_inds = []
                # first try to match using names only
                for index, antname in enumerate(self.antenna_names):
                    if antname in telescope_obj.antenna_names:
                        ant_inds.append(index)
                        telescope_ant_inds.append(
                            np.where(telescope_obj.antenna_names == antname)[0][0]
                        )
                # next try using numbers
                if len(ant_inds) != self.Nants_telescope:
                    for index, antnum in enumerate(self.antenna_numbers):
                        # only update if not already found
                        if (
                            index not in ant_inds
                            and antnum in telescope_obj.antenna_numbers
                        ):
                            this_ant_ind = np.where(
                                telescope_obj.antenna_numbers == antnum
                            )[0][0]
                            # make sure we don't already have this antenna associated
                            # with another antenna
                            if this_ant_ind not in telescope_ant_inds:
                                ant_inds.append(index)
                                telescope_ant_inds.append(this_ant_ind)
                if len(ant_inds) != self.Nants_telescope:
                    warnings.warn(
                        "Not all antennas have positions in the telescope object. "
                        "Not setting antenna_positions."
                    )
                else:
                    warnings.warn(
                        "antenna_positions is not set. Using known values "
                        f"for {telescope_obj.telescope_name}."
                    )
                    if overwrite:
                        self.antenna_names = telescope_obj.antenna_names
                        self.antenna_numbers = telescope_obj.antenna_numbers
                        self.antenna_positions = telescope_obj.antenna_positions
                        self.Nants_telescope = telescope_obj.Nants_telescope
                    else:
                        telescope_ant_inds = np.array(telescope_ant_inds)
                        self.antenna_positions = telescope_obj.antenna_positions[
                            telescope_ant_inds, :
                        ]
        else:
            raise ValueError(
                f"Telescope {self.telescope_name} is not in known_telescopes."
            )

    def _set_lsts_helper(self):
        latitude, longitude, altitude = self.telescope_location_lat_lon_alt_degrees
        unique_times, inverse_inds = np.unique(self.time_array, return_inverse=True)
        unique_lst_array = uvutils.get_lst_for_time(
            unique_times, latitude, longitude, altitude
        )
        self.lst_array = unique_lst_array[inverse_inds]
        return

    def set_lsts_from_time_array(self, background=False):
        """Set the lst_array based from the time_array.

        Parameters
        ----------
        background : bool, False
            When set to True, start the calculation on a threading.Thread in the
            background and return the thread to the user.

        Returns
        -------
        proc : None or threading.Thread instance
            When background is set to True, a thread is returned which must be
            joined before the lst_array exists on the UVCal object.

        """
        if not background:
            self._set_lsts_helper()
            return
        else:
            proc = threading.Thread(target=self._set_lsts_helper)
            proc.start()
            return proc

    def _check_flex_spw_contiguous(self):
        """
        Check if the spectral windows are contiguous for flex_spw datasets.

        This checks the flex_spw_id_array to make sure that all channels for each
        spectral window are together in one block, versus being interspersed (e.g.,
        channel #1 and #3 is in spw #1, channels #2 and #4 are in spw #2).

        """
        if self.flex_spw:
            uvutils._check_flex_spw_contiguous(self.spw_array, self.flex_spw_id_array)

    def _check_freq_spacing(self, raise_errors=True):
        """
        Check if frequencies are evenly spaced and separated by their channel width.

        This is a requirement for writing calfits files.

        Parameters
        ----------
        raise_errors : bool
            Option to raise errors if the various checks do not pass.

        Returns
        -------
        spacing_error : bool
            Flag that channel spacings or channel widths are not equal.
        chanwidth_error : bool
            Flag that channel spacing does not match channel width.

        """
        if self.freq_array is None and self.Nfreqs == 1:
            return False, False
        return uvutils._check_freq_spacing(
            self.freq_array,
            self._freq_array.tols,
            self.channel_width,
            self._channel_width.tols,
            self.flex_spw,
            self.future_array_shapes,
            self.spw_array,
            self.flex_spw_id_array,
            raise_errors=raise_errors,
        )

    def check(
        self, check_extra=True, run_check_acceptability=True, check_freq_spacing=False
    ):
        """
        Add some extra checks on top of checks on UVBase class.

        Check that required parameters exist. Check that parameters have
        appropriate shapes and optionally that the values are acceptable.

        Parameters
        ----------
        check_extra : bool
            If true, check all parameters, otherwise only check required parameters.
        run_check_acceptability : bool
            Option to check if values in parameters are acceptable.
        check_freq_spacing :  bool
            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.

        Returns
        -------
        bool
            True if check passes

        Raises
        ------
        ValueError
            if parameter shapes or types are wrong or do not have acceptable
            values (if run_check_acceptability is True)

        """
        # Make sure requirements are set properly for cal_style
        if self.cal_style == "sky":
            self._set_sky()
        elif self.cal_style == "redundant":
            self._set_redundant()

        # if wide_band is True, Nfreqs must be 1.
        if self.wide_band:
            if self.Nfreqs != 1:
                warnings.warn(
                    "Nfreqs will be required to be 1 for wide_band cals (including "
                    "all delay cals) starting in version 3.0",
                    category=DeprecationWarning,
                )

        # deprecate 'unknown' cal_type
        if self.cal_type == "unknown":
            warnings.warn(
                "The 'unknown' cal_type is deprecated and will be removed in version "
                "2.5",
                DeprecationWarning,
            )

        # first run the basic check from UVBase
        super(UVCal, self).check(
            check_extra=check_extra, run_check_acceptability=run_check_acceptability
        )

        # require that all entries in ant_array exist in antenna_numbers
        if not all(ant in self.antenna_numbers for ant in self.ant_array):
            raise ValueError("All antennas in ant_array must be in antenna_numbers.")

        # issue warning if extra_keywords keys are longer than 8 characters
        for key in self.extra_keywords.keys():
            if len(key) > 8:
                warnings.warn(
                    "key {key} in extra_keywords is longer than 8 "
                    "characters. It will be truncated to 8 if written "
                    "to a calfits file format.".format(key=key)
                )

        # issue warning if extra_keywords values are lists, arrays or dicts
        for key, value in self.extra_keywords.items():
            if isinstance(value, (list, dict, np.ndarray)):
                warnings.warn(
                    "{key} in extra_keywords is a list, array or dict, "
                    "which will raise an error when writing calfits "
                    "files".format(key=key)
                )

        if not self.wide_band and self.cal_type == "gain":
            if self.flex_spw_id_array is None:
                warnings.warn(
                    "flex_spw_id_array is not set. It will be required starting in "
                    "version 3.0 for non-wide-band objects",
                    DeprecationWarning,
                )
            else:
                # Check that all values in flex_spw_id_array are entries in the
                # spw_array
                if not np.all(np.isin(self.flex_spw_id_array, self.spw_array)):
                    raise ValueError(
                        "All values in the flex_spw_id_array must exist in the "
                        "spw_array."
                    )

        if self.input_flag_array is not None:
            warnings.warn(
                "The input_flag_array is deprecated and will be removed in version 2.5",
                DeprecationWarning,
            )

        if check_freq_spacing:
            self._check_freq_spacing()

        return True

    def copy(self, metadata_only=False):
        """
        Make and return a copy of the UVCal object.

        Parameters
        ----------
        metadata_only : bool
            If True, only copy the metadata of the object.

        Returns
        -------
        UVCal
            Copy of self.
        """
        if not metadata_only:
            return super(UVCal, self).copy()
        else:
            uv = UVCal()
            # include all attributes, not just UVParameter ones.
            for attr in self.__iter__(uvparams_only=False):
                # skip properties
                if isinstance(getattr(type(self), attr, None), property):
                    continue

                # skip data like parameters
                # parameter names have a leading underscore we want to ignore
                if attr.lstrip("_") in self._data_params:
                    continue
                setattr(uv, attr, copy.deepcopy(getattr(self, attr)))

            return uv

    def _has_key(self, antnum=None, jpol=None):
        """
        Check if this UVCal has the requested antenna or polarization.

        Parameters
        ----------
        antnum : int
            Antenna number to check.
        jpol : str or int
            Antenna polarization string or integer to check.

        Returns
        -------
        bool
            Boolean indicator of whether the antenna and/or antenna
            polarization is present on this object.
        """
        if antnum is not None:
            if antnum not in self.ant_array:
                return False
        if jpol is not None:
            if isinstance(jpol, (str, np.str_)):
                jpol = uvutils.jstr2num(jpol, x_orientation=self.x_orientation)
            if jpol not in self.jones_array:
                return False

        return True

    def ant2ind(self, antnum):
        """
        Get the index in data arrays for an antenna number.

        Parameters
        ----------
        antnum : int
            Antenna number to get index for.

        Returns
        -------
        int
            Antenna index in data arrays.
        """
        if not self._has_key(antnum=antnum):
            raise ValueError("{} not found in ant_array".format(antnum))

        return np.argmin(np.abs(self.ant_array - antnum))

    def jpol2ind(self, jpol):
        """
        Get the index in data arrays for an antenna polarization.

        Parameters
        ----------
        jpol : int or str
            Antenna polarization to get index for.

        Returns
        -------
        int
            Antenna polarization index in data arrays
        """
        if isinstance(jpol, (str, np.str_)):
            jpol = uvutils.jstr2num(jpol, x_orientation=self.x_orientation)

        if not self._has_key(jpol=jpol):
            raise ValueError("{} not found in jones_array".format(jpol))

        return np.argmin(np.abs(self.jones_array - jpol))

    def _slice_array(self, key, data_array, squeeze_pol=True):
        """
        Slice a data array given a data key.

        Parameters
        ----------
        key : int or length 2 tuple of ints or int and str
            Antenna or antenna and polarization to get slice for. If it's a length
            2 tuple, the second value must be an antenna polarization int or string
            parsable by jpol2ind.
        data_array : :class: numpy ndarray
            Array to get slice of. Must have the shape of the gain_array or delay_array.
        squeeze_pol : bool
            Option to squeeze pol dimension if possible.

        Returns
        -------
        :class: numpy ndarray
            Slice of the data_array for the key.
        """
        key = uvutils._get_iterable(key)
        if len(key) == 1:
            # interpret as a single antenna
            if self.future_array_shapes:
                output = data_array[self.ant2ind(key[0]), :, :, :]
            else:
                output = data_array[self.ant2ind(key[0]), 0, :, :, :]
            if squeeze_pol and output.shape[-1] == 1:
                output = output[:, :, 0]
            return output
        elif len(key) == 2:
            # interpret as an antenna-pol pair
            if self.future_array_shapes:
                output = data_array[self.ant2ind(key[0]), :, :, self.jpol2ind(key[1])]
            else:
                output = data_array[
                    self.ant2ind(key[0]), 0, :, :, self.jpol2ind(key[1])
                ]
            return output

    def _parse_key(self, ant, jpol=None):
        """
        Parse key inputs and return a standard antenna-polarization key.

        Parameters
        ----------
        ant : int or length 2 tuple of ints or int and str
            Antenna or antenna and polarization to get key for. If it's a length
            2 tuple, the second value must be an antenna polarization int or string
            parsable by jpol2ind.
        jpol : int or str
            Antenna polarization int or string parsable by jpol2ind. Only used
            if `ant` is an integer.

        Returns
        -------
        tuple
            Standard key tuple.

        """
        if isinstance(ant, (list, tuple)):
            # interpret ant as (ant,) or (ant, jpol)
            key = tuple(ant)
        elif isinstance(ant, (int, np.integer)):
            # interpret ant as antenna number
            key = (ant,)
            # add jpol if fed
            if jpol is not None:
                key += (jpol,)

        return key

    def get_gains(self, ant, jpol=None, squeeze_pol=True):
        """
        Get the gain associated with an antenna and/or polarization.

        Parameters
        ----------
        ant : int or length 2 tuple of ints or int and str
            Antenna or antenna and polarization to get gains for. If it's a length
            2 tuple, the second value must be an antenna polarization int or string
            parsable by jpol2ind.
        jpol : int or str, optional
            Instrumental polarization to request. Ex. 'Jxx'
        squeeze_pol : bool
            Option to squeeze pol dimension if possible.

        Returns
        -------
        complex ndarray
            Gain solution of shape (Nfreqs, Ntimes, Njones) or (Nfreqs, Ntimes)
            if jpol is set or if squeeze_pol is True and Njones = 1.
        """
        if self.cal_type != "gain":
            raise ValueError("cal_type must be 'gain' for get_gains() method")

        return self._slice_array(
            self._parse_key(ant, jpol=jpol), self.gain_array, squeeze_pol=squeeze_pol
        )

    def get_flags(self, ant, jpol=None, squeeze_pol=True):
        """
        Get the flags associated with an antenna and/or polarization.

        Parameters
        ----------
        ant : int or length 2 tuple of ints or int and str
            Antenna or antenna and polarization to get gains for. If it's a length
            2 tuple, the second value must be an antenna polarization int or string
            parsable by jpol2ind.
        jpol : int or str, optional
            Instrumental polarization to request. Ex. 'Jxx'
        squeeze_pol : bool
            Option to squeeze pol dimension if possible.

        Returns
        -------
        boolean ndarray
            Flags of shape (Nfreqs, Ntimes, Njones) or (Nfreqs, Ntimes)
            if jpol is set or if squeeze_pol is True and Njones = 1.
        """
        return self._slice_array(
            self._parse_key(ant, jpol=jpol), self.flag_array, squeeze_pol=squeeze_pol
        )

    def get_quality(self, ant, jpol=None, squeeze_pol=True):
        """
        Get the qualities associated with an antenna and/or polarization.

        Parameters
        ----------
        ant : int or length 2 tuple of ints or int and str
            Antenna or antenna and polarization to get gains for. If it's a length
            2 tuple, the second value must be an antenna polarization int or string
            parsable by jpol2ind.
        jpol : int or str, optional
            Instrumental polarization to request. Ex. 'Jxx'
        squeeze_pol : bool
            Option to squeeze pol dimension if possible.

        Returns
        -------
        float ndarray
            Qualities of shape (Nfreqs, Ntimes, Njones) or (Nfreqs, Ntimes)
            if jpol is not None or if squeeze_pol is True and Njones = 1.
        """
        return self._slice_array(
            self._parse_key(ant, jpol=jpol), self.quality_array, squeeze_pol=squeeze_pol
        )

    def reorder_antennas(
        self,
        order="number",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Arrange the antenna axis according to desired order.

        Parameters
        ----------
        order: str or array like of int
            If a string, allowed values are "name" and "number" to sort on the antenna
            name or number respectively. A '-' can be prepended to signify descending
            order instead of the default ascending order (e.g. "-number"). An array of
            integers of length Nants representing indexes along the existing `ant_array`
            can also be supplied to sort in any desired order (note these are indices
            into the `ant_array` not antenna numbers).

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Raised if order is not an allowed string or is an array that does not
            contain all the required numbers.

        """
        if isinstance(order, (np.ndarray, list, tuple)):
            order = np.array(order)
            if not order.size == self.Nants_data or not np.all(
                np.sort(order) == np.arange(self.Nants_data)
            ):
                raise ValueError(
                    "If order is an index array, it must contain all indicies for the"
                    "ant_array, without duplicates."
                )
            index_array = order
        else:
            if order not in ["number", "name", "-number", "-name"]:
                raise ValueError(
                    "order must be one of 'number', 'name', '-number', '-name' or an "
                    "index array of length Nants_data"
                )

            if "number" in order:
                index_array = np.argsort(self.ant_array)
            elif "name" in order:
                temp = np.asarray(self.antenna_names)
                dtype_use = temp.dtype
                name_array = np.zeros_like(self.ant_array, dtype=dtype_use)
                # there has to be a better way to do this without a loop...
                for ind, ant in enumerate(self.ant_array):
                    name_array[ind] = self.antenna_names[
                        np.nonzero(self.antenna_numbers == ant)[0][0]
                    ]
                index_array = np.argsort(name_array)

            if order[0] == "-":
                index_array = np.flip(index_array)

        if np.all(index_array[1:] > index_array[:-1]):
            # Nothing to do - the data are already sorted!
            return

        # update all the relevant arrays
        self.ant_array = self.ant_array[index_array]
        for param_name in self._data_params:
            if param_name == "total_quality_array":
                continue
            param = getattr(self, param_name)
            if param is not None:
                setattr(self, param_name, param[index_array])

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def reorder_freqs(
        self,
        spw_order=None,
        channel_order=None,
        select_spw=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Arrange the frequency axis according to desired order.

        Parameters
        ----------
        spw_order : str or array_like of int
            A string describing the desired order of spectral windows along the
            frequecy axis. Allowed strings include `number` (sort on spectral window
            number) and `freq` (sort on median frequency). A '-' can be prepended
            to signify descending order instead of the default ascending order,
            e.g., if you have SPW #1 and 2, and wanted them ordered as [2, 1],
            you would specify `-number`. Alternatively, one can supply an index array
            of length Nspws that specifies how to shuffle the spws (this is not the
            desired final spw order). Default is to apply no sorting of spectral
            windows.
        channel_order : str or array_like of int
            A string describing the desired order of frequency channels within a
            spectral window. Allowed strings are "freq" and "-freq", which will sort
            channels within a spectral window by ascending or descending frequency
            respectively.  Alternatively, one can supply an index array of length
            Nfreqs that specifies the new order. Default is to apply no sorting of
            channels within a single spectral window. Note that proving an array_like
            of ints will cause the values given to `spw_order` and `select_spw` to be
            ignored.
        select_spw : int or array_like of int
            An int or array_like of ints which specifies which spectral windows to
            apply sorting. Note that setting this argument will cause the value
            given to `spw_order` to be ignored.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Raised if select_spw contains values not in spw_array, or if channel_order
            is not the same length as freq_array.

        """
        if self.Nspws == 1 and self.Nfreqs == 1:
            warnings.warn(
                "Cannot reorder the frequency/spw axis with only one frequency and "
                "spw. Returning the object unchanged."
            )
            return

        if self.wide_band:
            # we have an spw axis, not a frequency axis
            if channel_order is not None or select_spw is not None:
                warnings.warn(
                    "channel_order and select_spws are ignored for wide-band "
                    "calibration solutions"
                )
            if spw_order is None:
                # default sensibly
                spw_order = "number"

            flip_spws = spw_order[0] == "-"

            if isinstance(spw_order, (np.ndarray, list, tuple)):
                spw_order = np.asarray(spw_order)
                if not spw_order.size == self.Nspws or not np.all(
                    np.sort(spw_order) == np.arange(self.Nspws)
                ):
                    raise ValueError(
                        "If spw_order is an array, it must contain all indicies for "
                        "the spw_array, without duplicates."
                    )
                index_array = np.asarray(
                    [
                        np.nonzero(self.spw_array == spw)[0][0]
                        for spw in self.spw_array[spw_order]
                    ]
                )
            else:
                if spw_order not in ["number", "freq", "-number", "-freq", None]:
                    raise ValueError(
                        "spw_order can only be one of 'number', '-number', "
                        "'freq', '-freq', None or an index array of length Nspws"
                    )
                if "number" in spw_order:
                    index_array = np.argsort(self.spw_array)
                elif "freq" in spw_order:
                    mean_freq = np.mean(self.freq_range, axis=1)
                    index_array = np.argsort(mean_freq)

            if flip_spws:
                index_array = np.flip(index_array)

        else:
            index_array = uvutils._sort_freq_helper(
                self.Nfreqs,
                self.freq_array,
                self.Nspws,
                self.spw_array,
                self.flex_spw,
                self.flex_spw_id_array,
                self.future_array_shapes,
                spw_order,
                channel_order,
                select_spw,
            )

            if index_array is None:
                # This only happens if no sorting is needed
                return

        # update all the relevant arrays
        if self.future_array_shapes:
            if self.wide_band:
                self.spw_array = self.spw_array[index_array]
                self.freq_range = self.freq_range[index_array]
            else:
                self.freq_array = self.freq_array[index_array]
            for param_name in self._data_params:
                param = getattr(self, param_name)
                if param is not None:
                    if param_name == "total_quality_array":
                        self.total_quality_array = self.total_quality_array[index_array]
                    else:
                        setattr(self, param_name, param[:, index_array])
        else:
            self.freq_array = self.freq_array[:, index_array]
            if self.cal_type != "delay":
                for param_name in self._data_params:
                    param = getattr(self, param_name)
                    if param is not None:
                        if param_name == "total_quality_array":
                            self.total_quality_array = self.total_quality_array[
                                :, index_array
                            ]
                        else:
                            setattr(self, param_name, param[:, :, index_array])
        if self.flex_spw_id_array is not None:
            self.flex_spw_id_array = self.flex_spw_id_array[index_array]

            if self.Nspws > 1:
                # Reorder the spw-axis items based on their first appearance in the data
                unique_index = np.sort(
                    np.unique(self.flex_spw_id_array, return_index=True)[1]
                )
                self.spw_array = self.flex_spw_id_array[unique_index]

        if (self.future_array_shapes or self.flex_spw) and not self.wide_band:
            self.channel_width = self.channel_width[index_array]

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def reorder_times(
        self,
        order="time",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Arrange the time axis according to desired order.

        Parameters
        ----------
        order: str or array like of int
            If a string, allowed value is "time" or "-time" to sort on the time in
            ascending or descending order respectively. An array of integers of length
            Ntimes representing indexes along the existing `time_array` can also be
            supplied to sort in any desired order.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Raised if order is not an allowed string or is an array that does not
            contain all the required indices.

        """
        if isinstance(order, (np.ndarray, list, tuple)):
            order = np.array(order)
            if not order.size == self.Ntimes or not np.all(
                np.sort(order) == np.arange(self.Ntimes)
            ):
                raise ValueError(
                    "If order is an array, it must contain all indicies for "
                    "the time axis, without duplicates."
                )
            index_array = order
        else:
            if order not in ["time", "-time"]:
                raise ValueError(
                    "order must be one of 'time', '-time' or an "
                    "index array of length Ntimes"
                )

            index_array = np.argsort(self.time_array)

            if order[0] == "-":
                index_array = np.flip(index_array)

        if np.all(index_array[1:] > index_array[:-1]):
            # Nothing to do - the data are already sorted!
            return

        # update all the relevant arrays
        self.time_array = self.time_array[index_array]
        self.lst_array = self.lst_array[index_array]
        if self.future_array_shapes:
            self.integration_time = self.integration_time[index_array]
            for param_name in self._data_params:
                param = getattr(self, param_name)
                if param is not None:
                    if param_name == "total_quality_array":
                        self.total_quality_array = self.total_quality_array[
                            :, index_array
                        ]
                    else:
                        setattr(self, param_name, param[:, :, index_array])
        else:
            for param_name in self._data_params:
                param = getattr(self, param_name)
                if param is not None:
                    if param_name == "total_quality_array":
                        self.total_quality_array = self.total_quality_array[
                            :, :, index_array
                        ]
                    else:
                        setattr(self, param_name, param[:, :, :, index_array])

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def reorder_jones(
        self,
        order="name",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Arrange the jones element axis according to desired order.

        Parameters
        ----------
        order: str or array like of int
            If a string, allowed values are "name" and "number" to sort on the jones
            element name or number respectively. A '-' can be prepended to signify
            descending order instead of the default ascending order (e.g. "-number").
            An array of integers of length Njones representing indexes along the
            existing `jones_array` can also be supplied to sort in any desired order.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Raised if order is not an allowed string or is an array that does not
            contain all the required indices.

        """
        if isinstance(order, (np.ndarray, list, tuple)):
            order = np.array(order)
            if not order.size == self.Njones or not np.all(
                np.sort(order) == np.arange(self.Njones)
            ):
                raise ValueError(
                    "If order is an array, it must contain all indicies for "
                    "the jones axis, without duplicates."
                )
            index_array = order
        else:
            if order not in ["number", "name", "-number", "-name"]:
                raise ValueError(
                    "order must be one of 'number', 'name', '-number', '-name' or an "
                    "index array of length Njones"
                )

            if "number" in order:
                index_array = np.argsort(self.jones_array)
            elif "name" in order:
                name_array = np.asarray(
                    uvutils.jnum2str(self.jones_array, x_orientation=self.x_orientation)
                )
                index_array = np.argsort(name_array)

            if order[0] == "-":
                index_array = np.flip(index_array)

        if np.all(index_array[1:] > index_array[:-1]):
            # Nothing to do - the data are already sorted!
            return

        # update all the relevant arrays
        self.jones_array = self.jones_array[index_array]
        if self.future_array_shapes:
            for param_name in self._data_params:
                param = getattr(self, param_name)
                if param is not None:
                    if param_name == "total_quality_array":
                        self.total_quality_array = self.total_quality_array[
                            :, :, index_array
                        ]
                    else:
                        setattr(self, param_name, param[:, :, :, index_array])
        else:
            for param_name in self._data_params:
                param = getattr(self, param_name)
                if param is not None:
                    if param_name == "total_quality_array":
                        self.total_quality_array = self.total_quality_array[
                            :, :, :, index_array
                        ]
                    else:
                        setattr(self, param_name, param[:, :, :, :, index_array])

        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def convert_to_gain(
        self,
        freq_array=None,
        channel_width=None,
        delay_convention="minus",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Convert non-gain cal_types to gains.

        For the delay cal_type the gain is calculated as:
            gain = 1 * exp((+/-) * 2 * pi * j * delay * frequency)
            where the (+/-) is dictated by the delay_convention

        Parameters
        ----------
        delay_convention : str
            Exponent sign to use in the conversion, can be "plus" or "minus".
        freq_array : array of float
            Frequencies to convert to gain at, units Hz. Not providing a freq_array is
            deprecated, but until version 3.0, if it is not provided and `freq_array`
            exists on the object, `freq_array` will be used.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting.
        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
            converting.

        """
        if self.cal_type == "gain":
            raise ValueError("The data is already a gain cal_type.")
        elif self.cal_type != "delay":
            # TODO remove this when the unknown cal_type is removed.
            raise ValueError("cal_type is unknown, cannot convert to gain")

        if self.Nspws > 1:
            raise ValueError(
                "convert_to_gain currently does not support multiple spectral windows"
            )

        if delay_convention == "minus":
            conv = -1
        elif delay_convention == "plus":
            conv = 1
        else:
            raise ValueError("delay_convention can only be 'minus' or 'plus'")

        if freq_array is None or channel_width is None:
            if self.freq_array is None or self.channel_width is None:
                raise ValueError(
                    "freq_array and channel_width must be provided if there is no "
                    "freq_array or no channel_width on the object."
                )

            warnings.warn(
                "In version 3.0 and later freq_array and channel_width will be "
                "required parameters. Using the freq_array and channel_width on the "
                "object.",
                category=DeprecationWarning,
            )
            if self.future_array_shapes:
                freq_array_use = self.freq_array
                channel_width = self.channel_width
            else:
                freq_array_use = self.freq_array[0, :]
                channel_width = self.channel_width
            Nfreqs_use = self.Nfreqs
        else:
            if freq_array.ndim > 1:
                raise ValueError("freq_array parameter must be a one dimensional array")
            if self.future_array_shapes:
                if (
                    not isinstance(channel_width, np.ndarray)
                    or channel_width.shape != freq_array.shape
                ):
                    raise ValueError(
                        "This object is using the future array shapes, so the "
                        "channel_width parameter be an array shaped like the freq_array"
                    )
            else:
                if isinstance(channel_width, np.ndarray):
                    if channel_width.size > 1:
                        raise ValueError(
                            "This object is using the current array shapes, so the "
                            "channel_width parameter must be a scalar value"
                        )
                    channel_width = channel_width[0]
            if self.freq_range is not None:
                # Already errored if more than one spw, so just use the first one here
                if not self.future_array_shapes:
                    freq_range_use = np.asarray(self.freq_range)
                else:
                    freq_range_use = self.freq_range[0, :]
                if np.any(freq_array < freq_range_use[0]) or np.any(
                    freq_array > freq_range_use[1]
                ):
                    raise ValueError(
                        "freq_array contains values outside the freq_range."
                    )
            freq_array_use = freq_array
            Nfreqs_use = freq_array.size

        self.history += "  Converted from delays to gains using pyuvdata."

        if self.future_array_shapes:
            phase_array = np.zeros(
                (self.Nants_data, Nfreqs_use, self.Ntimes, self.Njones)
            )
        else:
            phase_array = np.zeros(
                (self.Nants_data, 1, Nfreqs_use, self.Ntimes, self.Njones)
            )

        if self.future_array_shapes:
            temp = (
                conv
                * 2
                * np.pi
                * np.dot(
                    self.delay_array[:, 0, :, :, np.newaxis],
                    freq_array_use[np.newaxis, :],
                )
            )
            temp = np.transpose(temp, (0, 3, 1, 2))
            phase_array = temp
        else:
            temp = (
                conv
                * 2
                * np.pi
                * np.dot(
                    self.delay_array[:, 0, 0, :, :, np.newaxis],
                    freq_array_use[np.newaxis, :],
                )
            )
            temp = np.transpose(temp, (0, 3, 1, 2))
            phase_array[:, 0, :, :, :] = temp

        gain_array = np.exp(1j * phase_array)
        if self.future_array_shapes:
            freq_axis = 1
        else:
            freq_axis = 2
        new_quality = np.repeat(self.quality_array, Nfreqs_use, axis=freq_axis)
        self._set_gain()
        self._set_wide_band(wide_band=False)
        self.channel_width = channel_width
        self.gain_array = gain_array
        self.quality_array = new_quality
        self.delay_array = None
        if self.Nfreqs > 1 and not self.future_array_shapes:
            if (
                self.freq_array is None
                or self.Nfreqs != Nfreqs_use
                or not np.allclose(
                    self.freq_array,
                    freq_array_use,
                    rtol=self._freq_array.tols[0],
                    atol=self._freq_array.tols[1],
                )
            ):
                warnings.warn(
                    "Existing flag array has a frequency axis of length > 1 but "
                    "frequencies do not match freq_array. The existing flag array "
                    "(and input_flag_array if it exists) will be collapsed using "
                    "the `pyuvdata.utils.and_collapse` function which will only "
                    "flag an antpol-time if all of the frequecies are flagged for "
                    "that antpol-time. Then it will be broadcast to all the new "
                    "frequencies. To preserve the original flag information, "
                    "create a UVFlag object from this cal object before this "
                    "operation. In the future, these flag arrays will be removed from "
                    "UVCal objects in favor of using UVFlag objects."
                )
                new_flag_array = np.expand_dims(
                    uvutils.and_collapse(self.flag_array, axis=freq_axis),
                    axis=freq_axis,
                )
                self.flag_array = np.repeat(new_flag_array, Nfreqs_use, axis=freq_axis)
                if self.input_flag_array is not None:
                    new_input_flag_array = np.expand_dims(
                        uvutils.and_collapse(self.input_flag_array, axis=freq_axis),
                        axis=freq_axis,
                    )
                    self.input_flag_array = np.repeat(
                        new_input_flag_array, Nfreqs_use, axis=freq_axis
                    )
        else:
            new_flag_array = np.repeat(self.flag_array, Nfreqs_use, axis=freq_axis)
            self.flag_array = new_flag_array
            if self.input_flag_array is not None:
                new_input_flag_array = np.repeat(
                    self.input_flag_array, Nfreqs_use, axis=freq_axis
                )
                self.input_flag_array = new_input_flag_array

        if self.total_quality_array is not None:
            if self.future_array_shapes:
                freq_axis = 0
            else:
                freq_axis = 1
            new_total_quality_array = np.repeat(
                self.total_quality_array, Nfreqs_use, axis=freq_axis
            )
            self.total_quality_array = new_total_quality_array
        if self.future_array_shapes:
            self.freq_array = freq_array_use
        else:
            self.freq_array = freq_array_use[np.newaxis, :]
        self.Nfreqs = Nfreqs_use

        self.flex_spw_id_array = np.full(self.Nfreqs, self.spw_array[0], dtype=int)

        # check if object is self-consistent
        if run_check:
            self.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

    def __add__(
        self,
        other,
        verbose_history=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        inplace=False,
    ):
        """
        Combine two UVCal objects along antenna, frequency, time, and/or Jones axis.

        Parameters
        ----------
        other : :class: UVCal
            Another UVCal object which will be added to self.
        verbose_history : bool
            Option to allow more verbose history. If True and if the histories for the
            two objects are different, the combined object will keep all the history of
            both input objects (if many objects are combined in succession this can
            lead to very long histories). If False and if the histories for the two
            objects are different, the combined object will have the history of the
            first object and only the parts of the second object history that are unique
            (this is done word by word and can result in hard to interpret histories).
        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
            Option to overwrite self as we go, otherwise create a third object
            as the sum of the two.
        """
        if inplace:
            this = self
        else:
            this = self.copy()
        # Check that both objects are UVCal 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 UVCal (or subclass) objects can be added to "
                    "a UVCal (or subclass) object"
                )
        other.check(
            check_extra=check_extra, run_check_acceptability=run_check_acceptability
        )

        # check that both objects have the same array shapes
        if this.future_array_shapes != other.future_array_shapes:
            raise ValueError(
                "Both objects must have the same `future_array_shapes` parameter. "
                "Use the `use_future_array_shapes` or `use_current_array_shapes` "
                "methods to convert them."
            )

        # Check to make sure that both objects are consistent w/ use of flex_spw
        if this.flex_spw != other.flex_spw:
            raise ValueError(
                "To combine these data, flex_spw must be set to the same "
                "value (True or False) for both objects."
            )

        # Check that both objects are either wide_band or not
        if this.wide_band != other.wide_band:
            raise ValueError(
                "To combine these data, wide_band must be set to the same "
                "value (True or False) for both objects."
            )

        this_has_spw_id = this.flex_spw_id_array is not None
        other_has_spw_id = other.flex_spw_id_array is not None
        if this_has_spw_id != other_has_spw_id:
            warnings.warn(
                "One object has the flex_spw_id_array set and one does not. Combined "
                "object will have it set."
            )

        # Check objects are compatible
        compatibility_params = [
            "_cal_type",
            "_telescope_name",
            "_gain_convention",
            "_x_orientation",
            "_cal_style",
            "_ref_antenna_name",
        ]
        if not this.future_array_shapes:
            compatibility_params.append("_integration_time")
            if not this.flex_spw:
                compatibility_params.append("_channel_width")

        warning_params = [
            "_observer",
            "_git_hash_cal",
            "_sky_field",
            "_sky_catalog",
            "_Nsources",
            "_baseline_range",
            "_diffuse_model",
        ]

        for a in compatibility_params:
            if getattr(this, a) != getattr(other, a):
                msg = (
                    "UVParameter " + a[1:] + " does not match. Cannot combine objects."
                )
                raise ValueError(msg)
        for a in warning_params:
            if getattr(this, a) != getattr(other, a):
                msg = "UVParameter " + a[1:] + " does not match. Combining anyway."
                warnings.warn(msg)

        # Build up history string
        history_update_string = " Combined data along "
        n_axes = 0

        # Check we don't have overlapping data
        both_jones, this_jones_ind, other_jones_ind = np.intersect1d(
            this.jones_array, other.jones_array, return_indices=True
        )
        both_times, this_times_ind, other_times_ind = np.intersect1d(
            this.time_array, other.time_array, return_indices=True
        )
        if this.cal_type != "delay" and not this.wide_band:
            # With flexible spectral window, the handling here becomes a bit funky,
            # because we are allowed to have channels with the same frequency *if* they
            # belong to different spectral windows (one real-life example: might want
            # to preserve guard bands in the correlator, which can have overlaping RF
            # frequency channels)
            if this.flex_spw:
                this_freq_ind = np.array([], dtype=np.int64)
                other_freq_ind = np.array([], dtype=np.int64)
                both_freq = np.array([], dtype=float)
                both_spw = np.intersect1d(this.spw_array, other.spw_array)
                for idx in both_spw:
                    this_mask = np.where(this.flex_spw_id_array == idx)[0]
                    other_mask = np.where(other.flex_spw_id_array == idx)[0]
                    if this.future_array_shapes:
                        both_spw_freq, this_spw_ind, other_spw_ind = np.intersect1d(
                            this.freq_array[this_mask],
                            other.freq_array[other_mask],
                            return_indices=True,
                        )
                    else:
                        both_spw_freq, this_spw_ind, other_spw_ind = np.intersect1d(
                            this.freq_array[0, this_mask],
                            other.freq_array[0, other_mask],
                            return_indices=True,
                        )
                    this_freq_ind = np.append(this_freq_ind, this_mask[this_spw_ind])
                    other_freq_ind = np.append(
                        other_freq_ind, other_mask[other_spw_ind]
                    )
                    both_freq = np.append(both_freq, both_spw_freq)
            else:
                if this.future_array_shapes:
                    both_freq, this_freq_ind, other_freq_ind = np.intersect1d(
                        this.freq_array, other.freq_array, return_indices=True
                    )
                else:
                    both_freq, this_freq_ind, other_freq_ind = np.intersect1d(
                        this.freq_array[0, :],
                        other.freq_array[0, :],
                        return_indices=True,
                    )

        elif this.wide_band:
            # this is really about spws, but that replaces the freq axis for wide_band
            both_freq, this_freq_ind, other_freq_ind = np.intersect1d(
                this.spw_array, other.spw_array, return_indices=True
            )
        else:
            # delay type cal
            # Make a non-empty array so we raise an error if other data is duplicated
            both_freq = [0]
            this_freq_ind = []

        both_ants, this_ants_ind, other_ants_ind = np.intersect1d(
            this.ant_array, other.ant_array, return_indices=True
        )
        if len(both_jones) > 0:
            if len(both_times) > 0:
                if len(both_freq) > 0:
                    if len(both_ants) > 0:
                        raise ValueError(
                            "These objects have overlapping data and"
                            " cannot be combined."
                        )

        # Next, we want to make sure that the ordering of the _overlapping_ data is
        # the same, so that things can get plugged together in a sensible way.
        if len(this_ants_ind) != 0:
            this_argsort = np.argsort(this_ants_ind)
            other_argsort = np.argsort(other_ants_ind)

            if np.any(this_argsort != other_argsort):
                temp_ind = np.arange(this.Nants_data)
                temp_ind[this_ants_ind[this_argsort]] = temp_ind[
                    this_ants_ind[other_argsort]
                ]

                this.reorder_antennas(temp_ind)

        if len(this_times_ind) != 0:
            this_argsort = np.argsort(this_times_ind)
            other_argsort = np.argsort(other_times_ind)

            if np.any(this_argsort != other_argsort):
                temp_ind = np.arange(this.Ntimes)
                temp_ind[this_times_ind[this_argsort]] = temp_ind[
                    this_times_ind[other_argsort]
                ]

                this.reorder_times(temp_ind)

        if len(this_freq_ind) != 0:
            this_argsort = np.argsort(this_freq_ind)
            other_argsort = np.argsort(other_freq_ind)
            if np.any(this_argsort != other_argsort):
                if this.wide_band:
                    temp_ind = np.arange(this.Nspws)
                    temp_ind[this_freq_ind[this_argsort]] = temp_ind[
                        this_freq_ind[other_argsort]
                    ]

                    this.reorder_freqs(spw_order=temp_ind)
                else:
                    temp_ind = np.arange(this.Nfreqs)
                    temp_ind[this_freq_ind[this_argsort]] = temp_ind[
                        this_freq_ind[other_argsort]
                    ]

                    this.reorder_freqs(channel_order=temp_ind)

        if len(this_jones_ind) != 0:
            this_argsort = np.argsort(this_jones_ind)
            other_argsort = np.argsort(other_jones_ind)
            if np.any(this_argsort != other_argsort):
                temp_ind = np.arange(this.Njones)
                temp_ind[this_jones_ind[this_argsort]] = temp_ind[
                    this_jones_ind[other_argsort]
                ]

                this.reorder_jones(temp_ind)

        # Update filename parameter
        this.filename = uvutils._combine_filenames(this.filename, other.filename)
        if this.filename is not None:
            this._filename.form = (len(this.filename),)

        temp = np.nonzero(~np.in1d(other.ant_array, this.ant_array))[0]
        if len(temp) > 0:
            anew_inds = temp
            history_update_string += "antenna"
            n_axes += 1
        else:
            anew_inds = []

        temp = np.nonzero(~np.in1d(other.time_array, this.time_array))[0]
        if len(temp) > 0:
            tnew_inds = temp
            if n_axes > 0:
                history_update_string += ", time"
            else:
                history_update_string += "time"
            n_axes += 1
        else:
            tnew_inds = []

        if this.cal_type == "gain" and not this.wide_band:
            # find the freq indices in "other" but not in "this"
            if self.flex_spw:
                other_mask = np.ones_like(other.flex_spw_id_array, dtype=bool)
                for idx in np.intersect1d(this.spw_array, other.spw_array):
                    if this.future_array_shapes:
                        other_mask[other.flex_spw_id_array == idx] = np.isin(
                            other.freq_array[other.flex_spw_id_array == idx],
                            this.freq_array[this.flex_spw_id_array == idx],
                            invert=True,
                        )
                    else:
                        other_mask[other.flex_spw_id_array == idx] = np.isin(
                            other.freq_array[0, other.flex_spw_id_array == idx],
                            this.freq_array[0, this.flex_spw_id_array == idx],
                            invert=True,
                        )
                temp = np.where(other_mask)[0]
            else:
                if this.future_array_shapes:
                    temp = np.nonzero(~np.in1d(other.freq_array, this.freq_array))[0]
                else:
                    temp = np.nonzero(
                        ~np.in1d(other.freq_array[0, :], this.freq_array[0, :])
                    )[0]
            if len(temp) > 0:
                fnew_inds = temp
                if n_axes > 0:
                    history_update_string += ", frequency"
                else:
                    history_update_string += "frequency"
                n_axes += 1
            else:
                fnew_inds = []
        elif this.wide_band:
            temp = np.nonzero(~np.in1d(other.spw_array, this.spw_array))[0]
            if len(temp) > 0:
                fnew_inds = temp
                if n_axes > 0:
                    history_update_string += ", spectral window"
                else:
                    history_update_string += "spectral window"
                n_axes += 1
            else:
                fnew_inds = []
        else:
            # adding along frequency axis is not supported for old delay-type
            # delay type, set fnew_inds to an empty list
            fnew_inds = []

        temp = np.nonzero(~np.in1d(other.jones_array, this.jones_array))[0]
        if len(temp) > 0:
            jnew_inds = temp
            if n_axes > 0:
                history_update_string += ", jones"
            else:
                history_update_string += "jones"
            n_axes += 1
        else:
            jnew_inds = []

        # Initialize tqa variables
        can_combine_tqa = True
        if this.cal_type == "delay":
            Nf_tqa = 1
        else:
            Nf_tqa = this.Nfreqs

        if (
            not self.metadata_only
            and this.input_flag_array is None
            and other.input_flag_array is not None
        ):
            this.input_flag_array = np.full(
                this._input_flag_array.expected_shape(this), True, dtype=bool
            )

        # Pad out self to accommodate new data
        ant_order = None
        if len(anew_inds) > 0:
            this.ant_array = np.concatenate(
                [this.ant_array, other.ant_array[anew_inds]]
            )
            ant_order = np.argsort(this.ant_array)
            if not self.metadata_only:
                if self.future_array_shapes:
                    zero_pad_data = np.zeros(
                        (
                            len(anew_inds),
                            this.quality_array.shape[1],
                            this.Ntimes,
                            this.Njones,
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (
                            len(anew_inds),
                            this.quality_array.shape[1],
                            this.Ntimes,
                            this.Njones,
                        )
                    )
                else:
                    zero_pad_data = np.zeros(
                        (
                            len(anew_inds),
                            1,
                            this.quality_array.shape[2],
                            this.Ntimes,
                            this.Njones,
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (len(anew_inds), 1, this.Nfreqs, this.Ntimes, this.Njones)
                    )
                if this.cal_type == "delay":
                    this.delay_array = np.concatenate(
                        [this.delay_array, zero_pad_data], axis=0
                    )
                else:
                    this.gain_array = np.concatenate(
                        [this.gain_array, zero_pad_data], axis=0
                    )
                this.flag_array = np.concatenate(
                    [this.flag_array, 1 - zero_pad_flags], axis=0
                ).astype(np.bool_)
                this.quality_array = np.concatenate(
                    [this.quality_array, zero_pad_data], axis=0
                )

                # If total_quality_array exists, we set it to None and warn the user
                if (
                    this.total_quality_array is not None
                    or other.total_quality_array is not None
                ):
                    warnings.warn(
                        "Total quality array detected in at least one file; the "
                        "array in the new object will be set to 'None' because "
                        "whole-array values cannot be combined when adding antennas"
                    )
                    this.total_quality_array = None
                    can_combine_tqa = False

                if this.input_flag_array is not None:
                    if self.future_array_shapes:
                        zero_pad = np.zeros(
                            (
                                len(anew_inds),
                                this.quality_array.shape[1],
                                this.Ntimes,
                                this.Njones,
                            )
                        )
                    else:
                        zero_pad = np.zeros(
                            (len(anew_inds), 1, this.Nfreqs, this.Ntimes, this.Njones)
                        )
                    this.input_flag_array = np.concatenate(
                        [this.input_flag_array, 1 - zero_pad], axis=0
                    ).astype(np.bool_)

        f_order = None
        if len(fnew_inds) > 0:
            if this.wide_band:
                this.spw_array = np.concatenate(
                    [this.spw_array, other.spw_array[fnew_inds]]
                )
                this.freq_range = np.concatenate(
                    [this.freq_range, other.freq_range[fnew_inds]], axis=0
                )
            elif self.future_array_shapes:
                this.freq_array = np.concatenate(
                    [this.freq_array, other.freq_array[fnew_inds]]
                )
            else:
                this.freq_array = np.concatenate(
                    [this.freq_array, other.freq_array[:, fnew_inds]], axis=1
                )

            if this.flex_spw and not this.wide_band:
                this.flex_spw_id_array = np.concatenate(
                    [this.flex_spw_id_array, other.flex_spw_id_array[fnew_inds]]
                )
                concat_spw_array = np.concatenate([this.spw_array, other.spw_array])
                # We want to preserve per-spw information based on first appearance
                # in the concatenated array.
                unique_index = np.sort(
                    np.unique(this.flex_spw_id_array, return_index=True)[1]
                )
                this.spw_array = this.flex_spw_id_array[unique_index]
                spw_index = np.asarray(
                    [
                        np.nonzero(concat_spw_array == spw)[0][0]
                        for spw in this.spw_array
                    ]
                )
                if this.freq_range is not None and other.freq_range is not None:
                    if this.future_array_shapes:
                        this.freq_range = np.concatenate(
                            [this.freq_range, other.freq_range], axis=0
                        )
                        this.freq_range = this.freq_range[spw_index, :]
                    else:
                        this.freq_range = [
                            min([this.freq_range[0], other.freq_range[0]]),
                            max([this.freq_range[1], other.freq_range[1]]),
                        ]
                elif this.freq_range is not None or other.freq_range is not None:
                    warnings.warn(
                        "One object has the freq_range set and one does not. Combined "
                        "object will not have it set."
                    )
                    this.freq_range = None

                # If we have a multi-spw data set, need to sort out the order of
                # the individual windows first.
                f_order = np.concatenate(
                    [
                        np.where(this.flex_spw_id_array == idx)[0]
                        for idx in sorted(this.spw_array)
                    ]
                )

                # With spectral windows sorted, check and see if channels within
                # windows need sorting. If they are ordered in ascending or descending
                # fashion, leave them be. If not, sort in ascending order
                for idx in this.spw_array:
                    select_mask = this.flex_spw_id_array[f_order] == idx
                    check_freqs = (
                        this.freq_array[f_order[select_mask]]
                        if this.future_array_shapes
                        else this.freq_array[0, f_order[select_mask]]
                    )
                    if (not np.all(check_freqs[1:] > check_freqs[:-1])) and (
                        not np.all(check_freqs[1:] < check_freqs[:-1])
                    ):
                        subsort_order = f_order[select_mask]
                        f_order[select_mask] = subsort_order[np.argsort(check_freqs)]
                this.spw_array = np.array(sorted(this.spw_array))
            else:
                if this_has_spw_id or other_has_spw_id:
                    this.flex_spw_id_array = np.full(
                        this.freq_array.size, this.spw_array[0], dtype=int
                    )
                if this.wide_band:
                    f_order = np.argsort(this.spw_array)
                else:
                    if this.future_array_shapes:
                        f_order = np.argsort(this.freq_array)
                    else:
                        f_order = np.argsort(this.freq_array[0, :])

                    if this.freq_range is not None and other.freq_range is not None:
                        temp = np.concatenate((this.freq_range, other.freq_range))
                        this.freq_range = [np.min(temp), np.max(temp)]
                        if this.future_array_shapes:
                            this.freq_range = np.array(this.freq_range)[np.newaxis, :]
                    elif this.freq_range is not None or other.freq_range is not None:
                        warnings.warn(
                            "One object has the freq_range set and one does not. "
                            "Combined object will not have it set."
                        )
                        this.freq_range = None

            if not this.wide_band and (this.flex_spw or this.future_array_shapes):
                this.channel_width = np.concatenate(
                    [this.channel_width, other.channel_width[fnew_inds]]
                )

            if not self.metadata_only:
                if self.future_array_shapes:
                    # Exploit the fact that quality array has the same dimensions as the
                    # main data.
                    zero_pad = np.zeros(
                        (
                            this.quality_array.shape[0],
                            len(fnew_inds),
                            this.Ntimes,
                            this.Njones,
                        )
                    )
                    if this.cal_type == "gain":
                        this.gain_array = np.concatenate(
                            [this.gain_array, zero_pad], axis=1
                        )
                    else:
                        this.delay_array = np.concatenate(
                            [this.delay_array, zero_pad], axis=1
                        )

                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=1
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad], axis=1
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros((len(fnew_inds), this.Ntimes, this.Njones))
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=0
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros((len(fnew_inds), this.Ntimes, this.Njones))
                        this.total_quality_array = np.zeros(
                            (Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=0
                        )
                else:
                    zero_pad = np.zeros(
                        (
                            this.quality_array.shape[0],
                            1,
                            len(fnew_inds),
                            this.Ntimes,
                            this.Njones,
                        )
                    )
                    this.gain_array = np.concatenate(
                        [this.gain_array, zero_pad], axis=2
                    )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=2
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad], axis=2
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (1, len(fnew_inds), this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=1
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (1, len(fnew_inds), this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.zeros(
                            (1, Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=1
                        )

                if this.input_flag_array is not None:
                    if self.future_array_shapes:
                        zero_pad = np.zeros(
                            (
                                this.flag_array.shape[0],
                                len(fnew_inds),
                                this.Ntimes,
                                this.Njones,
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=1
                        ).astype(np.bool_)
                    else:
                        zero_pad = np.zeros(
                            (
                                this.flag_array.shape[0],
                                1,
                                len(fnew_inds),
                                this.Ntimes,
                                this.Njones,
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=2
                        ).astype(np.bool_)

        t_order = None
        if len(tnew_inds) > 0:
            # Exploit the fact that quality array has the same dimensions as
            # the main data
            this.time_array = np.concatenate(
                [this.time_array, other.time_array[tnew_inds]]
            )
            this.lst_array = np.concatenate(
                [this.lst_array, other.lst_array[tnew_inds]]
            )
            t_order = np.argsort(this.time_array)
            if self.future_array_shapes:
                this.integration_time = np.concatenate(
                    [this.integration_time, other.integration_time[tnew_inds]]
                )

            if not self.metadata_only:
                if self.future_array_shapes:
                    zero_pad_data = np.zeros(
                        (
                            this.quality_array.shape[0],
                            this.quality_array.shape[1],
                            len(tnew_inds),
                            this.Njones,
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (
                            this.flag_array.shape[0],
                            this.flag_array.shape[1],
                            len(tnew_inds),
                            this.Njones,
                        )
                    )
                    if this.cal_type == "delay":
                        this.delay_array = np.concatenate(
                            [this.delay_array, zero_pad_data], axis=2
                        )
                    else:
                        this.gain_array = np.concatenate(
                            [this.gain_array, zero_pad_data], axis=2
                        )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad_flags], axis=2
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad_data], axis=2
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (this.quality_array.shape[1], len(tnew_inds), this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=1
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (this.quality_array.shape[1], len(tnew_inds), this.Njones)
                        )
                        this.total_quality_array = np.zeros(
                            (Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=1
                        )

                    if this.input_flag_array is not None:
                        zero_pad = np.zeros(
                            (
                                this.input_flag_array.shape[0],
                                this.input_flag_array.shape[1],
                                len(tnew_inds),
                                this.Njones,
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=2
                        ).astype(np.bool_)

                else:
                    zero_pad_data = np.zeros(
                        (
                            this.quality_array.shape[0],
                            1,
                            this.quality_array.shape[2],
                            len(tnew_inds),
                            this.Njones,
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (
                            this.flag_array.shape[0],
                            1,
                            this.flag_array.shape[2],
                            len(tnew_inds),
                            this.Njones,
                        )
                    )
                    if this.cal_type == "delay":
                        this.delay_array = np.concatenate(
                            [this.delay_array, zero_pad_data], axis=3
                        )
                    else:
                        this.gain_array = np.concatenate(
                            [this.gain_array, zero_pad_data], axis=3
                        )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad_flags], axis=3
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad_data], axis=3
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                1,
                                this.quality_array.shape[2],
                                len(tnew_inds),
                                this.Njones,
                            )
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=2
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                1,
                                this.quality_array.shape[2],
                                len(tnew_inds),
                                this.Njones,
                            )
                        )
                        this.total_quality_array = np.zeros(
                            (1, Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=2
                        )

                    if this.input_flag_array is not None:
                        zero_pad = np.zeros(
                            (
                                this.input_flag_array.shape[0],
                                1,
                                this.input_flag_array.shape[2],
                                len(tnew_inds),
                                this.Njones,
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=3
                        ).astype(np.bool_)

        j_order = None
        if len(jnew_inds) > 0:
            # Exploit the fact that quality array has the same dimensions as
            # the main data
            this.jones_array = np.concatenate(
                [this.jones_array, other.jones_array[jnew_inds]]
            )
            j_order = np.argsort(np.abs(this.jones_array))
            if not self.metadata_only:
                if self.future_array_shapes:
                    zero_pad_data = np.zeros(
                        (
                            this.quality_array.shape[0],
                            this.quality_array.shape[1],
                            this.quality_array.shape[2],
                            len(jnew_inds),
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (
                            this.flag_array.shape[0],
                            this.flag_array.shape[1],
                            this.flag_array.shape[2],
                            len(jnew_inds),
                        )
                    )
                    if this.cal_type == "delay":
                        this.delay_array = np.concatenate(
                            [this.delay_array, zero_pad_data], axis=3
                        )
                    else:
                        this.gain_array = np.concatenate(
                            [this.gain_array, zero_pad_data], axis=3
                        )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad_flags], axis=3
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad_data], axis=3
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                this.quality_array.shape[1],
                                this.quality_array.shape[2],
                                len(jnew_inds),
                            )
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=2
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                this.quality_array.shape[1],
                                this.quality_array.shape[2],
                                len(jnew_inds),
                            )
                        )
                        this.total_quality_array = np.zeros(
                            (Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=2
                        )

                    if this.input_flag_array is not None:
                        zero_pad = np.zeros(
                            (
                                this.input_flag_array.shape[0],
                                this.input_flag_array.shape[1],
                                this.input_flag_array.shape[2],
                                len(jnew_inds),
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=3
                        ).astype(np.bool_)
                else:
                    zero_pad_data = np.zeros(
                        (
                            this.quality_array.shape[0],
                            1,
                            this.quality_array.shape[2],
                            this.quality_array.shape[3],
                            len(jnew_inds),
                        )
                    )
                    zero_pad_flags = np.zeros(
                        (
                            this.flag_array.shape[0],
                            1,
                            this.flag_array.shape[2],
                            this.flag_array.shape[3],
                            len(jnew_inds),
                        )
                    )
                    if this.cal_type == "delay":
                        this.delay_array = np.concatenate(
                            [this.delay_array, zero_pad_data], axis=4
                        )
                    else:
                        this.gain_array = np.concatenate(
                            [this.gain_array, zero_pad_data], axis=4
                        )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad_flags], axis=4
                    ).astype(np.bool_)
                    this.quality_array = np.concatenate(
                        [this.quality_array, zero_pad_data], axis=4
                    )

                    if this.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                1,
                                this.quality_array.shape[2],
                                this.quality_array.shape[3],
                                len(jnew_inds),
                            )
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=3
                        )
                    elif other.total_quality_array is not None and can_combine_tqa:
                        zero_pad = np.zeros(
                            (
                                1,
                                this.quality_array.shape[2],
                                this.quality_array.shape[3],
                                len(jnew_inds),
                            )
                        )
                        this.total_quality_array = np.zeros(
                            (1, Nf_tqa, this.Ntimes, this.Njones)
                        )
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array, zero_pad], axis=3
                        )

                    if this.input_flag_array is not None:
                        zero_pad = np.zeros(
                            (
                                this.input_flag_array.shape[0],
                                1,
                                this.input_flag_array.shape[2],
                                this.input_flag_array.shape[3],
                                len(jnew_inds),
                            )
                        )
                        this.input_flag_array = np.concatenate(
                            [this.input_flag_array, 1 - zero_pad], axis=4
                        ).astype(np.bool_)

        # Now populate the data
        if not self.metadata_only:
            jones_t2o = np.nonzero(np.in1d(this.jones_array, other.jones_array))[0]
            times_t2o = np.nonzero(np.in1d(this.time_array, other.time_array))[0]
            if self.wide_band:
                freqs_t2o = np.nonzero(np.in1d(this.spw_array, other.spw_array))[0]
            elif self.future_array_shapes:
                freqs_t2o = np.nonzero(np.in1d(this.freq_array, other.freq_array))[0]
            else:
                if self.cal_type == "gain":
                    freqs_t2o = np.nonzero(
                        np.in1d(this.freq_array[0, :], other.freq_array[0, :])
                    )[0]
                else:
                    freqs_t2o = [0]
            ants_t2o = np.nonzero(np.in1d(this.ant_array, other.ant_array))[0]
            if self.future_array_shapes:
                if this.cal_type == "delay":
                    this.delay_array[
                        np.ix_(ants_t2o, freqs_t2o, times_t2o, jones_t2o)
                    ] = other.delay_array
                else:
                    this.gain_array[
                        np.ix_(ants_t2o, freqs_t2o, times_t2o, jones_t2o)
                    ] = other.gain_array
                this.quality_array[
                    np.ix_(ants_t2o, freqs_t2o, times_t2o, jones_t2o)
                ] = other.quality_array
                this.flag_array[
                    np.ix_(ants_t2o, freqs_t2o, times_t2o, jones_t2o)
                ] = other.flag_array
                if this.total_quality_array is not None:
                    if other.total_quality_array is not None:
                        this.total_quality_array[
                            np.ix_(freqs_t2o, times_t2o, jones_t2o)
                        ] = other.total_quality_array
                if this.input_flag_array is not None:
                    if other.input_flag_array is not None:
                        this.input_flag_array[
                            np.ix_(ants_t2o, freqs_t2o, times_t2o, jones_t2o)
                        ] = other.input_flag_array
            else:
                if this.cal_type == "delay":
                    this.delay_array[
                        np.ix_(ants_t2o, [0], [0], times_t2o, jones_t2o)
                    ] = other.delay_array
                    this.quality_array[
                        np.ix_(ants_t2o, [0], [0], times_t2o, jones_t2o)
                    ] = other.quality_array
                else:
                    this.gain_array[
                        np.ix_(ants_t2o, [0], freqs_t2o, times_t2o, jones_t2o)
                    ] = other.gain_array
                    this.quality_array[
                        np.ix_(ants_t2o, [0], freqs_t2o, times_t2o, jones_t2o)
                    ] = other.quality_array
                this.flag_array[
                    np.ix_(ants_t2o, [0], freqs_t2o, times_t2o, jones_t2o)
                ] = other.flag_array
                if this.total_quality_array is not None:
                    if other.total_quality_array is not None:
                        if this.cal_type == "delay":
                            this.total_quality_array[
                                np.ix_([0], [0], times_t2o, jones_t2o)
                            ] = other.total_quality_array
                        else:
                            this.total_quality_array[
                                np.ix_([0], freqs_t2o, times_t2o, jones_t2o)
                            ] = other.total_quality_array
                if this.input_flag_array is not None:
                    if other.input_flag_array is not None:
                        this.input_flag_array[
                            np.ix_(ants_t2o, [0], freqs_t2o, times_t2o, jones_t2o)
                        ] = other.input_flag_array

            # Fix ordering
            ant_axis_num = 0
            if this.future_array_shapes:
                faxis_num = 1
                taxis_num = 2
                jaxis_num = 3
            else:
                faxis_num = 2
                taxis_num = 3
                jaxis_num = 4

            axis_dict = {
                ant_axis_num: {"inds": anew_inds, "order": ant_order},
                faxis_num: {"inds": fnew_inds, "order": f_order},
                taxis_num: {"inds": tnew_inds, "order": t_order},
                jaxis_num: {"inds": jnew_inds, "order": j_order},
            }
            for axis, subdict in axis_dict.items():
                for name, param in zip(this._data_params, this.data_like_parameters):
                    if param is None:
                        continue
                    axis_delta = 0
                    if name == "total_quality_array":
                        axis_delta = 1

                    if len(subdict["inds"]) > 0:
                        unique_order_diffs = np.unique(np.diff(subdict["order"]))
                        if np.array_equal(unique_order_diffs, np.array([1])):
                            # everything is already in order
                            continue
                        setattr(
                            this,
                            name,
                            np.take(param, subdict["order"], axis=axis - axis_delta),
                        )

        if len(anew_inds) > 0:
            this.ant_array = this.ant_array[ant_order]
        if len(fnew_inds) > 0:
            if this.wide_band:
                this.spw_array = this.spw_array[f_order]
                this.freq_range = this.freq_range[f_order]
            else:
                if this.future_array_shapes:
                    this.freq_array = this.freq_array[f_order]
                else:
                    this.freq_array = this.freq_array[:, f_order]
                if this.flex_spw or this.future_array_shapes:
                    this.channel_width = this.channel_width[f_order]
                if this.flex_spw_id_array is not None:
                    this.flex_spw_id_array = this.flex_spw_id_array[f_order]
        if len(tnew_inds) > 0:
            this.time_array = this.time_array[t_order]
            this.lst_array = this.lst_array[t_order]
            if self.future_array_shapes:
                this.integration_time = this.integration_time[t_order]
        if len(jnew_inds) > 0:
            this.jones_array = this.jones_array[j_order]

        # Update N parameters (e.g. Njones)
        this.Njones = this.jones_array.shape[0]
        this.Ntimes = this.time_array.shape[0]
        if this.cal_type == "gain" and not this.wide_band:
            this.Nfreqs = this.freq_array.size
        this.Nspws = this.spw_array.size
        this.Nants_data = len(
            np.unique(this.ant_array.tolist() + other.ant_array.tolist())
        )

        # Check specific requirements
        if this.cal_type == "gain" and this.Nfreqs > 1:
            spacing_error, chanwidth_error = this._check_freq_spacing(
                raise_errors=False
            )

            if spacing_error:
                warnings.warn(
                    "Combined frequencies are not evenly spaced or have differing "
                    "values of channel widths. This will make it impossible to write "
                    "this data out to some file types."
                )
            elif chanwidth_error:
                warnings.warn(
                    "Combined frequencies are separated by more than their "
                    "channel width. This will make it impossible to write this data "
                    "out to some file types."
                )

        if this.Njones > 2:
            if not uvutils._test_array_constant_spacing(this._jones_array):
                warnings.warn(
                    "Combined Jones elements are not evenly spaced. This will "
                    "make it impossible to write this data out to some file types."
                )

        if n_axes > 0:
            history_update_string += " axis using pyuvdata."

            histories_match = uvutils._check_histories(this.history, other.history)

            this.history += history_update_string
            if not histories_match:
                if verbose_history:
                    this.history += " Next object history follows. " + other.history
                else:
                    extra_history = uvutils._combine_history_addition(
                        this.history, other.history
                    )
                    if extra_history is not None:
                        this.history += (
                            " Unique part of next object history follows. "
                            + extra_history
                        )

        # 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 __iadd__(
        self, other, run_check=True, check_extra=True, run_check_acceptability=True
    ):
        """
        Combine two UVCal objects in place.

        Along antenna, frequency, time, and/or Jones axis.

        Parameters
        ----------
        other : :class: UVCal
            Another UVCal 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.
        """
        self.__add__(
            other,
            inplace=True,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
        )
        return self

    def fast_concat(
        self,
        other,
        axis,
        inplace=False,
        verbose_history=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Concatenate two UVCal objects along specified axis with almost no checking.

        Warning! This method assumes all the metadata along other axes is sorted
        the same way. The __add__ method is much safer, it checks all the metadata,
        but it is slower. Some quick checks are run, but this method doesn't
        make any guarantees that the resulting object is correct.

        Parameters
        ----------
        other : UVCal object or list of UVCal objects
            UVCal object or list of UVCal objects which will be added to self.
        axis : str
            Axis to concatenate files along. This enables fast concatenation
            along the specified axis without the normal checking that all other
            metadata agrees. Allowed values are: 'antenna', 'time', 'freq', 'spw',
            'jones' ('freq' is not allowed for delay or wideband objects and 'spw' is
            only allowed for wideband objects).
        inplace : bool
            If True, overwrite self as we go, otherwise create a third object
            as the sum of the two.
        verbose_history : bool
            Option to allow more verbose history. If True and if the histories for the
            two objects are different, the combined object will keep all the history of
            both input objects (if many objects are combined in succession this can
            lead to very long histories). If False and if the histories for the two
            objects are different, the combined object will have the history of the
            first object and only the parts of the second object history that are unique
            (this is done word by word and can result in hard to interpret histories).
        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.

        """
        if inplace:
            this = self
        else:
            this = self.copy()
        if not isinstance(other, (list, tuple, np.ndarray)):
            # if this is a UVCal object already, stick it in a list
            other = [other]
        # Check that both objects are UVCal and valid
        this.check(
            check_extra=check_extra, run_check_acceptability=run_check_acceptability
        )
        for obj in other:
            if not issubclass(obj.__class__, this.__class__):
                if not issubclass(this.__class__, obj.__class__):
                    raise ValueError(
                        "Only UVCal (or subclass) objects can be "
                        "added to a UVCal (or subclass) object"
                    )
            obj.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

        # check that all objects have the same array shapes
        for obj in other:
            if this.future_array_shapes != obj.future_array_shapes:
                raise ValueError(
                    "All objects must have the same `future_array_shapes` parameter. "
                    "Use the `use_future_array_shapes` or `use_current_array_shapes` "
                    "methods to convert them."
                )

        # Check that all objects are consistent w/ use of flex_spw
        for obj in other:
            if this.flex_spw != obj.flex_spw:
                raise ValueError(
                    "To combine these data, flex_spw must be set to the same "
                    "value (True or False) for all objects."
                )

        # Check that all objects are consistent w/ use of wide_band or not
        for obj in other:
            if this.wide_band != obj.wide_band:
                raise ValueError(
                    "To combine these data, wide_band must be set to the same "
                    "value (True or False) for all objects."
                )

        this_has_spw_id = this.flex_spw_id_array is not None
        other_has_spw_id = np.array(
            [obj.flex_spw_id_array is not None for obj in other]
        )
        if not np.all(other_has_spw_id == this_has_spw_id):
            warnings.warn(
                "Some objects have the flex_spw_id_array set and some do not. Combined "
                "object will have it set."
            )

        allowed_axes = ["antenna", "time", "jones"]
        if this.wide_band is True:
            allowed_axes.append("spw")
        elif self.cal_type == "gain":
            allowed_axes.append("freq")
        if axis not in allowed_axes:
            raise ValueError("Axis must be one of: " + ", ".join(allowed_axes))

        # Check objects are compatible
        compatibility_params = [
            "_cal_type",
            "_telescope_name",
            "_gain_convention",
            "_x_orientation",
            "_cal_style",
            "_ref_antenna_name",
        ]
        if not this.future_array_shapes:
            compatibility_params.append("_integration_time")
            if not this.flex_spw:
                compatibility_params.append("_channel_width")

        warning_params = [
            "_observer",
            "_git_hash_cal",
            "_sky_field",
            "_sky_catalog",
            "_Nsources",
            "_baseline_range",
            "_diffuse_model",
        ]

        history_update_string = " Combined data along "

        if axis == "freq" or axis == "spw":
            if axis == "freq":
                history_update_string += "frequency"
            else:
                history_update_string += "spectral window"
            compatibility_params += [
                "_jones_array",
                "_ant_array",
                "_time_array",
                "_integration_time",
                "_lst_array",
                "_time_range",
            ]
        elif axis == "jones":
            history_update_string += "jones"
            compatibility_params += [
                "_freq_array",
                "_channel_width",
                "_ant_array",
                "_time_array",
                "_integration_time",
                "_lst_array",
                "_time_range",
            ]
        elif axis == "antenna":
            history_update_string += "antenna"
            compatibility_params += [
                "_freq_array",
                "_channel_width",
                "_jones_array",
                "_time_array",
                "_integration_time",
                "_lst_array",
                "_time_range",
            ]
        elif axis == "time":
            history_update_string += "time"
            compatibility_params += [
                "_freq_array",
                "_channel_width",
                "_jones_array",
                "_ant_array",
            ]
            if not this.future_array_shapes:
                compatibility_params += ["_integration_time"]

        history_update_string += " axis using pyuvdata."
        histories_match = []
        for obj in other:
            histories_match.append(uvutils._check_histories(this.history, obj.history))

        this.history += history_update_string
        for obj_num, obj in enumerate(other):
            if not histories_match[obj_num]:
                if verbose_history:
                    this.history += " Next object history follows. " + obj.history
                else:
                    extra_history = uvutils._combine_history_addition(
                        this.history, obj.history
                    )
                    if extra_history is not None:
                        this.history += (
                            " Unique part of next object history follows. "
                            + extra_history
                        )
        # Actually check compatibility parameters
        for obj in other:
            for a in compatibility_params:
                params_match = getattr(this, a) == getattr(obj, a)
                if not params_match:
                    msg = (
                        "UVParameter "
                        + a[1:]
                        + " does not match. Cannot combine objects."
                    )
                    raise ValueError(msg)

            for a in warning_params:
                params_match = getattr(this, a) == getattr(obj, a)
                if not params_match:
                    msg = "UVParameter " + a[1:] + " does not match. Combining anyway."
                    warnings.warn(msg)

        total_quality_exists = [this.total_quality_array is not None] + [
            obj.total_quality_array is not None for obj in other
        ]
        this_tqa_exp_shape = this._total_quality_array.expected_shape(this)

        input_flag_exists = [this.input_flag_array is not None] + [
            obj.input_flag_array is not None for obj in other
        ]
        this_ifa_exp_shape = this._input_flag_array.expected_shape(this)

        freq_range_exists = [this.freq_range is not None] + [
            obj.freq_range is not None for obj in other
        ]

        if axis == "antenna":
            this.Nants_data = sum([this.Nants_data] + [obj.Nants_data for obj in other])
            this.ant_array = np.concatenate(
                [this.ant_array] + [obj.ant_array for obj in other]
            )
            axis_num = 0
        elif axis == "freq":
            this.Nfreqs = sum([this.Nfreqs] + [obj.Nfreqs for obj in other])
            if this.future_array_shapes:
                this.freq_array = np.concatenate(
                    [this.freq_array] + [obj.freq_array for obj in other]
                )
            else:
                this.freq_array = np.concatenate(
                    [this.freq_array] + [obj.freq_array for obj in other], axis=1
                )
            if this.flex_spw or this.future_array_shapes:
                this.channel_width = np.concatenate(
                    [this.channel_width] + [obj.channel_width for obj in other]
                )
            if this.flex_spw:
                this.flex_spw_id_array = np.concatenate(
                    [this.flex_spw_id_array] + [obj.flex_spw_id_array for obj in other]
                )
                concat_spw_array = np.concatenate(
                    [this.spw_array] + [obj.spw_array for obj in other]
                )
                # We want to preserve per-spw information based on first appearance
                # in the concatenated array.
                unique_index = np.sort(
                    np.unique(this.flex_spw_id_array, return_index=True)[1]
                )
                this.spw_array = this.flex_spw_id_array[unique_index]
                this.Nspws = this.spw_array.size
                spw_index = np.asarray(
                    [
                        np.nonzero(concat_spw_array == spw)[0][0]
                        for spw in this.spw_array
                    ]
                )
                if np.all(freq_range_exists):
                    if this.future_array_shapes:
                        this.freq_range = np.concatenate(
                            [this.freq_range] + [obj.freq_range for obj in other],
                            axis=0,
                        )
                        this.freq_range = this.freq_range[spw_index, :]
                    else:
                        temp = np.concatenate(
                            ([this.freq_range] + [obj.freq_range for obj in other])
                        )
                        this.freq_range = [np.min(temp), np.max(temp)]
                elif np.any(freq_range_exists):
                    warnings.warn(
                        "Some objects have the freq_range set and some do not. "
                        "Combined object will not have it set."
                    )
                    this.freq_range = None
            else:
                if this_has_spw_id or other_has_spw_id:
                    this.flex_spw_id_array = np.full(
                        this.Nfreqs, this.spw_array[0], dtype=int
                    )
                if np.all(freq_range_exists):
                    temp = np.concatenate(
                        ([this.freq_range] + [obj.freq_range for obj in other])
                    )
                    this.freq_range = [np.min(temp), np.max(temp)]
                    if this.future_array_shapes:
                        this.freq_range = np.array(this.freq_range)
                        this.freq_range = this.freq_range[np.newaxis, :]
                elif np.any(freq_range_exists):
                    warnings.warn(
                        "Some objects have the freq_range set and and some do not. "
                        "Combined object will not have it set."
                    )
                    this.freq_range = None

            spacing_error, chanwidth_error = this._check_freq_spacing(
                raise_errors=False
            )
            if spacing_error:
                warnings.warn(
                    "Combined frequencies are not evenly spaced or have differing "
                    "values of channel widths. This will make it impossible to write "
                    "this data out to some file types."
                )
            elif chanwidth_error:
                warnings.warn(
                    "Combined frequencies are separated by more than their "
                    "channel width. This will make it impossible to write this data "
                    "out to some file types."
                )

            if this.future_array_shapes:
                axis_num = 1
            else:
                axis_num = 2
        elif axis == "spw":
            # only get here for a wide-band cal (so only if future_array_shapes is True)
            this.Nspws = sum([this.Nspws] + [obj.Nspws for obj in other])
            this.spw_array = np.concatenate(
                [this.spw_array] + [obj.spw_array for obj in other]
            )
            this.freq_range = np.concatenate(
                [this.freq_range] + [obj.freq_range for obj in other], axis=0
            )
            axis_num = 1
        elif axis == "time":
            this.Ntimes = sum([this.Ntimes] + [obj.Ntimes for obj in other])
            this.time_array = np.concatenate(
                [this.time_array] + [obj.time_array for obj in other]
            )
            if this.future_array_shapes:
                this.integration_time = np.concatenate(
                    [this.integration_time] + [obj.integration_time for obj in other]
                )
            this.lst_array = np.concatenate(
                [this.lst_array] + [obj.lst_array for obj in other]
            )
            if this.future_array_shapes:
                axis_num = 2
            else:
                axis_num = 3
        elif axis == "jones":
            this.Njones = sum([this.Njones] + [obj.Njones for obj in other])
            this.jones_array = np.concatenate(
                [this.jones_array] + [obj.jones_array for obj in other]
            )
            if this.future_array_shapes:
                axis_num = 3
            else:
                axis_num = 4

        if not self.metadata_only:
            this.quality_array = np.concatenate(
                [this.quality_array] + [obj.quality_array for obj in other],
                axis=axis_num,
            )
            this.flag_array = np.concatenate(
                [this.flag_array] + [obj.flag_array for obj in other], axis=axis_num
            )

            if this.cal_type == "gain":
                this.gain_array = np.concatenate(
                    [this.gain_array] + [obj.gain_array for obj in other], axis=axis_num
                )
            else:
                this.delay_array = np.concatenate(
                    [this.delay_array] + [obj.delay_array for obj in other],
                    axis=axis_num,
                )

            if np.any(input_flag_exists):
                if np.all(input_flag_exists):
                    this.input_flag_array = np.concatenate(
                        [this.input_flag_array]
                        + [obj.input_flag_array for obj in other],
                        axis=axis_num,
                    )
                else:
                    ifa_list = []
                    if this.input_flag_array is None:
                        ifa_list.append(np.full(this_ifa_exp_shape, True, dtype=bool))
                    else:
                        ifa_list.append(this.input_flag_array)
                    for obj in other:
                        if obj.input_flag_array is None:
                            ifa_list.append(
                                np.full(
                                    (obj._input_flag_array.expected_shape(obj)),
                                    True,
                                    dtype=bool,
                                )
                            )
                        else:
                            ifa_list.append(obj.input_flag_array)
                    this.input_flag_array = np.concatenate(ifa_list, axis=axis_num)

            if np.any(total_quality_exists):
                if axis == "antenna":
                    warnings.warn(
                        "Total quality array detected in at least one file; the "
                        "array in the new object will be set to 'None' because "
                        "whole-array values cannot be combined when adding antennas"
                    )
                    this.total_quality_array = None
                else:
                    if np.all(total_quality_exists):
                        this.total_quality_array = np.concatenate(
                            [this.total_quality_array]
                            + [obj.total_quality_array for obj in other],
                            axis=axis_num - 1,
                        )
                    else:
                        tqa_list = []
                        if this.total_quality_array is None:
                            tqa_list.append(np.zeros(this_tqa_exp_shape, dtype=float))
                        else:
                            tqa_list.append(this.total_quality_array)
                        for obj in other:
                            if obj.total_quality_array is None:
                                tqa_list.append(
                                    np.zeros(
                                        (obj._total_quality_array.expected_shape(obj)),
                                        dtype=float,
                                    )
                                )
                            else:
                                tqa_list.append(obj.total_quality_array)
                        this.total_quality_array = np.concatenate(
                            tqa_list, axis=axis_num - 1
                        )

        # update filename attribute
        for obj in other:
            this.filename = uvutils._combine_filenames(this.filename, obj.filename)
        if this.filename is not None:
            this._filename.form = len(this.filename)

        # 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 select(
        self,
        antenna_nums=None,
        antenna_names=None,
        frequencies=None,
        freq_chans=None,
        spws=None,
        times=None,
        jones=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        inplace=True,
    ):
        """
        Downselect data to keep on the object along various axes.

        Axes that can be selected along include antennas, frequencies, times and
        antenna polarization (jones).

        The history attribute on the object will be updated to identify the
        operations performed.

        Parameters
        ----------
        antenna_nums : array_like of int, optional
            The antennas numbers to keep in the object (antenna positions and
            names for the removed antennas will be retained).
            This cannot be provided if `antenna_names` is also provided.
        antenna_names : array_like of str, optional
            The antennas names to keep in the object (antenna positions and
            names for the removed antennas will be retained).
            This cannot be provided if `antenna_nums` is also provided.
        frequencies : array_like of float, optional
            The frequencies to keep in the object, each value passed here should
            exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to keep in the object.
        spws : array_like of in, optional
            The spectral window numbers to keep in the object. If this is not a
            wide-band object and `frequencies` or `freq_chans` is not None, frequencies
            that match any of the specifications will be kept (i.e. the selections will
            be OR'ed together).
        times : array_like of float, optional
            The times to keep in the object, each value passed here should
            exist in the time_array.
        jones : array_like of int or str, optional
            The antenna polarizations numbers to keep in the object, each value
            passed here should exist in the jones_array. If passing strings, the
            canonical polarization strings (e.g. "Jxx", "Jrr") are supported and if the
            `x_orientation` attribute is set, the physical dipole strings
            (e.g. "Jnn", "Jee") are also supported.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after downselecting data on this object (the default is True,
            meaning the check will be run).
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            downselecting data on this object (the default is True, meaning the
            acceptable range check will be done).
        inplace : bool
            Option to perform the select directly on self or return a new UVCal
            object with just the selected data (the default is True, meaning the
            select will be done on self).
        """
        if inplace:
            cal_object = self
        else:
            cal_object = self.copy()

        # build up history string as we go
        history_update_string = "  Downselected to specific "
        n_selects = 0

        if antenna_names is not None:
            if antenna_nums is not None:
                raise ValueError(
                    "Only one of antenna_nums and antenna_names can be provided."
                )

            antenna_names = uvutils._get_iterable(antenna_names)
            antenna_nums = []
            for s in antenna_names:
                if s not in cal_object.antenna_names:
                    raise ValueError(
                        f"Antenna name {s} is not present in the antenna_names array"
                    )
                ind = np.where(np.array(cal_object.antenna_names) == s)[0][0]
                antenna_nums.append(cal_object.antenna_numbers[ind])

        if antenna_nums is not None:
            antenna_nums = uvutils._get_iterable(antenna_nums)
            history_update_string += "antennas"
            n_selects += 1

            ant_inds = np.zeros(0, dtype=np.int64)
            for ant in antenna_nums:
                if ant in cal_object.ant_array:
                    ant_inds = np.append(
                        ant_inds, np.where(cal_object.ant_array == ant)[0]
                    )
                else:
                    raise ValueError(
                        f"Antenna number {ant} is not present in the array"
                    )

            ant_inds = sorted(set(ant_inds))
            cal_object.Nants_data = len(ant_inds)
            cal_object.ant_array = cal_object.ant_array[ant_inds]
            if not self.metadata_only:
                cal_object.flag_array = cal_object.flag_array[ant_inds]
                cal_object.quality_array = cal_object.quality_array[ant_inds]
                if cal_object.cal_type == "delay":
                    cal_object.delay_array = cal_object.delay_array[ant_inds]
                else:
                    cal_object.gain_array = cal_object.gain_array[ant_inds]

                if cal_object.input_flag_array is not None:
                    cal_object.input_flag_array = cal_object.input_flag_array[ant_inds]

                if cal_object.total_quality_array is not None:
                    warnings.warn(
                        "Cannot preserve total_quality_array when changing "
                        "number of antennas; discarding"
                    )
                    cal_object.total_quality_array = None

        if times is not None:
            times = uvutils._get_iterable(times)
            if n_selects > 0:
                history_update_string += ", times"
            else:
                history_update_string += "times"
            n_selects += 1

            time_inds = np.zeros(0, dtype=np.int64)
            for jd in times:
                if jd in cal_object.time_array:
                    time_inds = np.append(
                        time_inds, np.where(cal_object.time_array == jd)[0]
                    )
                else:
                    raise ValueError(
                        "Time {t} is not present in the time_array".format(t=jd)
                    )

            time_inds = sorted(set(time_inds))
            cal_object.Ntimes = len(time_inds)
            cal_object.time_array = cal_object.time_array[time_inds]
            if cal_object.lst_array is not None:
                cal_object.lst_array = cal_object.lst_array[time_inds]
            if self.future_array_shapes:
                cal_object.integration_time = cal_object.integration_time[time_inds]

            if cal_object.Ntimes > 1:
                if not uvutils._test_array_constant_spacing(cal_object._time_array):
                    warnings.warn(
                        "Selected times are not evenly spaced. This "
                        "is not supported by the calfits format."
                    )

            if not self.metadata_only:
                if self.future_array_shapes:
                    cal_object.flag_array = cal_object.flag_array[:, :, time_inds, :]
                    cal_object.quality_array = cal_object.quality_array[
                        :, :, time_inds, :
                    ]
                    if cal_object.cal_type == "delay":
                        cal_object.delay_array = cal_object.delay_array[
                            :, :, time_inds, :
                        ]
                    else:
                        cal_object.gain_array = cal_object.gain_array[
                            :, :, time_inds, :
                        ]

                    if cal_object.input_flag_array is not None:
                        cal_object.input_flag_array = cal_object.input_flag_array[
                            :, :, time_inds, :
                        ]

                    if cal_object.total_quality_array is not None:
                        cal_object.total_quality_array = cal_object.total_quality_array[
                            :, time_inds, :
                        ]
                else:
                    cal_object.flag_array = cal_object.flag_array[:, :, :, time_inds, :]
                    cal_object.quality_array = cal_object.quality_array[
                        :, :, :, time_inds, :
                    ]
                    if cal_object.cal_type == "delay":
                        cal_object.delay_array = cal_object.delay_array[
                            :, :, :, time_inds, :
                        ]
                    else:
                        cal_object.gain_array = cal_object.gain_array[
                            :, :, :, time_inds, :
                        ]

                    if cal_object.input_flag_array is not None:
                        cal_object.input_flag_array = cal_object.input_flag_array[
                            :, :, :, time_inds, :
                        ]

                    if cal_object.total_quality_array is not None:
                        cal_object.total_quality_array = cal_object.total_quality_array[
                            :, :, time_inds, :
                        ]

        if spws is not None:
            if cal_object.Nspws == 1:
                warnings.warn(
                    "Cannot select on spws if Nspws=1. Ignoring the spw parameter."
                )
            else:
                if not cal_object.wide_band:
                    assert cal_object.flex_spw is True, (
                        "The `flex_spw` parameter must be True if there are multiple "
                        "spectral windows and the `wide_band` parameter is not True."
                    )
                    # Translate the spws into frequencies
                    if frequencies is None:
                        if self.future_array_shapes:
                            frequencies = self.freq_array[
                                np.isin(cal_object.flex_spw_id_array, spws)
                            ]
                        else:
                            frequencies = self.freq_array[
                                0, np.isin(cal_object.flex_spw_id_array, spws)
                            ]
                else:
                    assert self.future_array_shapes, (
                        "The `future_array_shapes` parameter must be True if the "
                        "`wide_band` parameter is True"
                    )
                    if n_selects > 0:
                        history_update_string += ", spectral windows"
                    else:
                        history_update_string += "spectral windows"
                    n_selects += 1

                    # Check and see that all requested spws are available
                    spw_check = np.isin(spws, cal_object.spw_array)
                    if not np.all(spw_check):
                        raise ValueError(
                            f"SPW number {spws[np.where(~spw_check)[0][0]]} is not "
                            "present in the spw_array"
                        )

                    spw_inds = np.where(np.isin(cal_object.spw_array, spws))[0]

                    spw_inds = sorted(set(spw_inds))
                    cal_object.Nspws = len(spw_inds)
                    cal_object.freq_range = cal_object.freq_range[spw_inds, :]
                    cal_object.spw_array = cal_object.spw_array[spw_inds]

                    if not cal_object.metadata_only:
                        if cal_object.cal_type == "delay":
                            cal_object.delay_array = cal_object.delay_array[
                                :, spw_inds, :, :
                            ]
                        else:
                            cal_object.gain_array = cal_object.gain_array[
                                :, spw_inds, :, :
                            ]

                        cal_object.flag_array = cal_object.flag_array[:, spw_inds, :, :]
                        if cal_object.input_flag_array is not None:
                            cal_object.input_flag_array = cal_object.input_flag_array[
                                :, spw_inds, :, :
                            ]
                        cal_object.quality_array = cal_object.quality_array[
                            :, spw_inds, :, :
                        ]
                        if cal_object.total_quality_array is not None:
                            tqa = cal_object.total_quality_array[spw_inds, :, :]
                            cal_object.total_quality_array = tqa

        if self.freq_array is None and (
            freq_chans is not None or frequencies is not None
        ):
            raise ValueError(
                "Cannot select on frequencies because this is a wide_band object with "
                "no freq_array."
            )
        if freq_chans is not None:
            freq_chans = uvutils._get_iterable(freq_chans)
            if frequencies is None:
                if self.future_array_shapes:
                    frequencies = cal_object.freq_array[freq_chans]
                else:
                    frequencies = cal_object.freq_array[0, freq_chans]
            else:
                frequencies = uvutils._get_iterable(frequencies)
                if self.future_array_shapes:
                    frequencies = np.sort(
                        list(set(frequencies) | set(cal_object.freq_array[freq_chans]))
                    )
                else:
                    frequencies = np.sort(
                        list(
                            set(frequencies) | set(cal_object.freq_array[0, freq_chans])
                        )
                    )

        if frequencies is not None:
            frequencies = uvutils._get_iterable(frequencies)
            if n_selects > 0:
                history_update_string += ", frequencies"
            else:
                history_update_string += "frequencies"
            n_selects += 1

            if cal_object.future_array_shapes:
                freq_arr_use = self.freq_array
            else:
                freq_arr_use = self.freq_array[0, :]

            # Check and see that all requested freqs are available
            freq_check = np.isin(frequencies, freq_arr_use)
            if not np.all(freq_check):
                raise ValueError(
                    f"Frequency {frequencies[np.where(~freq_check)[0][0]]} is not "
                    "present in the freq_array"
                )
            freq_inds = np.where(np.isin(freq_arr_use, frequencies))[0]

            freq_inds = sorted(set(freq_inds))
            cal_object.Nfreqs = len(freq_inds)
            if cal_object.future_array_shapes:
                cal_object.freq_array = cal_object.freq_array[freq_inds]
            else:
                cal_object.freq_array = cal_object.freq_array[:, freq_inds]

            if cal_object.future_array_shapes or cal_object.flex_spw:
                cal_object.channel_width = cal_object.channel_width[freq_inds]

            if cal_object.flex_spw_id_array is not None:
                cal_object.flex_spw_id_array = cal_object.flex_spw_id_array[freq_inds]

            if cal_object.flex_spw:
                spw_mask = np.isin(cal_object.spw_array, cal_object.flex_spw_id_array)
                cal_object.spw_array = cal_object.spw_array[spw_mask]
                cal_object.Nspws = cal_object.spw_array.size
                if cal_object.freq_range is not None and cal_object.future_array_shapes:
                    cal_object.freq_range = np.zeros(
                        (cal_object.Nspws, 2), dtype=cal_object.freq_array.dtype
                    )
                    for index, spw in enumerate(cal_object.spw_array):
                        spw_inds = np.nonzero(cal_object.flex_spw_id_array == spw)[0]
                        cal_object.freq_range[index, 0] = np.min(
                            cal_object.freq_array[spw_inds]
                        )
                        cal_object.freq_range[index, 1] = np.max(
                            cal_object.freq_array[spw_inds]
                        )
            else:
                if cal_object.freq_range is not None:
                    cal_object.freq_range = [
                        np.min(cal_object.freq_array),
                        np.max(cal_object.freq_array),
                    ]
                    if cal_object.future_array_shapes:
                        cal_object.freq_range = np.asarray(cal_object.freq_range)[
                            np.newaxis, :
                        ]

            if cal_object.Nfreqs > 1:
                spacing_error, chanwidth_error = cal_object._check_freq_spacing(
                    raise_errors=False
                )
                if spacing_error:
                    warnings.warn(
                        "Selected frequencies are not evenly spaced. This "
                        "will make it impossible to write this data out to "
                        "some file types"
                    )
                elif chanwidth_error:
                    warnings.warn(
                        "Selected frequencies are not contiguous. This "
                        "will make it impossible to write this data out to "
                        "some file types."
                    )

            if not cal_object.metadata_only:
                if not cal_object.future_array_shapes:
                    cal_object.flag_array = cal_object.flag_array[:, :, freq_inds, :, :]
                    if cal_object.input_flag_array is not None:
                        cal_object.input_flag_array = cal_object.input_flag_array[
                            :, :, freq_inds, :, :
                        ]

                if cal_object.cal_type == "delay":
                    pass
                else:
                    if cal_object.future_array_shapes:
                        cal_object.flag_array = cal_object.flag_array[
                            :, freq_inds, :, :
                        ]
                        if cal_object.input_flag_array is not None:
                            cal_object.input_flag_array = cal_object.input_flag_array[
                                :, freq_inds, :, :
                            ]
                        cal_object.quality_array = cal_object.quality_array[
                            :, freq_inds, :, :
                        ]
                        cal_object.gain_array = cal_object.gain_array[
                            :, freq_inds, :, :
                        ]

                        if cal_object.total_quality_array is not None:
                            tqa = cal_object.total_quality_array[freq_inds, :, :]
                            cal_object.total_quality_array = tqa
                    else:
                        cal_object.quality_array = cal_object.quality_array[
                            :, :, freq_inds, :, :
                        ]
                        cal_object.gain_array = cal_object.gain_array[
                            :, :, freq_inds, :, :
                        ]

                        if cal_object.total_quality_array is not None:
                            tqa = cal_object.total_quality_array[:, freq_inds, :, :]
                            cal_object.total_quality_array = tqa

        if jones is not None:
            jones = uvutils._get_iterable(jones)
            if np.array(jones).ndim > 1:
                jones = np.array(jones).flatten()
            if n_selects > 0:
                history_update_string += ", jones polarization terms"
            else:
                history_update_string += "jones polarization terms"
            n_selects += 1

            jones_inds = np.zeros(0, dtype=np.int64)
            for j in jones:
                if isinstance(j, str):
                    j_num = uvutils.jstr2num(j, x_orientation=self.x_orientation)
                else:
                    j_num = j
                if j_num in cal_object.jones_array:
                    jones_inds = np.append(
                        jones_inds, np.where(cal_object.jones_array == j_num)[0]
                    )
                else:
                    raise ValueError(
                        "Jones term {j} is not present in the jones_array".format(j=j)
                    )

            jones_inds = sorted(set(jones_inds))
            cal_object.Njones = len(jones_inds)
            cal_object.jones_array = cal_object.jones_array[jones_inds]
            if len(jones_inds) > 2:
                jones_separation = (
                    cal_object.jones_array[1:] - cal_object.jones_array[:-1]
                )
                if not uvutils._test_array_constant(jones_separation):
                    warnings.warn(
                        "Selected jones polarization terms are not evenly spaced. This "
                        "is not supported by the calfits format"
                    )

            if not cal_object.metadata_only:
                if cal_object.future_array_shapes:
                    cal_object.flag_array = cal_object.flag_array[:, :, :, jones_inds]
                    cal_object.quality_array = cal_object.quality_array[
                        :, :, :, jones_inds
                    ]
                    if cal_object.cal_type == "delay":
                        cal_object.delay_array = cal_object.delay_array[
                            :, :, :, jones_inds
                        ]
                    else:
                        cal_object.gain_array = cal_object.gain_array[
                            :, :, :, jones_inds
                        ]

                    if cal_object.input_flag_array is not None:
                        cal_object.input_flag_array = cal_object.input_flag_array[
                            :, :, :, jones_inds
                        ]

                    if cal_object.total_quality_array is not None:
                        cal_object.total_quality_array = cal_object.total_quality_array[
                            :, :, jones_inds
                        ]
                else:
                    cal_object.flag_array = cal_object.flag_array[
                        :, :, :, :, jones_inds
                    ]
                    cal_object.quality_array = cal_object.quality_array[
                        :, :, :, :, jones_inds
                    ]
                    if cal_object.cal_type == "delay":
                        cal_object.delay_array = cal_object.delay_array[
                            :, :, :, :, jones_inds
                        ]
                    else:
                        cal_object.gain_array = cal_object.gain_array[
                            :, :, :, :, jones_inds
                        ]

                    if cal_object.input_flag_array is not None:
                        cal_object.input_flag_array = cal_object.input_flag_array[
                            :, :, :, :, jones_inds
                        ]

                    if cal_object.total_quality_array is not None:
                        cal_object.total_quality_array = cal_object.total_quality_array[
                            :, :, :, jones_inds
                        ]

        if n_selects > 0:
            history_update_string += " using pyuvdata."
            cal_object.history = cal_object.history + history_update_string

        # check if object is self-consistent
        if run_check:
            cal_object.check(
                check_extra=check_extra, run_check_acceptability=run_check_acceptability
            )

        if not inplace:
            return cal_object

    def _convert_from_filetype(self, other):
        for p in other:
            param = getattr(other, p)
            setattr(self, p, param)

    def _convert_to_filetype(self, filetype):
        if filetype == "calfits":
            from . import calfits

            other_obj = calfits.CALFITS()
        else:
            raise ValueError("filetype must be calfits.")
        for p in self:
            param = getattr(self, p)
            setattr(other_obj, p, param)
        return other_obj

    @classmethod
    def initialize_from_uvdata(
        cls,
        uvdata,
        gain_convention,
        cal_style,
        future_array_shapes=True,
        metadata_only=True,
        include_uvdata_history=True,
        cal_type="gain",
        times=None,
        integration_time=None,
        time_range=None,
        frequencies=None,
        channel_width=None,
        flex_spw=None,
        flex_spw_id_array=None,
        wide_band=None,
        freq_range=None,
        spw_array=None,
        jones=None,
        ref_antenna_name=None,
        sky_catalog=None,
        sky_field=None,
        diffuse_model=None,
        baseline_range=None,
        Nsources=None,  # noqa
        observer=None,
        gain_scale=None,
        git_hash_cal=None,
        git_origin_cal=None,
        extra_keywords=None,
    ):
        """
        Initialize this object based on a UVData object.

        Parameters
        ----------
        uvdata : UVData object
            The UVData object to initialize from.
        gain_convention : str
            What gain convention the UVCal object should be initialized to
            ("multiply" or "divide").
        cal_style : str
            What calibration style the UVCal object should be initialized to
            ("sky" or "redundant").
        future_array_shapes : bool
            Option to use the future array shapes (see `use_future_array_shapes`
            for details).
        metadata_only : bool
            Option to only initialize the metadata. If False, this method also
            initializes the data-like arrays to zeros (or False for the flag_array)
            with the appropriate sizes.
        include_uvdata_history : bool
            Option to include the history from the uvdata object in the uvcal history.
        cal_type : str
            What cal_type the UVCal object should be initialized to
            ("gain", or "delay").
        times : array_like of float, optional
            Calibration times in decimal Julian date. If None, use all unique times from
            uvdata.
        integration_time : float or array_like of float, optional
            Calibration integration time in seconds, an array of shape (Ntimes,)
            or a scalar if `future_array_shapes` is False. Required if `time_array` is
            not None, ignored otherwise.
        time_range : array_like of float, optional
            Range of times that calibration is valid for in decimal Julian dates,
            shape (2,). Should only be set if `time_array` is size (1,)
        frequencies : array_like of float, optional
            Calibration frequencies (units Hz), shape (Nfreqs,). Defaulted to the
            freq_array from uvdata if `cal_type="gain"` and `wide_band` is not set to
            `True`. Ignored if `cal_type="delay"` or `wide_band=True`.
        channel_width : float or array_like of float, optional
            Calibration channel width in Hz, an array of shape (Nfreqs,)
            or a scalar if `future_array_shapes` is False. Required if freq_array is
            not None and `cal_type="gain"` and `wide_band` is not set to
            `True`, ignored otherwise.
        flex_spw : bool, optional
            Option to use flexible spectral windows. Ignored if freq_array is None or
            `cal_type="delay"` or `wide_band=True`.
        flex_spw_id_array : array_like of int, optional
            Array giving the spectral window value for each frequency channel,
            shape (Nfreqs,). Ignored if freq_array is None or `cal_type="delay"` or
            `wide_band=True`. Required if freq_array is not None and flex_spw is True
            and `cal_type="gain"` and `wide_band` is not set to `True`.
        wide_band : bool, optional
            Option to use wide-band calibration. Requires `future_array_shapes` to be
            `True`. Defaulted to `True` if `future_array_shapes` is True and
            `cal_type="delay"`, defaulted to `False` otherwise.
        freq_range : array_like of float, optional
            Frequency range that solutions are valid for in Hz, shape (Nspws, 2) if
            `future_array_shapes` is True, shape (2,) otherwise.
            Defaulted to the min, max of freq_array if `wide_band` is True or
            `cal_type="delay"`. Defaulting is done per spectral window if uvdata has
            multiple spectral windows and `future_array_shapes` is True.
        spw_array : array_like of int, optional
            Array giving the spectral window numbers. Required if either `wide_band` is
            True or `cal_type="delay"` and if freq_range is not None and has multiple
            spectral windows, ignored otherwise. Defaulted to uvdata.spw_array if
            either `wide_band` is True or `cal_type="delay"` and if freq_range is None.
        jones : array_like of int, optional
            Calibration Jones elements. If None, defaults to [-5, -6] (jxx, jyy) if
            uvdata is in linear pol. [-1, -2] (jrr, jll) if uvdata is in circular pol.
            A ValueError is raised if jones_array is None and uvdata is in
            psuedo-stokes.
        ref_antenna_name : str, optional
            Phase reference antenna, required if cal_style = "sky".
        sky_catalog : str, optional
            Name of calibration catalog, required if cal_sky = "sky".
        sky_field : str, optional
            Short string describing field center or dominant source, required if
            cal_sky = "sky".
        diffuse_model : str, optional
            Name of diffuse model.
        baseline_range : array_like of float, optional
            Range of baselines used for calibration.
        Nsources : int, optional
            Number of sources used.
        observer : str, optional
            Name of observer who calculated calibration solutions.
        gain_scale : str, optional
            The gain scale of the calibration, which indicates the units of the
            calibrated visibilities. For example, Jy or K str.
        git_hash_cal : str, optional
            Commit hash of calibration software (from git_origin_cal) used to generate
            solutions.
        git_origin_cal : str, optional
            Origin (on github for e.g) of calibration software. Url and branch.
        extra_keywords : dict, optional
            Any user supplied extra keywords, type=dict.

        Raises
        ------
        ValueError
            If cal_style is 'sky' and ref_antenna_name, sky_catalog or sky_field are not
            provided;
            if freq_array is not None, flex_spw is True and flex_spw_id_array is None;
            if freq_array and channel_width are None and the uvdata object does not use
            flexible spectral windows and the uvdata channel width varies;
            if time_array and integration_time are None and the uvdata integration
            time varies;
            if time_array is not None and integration_time is not specified or is the
            wrong type;
            if jones_array is None and uvdata is in psuedo-stokes.

        """
        if not issubclass(type(uvdata), UVData):
            raise ValueError("uvdata must be a UVData (or subclassed) object.")

        uvc = cls()

        if cal_type not in ["delay", "gain"]:
            raise ValueError("cal_type must be either 'gain' or 'delay'.")

        if cal_type == "gain":
            uvc._set_gain()
        elif cal_type == "delay":
            uvc._set_delay()

        if future_array_shapes:
            uvc._set_future_array_shapes()

        if wide_band is not None:
            uvc._set_wide_band(wide_band=wide_band)

        uvc.cal_style = cal_style
        uvc.gain_convention = gain_convention

        if cal_style == "sky" and (
            ref_antenna_name is None or sky_catalog is None or sky_field is None
        ):
            raise ValueError(
                "If cal_style is 'sky', ref_antenna_name, sky_catalog and sky_field "
                "must all be provided."
            )
        if ref_antenna_name is not None:
            uvc.ref_antenna_name = ref_antenna_name
        if sky_catalog is not None:
            uvc.sky_catalog = sky_catalog
        if sky_field is not None:
            uvc.sky_field = sky_field
        if diffuse_model is not None:
            uvc.diffuse_model = diffuse_model
        if baseline_range is not None:
            uvc.baseline_range = baseline_range
        if Nsources is not None:
            uvc.Nsources = Nsources
        if observer is not None:
            uvc.observer = observer
        if gain_scale is not None:
            uvc.gain_scale = gain_scale
        if git_hash_cal is not None:
            uvc.git_hash_cal = git_hash_cal
        if git_origin_cal is not None:
            uvc.git_origin_cal = git_origin_cal
        if extra_keywords is not None:
            uvc.extra_keywords = extra_keywords

        params_to_copy = [
            "telescope_name",
            "telescope_location",
            "antenna_numbers",
            "antenna_names",
            "antenna_positions",
            "Nants_telescope",
            "Nants_data",
            "x_orientation",
        ]
        if uvc.cal_type != "delay" and not uvc.wide_band:
            if frequencies is None:
                params_to_copy.extend(["Nfreqs", "flex_spw", "spw_array", "Nspws"])
                if uvdata.flex_spw:
                    uvc._set_flex_spw()
                if uvdata.future_array_shapes == uvc.future_array_shapes:
                    params_to_copy.extend(["freq_array"])
                else:
                    if uvc.future_array_shapes:
                        uvc.freq_array = uvdata.freq_array[0, :]
                    else:
                        uvc.freq_array = uvdata.freq_array[np.newaxis, :]

                if not future_array_shapes:
                    uvc.freq_range = [
                        np.min(uvdata.freq_array),
                        np.max(uvdata.freq_array),
                    ]

                if (
                    uvdata.flex_spw
                    or uvdata.future_array_shapes == uvc.future_array_shapes
                ):
                    params_to_copy.extend(["channel_width"])
                else:
                    if uvc.future_array_shapes:
                        uvc.channel_width = np.full(
                            uvc.freq_array.size, uvdata.channel_width, dtype=np.float64
                        )
                    else:
                        uvdata_channel_widths = np.unique(uvdata.channel_width)
                        if uvdata_channel_widths.size == 1:
                            uvc.channel_width = uvdata_channel_widths[0]
                        else:
                            raise ValueError(
                                "uvdata has varying channel widths but does not have "
                                "flexible spectral windows and future_array_shapes is "
                                "False. Please specify frequencies and channel_width."
                            )
                if uvdata.flex_spw_id_array is not None:
                    params_to_copy.extend(["flex_spw_id_array"])
                else:
                    # default sensibly
                    uvc.flex_spw_id_array = np.full(
                        uvdata.Nfreqs, uvdata.spw_array[0], dtype=int
                    )
            else:
                if frequencies.ndim != 1:
                    raise ValueError("Frequencies must be a 1 dimensional array")

                if future_array_shapes:
                    uvc.freq_array = frequencies
                else:
                    uvc.freq_array = frequencies[np.newaxis, :]
                    uvc.freq_range = [np.min(frequencies), np.max(frequencies)]

                uvc.Nfreqs = frequencies.size

                if flex_spw:
                    uvc._set_flex_spw()
                    if flex_spw_id_array is None:
                        raise ValueError(
                            "If frequencies is provided and flex_spw is True, a "
                            "flex_spw_id_array must be provided."
                        )
                    uvc.flex_spw_id_array = flex_spw_id_array
                    uvc.spw_array = np.unique(uvc.flex_spw_id_array)
                    uvc.Nspws = uvc.spw_array.size
                else:
                    uvc.spw_array = np.array([0])
                    uvc.Nspws = 1
                    if flex_spw_id_array is not None:
                        uvc.flex_spw_id_array = flex_spw_id_array
                    else:
                        # default sensibly
                        uvc.flex_spw_id_array = np.zeros(uvc.Nfreqs, dtype=int)
                if channel_width is None:
                    raise ValueError(
                        "channel_width must be provided if frequencies is provided"
                    )
                if future_array_shapes or flex_spw:
                    if isinstance(channel_width, (np.ndarray, list)):
                        uvc.channel_width = np.asarray(channel_width)
                    else:
                        uvc.channel_width = np.full(
                            uvc.Nfreqs, channel_width, dtype=np.float64
                        )
                else:
                    if isinstance(channel_width, (np.ndarray, list)):
                        raise ValueError(
                            "channel_width must be scalar if both future_array_shapes "
                            "and flex_spw are False."
                        )
                    uvc.channel_width = channel_width
        else:
            uvc.Nfreqs = 1
            if freq_range is None:
                if uvc.future_array_shapes:
                    params_to_copy.extend(["spw_array", "Nspws"])
                    if uvdata.flex_spw:
                        uvc.freq_range = np.zeros((uvdata.Nspws, 2), dtype=float)
                        for spw_ind, spw in enumerate(uvdata.spw_array):
                            if uvdata.future_array_shapes:
                                freqs_in_spw = uvdata.freq_array[
                                    np.nonzero(uvdata.flex_spw_id_array == spw)
                                ]
                            else:
                                freqs_in_spw = uvdata.freq_array[
                                    0, np.nonzero(uvdata.flex_spw_id_array == spw)
                                ]
                            uvc.freq_range[spw_ind, :] = np.asarray(
                                [np.min(freqs_in_spw), np.max(freqs_in_spw)]
                            )
                    else:
                        uvc.freq_range = np.asarray(
                            [[np.min(uvdata.freq_array), np.max(uvdata.freq_array)]]
                        )
                else:
                    uvc.Nspws = 1
                    uvc.spw_array = np.asarray([0])
                    uvc.freq_range = [
                        np.min(uvdata.freq_array),
                        np.max(uvdata.freq_array),
                    ]
                    uvc.freq_array = np.asarray([[uvdata.freq_array.item(0)]])
                    uvc.Nfreqs = 1
            else:
                freq_range_use = np.asarray(freq_range)
                if future_array_shapes:
                    if freq_range_use.shape == (2,):
                        freq_range_use = freq_range_use[np.newaxis, :]
                    if freq_range_use.ndim != 2 or freq_range_use.shape[1] != 2:
                        raise ValueError(
                            "if future_array_shapes is True, freq_range must be an "
                            "array shaped like (Nspws, 2)."
                        )
                    uvc.freq_range = freq_range_use
                    uvc.Nspws = uvc.freq_range.shape[0]
                    if uvc.Nspws > 1:
                        if spw_array is None:
                            raise ValueError(
                                "An spw_array must be provided for delay or wide-band "
                                "cals if freq_range has multiple spectral windows"
                            )
                        uvc.spw_array = spw_array
                    else:
                        uvc.spw_array = np.asarray([0])
                else:
                    uvc.Nspws = 1
                    uvc.spw_array = np.asarray([0])
                    if freq_range_use.size == 2:
                        uvc.freq_range = freq_range_use.tolist()
                    else:
                        raise ValueError(
                            "if future_array_shapes is False, freq_range must have "
                            "2 elements."
                        )
                    uvc.freq_array = np.asarray([[freq_range_use[0]]])
                    uvc.Nfreqs = 1

        for param_name in params_to_copy:
            setattr(uvc, param_name, getattr(uvdata, param_name))

        # sort the antenna information (the order in the UVData object may be strange)
        ant_order = np.argsort(uvc.antenna_numbers)
        uvc.antenna_numbers = uvc.antenna_numbers[ant_order]
        uvc.antenna_names = ((np.asarray(uvc.antenna_names))[ant_order]).tolist()
        uvc.antenna_positions = uvc.antenna_positions[ant_order, :]

        if times is None:
            # get all unique times
            uvc.time_array = np.unique(uvdata.time_array)
            uvdata_int_times = np.unique(uvdata.integration_time)
            if uvdata_int_times.size == 1:
                uvdata_int_times = uvdata_int_times[0]
                if uvc.future_array_shapes:
                    uvc.integration_time = np.full(
                        uvc.time_array.size, uvdata_int_times, dtype=np.float64
                    )
                else:
                    uvc.integration_time = uvdata_int_times
            else:
                raise ValueError(
                    "uvdata integration times vary. Please specify times and "
                    "integration_time"
                )
        else:
            uvc.time_array = times
            if integration_time is None:
                raise ValueError(
                    "integation_time must be provided if times is provided"
                )
            if future_array_shapes:
                if isinstance(integration_time, (np.ndarray, list)):
                    uvc.integration_time = np.asarray(integration_time)
                else:
                    uvc.integration_time = np.full(
                        uvc.time_array.size, integration_time, dtype=np.float64
                    )
            else:
                if isinstance(integration_time, (np.ndarray, list)):
                    raise ValueError(
                        "integration_time must be scalar if future_array_shapes is "
                        "False."
                    )
                uvc.integration_time = integration_time
        uvc.Ntimes = uvc.time_array.size
        uvc.set_lsts_from_time_array()

        if time_range is not None:
            uvc.time_range = time_range

        if jones is None:
            if np.all(uvdata.polarization_array < -4):
                if uvdata.Npols == 1 and uvdata.polarization_array[0] > -7:
                    # single pol data, make a single pol cal object
                    uvc.jones_array = uvdata.polarization_array
                else:
                    uvc.jones_array = np.array([-5, -6])
            elif np.all(uvdata.polarization_array < 0):
                if uvdata.Npols == 1 and uvdata.polarization_array[0] > -3:
                    # single pol data, make a single pol cal object
                    uvc.jones_array = uvdata.polarization_array
                else:
                    uvc.jones_array = np.array([-1, -2])
            else:
                raise ValueError(
                    "jones parameter is None and uvdata object is in "
                    "psuedo-stokes polarization. Please set jones."
                )
        else:
            uvc.jones_array = np.asarray(jones)
        uvc.Njones = uvc.jones_array.size

        uvc.ant_array = np.union1d(uvdata.ant_1_array, uvdata.ant_2_array)

        uvc.history = "Initialized from a UVData object with pyuvdata."
        if include_uvdata_history:
            uvc.history += " UVData history is: " + uvdata.history

        if not metadata_only:
            for param in uvc._required_data_params:
                uvparam = getattr(uvc, "_" + param)
                expected_type = uvparam.expected_type
                # all data like params on UVCal have expected types that are tuples.
                # since uvc is re-initialized at the start of this method, the user
                # can't affect this, so don't need handling for non-tuples
                dtype_use = expected_type[0]
                setattr(
                    uvc, param, np.zeros(uvparam.expected_shape(uvc), dtype=dtype_use)
                )

        uvc.check()

        return uvc

    def read_calfits(
        self,
        filename,
        read_data=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        use_future_array_shapes=False,
    ):
        """
        Read in data from calfits file(s).

        Parameters
        ----------
        filename : str or list of str
            The calfits file(s) to read from.
        read_data : bool
            Read in the gains or delays, quality arrays and flag arrays.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metadata only object.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        """
        from . import calfits

        if isinstance(filename, (list, tuple)):
            warnings.warn(
                "Reading multiple files from file specific read methods is deprecated. "
                "Use the generic `UVCal.read` method instead. This will become an "
                "error in version 2.5",
                DeprecationWarning,
            )

            # cannot just call `read` here and let it handle the recursion because we
            # can get a max recursion depth error. So leave the old handling for
            # recursion until v2.5
            self.read_calfits(
                filename[0],
                read_data=read_data,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                use_future_array_shapes=use_future_array_shapes,
            )
            if len(filename) > 1:
                for f in filename[1:]:
                    uvcal2 = UVCal()
                    uvcal2.read_calfits(
                        f,
                        read_data=read_data,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                        use_future_array_shapes=use_future_array_shapes,
                    )
                    self += uvcal2
                del uvcal2
        else:
            calfits_obj = calfits.CALFITS()
            calfits_obj.read_calfits(
                filename,
                read_data=read_data,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                use_future_array_shapes=use_future_array_shapes,
            )
            self._convert_from_filetype(calfits_obj)
            del calfits_obj

    def read_fhd_cal(
        self,
        cal_file,
        obs_file,
        layout_file=None,
        settings_file=None,
        raw=True,
        read_data=True,
        extra_history=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        use_future_array_shapes=False,
    ):
        """
        Read data from an FHD cal.sav file.

        Parameters
        ----------
        cal_file : str or list of str
            The cal.sav file or list of files to read from.
        obs_file : str or list of str
            The obs.sav file or list of files to read from.
        layout_file : str
            The FHD layout file. Required for antenna_positions to be set.
        settings_file : str or list of str, optional
            The settings_file or list of files to read from. Optional,
            but very useful for provenance.
        raw : bool
            Option to use the raw (per antenna, per frequency) solution or
            to use the fitted (polynomial over phase/amplitude) solution.
            Default is True (meaning use the raw solutions).
        read_data : bool
            Read in the gains, quality array and flag data. If set to False, only
            the metadata will be read in. Setting read_data to False results in
            a metadata only object. Note that if read_data is False, metadata is
            derived entirely from the obs_file, which may result in slightly different
            values than if it is derived from the cal file.
        extra_history : str or list of str, optional
            String(s) to add to the object's history parameter.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        """
        from . import fhd_cal

        if (
            isinstance(cal_file, (list, tuple))
            or isinstance(obs_file, (list, tuple))
            or isinstance(layout_file, (list, tuple))
            or isinstance(settings_file, (list, tuple))
        ):
            warnings.warn(
                "Reading multiple files from file specific read methods is deprecated. "
                "Use the generic `UVCal.read` method instead. This will become an "
                "error in version 2.5",
                DeprecationWarning,
            )
            # cannot just call `read` here and let it handle the recursion because we
            # can get a max recursion depth error. So leave the old handling for
            # recursion until v2.5

        if isinstance(cal_file, (list, tuple)):
            if isinstance(obs_file, (list, tuple)):
                if len(obs_file) != len(cal_file):
                    raise ValueError(
                        "Number of obs_files must match number of cal_files"
                    )
            else:
                raise ValueError("Number of obs_files must match number of cal_files")

            if layout_file is not None:
                if isinstance(layout_file, (list, tuple)):
                    if len(layout_file) != len(cal_file):
                        raise ValueError(
                            "Number of layout_files must match number of cal_files"
                        )
                else:
                    raise ValueError(
                        "Number of layout_files must match number of cal_files"
                    )
                layout_file_use = layout_file[0]
            else:
                layout_file_use = None

            if settings_file is not None:
                if isinstance(settings_file, (list, tuple)):
                    if len(settings_file) != len(cal_file):
                        raise ValueError(
                            "Number of settings_files must match number of cal_files"
                        )
                else:
                    raise ValueError(
                        "Number of settings_files must match number of cal_files"
                    )
                settings_file_use = settings_file[0]
            else:
                settings_file_use = None

            self.read_fhd_cal(
                cal_file[0],
                obs_file[0],
                layout_file=layout_file_use,
                settings_file=settings_file_use,
                raw=raw,
                read_data=read_data,
                extra_history=extra_history,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                use_future_array_shapes=use_future_array_shapes,
            )
            if len(cal_file) > 1:
                for ind, f in enumerate(cal_file[1:]):
                    uvcal2 = UVCal()
                    if settings_file is not None:
                        settings_file_use = settings_file[ind + 1]
                    if layout_file is not None:
                        layout_file_use = layout_file[ind + 1]
                    uvcal2.read_fhd_cal(
                        f,
                        obs_file[ind + 1],
                        layout_file=layout_file_use,
                        settings_file=settings_file_use,
                        raw=raw,
                        read_data=read_data,
                        extra_history=extra_history,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                        use_future_array_shapes=use_future_array_shapes,
                    )

                    self += uvcal2
                del uvcal2
        else:
            if isinstance(obs_file, (list, tuple)):
                raise ValueError("Number of obs_files must match number of cal_files")
            if layout_file is not None:
                if isinstance(layout_file, (list, tuple)) and len(layout_file) > 1:
                    raise ValueError(
                        "Number of layout_files must match number of cal_files"
                    )
            if settings_file is not None:
                if isinstance(settings_file, (list, tuple)) and len(settings_file) > 1:
                    raise ValueError(
                        "Number of settings_files must match number of cal_files"
                    )

            fhd_cal_obj = fhd_cal.FHDCal()
            fhd_cal_obj.read_fhd_cal(
                cal_file,
                obs_file,
                layout_file=layout_file,
                settings_file=settings_file,
                raw=raw,
                read_data=read_data,
                extra_history=extra_history,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                use_future_array_shapes=use_future_array_shapes,
            )
            self._convert_from_filetype(fhd_cal_obj)
            del fhd_cal_obj

    def read(
        self,
        filename,
        *,
        axis=None,
        file_type=None,
        read_data=True,
        use_future_array_shapes=False,
        # checking parameters
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        # file-type specific parameters
        # FHD
        obs_file=None,
        layout_file=None,
        settings_file=None,
        raw=True,
        extra_history=None,
    ):
        """
        Read a generic file into a UVCal object.

        This method supports a number of different types of files.
        Universal parameters (required and optional) are listed directly below,
        followed by parameters used by all file types related to checking. Each file
        type also has its own set of optional parameters that are listed at the end of
        this docstring.

        Parameters
        ----------
        filename : str or array_like of str
            The file(s) or list(s) (or array(s)) of files to read from.
        file_type : str
            One of ['calfits', 'fhd'] or None. If None, the code attempts to guess what
            the file type is based on file extensions (FHD: .sav, .txt;
            uvfits: .calfits). Note that if a list of datasets is passed, the file type
            is determined from the first dataset.
        axis : str
            Axis to concatenate files along. This enables fast concatenation
            along the specified axis without the normal checking that all other
            metadata agrees. This method does not guarantee correct resulting
            objects. Please see the docstring for fast_concat for details.
            Allowed values are: 'antenna', 'time', 'freq', 'spw', 'jones' ('freq' is
            not allowed for delay or wideband objects and 'spw' is only allowed for
            wideband objects). Only used if multiple files are passed.
        read_data : bool
            Read in the gains or delays, quality arrays and flag arrays.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metadata only object.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        Checking
        --------
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.

        FHD
        ---
        obs_file : str or list of str
            The obs.sav file or list of files to read from. This is required for FHD
            files.
        layout_file : str
            The FHD layout file. Required for antenna_positions to be set.
        settings_file : str or list of str, optional
            The settings_file or list of files to read from. Optional,
            but very useful for provenance.
        raw : bool
            Option to use the raw (per antenna, per frequency) solution or
            to use the fitted (polynomial over phase/amplitude) solution.
            Default is True (meaning use the raw solutions).

        """
        if isinstance(filename, (list, tuple, np.ndarray)):
            for ind in range(len(filename)):
                if isinstance(filename[ind], (list, tuple, np.ndarray)):
                    raise ValueError(
                        "If filename is a list, tuple or array it cannot be nested or "
                        "multi dimensional."
                    )
            _, extension = os.path.splitext(filename[0])
            n_files = len(filename)
        else:
            _, extension = os.path.splitext(filename)
            n_files = 1

        multi = False
        if n_files > 1:
            multi = True
        elif isinstance(filename, (list, tuple, np.ndarray)):
            filename = filename[0]

        if file_type is None:
            if extension == ".sav" or extension == ".txt":
                file_type = "fhd"
            elif "fits" in extension:
                file_type = "calfits"
            else:
                raise ValueError(
                    "File type could not be determined, use the "
                    "file_type keyword to specify the type."
                )

        if file_type not in ["calfits", "fhd"]:
            raise ValueError("The only supported file_types are 'calfits' and 'fhd'.")

        obs_file_use = None
        layout_file_use = None
        settings_file_use = None
        if file_type == "fhd":
            if obs_file is None:
                raise ValueError("obs_file parameter must be set for FHD files.")
            else:
                if isinstance(obs_file, (list, tuple)):
                    n_obs = len(obs_file)
                    obs_file_use = obs_file[0]
                else:
                    n_obs = 1
                    obs_file_use = obs_file
            if n_obs != n_files:
                raise ValueError("Number of obs_files must match number of cal_files")

            if layout_file is not None:
                if isinstance(layout_file, (list, tuple)):
                    n_layout = len(layout_file)
                    layout_file_use = layout_file[0]
                else:
                    n_layout = 1
                    layout_file_use = layout_file
                if n_layout != n_files:
                    raise ValueError(
                        "Number of layout_files must match number of cal_files"
                    )

            if settings_file is not None:
                if isinstance(settings_file, (list, tuple)):
                    n_settings = len(settings_file)
                    settings_file_use = settings_file[0]
                else:
                    n_settings = 1
                    settings_file_use = settings_file
                if n_settings != n_files:
                    raise ValueError(
                        "Number of settings_files must match number of cal_files"
                    )

        if multi:
            self.read(
                filename[0],
                file_type=file_type,
                read_data=read_data,
                use_future_array_shapes=use_future_array_shapes,
                # checking parameters
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                # file-type specific parameters
                # FHD
                obs_file=obs_file_use,
                layout_file=layout_file_use,
                settings_file=settings_file_use,
                raw=raw,
                extra_history=extra_history,
            )
            uv_list = []
            for ind, file in enumerate(filename[1:]):
                if file_type == "fhd":
                    file_index = ind + 1
                    obs_file_use = obs_file[file_index]
                    if layout_file is not None:
                        layout_file_use = layout_file[file_index]
                    if settings_file is not None:
                        settings_file_use = settings_file[file_index]

                uvcal2 = UVCal()
                uvcal2.read(
                    file,
                    read_data=read_data,
                    file_type=file_type,
                    use_future_array_shapes=use_future_array_shapes,
                    # checking parameters
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    # file-type specific parameters
                    # FHD
                    obs_file=obs_file_use,
                    layout_file=layout_file_use,
                    settings_file=settings_file_use,
                    raw=raw,
                    extra_history=extra_history,
                )
                uv_list.append(uvcal2)
            # Concatenate once at end
            if axis is not None:
                # fast_concat to operates on lists
                self.fast_concat(
                    uv_list,
                    axis,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    inplace=True,
                )
            else:
                # Too much work to rewrite __add__ to operate on lists
                # of files, so instead doing a binary tree merge
                uv_list = [self] + uv_list
                while len(uv_list) > 1:
                    for uv1, uv2 in zip(uv_list[0::2], uv_list[1::2]):
                        uv1.__iadd__(
                            uv2,
                            run_check=run_check,
                            check_extra=check_extra,
                            run_check_acceptability=run_check_acceptability,
                        )
                    uv_list = uv_list[0::2]
                # Because self was at the beginning of the list,
                # everything is merged into it at the end of this loop
        else:
            if file_type == "calfits":
                self.read_calfits(
                    filename,
                    read_data=read_data,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "fhd":
                self.read_fhd_cal(
                    filename,
                    obs_file,
                    layout_file=layout_file,
                    settings_file=settings_file,
                    raw=raw,
                    read_data=read_data,
                    extra_history=extra_history,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    use_future_array_shapes=use_future_array_shapes,
                )

    @classmethod
    def from_file(
        cls,
        filename,
        *,
        axis=None,
        file_type=None,
        read_data=True,
        use_future_array_shapes=False,
        # checking parameters
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        # file-type specific parameters
        # FHD
        obs_file=None,
        layout_file=None,
        settings_file=None,
        raw=True,
        extra_history=None,
    ):
        """
        Initialize a new UVCal object by reading the input file.

        This method supports a number of different types of files.
        Universal parameters (required and optional) are listed directly below,
        followed by parameters used by all file types related to checking. Each file
        type also has its own set of optional parameters that are listed at the end of
        this docstring.

        Parameters
        ----------
        filename : str or array_like of str
            The file(s) or list(s) (or array(s)) of files to read from.
        file_type : str
            One of ['calfits', 'fhd'] or None. If None, the code attempts to guess what
            the file type is based on file extensions (FHD: .sav, .txt;
            uvfits: .calfits). Note that if a list of datasets is passed, the file type
            is determined from the first dataset.
        axis : str
            Axis to concatenate files along. This enables fast concatenation
            along the specified axis without the normal checking that all other
            metadata agrees. This method does not guarantee correct resulting
            objects. Please see the docstring for fast_concat for details.
            Allowed values are: 'antenna', 'time', 'freq', 'spw', 'jones' ('freq' is
            not allowed for delay or wideband objects and 'spw' is only allowed for
            wideband objects). Only used if multiple files are passed.
        read_data : bool
            Read in the gains or delays, quality arrays and flag arrays.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metadata only object.
        use_future_array_shapes : bool
            Option to convert to the future planned array shapes before the changes go
            into effect by removing the spectral window axis.

        Checking
        --------
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters after reading in the file.
        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 reading in the file.

        FHD
        ---
        obs_file : str or list of str
            The obs.sav file or list of files to read from. This is required for FHD
            files.
        layout_file : str
            The FHD layout file. Required for antenna_positions to be set.
        settings_file : str or list of str, optional
            The settings_file or list of files to read from. Optional,
            but very useful for provenance.
        raw : bool
            Option to use the raw (per antenna, per frequency) solution or
            to use the fitted (polynomial over phase/amplitude) solution.
            Default is True (meaning use the raw solutions).

        """
        uvc = cls()
        uvc.read(
            filename,
            axis=axis,
            file_type=file_type,
            read_data=read_data,
            use_future_array_shapes=use_future_array_shapes,
            # checking parameters
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            # file-type specific parameters
            # FHD
            obs_file=obs_file,
            layout_file=layout_file,
            settings_file=settings_file,
            raw=raw,
            extra_history=extra_history,
        )
        return uvc

    def write_calfits(
        self,
        filename,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        clobber=False,
    ):
        """
        Write the data to a calfits file.

        Parameters
        ----------
        filename : str
            The calfits file to write to.
        run_check : bool
            Option to check for the existence and proper shapes of
            parameters before writing the file.
        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 before writing the file.
        clobber : bool
            Option to overwrite the filename if the file already exists.

        Raises
        ------
        ValueError
            If the UVCal object is a metadata only object.

        """
        if self.metadata_only:
            raise ValueError(
                "Cannot write out metadata only objects to a calfits file."
            )

        calfits_obj = self._convert_to_filetype("calfits")
        calfits_obj.write_calfits(
            filename,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            clobber=clobber,
        )
        del calfits_obj


# pyuvdata/uvcal/initializers.py
"""From-memory initializers for UVCal objects."""
from __future__ import annotations

from typing import Literal, Sequence

import numpy as np
from astropy.time import Time

from .. import __version__
from ..uvdata.initializers import (
    Locations,
    get_antenna_params,
    get_freq_params,
    get_time_params,
)


def new_uvcal(
    freq_array: np.ndarray,
    time_array: np.ndarray,
    antenna_positions: np.ndarray | dict[str | int, np.ndarray],
    telescope_location: Locations,
    telescope_name: str,
    cal_style: Literal["sky", "redundant"],
    gain_convention: Literal["divide", "multiply"],
    x_orientation: Literal["east", "north", "e", "n", "ew", "ns"],
    jones_array: np.ndarray | str,
    delay_array: np.ndarray | None = None,
    spw_array: Sequence[int] | np.ndarray = (0,),
    cal_type: Literal["delay", "gain", "unknown"] | None = None,
    integration_time: float | np.ndarray | None = None,
    channel_width: float | np.ndarray | None = None,
    antenna_names: list[str] | None = None,
    antenna_numbers: list[int] | None = None,
    antname_format: str = "{0:03d}",
    ant_array: np.ndarray | None = None,
    wide_band: bool = False,
    flex_spw_id_array: np.ndarray | None = None,
    ref_antenna_name: str | None = None,
    sky_catalog: str | None = None,
    sky_field: str | None = None,
    empty: bool = False,
    history: str = "",
    **kwargs,
):
    """Create a new UVCal object with default parameters.

    Returns
    -------
    UVCal
        A new UVCal object with default parameters.

    """
    from .uvcal import UVCal

    uvc = UVCal()

    if wide_band:
        raise NotImplementedError(
            "Wideband calibrations are not yet supported for this constructor."
        )
    if not kwargs.get("flex_spw", True):
        raise ValueError("flex_spw must be True for this constructor.")

    antenna_positions, antenna_names, antenna_numbers = get_antenna_params(
        antenna_positions, antenna_names, antenna_numbers, antname_format
    )
    if ant_array is None:
        ant_array = antenna_numbers
    else:
        # Ensure they all exist
        missing = [ant for ant in ant_array if ant not in antenna_numbers]
        if missing:
            raise ValueError(
                f"The following ants are not in antenna_numbers: {missing}"
            )

    lst_array, integration_time = get_time_params(
        telescope_location, time_array, integration_time
    )

    freq_array, channel_width = get_freq_params(freq_array, channel_width)

    spw_array = np.array(spw_array)
    if len(spw_array) != 1 and flex_spw_id_array is None:
        raise ValueError(
            "If spw_array is not length 1, flex_spw_id_array must be provided."
        )
    elif len(spw_array) == 1:
        flex_spw_id_array = np.zeros_like(freq_array, dtype=int)

    if jones_array == "linear":
        jones_array = np.array([-5, -6, -7, -8])
    elif jones_array == "circular":
        jones_array = np.array([-1, -2, -3, -4])
    else:
        jones_array = np.array(jones_array)

    history += (
        f"Object created by new_uvcal() at {Time.now().iso} using "
        f"pyuvdata version {__version__}."
    )

    XORIENTMAP = {
        "east": "east",
        "north": "north",
        "e": "east",
        "n": "north",
        "ew": "east",
        "ns": "north",
    }
    x_orientation = XORIENTMAP[x_orientation.lower()]

    if delay_array is not None and cal_type is None:
        cal_type = "delay"
    elif cal_type == "delay" and delay_array is None:
        raise ValueError("If cal_type is delay, delay_array must be provided.")

    if cal_style == "sky":
        if ref_antenna_name is None:
            raise ValueError("If cal_style is sky, ref_antenna_name must be provided.")
        if sky_catalog is None:
            raise ValueError("If cal_style is sky, sky_catalog must be provided.")
        if sky_field is None:
            raise ValueError("If cal_style is sky, sky_field must be provided.")

    # Now set all the metadata
    uvc.freq_array = freq_array
    uvc.antenna_positions = antenna_positions
    uvc.telescope_location = [
        telescope_location.x.to_value("m"),
        telescope_location.y.to_value("m"),
        telescope_location.z.to_value("m"),
    ]
    uvc.telescope_name = telescope_name
    uvc.time_array = time_array
    uvc.lst_array = lst_array
    uvc.integration_time = integration_time
    uvc.channel_width = channel_width
    uvc.antenna_names = antenna_names
    uvc.antenna_numbers = antenna_numbers
    uvc.flex_spw = True
    uvc.history = history
    uvc.ant_array = ant_array
    uvc.telescope_name = telescope_name
    uvc.cal_style = cal_style
    uvc.cal_type = cal_type
    uvc.gain_convention = gain_convention
    uvc.x_orientation = x_orientation
    uvc.ref_antenna_name = ref_antenna_name
    uvc.sky_catalog = sky_catalog
    uvc.sky_field = sky_field
    uvc.flex_spw_id_array = flex_spw_id_array
    uvc.jones_array = jones_array

    if cal_type == "delay":
        uvc._set_delay()
    elif cal_type == "gain":
        uvc._set_gain()
    elif cal_type == "unknown":
        uvc._set_unknown_cal_type()

    if cal_style == "redundant":
        uvc._set_redundant()
    elif cal_style == "sky":
        uvc._set_sky()

    print(uvc.cal_type)

    uvc.Nants_data = len(ant_array)
    uvc.Nants_telescope = len(antenna_numbers)
    uvc.Nfreqs = len(freq_array)
    uvc.Ntimes = len(time_array)
    uvc.Nspws = 1
    uvc.Njones = len(jones_array)

    uvc.spw_array = np.array([0])

    # We always make the following true.
    uvc._set_future_array_shapes()
    uvc._set_flex_spw()

    for k, v in kwargs.items():
        if hasattr(uvc, k):
            setattr(uvc, k, v)
        else:
            raise ValueError(f"Unrecognized keyword argument: {k}")

    if empty:
        # Flag array
        uvc.flag_array = np.zeros(
            uvc.Nants_data, uvc.Nfreqs, uvc.Ntimes, uvc.Njones, dtype=bool
        )
        uvc.gain_array = np.ones(
            uvc.Nants_data, uvc.Nfreqs, uvc.Ntimes, uvc.Njones, dtype=complex
        )
        uvc.input_flag_array = uvc.flag_array.copy()
        if cal_type == "delay":
            uvc.quality_array = np.zeros(
                uvc.Nants_data, uvc.Nspws, uvc.Ntimes, uvc.Njones, dtype=float
            )
            uvc.total_quality_array = np.zeros(
                uvc.Nspws, uvc.Ntimes, uvc.Njones, dtype=float
            )
        else:
            uvc.quality_array = np.zeros(
                uvc.Nants_data, uvc.Nfreqs, uvc.Ntimes, uvc.Njones, dtype=float
            )
            uvc.total_quality_array = np.zeros(
                uvc.Nfreqs, uvc.Ntimes, uvc.Njones, dtype=float
            )

    uvc.check()
    return uvc
